first commit!
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..a099036
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1 @@
+TBD
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..908381e
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,5 @@
+init:
+	pip install -r requirements.txt
+
+# test:
+# 	nosetests tests
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..757c4ff
--- /dev/null
+++ b/README.md
@@ -0,0 +1,4 @@
+MCP sanity checks
+========================
+
+This is salt-based set of tests for basic verification of MCP deployments 
diff --git a/cvp_checks/__init__.py b/cvp_checks/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/cvp_checks/__init__.py
diff --git a/cvp_checks/__init__.pyc b/cvp_checks/__init__.pyc
new file mode 100644
index 0000000..3e354f3
--- /dev/null
+++ b/cvp_checks/__init__.pyc
Binary files differ
diff --git a/cvp_checks/fixtures/__init__.py b/cvp_checks/fixtures/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/cvp_checks/fixtures/__init__.py
diff --git a/cvp_checks/fixtures/__init__.pyc b/cvp_checks/fixtures/__init__.pyc
new file mode 100644
index 0000000..cc3d431
--- /dev/null
+++ b/cvp_checks/fixtures/__init__.pyc
Binary files differ
diff --git a/cvp_checks/fixtures/base.py b/cvp_checks/fixtures/base.py
new file mode 100644
index 0000000..f1893fa
--- /dev/null
+++ b/cvp_checks/fixtures/base.py
@@ -0,0 +1,7 @@
+import pytest
+import cvp_checks.utils as utils
+
+
+@pytest.fixture
+def local_salt_client():
+    return utils.init_salt_client()
diff --git a/cvp_checks/fixtures/base.pyc b/cvp_checks/fixtures/base.pyc
new file mode 100644
index 0000000..f36eff1
--- /dev/null
+++ b/cvp_checks/fixtures/base.pyc
Binary files differ
diff --git a/cvp_checks/global_config.yaml b/cvp_checks/global_config.yaml
new file mode 100644
index 0000000..9edfc0d
--- /dev/null
+++ b/cvp_checks/global_config.yaml
@@ -0,0 +1,15 @@
+---
+# MANDATORY: Credentials for Salt master
+#SALT_URL: <salt_url> 
+#SALT_USERNAME: <salt_usr>
+#SALT_PASSWORD: <salt_pwd>
+
+# List of nodes to skip in tests
+skipped_nodes: []
+
+# ntp test setting
+time_deviation: 30
+
+# mtu test setting
+# mask for interfaces to skip
+skipped_ifaces: ["bonding_masters", "lo", "veth", "tap", "cali"]
diff --git a/cvp_checks/tests/__init__.py b/cvp_checks/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/cvp_checks/tests/__init__.py
diff --git a/cvp_checks/tests/__init__.pyc b/cvp_checks/tests/__init__.pyc
new file mode 100644
index 0000000..c5b5b46
--- /dev/null
+++ b/cvp_checks/tests/__init__.pyc
Binary files differ
diff --git a/cvp_checks/tests/__pycache__/conftest.cpython-27-PYTEST.pyc b/cvp_checks/tests/__pycache__/conftest.cpython-27-PYTEST.pyc
new file mode 100644
index 0000000..9529827
--- /dev/null
+++ b/cvp_checks/tests/__pycache__/conftest.cpython-27-PYTEST.pyc
Binary files differ
diff --git a/cvp_checks/tests/__pycache__/test_contrail.cpython-27-PYTEST.pyc b/cvp_checks/tests/__pycache__/test_contrail.cpython-27-PYTEST.pyc
new file mode 100644
index 0000000..12c3420
--- /dev/null
+++ b/cvp_checks/tests/__pycache__/test_contrail.cpython-27-PYTEST.pyc
Binary files differ
diff --git a/cvp_checks/tests/__pycache__/test_default_gateway.cpython-27-PYTEST.pyc b/cvp_checks/tests/__pycache__/test_default_gateway.cpython-27-PYTEST.pyc
new file mode 100644
index 0000000..d449fb7
--- /dev/null
+++ b/cvp_checks/tests/__pycache__/test_default_gateway.cpython-27-PYTEST.pyc
Binary files differ
diff --git a/cvp_checks/tests/__pycache__/test_galera_cluster.cpython-27-PYTEST.pyc b/cvp_checks/tests/__pycache__/test_galera_cluster.cpython-27-PYTEST.pyc
new file mode 100644
index 0000000..2dd98e0
--- /dev/null
+++ b/cvp_checks/tests/__pycache__/test_galera_cluster.cpython-27-PYTEST.pyc
Binary files differ
diff --git a/cvp_checks/tests/__pycache__/test_mtu.cpython-27-PYTEST.pyc b/cvp_checks/tests/__pycache__/test_mtu.cpython-27-PYTEST.pyc
new file mode 100644
index 0000000..4fab58c
--- /dev/null
+++ b/cvp_checks/tests/__pycache__/test_mtu.cpython-27-PYTEST.pyc
Binary files differ
diff --git a/cvp_checks/tests/__pycache__/test_ntp_sync.cpython-27-PYTEST.pyc b/cvp_checks/tests/__pycache__/test_ntp_sync.cpython-27-PYTEST.pyc
new file mode 100644
index 0000000..0c139d4
--- /dev/null
+++ b/cvp_checks/tests/__pycache__/test_ntp_sync.cpython-27-PYTEST.pyc
Binary files differ
diff --git a/cvp_checks/tests/__pycache__/test_packet_checker.cpython-27-PYTEST.pyc b/cvp_checks/tests/__pycache__/test_packet_checker.cpython-27-PYTEST.pyc
new file mode 100644
index 0000000..816e78f
--- /dev/null
+++ b/cvp_checks/tests/__pycache__/test_packet_checker.cpython-27-PYTEST.pyc
Binary files differ
diff --git a/cvp_checks/tests/__pycache__/test_rabbit_cluster.cpython-27-PYTEST.pyc b/cvp_checks/tests/__pycache__/test_rabbit_cluster.cpython-27-PYTEST.pyc
new file mode 100644
index 0000000..ff5eeb0
--- /dev/null
+++ b/cvp_checks/tests/__pycache__/test_rabbit_cluster.cpython-27-PYTEST.pyc
Binary files differ
diff --git a/cvp_checks/tests/__pycache__/test_repo_list.cpython-27-PYTEST.pyc b/cvp_checks/tests/__pycache__/test_repo_list.cpython-27-PYTEST.pyc
new file mode 100644
index 0000000..1de85b4
--- /dev/null
+++ b/cvp_checks/tests/__pycache__/test_repo_list.cpython-27-PYTEST.pyc
Binary files differ
diff --git a/cvp_checks/tests/__pycache__/test_services.cpython-27-PYTEST.pyc b/cvp_checks/tests/__pycache__/test_services.cpython-27-PYTEST.pyc
new file mode 100644
index 0000000..f28935a
--- /dev/null
+++ b/cvp_checks/tests/__pycache__/test_services.cpython-27-PYTEST.pyc
Binary files differ
diff --git a/cvp_checks/tests/__pycache__/test_single_vip.cpython-27-PYTEST.pyc b/cvp_checks/tests/__pycache__/test_single_vip.cpython-27-PYTEST.pyc
new file mode 100644
index 0000000..692eefd
--- /dev/null
+++ b/cvp_checks/tests/__pycache__/test_single_vip.cpython-27-PYTEST.pyc
Binary files differ
diff --git a/cvp_checks/tests/ceph/__pycache__/test_ceph.cpython-27-PYTEST.pyc b/cvp_checks/tests/ceph/__pycache__/test_ceph.cpython-27-PYTEST.pyc
new file mode 100644
index 0000000..c0451ed
--- /dev/null
+++ b/cvp_checks/tests/ceph/__pycache__/test_ceph.cpython-27-PYTEST.pyc
Binary files differ
diff --git a/cvp_checks/tests/ceph/__pycache__/test_ceph_haproxy.cpython-27-PYTEST.pyc b/cvp_checks/tests/ceph/__pycache__/test_ceph_haproxy.cpython-27-PYTEST.pyc
new file mode 100644
index 0000000..71abde3
--- /dev/null
+++ b/cvp_checks/tests/ceph/__pycache__/test_ceph_haproxy.cpython-27-PYTEST.pyc
Binary files differ
diff --git a/cvp_checks/tests/ceph/config.yaml b/cvp_checks/tests/ceph/config.yaml
new file mode 100644
index 0000000..bfc8e70
--- /dev/null
+++ b/cvp_checks/tests/ceph/config.yaml
@@ -0,0 +1,3 @@
+---
+ceph_monitors: ['ceph-003*', 'ceph-004*', 'ceph-005*']
+ceph_osd_probe_node: ['ceph-001*']
diff --git a/cvp_checks/tests/ceph/test_ceph.py b/cvp_checks/tests/ceph/test_ceph.py
new file mode 100644
index 0000000..05a61c2
--- /dev/null
+++ b/cvp_checks/tests/ceph/test_ceph.py
@@ -0,0 +1,11 @@
+from cvp_checks import utils
+
+
+def test_check_ceph_osd(local_salt_client):
+    config = utils.get_configuration(__file__)
+    osd_fail = \
+        local_salt_client.cmd(config["ceph_osd_probe_node"][0], 'cmd.run',
+                              ['ceph osd tree | grep down'])
+    assert not osd_fail.values()[0], \
+        "Some osds are in down state or ceph is not found".format(
+        osd_fail.values()[0])
diff --git a/cvp_checks/tests/ceph/test_ceph_haproxy.py b/cvp_checks/tests/ceph/test_ceph_haproxy.py
new file mode 100644
index 0000000..2f2e1e0
--- /dev/null
+++ b/cvp_checks/tests/ceph/test_ceph_haproxy.py
@@ -0,0 +1,21 @@
+from cvp_checks import utils
+
+
+def test_ceph_haproxy(local_salt_client):
+    config = utils.get_configuration(__file__)
+
+    fail = {}
+
+    for monitor in config["ceph_monitors"]:
+        monitor_info = local_salt_client.cmd(monitor, 'cmd.run',
+                                             ["echo 'show stat' | nc -U "
+                                              "/var/run/haproxy/admin.sock | "
+                                              "grep ceph_mon_radosgw_cluster"])
+
+        for name, info in monitor_info.iteritems():
+            if "OPEN" and "UP" in info:
+                continue
+            else:
+                fail[name] = info
+
+    assert not fail, "Failed monitors: {}".format(fail)
diff --git a/cvp_checks/tests/conftest.py b/cvp_checks/tests/conftest.py
new file mode 100644
index 0000000..21af490
--- /dev/null
+++ b/cvp_checks/tests/conftest.py
@@ -0,0 +1 @@
+from cvp_checks.fixtures.base import *
diff --git a/cvp_checks/tests/test_contrail.py b/cvp_checks/tests/test_contrail.py
new file mode 100644
index 0000000..16c3389
--- /dev/null
+++ b/cvp_checks/tests/test_contrail.py
@@ -0,0 +1,78 @@
+import pytest
+import json
+
+def test_contrail_compute_status(local_salt_client):
+    cs = local_salt_client.cmd(
+        'nova:compute', 'cmd.run',
+        ['contrail-status | grep -Pv \'(==|^$)\''],
+        expr_form='pillar'
+    )
+    # TODO: what if compute lacks these service unintentionally?
+    if not cs:
+        pytest.skip("Contrail services were not found on compute nodes")
+    broken_services = []
+
+    for node in cs:
+        for line in cs[node].split('\n'):
+            line = line.strip()
+            name, status = line.split(None, 1)
+            if status not in {'active'}:
+                err_msg = "{node}:{service} - {status}".format(
+                    node=node, service=name, status=status)
+                broken_services.append(err_msg)
+
+    assert not broken_services, 'Broken services: {}'.format(json.dumps(
+                                                             broken_services,
+                                                             indent=4))
+
+
+def test_contrail_node_status(local_salt_client):
+    cs = local_salt_client.cmd(
+        'opencontrail:client:analytics_node', 'cmd.run',
+        ['contrail-status | grep -Pv \'(==|^$|Disk|unix|support)\''],
+        expr_form='pillar'
+    )
+    cs.update(local_salt_client.cmd(
+        'opencontrail:control', 'cmd.run',
+        ['contrail-status | grep -Pv \'(==|^$|Disk|unix|support)\''],
+        expr_form='pillar')
+    )
+    if not cs:
+        pytest.skip("Contrail nodes were not found on compute nodes")
+    broken_services = []
+    for node in cs:
+        for line in cs[node].split('\n'):
+            line = line.strip()
+            if 'crashes/core.java.' not in line:
+                name, status = line.split(None, 1)
+            else:
+                name, status = line,'FATAL'
+            if status not in {'active', 'backup'}:
+                err_msg = "{node}:{service} - {status}".format(
+                    node=node, service=name, status=status)
+                broken_services.append(err_msg)
+
+    assert not broken_services, 'Broken services: {}'.format(json.dumps(
+                                                             broken_services,
+                                                             indent=4))
+
+
+def test_contrail_vrouter_count(local_salt_client):
+    cs = local_salt_client.cmd(
+        'nova:compute', 'cmd.run', ['contrail-status | grep -Pv \'(==|^$)\''],
+        expr_form='pillar'
+    )
+    # TODO: what if compute lacks these service unintentionally?
+    if not cs:
+        pytest.skip("Contrail services were not found on compute nodes")
+
+    actual_vrouter_count = 0
+    for node in cs:
+        for line in cs[node].split('\n'):
+            if 'contrail-vrouter-nodemgr' in line:
+                actual_vrouter_count += 1
+
+    assert actual_vrouter_count == len(cs.keys()),\
+        'The length of vRouters {} differs' \
+        ' from the length of compute nodes {}'.format(actual_vrouter_count,
+                                                      len(cs.keys()))
diff --git a/cvp_checks/tests/test_default_gateway.py b/cvp_checks/tests/test_default_gateway.py
new file mode 100644
index 0000000..57a6d7b
--- /dev/null
+++ b/cvp_checks/tests/test_default_gateway.py
@@ -0,0 +1,29 @@
+import json
+import pytest
+
+from cvp_checks import utils
+
+
+@pytest.mark.parametrize(
+    "group",
+    utils.get_groups(utils.get_configuration(__file__))
+)
+def test_check_default_gateways(local_salt_client, group):
+    config = utils.get_configuration(__file__)
+    netstat_info = local_salt_client.cmd(
+        group, 'cmd.run', ['ip r | sed -n 1p'], expr_form='pcre')
+
+    gateways = {}
+    nodes = netstat_info.keys()
+
+    for node in nodes:
+        if netstat_info[node] not in gateways:
+            gateways[netstat_info[node]] = [node]
+        else:
+            gateways[netstat_info[node]].append(node)
+
+    assert len(gateways.keys()) == 1, \
+        "There were found few gateways within group {group}: {gw}".format(
+        group=group,
+        gw=json.dumps(gateways, indent=4)
+    )
diff --git a/cvp_checks/tests/test_galera_cluster.py b/cvp_checks/tests/test_galera_cluster.py
new file mode 100644
index 0000000..d2b2d5f
--- /dev/null
+++ b/cvp_checks/tests/test_galera_cluster.py
@@ -0,0 +1,17 @@
+def test_galera_cluster_status(local_salt_client):
+    gs = local_salt_client.cmd(
+        'I@galera:*',
+        'cmd.run',
+        ['salt-call mysql.status | grep -A1 wsrep_cluster_size | tail -n1'],
+        expr_form='pillar')
+
+    size_cluster = []
+    amount = len(gs)
+
+    for item in gs.values():
+        size_cluster.append(item.split('\n')[-1].strip())
+
+    assert all(item == str(amount) for item in size_cluster), \
+        '''There found inconsistency within cloud. MySQL galera cluster
+              is probably broken, the cluster size gathered from nodes:
+              {}'''.format(gs)
diff --git a/cvp_checks/tests/test_mtu.py b/cvp_checks/tests/test_mtu.py
new file mode 100644
index 0000000..f3c3511
--- /dev/null
+++ b/cvp_checks/tests/test_mtu.py
@@ -0,0 +1,68 @@
+import pytest
+import json
+from cvp_checks import utils
+
+
+@pytest.mark.parametrize(
+    "group",
+    utils.get_groups(utils.get_configuration(__file__))
+)
+def test_mtu(local_salt_client, group):
+    config = utils.get_configuration(__file__)
+    skipped_ifaces = config["skipped_ifaces"]
+    total = {}
+    network_info = local_salt_client.cmd(
+        group, 'cmd.run', ['ls /sys/class/net/'], expr_form='pcre')
+
+    kvm_nodes = local_salt_client.cmd(
+        'salt:control', 'test.ping', expr_form='pillar').keys()
+
+    if len(network_info.keys()) < 2:
+        pytest.skip("Nothing to compare - only 1 node")
+
+    for node, ifaces_info in network_info.iteritems():
+        if node in kvm_nodes:
+            kvm_info = local_salt_client.cmd(node, 'cmd.run',
+                                             ["virsh list | "
+                                              "awk '{print $2}' | "
+                                              "xargs -n1 virsh domiflist | "
+                                              "grep -v br-pxe | grep br- | "
+                                              "awk '{print $1}'"])
+            ifaces_info = kvm_info.get(node)
+        node_ifaces = ifaces_info.split('\n')
+        ifaces = {}
+        for iface in node_ifaces:
+            for skipped_iface in skipped_ifaces:
+                if skipped_iface in iface:
+                    break
+            else:
+                iface_mtu = local_salt_client.cmd(node, 'cmd.run',
+                                                  ['cat /sys/class/'
+                                                   'net/{}/mtu'.format(iface)])
+                ifaces[iface] = iface_mtu.get(node)
+        total[node] = ifaces
+
+    nodes = []
+    mtu_data = []
+    my_set = set()
+
+    for node in total:
+        nodes.append(node)
+        my_set.update(total[node].keys())
+    for interf in my_set:
+        diff = []
+        row = []
+        for node in nodes:
+            if interf in total[node].keys():
+                diff.append(total[node][interf])
+                row.append("{}: {}".format(node, total[node][interf]))
+            else:
+                row.append("{}: No interface".format(node))
+        if diff.count(diff[0]) < len(nodes):
+            row.sort()
+            row.insert(0, interf)
+            mtu_data.append(row)
+    assert len(mtu_data) == 0, \
+        "Several problems found for {0} group: {1}".format(
+        group, json.dumps(mtu_data, indent=4))
+
diff --git a/cvp_checks/tests/test_ntp_sync.py b/cvp_checks/tests/test_ntp_sync.py
new file mode 100644
index 0000000..72ca43e
--- /dev/null
+++ b/cvp_checks/tests/test_ntp_sync.py
@@ -0,0 +1,27 @@
+import pytest
+import os
+from cvp_checks import utils
+
+
+@pytest.mark.parametrize(
+    "node",
+    utils.get_active_nodes(utils.get_configuration(__file__))
+)
+def test_ntp_sync(local_salt_client, node):
+    config = utils.get_configuration(__file__)
+    fail = {}
+
+    saltmaster_time = int(local_salt_client.cmd(
+        os.uname()[1] + '*', 'cmd.run', ['date +%s']).values()[0])
+
+    nodes_time = local_salt_client.cmd(
+        node, 'cmd.run', ['date +%s'], expr_form='pcre')
+
+    for node, time in nodes_time.iteritems():
+        if (int(time) - saltmaster_time) > config["time_deviation"] or \
+                (int(time) - saltmaster_time) < -config["time_deviation"]:
+            fail[node] = time
+
+    assert not fail, 'SaltMaster time: {}\n' \
+                     'Nodes with time mismatch:\n {}'.format(saltmaster_time,
+                                                             fail)
diff --git a/cvp_checks/tests/test_packet_checker.py b/cvp_checks/tests/test_packet_checker.py
new file mode 100644
index 0000000..79915ec
--- /dev/null
+++ b/cvp_checks/tests/test_packet_checker.py
@@ -0,0 +1,83 @@
+import pytest
+import json
+from cvp_checks import utils
+
+
+@pytest.mark.parametrize(
+    "group",
+    utils.get_groups(utils.get_configuration(__file__))
+)
+def test_check_package_versions(local_salt_client, group):
+    config = utils.get_configuration(__file__)
+
+    output = local_salt_client.cmd(group, 'lowpkg.list_pkgs', expr_form='pcre')
+
+    if len(output.keys()) < 2:
+        pytest.skip("Nothing to compare - only 1 node")
+
+    nodes = []
+    pkts_data = []
+    my_set = set()
+
+    for node in output:
+        nodes.append(node)
+        my_set.update(output[node].keys())
+
+    for deb in my_set:
+        diff = []
+        row = []
+        for node in nodes:
+            if deb in output[node].keys():
+                diff.append(output[node][deb])
+                row.append("{}: {}".format(node, output[node][deb]))
+            else:
+                row.append("{}: No package".format(node))
+        if diff.count(diff[0]) < len(nodes):
+            row.sort()
+            row.insert(0, deb)
+            pkts_data.append(row)
+    assert len(pkts_data) <= 1, \
+        "Several problems found for {0} group: {1}".format(
+        group, json.dumps(pkts_data, indent=4))
+
+
+@pytest.mark.parametrize(
+    "group",
+    utils.get_groups(utils.get_configuration(__file__))
+)
+def test_check_module_versions(local_salt_client, group):
+    config = utils.get_configuration(__file__)
+
+    pre_check = local_salt_client.cmd(
+        group, 'cmd.run', ['dpkg -l | grep "python-pip "'], expr_form='pcre')
+    if pre_check.values().count('') > 0:
+        pytest.skip("pip is not installed on one or more nodes")
+    if len(pre_check.keys()) < 2:
+        pytest.skip("Nothing to compare - only 1 node")
+    output = local_salt_client.cmd(group, 'pip.freeze', expr_form='pcre')
+
+    nodes = []
+    pkts_data = []
+    my_set = set()
+
+    for node in output:
+        nodes.append(node)
+        my_set.update([x.split("=")[0] for x in output[node]])
+        output[node] = dict([x.split("==") for x in output[node]])
+
+    for deb in my_set:
+        diff = []
+        row = []
+        for node in nodes:
+            if deb in output[node].keys():
+                diff.append(output[node][deb])
+                row.append("{}: {}".format(node, output[node][deb]))
+            else:
+                row.append("{}: No module".format(node))
+        if diff.count(diff[0]) < len(nodes):
+            row.sort()
+            row.insert(0, deb)
+            pkts_data.append(row)
+    assert len(pkts_data) <= 1, \
+        "Several problems found for {0} group: {1}".format(
+        group, json.dumps(pkts_data, indent=4))
diff --git a/cvp_checks/tests/test_rabbit_cluster.py b/cvp_checks/tests/test_rabbit_cluster.py
new file mode 100644
index 0000000..94de975
--- /dev/null
+++ b/cvp_checks/tests/test_rabbit_cluster.py
@@ -0,0 +1,43 @@
+import re
+
+
+def test_checking_rabbitmq_cluster(local_salt_client):
+    # disable config for this test
+    # it may be reintroduced in future
+    # config = utils.get_configuration(__file__)
+    # request pillar data from rmq nodes
+    rabbitmq_pillar_data = local_salt_client.cmd(
+        'rabbitmq:server', 'pillar.data',
+        ['rabbitmq:cluster'], expr_form='pillar')
+
+    # creating dictionary {node:cluster_size_for_the_node}
+    # with required cluster size for each node
+    control_dict = {}
+    required_cluster_size_dict = {}
+    for node in rabbitmq_pillar_data:
+        cluster_size_from_the_node = len(
+            rabbitmq_pillar_data[node]['rabbitmq:cluster']['members'])
+        required_cluster_size_dict.update({node: cluster_size_from_the_node})
+
+    # request actual data from rmq nodes
+    rabbit_actual_data = local_salt_client.cmd(
+        'rabbitmq:server', 'cmd.run',
+        ['rabbitmqctl cluster_status'], expr_form='pillar')
+
+    # find actual cluster size for each node
+    for node in rabbit_actual_data:
+        running_nodes_count = 0
+        for line in rabbit_actual_data[node].split('\n'):
+            if 'running_nodes' in line:
+                running_nodes_count = line.count('rabbit@')
+        # update control dictionary with values
+        # {node:actual_cluster_size_for_node}
+        if required_cluster_size_dict[node] != running_nodes_count:
+            control_dict.update({node: running_nodes_count})
+
+    assert not len(control_dict), "Inconsistency found within cloud. " \
+                                  "RabbitMQ cluster is probably broken, " \
+                                  "the cluster size for each node " \
+                                  "should be: {} but the following " \
+                                  "nodes has other values: {}".format(
+        len(required_cluster_size_dict.keys()), control_dict)
diff --git a/cvp_checks/tests/test_repo_list.py b/cvp_checks/tests/test_repo_list.py
new file mode 100644
index 0000000..6df6027
--- /dev/null
+++ b/cvp_checks/tests/test_repo_list.py
@@ -0,0 +1,43 @@
+import pytest
+from cvp_checks import utils
+
+
+@pytest.mark.parametrize(
+    "group",
+    utils.get_groups(utils.get_configuration(__file__))
+)
+def test_list_of_repo_on_nodes(local_salt_client, group):
+    info_salt = local_salt_client.cmd(
+        group, 'pillar.data', ['linux:system:repo'], expr_form='pcre')
+
+    raw_actual_info = local_salt_client.cmd(
+        group,
+        'cmd.run',
+        ['cat /etc/apt/sources.list.d/*;'
+         'cat /etc/apt/sources.list|grep deb|grep -v "#"'],
+        expr_form='pcre')
+    actual_repo_list = [item.replace('/ ', ' ')
+                        for item in raw_actual_info.values()[0].split('\n')]
+    expected_salt_data = [repo['source'].replace('/ ', ' ')
+                          for repo in info_salt.values()[0]
+                          ['linux:system:repo'].values()]
+
+    diff = {}
+    my_set = set()
+
+    my_set.update(actual_repo_list)
+    my_set.update(expected_salt_data)
+    import json
+    for repo in my_set:
+        rows = []
+        if repo not in actual_repo_list:
+            rows.append("{}: {}".format("pillars", "+"))
+            rows.append("{}: No repo".format('config'))
+            diff[repo] = rows
+        elif repo not in expected_salt_data:
+            rows.append("{}: {}".format("config", "+"))
+            rows.append("{}: No repo".format('pillars'))
+            diff[repo] = rows
+    assert len(diff) <= 1, \
+        "Several problems found for {0} group: {1}".format(
+        group, json.dumps(diff, indent=4))
diff --git a/cvp_checks/tests/test_services.py b/cvp_checks/tests/test_services.py
new file mode 100644
index 0000000..3d90218
--- /dev/null
+++ b/cvp_checks/tests/test_services.py
@@ -0,0 +1,41 @@
+import pytest
+import json
+from cvp_checks import utils
+
+
+@pytest.mark.parametrize(
+    "group",
+    utils.get_groups(utils.get_configuration(__file__))
+)
+def test_check_services(local_salt_client, group):
+    config = utils.get_configuration(__file__)
+
+    output = local_salt_client.cmd(group, 'service.get_all', expr_form='pcre')
+
+    if len(output.keys()) < 2:
+        pytest.skip("Nothing to compare - only 1 node")
+
+    nodes = []
+    pkts_data = []
+    my_set = set()
+
+    for node in output:
+        nodes.append(node)
+        my_set.update(output[node])
+
+    for srv in my_set:
+        diff = []
+        row = []
+        for node in nodes:
+            if srv in output[node]:
+                diff.append(srv)
+                row.append("{}: +".format(node))
+            else:
+                row.append("{}: No service".format(node))
+        if diff.count(diff[0]) < len(nodes):
+            row.sort()
+            row.insert(0, srv)
+            pkts_data.append(row)
+    assert len(pkts_data) <= 1, \
+        "Several problems found for {0} group: {1}".format(
+        group, json.dumps(pkts_data, indent=4))
diff --git a/cvp_checks/tests/test_single_vip.py b/cvp_checks/tests/test_single_vip.py
new file mode 100644
index 0000000..9e0f16e
--- /dev/null
+++ b/cvp_checks/tests/test_single_vip.py
@@ -0,0 +1,27 @@
+import pytest
+from cvp_checks import utils
+from collections import Counter
+
+
+@pytest.mark.parametrize(
+    "group",
+    utils.get_groups(utils.get_configuration(__file__))
+)
+def test_single_vip(local_salt_client, group):
+    local_salt_client.cmd(group, 'saltutil.sync_all', expr_form='pcre')
+    nodes_list = local_salt_client.cmd(
+        group, 'grains.item', ['ipv4'], expr_form='pcre')
+
+    ipv4_list = []
+
+    for node in nodes_list:
+        ipv4_list.extend(nodes_list.get(node).get('ipv4'))
+
+    cnt = Counter(ipv4_list)
+
+    for ip in cnt:
+        if ip == '127.0.0.1':
+            continue
+        elif cnt[ip] > 1:
+            assert "VIP IP duplicate found " \
+                   "in group {}\n{}".format(group, ipv4_list)
diff --git a/cvp_checks/utils/__init__.py b/cvp_checks/utils/__init__.py
new file mode 100644
index 0000000..9762e8e
--- /dev/null
+++ b/cvp_checks/utils/__init__.py
@@ -0,0 +1,119 @@
+import os
+import yaml
+import requests
+import re
+
+
+class salt_remote:
+    def cmd(self, tgt, fun, param=None,expr_form=None):
+        config = get_configuration(__file__)
+        for salt_cred in ['SALT_USERNAME', 'SALT_PASSWORD', 'SALT_URL']:
+            if os.environ.get(salt_cred):
+                config[salt_cred] = os.environ[salt_cred]
+        headers = {'Accept':'application/json'}
+        login_payload = {'username':config['SALT_USERNAME'],'password':config['SALT_PASSWORD'],'eauth':'pam'}
+        accept_key_payload = {'fun': fun,'tgt':tgt,'client':'local','expr_form':expr_form}
+        if param:
+            accept_key_payload['arg']=param
+
+        login_request = requests.post(os.path.join(config['SALT_URL'],'login'),headers=headers,data=login_payload)
+        request = requests.post(config['SALT_URL'],headers=headers,data=accept_key_payload,cookies=login_request.cookies)
+        return request.json()['return'][0]
+
+
+def init_salt_client():
+    local = salt_remote()
+    return local
+
+
+def get_active_nodes(config):
+    local_salt_client = init_salt_client()
+
+    skipped_nodes = config.get('skipped_nodes') or []
+    # TODO add skipped nodes to cmd command instead of filtering
+    nodes = local_salt_client.cmd('*', 'test.ping')
+    active_nodes = [
+        node_name for node_name in nodes
+        if nodes[node_name] and node_name not in skipped_nodes
+    ]
+    return active_nodes
+
+
+def get_groups(config):
+    # assume that node name is like <name>.domain
+    # last 1-3 digits of name are index, e.g. 001 in cpu001
+    # name doesn't contain dots
+    active_nodes = get_active_nodes(config)
+    skipped_group = config.get('skipped_group') or []
+    groups = []
+
+    for node in active_nodes:
+        index = re.search('[0-9]{1,3}$', node.split('.')[0])
+        if index:
+            group_name = node.split('.')[0][:-len(index.group(0))]
+        else:
+            group_name = node
+        if group_name not in skipped_group and group_name not in groups:
+            groups.append(group_name)
+    test_groups = []
+    groups_from_config = config.get('groups')
+    # check if config.yaml contains `groups` key
+    if groups_from_config is not None:
+        invalid_groups = []
+        for group in groups_from_config:
+            # check if group name from config
+            # is substring of one of the groups
+            grp = [x for x in groups if group in x]
+            if grp:
+                test_groups.append(grp[0])
+            else:
+                invalid_groups.append(group)
+        if invalid_groups:
+            raise ValueError('Config file contains'
+                             ' invalid groups name: {}'.format(invalid_groups))
+
+    groups = test_groups if test_groups else groups
+
+    # For splitting Ceph nodes
+    local_salt_client = init_salt_client()
+
+    if "ceph*" in groups:
+        groups.remove("ceph*")
+
+        ceph_status = local_salt_client.cmd(
+            'ceph*', "cmd.run", ["ps aux | grep ceph-mon | grep -v grep"])
+
+        mon = []
+        ceph = []
+        for node in ceph_status:
+            if ceph_status[node] != '':
+                mon.append(node.split('.')[0])
+            else:
+                ceph.append(node.split('.')[0])
+
+        mon_regex = "({0}.*)".format(".*|".join(mon))
+        groups.append(mon_regex)
+
+        ceph_regex = "({0}.*)".format(".*|".join(ceph))
+        groups.append(ceph_regex)
+
+    return groups
+
+
+def get_configuration(path_to_test):
+    """function returns configuration for environment
+
+    and for test if it's specified"""
+    global_config_file = os.path.join(
+        os.path.dirname(os.path.abspath(__file__)), "../global_config.yaml")
+    with open(global_config_file, 'r') as file:
+        global_config = yaml.load(file)
+
+    config_file = os.path.join(
+        os.path.dirname(os.path.abspath(path_to_test)), "config.yaml")
+
+    if os.path.exists(config_file):
+        with open(config_file, 'r') as file:
+            global_config.update(yaml.load(file))
+
+    return global_config
diff --git a/cvp_checks/utils/__init__.pyc b/cvp_checks/utils/__init__.pyc
new file mode 100644
index 0000000..1a1feed
--- /dev/null
+++ b/cvp_checks/utils/__init__.pyc
Binary files differ
diff --git a/docs/__init__.py b/docs/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/docs/__init__.py
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..396c0b8
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,3 @@
+pytest==3.0.6
+flake8
+PyYAML
diff --git a/setup.py b/setup.py
new file mode 100755
index 0000000..8d8f66d
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+
+from setuptools import setup, find_packages
+
+
+with open('README.md') as f:
+    readme = f.read()
+
+with open('LICENSE') as f:
+    license = f.read()
+
+setup(
+    name='cvp-sanity',
+    version='0.1',
+    description='set of tests for MCP verification',
+    long_description=readme,
+    author='Mirantis',
+    license=license,
+    packages=find_packages(exclude=('tests', 'docs'))
+)