Add testrail_upload_suite tool

Change-Id: Id3055c83225d0dcd69015658f05bd9ce07e8d88d
Related-prod: PRODX-942
diff --git a/testrail_upload_suites/README.md b/testrail_upload_suites/README.md
new file mode 100644
index 0000000..12b6619
--- /dev/null
+++ b/testrail_upload_suites/README.md
@@ -0,0 +1,78 @@
+# testrail_upload_suites
+## How to Description:
+
+1. Clone the repository `testrail_upload_suites`
+
+2. Install python 3.6 virtualenv:
+
+    ~~~~
+    apt install python3.6-venv
+    ~~~~
+
+3. Navigate to the `testrail_upload_suites/` folder:
+   ~~~
+   cd testrail_upload_suites/
+   ~~~
+
+4. Create virtualenv and activate this:
+
+    ~~~~
+    virtualenv --python=python3.6 .venv
+    . .venv/bin/activate
+    ~~~~
+
+5. In the `testrail_upload_suites` folder install requirements:
+
+    ~~~~
+    pip3 install -r requirements.txt
+    ~~~~
+
+6. In the environment set bash variables
+    ```bash
+    export TESTRAIL_URL='https://mirantis.testrail.com'
+    export TESTRAIL_PROJECT='Mirantis Cloud Platform'
+    export TESTRAIL_USER='you testrail username, for example sturivnyi@mirantis.com'
+    export TESTRAIL_PASSWORD='you super secret password'
+    export TESTRAIL_MILESTONE='MCP1.1 for example'
+    export TESTRAIL_SUITE='A new suite where tests will be uploaded'
+    ```
+
+7. Edit `SECTION_MAP` in the `config.py` file:
+   Comment sections that you do not want to be uploaded to the Testrail Suites.
+   There is `# "Barbican": ["barbican_tempest_plugin."]` section is commented.
+    ```python
+     SECTIONS_MAP = {
+        "Telemetry": ["telemetry_tempest_plugin."],
+        "Glance": ["image."],
+        "Keystone": ["identity."],
+        "Neutron": ["network."],
+        "Nova": ["compute."],
+        "Swift": ["object_storage."],
+        "Scenario": ["tempest.scenario."],
+        "Manila": ["manila_tempest_tests."],
+        "Ironic": ["ironic_tempest_plugin."],
+        "Heat": ["heat_tempest_plugin."],
+        "Designate": ["designate_tempest_plugin."],
+        # "Barbican": ["barbican_tempest_plugin."],
+        "Horizon": ["tempest_horizon."]
+     }
+    ```
+
+8. Prepare file with the tests for upload.
+   For example, `tests_for_upload.txt` the file with the tests that go one by one.
+
+    ```
+    ...
+    tempest.api.compute.servers.test_server_actions.ServerActionsTestJSON.test_reboot_server_hard
+    tempest.api.compute.servers.test_create_server.ServersTestJSON.test_list_servers
+    tempest.api.compute.servers.test_create_server.ServersTestJSON.test_verify_server_details
+    tempest.api.compute.test_versions.TestVersions.test_get_version_details
+    tempest.api.compute.test_versions.TestVersions.test_list_api_versions
+    ...
+    ```
+
+9. Upload thests to the testrail. In this example the command will be:
+
+    ~~~
+    python upload_suite.py tests_for_upload.txt
+    ~~~
diff --git a/testrail_upload_suites/base.py b/testrail_upload_suites/base.py
new file mode 100644
index 0000000..c82c9b4
--- /dev/null
+++ b/testrail_upload_suites/base.py
@@ -0,0 +1,221 @@
+from testrail import *
+import config
+
+
+class Base:
+    def __init__(self):
+        self.client = APIClient(config.URL)
+        self.client.user = config.USER
+        self.client.password = config.PASSWORD
+        self.project = self._get_project(config.PROJECT)
+
+    def _get_project(self, project_name):
+        projects_uri = 'get_projects'
+        projects = self.client.send_get(uri=projects_uri)
+        for project in projects:
+            if project['name'] == project_name:
+                return project
+        return None
+
+    def send_post_add_result(self, some_id, bug, status_id, add_result):
+        add_result['status_id'] = status_id
+        add_result['custom_launchpad_bug'] = bug
+        send_add_result = 'add_result/' + str(some_id)
+        return self.client.send_post(send_add_result, add_result)
+
+    def get_plans(self, project_id):  # !
+        return self.client.send_get('get_plans/{0}'.format(project_id))
+
+    def get_plan(self, plan_id):  # !
+        return self.client.send_get('get_plan/{0}'.format(plan_id))
+
+    def is_test_plan_exist(self, test_plan_name):
+        runs = self.get_plans(self.project['id'])
+        if True in map(lambda item: item['name'] == test_plan_name, runs):
+            return True
+        return False
+
+    def get_tests(self, plan_id):  # !
+        return self.client.send_get('get_tests/{0}'.format(plan_id))
+
+    def get_test_runs(self, plan_id, pattern=None):
+        plans_runs = self.get_plan(plan_id)  # !get_plans
+        runs = []
+        for run in plans_runs['entries']:
+            if pattern:
+                if pattern in run['name']:
+                    runs.append(run)
+            else:
+                runs.append(run)
+        return runs
+
+    def get_tempest_runs(self, plan_id):
+        runs = self.get_plan(plan_id)  # !get_plans
+        tempest_runs = []
+        for run in runs['entries']:
+            if 'Tempest' in run['name']:
+                tempest_runs.append(run)
+        return tempest_runs
+
+    def get_id_of_failed_tests(self, run_id):  # !
+        all_tests = self.get_tests(run_id)
+        test_ids = []
+        for test in all_tests:
+            if test['status_id'] == 5:
+                test_ids.append(test['id'])
+        return test_ids
+
+    def get_test_result(self, test_id):
+        return self.client.send_get('get_results/{0}'.format(test_id))
+
+    def get_test_results_for_run(self, run_id):
+        return self.client.send_get('get_results_for_run/{0}'.format(run_id))
+
+    def get_results_for_case(self, run_id, case_id):
+        return self.client.send_get('get_results_for_case/{0}/{1}'.
+                                    format(run_id, case_id))
+
+    def get_test(self, test_id):
+        return self.client.send_get('get_test/{0}'.format(test_id))
+
+    def get_runs(self, run_id):
+        return self.client.send_get('get_runs/{0}'.format(run_id))
+
+    def get_run(self, run_id):
+        return self.client.send_get('get_run/{0}'.format(run_id))
+
+    def get_milestones(self):
+        milestones_uri = 'get_milestones/{project_id}'.format(
+            project_id=self.project['id'])
+        return self.client.send_get(uri=milestones_uri)
+
+    def get_milestone(self, milestone_id):
+        milestone_uri = 'get_milestone/{milestone_id}'.format(
+            milestone_id=milestone_id)
+        return self.client.send_get(uri=milestone_uri)
+
+    def get_milestone_by_name(self, name):
+        for milestone in self.get_milestones():
+            if milestone['name'] == name:
+                return self.get_milestone(milestone_id=milestone['id'])
+
+    def add_plan(self, name, description, milestone_id, entries):
+        add_plan_uri = 'add_plan/{project_id}'.format(
+            project_id=self.project['id'])
+        new_plan = {
+            'name': name,
+            'description': description,
+            'milestone_id': milestone_id,
+            'entries': entries  # entries=[]
+        }
+        return self.client.send_post(add_plan_uri, new_plan)
+
+    def add_plan_entry(self, project_id, new_run):
+        add_plan_uri = 'add_plan_entry/{project_id}'.format(
+            project_id=project_id)
+        return self.client.send_post(add_plan_uri, new_run)
+
+    def get_suites(self):
+        suites_uri = 'get_suites/{project_id}'.format(
+            project_id=self.project['id'])
+        return self.client.send_get(uri=suites_uri)
+
+    def get_suite(self, suite_id):
+        suite_uri = 'get_suite/{suite_id}'.format(suite_id=suite_id)
+        return self.client.send_get(uri=suite_uri)
+
+    def get_suite_by_name(self, name):
+        for suite in self.get_suites():
+            if suite['name'] == name:
+                return self.get_suite(suite_id=suite['id'])
+
+    def get_plan_by_name(self, name):
+        for plan in self.get_plans(13):
+            if plan['name'] == name:
+                return self.get_plan(plan['id'])
+
+    def add_result(self, test_id, result_to_add):
+        return self.client.send_post('add_result/{0}'.format(test_id['id']),
+                                     result_to_add)
+
+    def add_suite(self, name, description=None):
+        return self.client.send_post('add_suite/' + str(self.project['id']),
+                                     dict(name=name, description=description))
+
+    def get_sections(self, suite_id):
+        sections_uri = 'get_sections/{project_id}&suite_id={suite_id}'.format(
+            project_id=self.project['id'],
+            suite_id=suite_id
+        )
+        return self.client.send_get(sections_uri)
+
+    def get_section(self, section_id):
+        section_uri = 'get_section/{section_id}'.format(section_id=section_id)
+        return self.client.send_get(section_uri)
+
+    def get_section_by_name(self, suite_id, section_name):
+        for section in self.get_sections(suite_id=suite_id):
+            if section['name'] == section_name:
+                return self.get_section(section_id=section['id'])
+
+    def add_section(self, suite_id, name, parent_id=None):
+        return self.client.send_post('add_section/' + str(self.project['id']),
+                                     dict(suite_id=suite_id, name=name,
+                                          parent_id=parent_id))
+
+    def delete_section(self, section_id):
+        # Not working bug in testrail
+        section = self.get_section(section_id)
+        print('SECTION', section)
+        try:
+            deleted = self.client.send_post('delete_section/{}'.format(section_id), section)
+            print('DELETED', deleted)
+        except Exception:
+            pass
+        return
+
+    def add_case(self, section_id, case):
+        add_case_uri = 'add_case/{section_id}'.format(section_id=section_id)
+        return self.client.send_post(add_case_uri, case)
+
+    @staticmethod
+    def prepare_common_results(tests, status_id):
+        results = {"results": []}
+
+        for test in tests:
+            results["results"].append({
+                "test_id": test['id'],
+                "status_id": status_id,
+                "comment": 'Deploy failed',
+            })
+        return results
+
+    @staticmethod
+    def get_result_by_name():
+        result = config.RESULT
+        if result == 'Blocked':
+            return 2
+        elif result == 'Passed':
+            return 1
+        elif result == 'Failed':
+            return 5
+        elif result == 'ProdFailed':
+            return 8
+        elif result == 'Skipped':
+            return 6
+
+    @staticmethod
+    def get_id_of_tempest_runs(tempest_runs):
+        tempest_runs_ids = {}  # []
+        for i in tempest_runs:
+            for item in i['runs']:
+                tempest_runs_ids.update({item['id']: item['name']})
+        return tempest_runs_ids
+
+    @staticmethod
+    def get_last_tempest_run(get_plans):
+        for plans in get_plans:
+            # print dict
+            if (plans.get(u'passed_count') > 1000 or plans.get(
+                    u'blocked_count') > 1000)and '9.1' in plans.get(u'name'):
+                return plans.get(u'id')
diff --git a/testrail_upload_suites/config.py b/testrail_upload_suites/config.py
new file mode 100644
index 0000000..be5fdb4
--- /dev/null
+++ b/testrail_upload_suites/config.py
@@ -0,0 +1,44 @@
+import os
+
+URL = os.environ.get('TESTRAIL_URL')
+USER = os.environ.get('TESTRAIL_USER')
+PROJECT = os.environ.get('TESTRAIL_PROJECT')
+PASSWORD = os.environ.get('TESTRAIL_PASSWORD')
+
+MILESTONE = os.environ.get('TESTRAIL_MILESTONE')
+SUITE = os.environ.get('TESTRAIL_SUITE')
+PLAN_NAME = os.environ.get('TESTRAIL_PLAN_NAME')
+RESULT = os.environ.get('TESTRAIL_RESULT')
+
+
+# Use test IDs for titles of TestRail test cases like
+# 'tempest.api.identity.admin.v2.test_rolesRolesTestJSON.test_list_roles[id-
+# 75d9593f-50b7-4fcf-bd64-e3fb4a278e23]' instead of test names.
+USE_TEST_IDs = True
+
+TEST_CASE_TYPE_ID = 1  # Automated
+TEST_CASE_PRIORITY_ID = 4  # P0
+QA_TEAM = 4  # MOS
+DELETE_OLD_SECTIONS = False  # User should have proper permissions to do it
+UPLOAD_THREADS_COUNT = 4
+
+SECTIONS_MAP = {
+     "Telemetry": ["telemetry_tempest_plugin."],
+     "Glance": ["image."],
+     "Keystone": ["identity."],
+     "Neutron": ["network."],
+     "Nova": ["compute."],
+     "Swift": ["object_storage."],
+     "Scenario": ["tempest.scenario."],
+     "Manila": ["manila_tempest_tests."],
+     "Ironic": ["ironic_tempest_plugin."],
+     "Heat": ["heat_tempest_plugin."],
+     "Designate": ["designate_tempest_plugin."],
+     "Barbican": ["barbican_tempest_plugin."],
+     "Horizon": ["tempest_horizon."]
+}
+
+# Logging
+LOGGER = 'upload_suite'
+LOG_FOLDER = '/tmp/'
+LOG_FILENAME = 'upload_suite.log'
diff --git a/testrail_upload_suites/requirements.txt b/testrail_upload_suites/requirements.txt
new file mode 100644
index 0000000..e000c3a
--- /dev/null
+++ b/testrail_upload_suites/requirements.txt
@@ -0,0 +1 @@
+requests>=2.22.0
\ No newline at end of file
diff --git a/testrail_upload_suites/testrail.py b/testrail_upload_suites/testrail.py
new file mode 100644
index 0000000..a2c6523
--- /dev/null
+++ b/testrail_upload_suites/testrail.py
@@ -0,0 +1,104 @@
+#
+# TestRail API binding for Python 3.x (API v2, available since
+# TestRail 3.0)
+# Compatible with TestRail 3.0 and later.
+#
+# Learn more:
+#
+# http://docs.gurock.com/testrail-api2/start
+# http://docs.gurock.com/testrail-api2/accessing
+#
+# Copyright Gurock Software GmbH. See license.md for details.
+#
+
+import requests
+import json
+import base64
+
+
+class APIClient:
+    def __init__(self, base_url):
+        self.user = ''
+        self.password = ''
+        if not base_url.endswith('/'):
+            base_url += '/'
+        self.__url = base_url + 'index.php?/api/v2/'
+
+    #
+    # Send Get
+    #
+    # Issues a GET request (read) against the API and returns the result
+    # (as Python dict) or filepath if successful file download
+    #
+    # Arguments:
+    #
+    # uri                 The API method to call including parameters
+    #                     (e.g. get_case/1)
+    #
+    # filepath            The path and file name for attachment download
+    #                     Used only for 'get_attachment/:attachment_id'
+    #
+    def send_get(self, uri, filepath=None):
+        return self.__send_request('GET', uri, filepath)
+
+    #
+    # Send POST
+    #
+    # Issues a POST request (write) against the API and returns the result
+    # (as Python dict).
+    #
+    # Arguments:
+    #
+    # uri                 The API method to call including parameters
+    #                     (e.g. add_case/1)
+    # data                The data to submit as part of the request (as
+    #                     Python dict, strings must be UTF-8 encoded)
+    #                     If adding an attachment, must be the path
+    #                     to the file
+    #
+    def send_post(self, uri, data):
+        return self.__send_request('POST', uri, data)
+
+    def __send_request(self, method, uri, data):
+        url = self.__url + uri
+
+        auth = str(
+            base64.b64encode(
+                bytes('%s:%s' % (self.user, self.password), 'utf-8')
+            ),
+            'ascii'
+        ).strip()
+        headers = {'Authorization': 'Basic ' + auth}
+
+        if method == 'POST':
+            if uri[:14] == 'add_attachment':    # add_attachment API method
+                files = {'attachment': (open(data, 'rb'))}
+                response = requests.post(url, headers=headers, files=files)
+                files['attachment'].close()
+            else:
+                headers['Content-Type'] = 'application/json'
+                payload = bytes(json.dumps(data), 'utf-8')
+                response = requests.post(url, headers=headers, data=payload)
+        else:
+            headers['Content-Type'] = 'application/json'
+            response = requests.get(url, headers=headers)
+
+        if response.status_code > 201:
+            try:
+                error = response.json()
+            except:     # response.content not formatted as JSON
+                error = str(response.content)
+            raise APIError('TestRail API returned HTTP %s (%s)' % (response.status_code, error))
+        else:
+            if uri[:15] == 'get_attachment/':   # Expecting file, not JSON
+                try:
+                    open(data, 'wb').write(response.content)
+                    return (data)
+                except:
+                    return ("Error saving attachment.")
+            else:
+                return response.json()
+
+
+class APIError(Exception):
+    pass
\ No newline at end of file
diff --git a/testrail_upload_suites/tests_for_upload.txt b/testrail_upload_suites/tests_for_upload.txt
new file mode 100644
index 0000000..73ff016
--- /dev/null
+++ b/testrail_upload_suites/tests_for_upload.txt
@@ -0,0 +1,110 @@
+tempest.api.compute.flavors.test_flavors.FlavorsV2TestJSON.test_get_flavor
+tempest.api.compute.flavors.test_flavors.FlavorsV2TestJSON.test_list_flavors
+tempest.api.compute.security_groups.test_security_group_rules.SecurityGroupRulesTestJSON.test_security_group_rules_create
+tempest.api.compute.security_groups.test_security_group_rules.SecurityGroupRulesTestJSON.test_security_group_rules_list
+tempest.api.compute.security_groups.test_security_groups.SecurityGroupsTestJSON.test_security_groups_create_list_delete
+tempest.api.compute.servers.test_attach_interfaces.AttachInterfacesTestJSON.test_add_remove_fixed_ip
+tempest.api.compute.servers.test_create_server.ServersTestBootFromVolume.test_list_servers
+tempest.api.compute.servers.test_create_server.ServersTestBootFromVolume.test_verify_server_details
+tempest.api.compute.servers.test_server_actions.ServerActionsTestJSON.test_reboot_server_hard
+tempest.api.compute.servers.test_create_server.ServersTestJSON.test_list_servers
+tempest.api.compute.servers.test_create_server.ServersTestJSON.test_verify_server_details
+tempest.api.compute.test_versions.TestVersions.test_get_version_details
+tempest.api.compute.test_versions.TestVersions.test_list_api_versions
+tempest.api.identity.admin.v3.test_credentials.CredentialsTestJSON.test_credentials_create_get_update_delete
+tempest.api.identity.admin.v3.test_domains.DefaultDomainTestJSON.test_default_domain_exists
+tempest.api.identity.admin.v3.test_domains.DomainsTestJSON.test_create_update_delete_domain
+tempest.api.identity.admin.v3.test_endpoints.EndPointsTestJSON.test_update_endpoint
+tempest.api.compute.servers.test_create_server.ServersTestManualDisk.test_list_servers
+tempest.api.compute.servers.test_create_server.ServersTestManualDisk.test_verify_server_details
+tempest.api.identity.admin.v3.test_groups.GroupsV3TestJSON.test_group_users_add_list_delete
+tempest.api.identity.admin.v3.test_regions.RegionsTestJSON.test_create_region_with_specific_id
+tempest.api.identity.admin.v3.test_services.ServicesTestJSON.test_create_update_get_service
+tempest.api.image.v2.test_images.BasicOperationsImagesTest.test_delete_image
+tempest.api.image.v2.test_images.BasicOperationsImagesTest.test_register_upload_get_image_file
+tempest.api.image.v2.test_images.BasicOperationsImagesTest.test_update_image
+tempest.api.image.v2.test_versions.VersionsTest.test_list_versions
+tempest.api.compute.servers.test_server_addresses.ServerAddressesTestJSON.test_list_server_addresses
+tempest.api.compute.servers.test_server_addresses.ServerAddressesTestJSON.test_list_server_addresses_by_network
+tempest.api.network.test_extensions.ExtensionsTestJSON.test_list_show_extensions
+tempest.api.network.test_networks.BulkNetworkOpsTest.test_bulk_create_delete_network
+tempest.api.identity.admin.v3.test_policies.PoliciesTestJSON.test_create_update_delete_policy
+tempest.api.network.test_networks.BulkNetworkOpsTest.test_bulk_create_delete_port
+tempest.api.identity.admin.v3.test_roles.RolesV3TestJSON.test_role_create_update_show_list
+tempest.api.network.test_networks.BulkNetworkOpsTest.test_bulk_create_delete_subnet
+tempest.api.identity.admin.v3.test_trusts.TrustsV3TestJSON.test_get_trusts_all
+tempest.api.identity.v3.test_api_discovery.TestApiDiscovery.test_api_media_types
+tempest.api.identity.v3.test_api_discovery.TestApiDiscovery.test_api_version_resources
+tempest.api.identity.v3.test_api_discovery.TestApiDiscovery.test_api_version_statuses
+tempest.api.identity.v3.test_api_discovery.TestApiDiscovery.test_list_api_versions
+tempest.api.network.test_networks.NetworksIpV6Test.test_create_update_delete_network_subnet
+tempest.api.network.test_networks.NetworksIpV6Test.test_external_network_visibility
+tempest.api.network.test_networks.NetworksIpV6Test.test_list_networks
+tempest.api.network.test_networks.NetworksIpV6Test.test_list_subnets
+tempest.api.network.test_networks.NetworksIpV6Test.test_show_network
+tempest.api.network.test_networks.NetworksIpV6Test.test_show_subnet
+tempest.api.network.test_floating_ips.FloatingIPTestJSON.test_create_floating_ip_specifying_a_fixed_ip_address
+tempest.api.network.test_ports.PortsTestJSON.test_create_port_in_allowed_allocation_pools
+tempest.api.network.test_floating_ips.FloatingIPTestJSON.test_create_list_show_update_delete_floating_ip
+tempest.api.network.test_ports.PortsTestJSON.test_create_port_with_no_securitygroups
+tempest.api.network.test_ports.PortsTestJSON.test_create_update_delete_port
+tempest.api.network.test_ports.PortsTestJSON.test_list_ports
+tempest.api.network.test_ports.PortsTestJSON.test_show_port
+tempest.api.network.test_networks.BulkNetworkOpsIpV6Test.test_bulk_create_delete_network
+tempest.api.network.test_networks.BulkNetworkOpsIpV6Test.test_bulk_create_delete_port
+tempest.api.network.test_routers.RoutersIpV6Test.test_add_multiple_router_interfaces
+tempest.api.network.test_networks.BulkNetworkOpsIpV6Test.test_bulk_create_delete_subnet
+tempest.api.network.test_routers.RoutersIpV6Test.test_add_remove_router_interface_with_port_id
+tempest.api.network.test_networks.NetworksTest.test_create_update_delete_network_subnet
+tempest.api.network.test_routers.RoutersIpV6Test.test_add_remove_router_interface_with_subnet_id
+tempest.api.network.test_networks.NetworksTest.test_external_network_visibility
+tempest.api.network.test_networks.NetworksTest.test_list_networks
+tempest.api.network.test_networks.NetworksTest.test_list_subnets
+tempest.api.network.test_networks.NetworksTest.test_show_network
+tempest.api.network.test_networks.NetworksTest.test_show_subnet
+tempest.api.network.test_routers.RoutersIpV6Test.test_create_show_list_update_delete_router
+tempest.api.network.test_ports.PortsIpV6TestJSON.test_create_port_in_allowed_allocation_pools
+tempest.api.network.test_ports.PortsIpV6TestJSON.test_create_port_with_no_securitygroups
+tempest.api.network.test_ports.PortsIpV6TestJSON.test_create_update_delete_port
+tempest.api.network.test_ports.PortsIpV6TestJSON.test_list_ports
+tempest.api.network.test_ports.PortsIpV6TestJSON.test_show_port
+tempest.api.network.test_security_groups.SecGroupIPv6Test.test_create_list_update_show_delete_security_group
+tempest.api.network.test_security_groups.SecGroupIPv6Test.test_create_show_delete_security_group_rule
+tempest.api.network.test_security_groups.SecGroupIPv6Test.test_list_security_groups
+tempest.api.network.test_security_groups.SecGroupTest.test_create_list_update_show_delete_security_group
+tempest.api.network.test_security_groups.SecGroupTest.test_create_show_delete_security_group_rule
+tempest.api.network.test_security_groups.SecGroupTest.test_list_security_groups
+tempest.api.network.test_routers.RoutersTest.test_add_multiple_router_interfaces
+tempest.api.network.test_subnetpools_extensions.SubnetPoolsTestJSON.test_create_list_show_update_delete_subnetpools
+tempest.api.network.test_versions.NetworksApiDiscovery.test_api_version_resources
+tempest.api.network.test_routers.RoutersTest.test_add_remove_router_interface_with_port_id
+tempest.api.volume.test_volumes_get.VolumesGetTest.test_volume_create_get_update_delete
+tempest.api.network.test_routers.RoutersTest.test_add_remove_router_interface_with_subnet_id
+tempest.api.network.test_routers.RoutersTest.test_create_show_list_update_delete_router
+tempest.api.volume.test_volumes_get.VolumesGetTest.test_volume_create_get_update_delete_from_image
+tempest.api.volume.test_volumes_list.VolumesListTestJSON.test_volume_list
+tempest.api.volume.test_versions.VersionsTest.test_list_versions
+heat_tempest_plugin.tests.api.test_heat_api.stacks_stack_list_smoke.test_request
+heat_tempest_plugin.tests.api.test_heat_api.stacks_create_empty_stack_smoke.test_request
+heat_tempest_plugin.tests.api.test_heat_api.stacks_delete_empty_stack_smoke.test_request
+heat_tempest_plugin.tests.api.test_heat_api.stacks_poll_for_stack_update_complete_smoke.test_request
+tempest.api.volume.test_volumes_actions.VolumesActionsTest.test_attach_detach_volume_to_instance
+heat_tempest_plugin.tests.api.test_heat_api.stacks_poll_for_stack_patch_update_complete_smoke.test_request
+heat_tempest_plugin.tests.api.test_heat_api.stacks_get_stack_output_smoke.test_request
+heat_tempest_plugin.tests.api.test_heat_api.stacks_delete_stack_smoke.test_request
+designate_tempest_plugin.tests.api.v2.test_zones.ZonesTest.test_delete_zone
+designate_tempest_plugin.tests.api.v2.test_zones_exports.ZonesExportTest.test_show_zone_export
+designate_tempest_plugin.tests.api.v2.test_zones_imports.ZonesImportTest.test_show_zone_import
+tempest.scenario.test_network_basic_ops.TestNetworkBasicOps.test_network_basic_ops
+tempest.scenario.test_server_basic_ops.TestServerBasicOps.test_server_basic_ops
+heat_tempest_plugin.tests.api.test_heat_api.stacks_poll_for_empty_create_complete_smoke.test_request
+heat_tempest_plugin.tests.api.test_heat_api.stacks_show_empty_stack_smoke.test_request
+heat_tempest_plugin.tests.api.test_heat_api.stacks_create_stack_smoke.test_request
+heat_tempest_plugin.tests.api.test_heat_api.stacks_poll_for_stack_create_complete_smoke.test_request
+heat_tempest_plugin.tests.api.test_heat_api.stacks_show_stack_smoke.test_request
+heat_tempest_plugin.tests.api.test_heat_api.stacks_update_stack_smoke.test_request
+heat_tempest_plugin.tests.api.test_heat_api.stacks_patch_update_stack_smoke.test_request
+heat_tempest_plugin.tests.api.test_heat_api.stacks_list_stack_outputs_smoke.test_request
+tempest_horizon.tests.scenario.test_dashboard_basic_ops.TestDashboardBasicOps.test_basic_scenario
+designate_tempest_plugin.tests.api.v2.test_recordset.RecordsetsTest.test_create_recordset
+designate_tempest_plugin.tests.scenario.v2.test_zones.ZonesTest.test_create_and_delete_zone
diff --git a/testrail_upload_suites/upload_suite.py b/testrail_upload_suites/upload_suite.py
new file mode 100644
index 0000000..3cabf05
--- /dev/null
+++ b/testrail_upload_suites/upload_suite.py
@@ -0,0 +1,161 @@
+#!/usr/bin/env python
+#
+#    Copyright 2017 Mirantis, Inc.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+import os
+import logging
+import sys
+
+from base import Base
+from testrail import APIError
+
+import config
+
+
+logging.basicConfig(
+    format='[%(asctime)s][%(name)s][%(levelname)s] %(message)s',
+    datefmt='%d-%m-%Y %H:%M:%S',
+    handlers=[logging.FileHandler('{}{}'.format(
+        config.LOG_FOLDER, config.LOG_FILENAME)), logging.StreamHandler()],
+    level=logging.INFO)
+logger = logging.getLogger(config.LOGGER)
+
+
+def choose_section_by_test_name(test_name):
+    for section, key_words in config.SECTIONS_MAP.items():
+        for key_word in key_words:
+            if key_word in test_name:
+                return section
+
+    return "Other"
+
+
+def get_tags_by_test_name(test_name):
+    tags = []
+    if test_name.find("[") > -1:
+        tags = test_name.split("[")[1][:-1].split(",")
+    return tags
+
+
+def create_tr_test_cases(test_cases, milestone_id, type_id=1, priority_id=4,
+                         qa_team=4):
+    tr_test_cases = []
+
+    for test_case_name in test_cases:
+        section = choose_section_by_test_name(test_case_name)
+        if section not in config.SECTIONS_MAP:
+            config.SECTIONS_MAP[section] = []
+        test_class, test_name = test_case_name.rsplit(".", 1)
+
+        report_label = test_name
+        for tag in get_tags_by_test_name(test_name):
+            if tag.startswith("id-"):
+                report_label = tag[3:]
+                break
+
+        test_case = {
+            "milestone_id": milestone_id,
+            "section": section,
+            "title": (("%s.%s" % (test_class, test_name)) if config.USE_TEST_IDs
+                      else test_name),
+            "type_id": type_id,
+            "priority_id": priority_id,
+            "custom_qa_team": qa_team,
+            "estimate": "1m",
+            "refs": "",
+            "custom_test_group": test_class,
+            "custom_test_case_description": test_name,
+            "custom_test_case_steps": [{"Run test": "passed"}],
+            "custom_report_label": report_label
+        }
+        tr_test_cases.append(test_case)
+
+    return tr_test_cases
+
+
+def add_tr_test_case(tr_client, suite_id, tr_test_case):
+    all_tests = []
+    for i in range(7):
+        try:
+            added_test = tr_client.add_case(suite_id, tr_test_case)
+            all_tests.append(added_test)
+        except APIError:
+            logging.info("APIError")
+        else:
+            break
+    return all_tests
+
+
+def main():
+    call = Base()
+    try:
+        tests_file_path = sys.argv[1]
+    except IndexError:
+        raise Exception("Path to a tests file should be provided!")
+
+    if os.path.exists(tests_file_path):
+        logger.info("Reading tests file '%s'..." % tests_file_path)
+        with open(tests_file_path) as f:
+            test_cases = [test for test in f.read().split("\n") if test]
+            logger.info("Tests file '%s' has been successfully read."
+                        % tests_file_path)
+    else:
+        raise Exception("Tests file '%s' doesn't exist!" % tests_file_path)
+
+    logger.info("Initializing TestRail client...")
+    logger.info("TestRail client has been successfully initialized.")
+    logger.info("Getting milestone '%s'..." % config.MILESTONE)
+
+    milestone = call.get_milestone_by_name(config.MILESTONE)
+
+    logger.info(milestone)
+    logger.info("Getting tests suite '%s'..." % config.SUITE)
+
+    suite = call.get_suite_by_name(config.SUITE)
+    if not suite:
+        logger.info("Tests suite '%s' not found. "
+                    "Creating tests suite..." % config.SUITE)
+
+        suite = call.add_suite(config.SUITE)
+        logger.info("Tests suite has benn successfully created.")
+    logger.info(suite)
+
+    logger.info("Creating test cases for TestRail...")
+    tr_test_cases = create_tr_test_cases(
+        test_cases, milestone["id"],
+        type_id=config.TEST_CASE_TYPE_ID,
+        priority_id=config.TEST_CASE_PRIORITY_ID,
+        qa_team=config.QA_TEAM)
+    logger.info("Test cases have been successfully created.")
+
+    sections_map = {}
+    for section in sorted(config.SECTIONS_MAP.keys()):
+        logger.info("Creating section '%s'..." % section)
+        s = call.add_section(suite["id"], section)
+        logger.info("Section '%s' has been successfully created." % section)
+        sections_map[section] = s["id"]
+
+    logger.info("Uploading created test cases to TestRail...")
+
+    all_added_test_cases = []
+    for t in tr_test_cases:
+        test_cases = add_tr_test_case(call, sections_map[t["section"]], t)
+        all_added_test_cases.append(test_cases)
+
+    logger.info("Test cases have been successfully uploaded.")
+
+
+if __name__ == "__main__":
+    main()