| # Copyright 2018 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 json |
| import os |
| |
| from devops.helpers import helpers |
| |
| from tcp_tests import logger |
| from tcp_tests import settings |
| |
| |
| LOG = logger.logger |
| |
| TEMPEST_CFG_DIR = '/tmp/test' |
| |
| |
| class RuntestManager(object): |
| """Helper manager for execution tempest via runtest-formula""" |
| |
| image_name = settings.TEMPEST_IMAGE |
| image_version = settings.TEMPEST_IMAGE_VERSION |
| container_name = 'run-tempest-ci' |
| master_host = "cfg01" |
| control_host = "ctl01" |
| compute_host = "cmp" |
| class_name = "runtest" |
| run_cmd = '/bin/bash -c "run-tempest"' |
| |
| def __init__(self, config, underlay, salt_api, cluster_name, |
| domain_name, tempest_threads, |
| tempest_pattern=settings.TEMPEST_PATTERN, |
| run_cmd=None, target='gtw01'): |
| self.__config = config |
| self.underlay = underlay |
| self.__salt_api = salt_api |
| self.cluster_name = cluster_name |
| self.domain_name = domain_name |
| self.tempest_threads = tempest_threads |
| self.tempest_pattern = tempest_pattern |
| self.run_cmd = run_cmd or self.run_cmd |
| self.target_name = self.underlay.get_target_node_names(target)[0] |
| self.master_name = self.underlay.get_target_node_names( |
| self.master_host)[0] |
| self.control_name = self.underlay.get_target_node_names( |
| self.control_host)[0] |
| self.compute_name = self.underlay.get_target_node_names( |
| self.compute_host)[0] |
| |
| @property |
| def salt_api(self): |
| return self.__salt_api |
| |
| @property |
| def runtest_pillar(self): |
| public_net = self.__config.underlay.dhcp_ranges[ |
| settings.EXTERNAL_ADDRESS_POOL_NAME] |
| public_gateway = public_net["gateway"].encode("ascii") |
| public_cidr = public_net["cidr"].encode("ascii") |
| public_allocation_start = public_net["start"].encode("ascii") |
| public_allocation_end = public_net["end"].encode("ascii") |
| |
| return { |
| 'classes': ['service.runtest.tempest', |
| 'service.runtest.tempest.public_net', |
| 'service.runtest.tempest.services.manila.glance'], |
| 'parameters': { |
| '_param': { |
| 'runtest_tempest_cfg_dir': TEMPEST_CFG_DIR, |
| 'runtest_tempest_cfg_name': 'tempest.conf', |
| 'runtest_tempest_public_net': 'public', |
| 'openstack_public_neutron_subnet_gateway': public_gateway, |
| 'openstack_public_neutron_subnet_cidr': public_cidr, |
| 'openstack_public_neutron_subnet_allocation_start': |
| public_allocation_start, |
| 'openstack_public_neutron_subnet_allocation_end': |
| public_allocation_end, |
| 'tempest_test_target': self.target_name.encode("ascii"), |
| }, |
| 'neutron': { |
| 'client': { |
| 'enabled': True |
| } |
| }, |
| 'runtest': { |
| 'enabled': True, |
| 'keystonerc_node': 'ctl01*', |
| 'tempest': { |
| 'enabled': True, |
| 'cfg_dir': '${_param:runtest_tempest_cfg_dir}', |
| 'cfg_name': '${_param:runtest_tempest_cfg_name}', |
| 'put_keystone_rc_enabled': True, |
| 'put_local_image_file_enabled': False, |
| 'DEFAULT': { |
| 'log_file': 'tempest.log' |
| }, |
| 'compute': { |
| 'min_compute_nodes': 2, |
| }, |
| 'convert_to_uuid': { |
| 'network': { |
| 'public_network_id': |
| '${_param:runtest_tempest_public_net}' |
| } |
| }, |
| 'heat_plugin': { |
| 'build_timeout': '600' |
| }, |
| 'share': { |
| 'capability_snapshot_support': True, |
| 'run_driver_assisted_migration_tests': False, |
| 'run_manage_unmanage_snapshot_tests': False, |
| 'run_manage_unmanage_tests': False, |
| 'run_migration_with_preserve_snapshots_tests': |
| False, |
| 'run_quota_tests': True, |
| 'run_replication_tests': False, |
| 'run_snapshot_tests': True, |
| }}}}} |
| |
| def fetch_arficats(self, username=None, file_format='xml'): |
| with self.underlay.remote(node_name=self.target_name, |
| username=None) as tgt: |
| result = tgt.execute('find {} -name "report_*.{}"'.format( |
| TEMPEST_CFG_DIR, file_format)) |
| LOG.debug("Find result {0}".format(result)) |
| assert len(result['stdout']) > 0, ('No report found, please check' |
| ' if test run was successful.') |
| report = result['stdout'][0].rstrip() |
| LOG.debug("Found files {0}".format(report)) |
| tgt.download( |
| destination=report, # noqa |
| target=os.getcwd()) |
| |
| def store_runtest_model(self, runtest_pillar=None): |
| with self.underlay.yaml_editor( |
| file_path="/srv/salt/reclass/classes/cluster/" |
| "{cluster_name}/infra/" |
| "{class_name}.yml".format( |
| cluster_name=self.cluster_name, |
| class_name=self.class_name), |
| node_name=self.master_name) as editor: |
| editor.content = runtest_pillar or self.runtest_pillar |
| with self.underlay.yaml_editor( |
| file_path="/srv/salt/reclass/nodes/_generated/" |
| "cfg01.{domain_name}.yml".format( |
| domain_name=self.domain_name), |
| node_name=self.master_name) as editor: |
| editor.content['classes'].append( |
| 'cluster.{cluster_name}.infra.{class_name}'.format( |
| cluster_name=self.cluster_name, |
| class_name=self.class_name)) |
| |
| def save_runtime_logs(self, logs=None, inspect=None): |
| if logs: |
| with open("{path}/{target}_tempest_run.log".format( |
| path=settings.LOGS_DIR, |
| target=self.target_name), 'w') as f: |
| LOG.info("Save tempest console log") |
| container_log = logs |
| f.write(container_log.encode('ascii', 'ignore')) |
| |
| if inspect: |
| with open("{path}/{target}_tempest_container_info.json.log".format( |
| path=settings.LOGS_DIR, |
| target=self.target_name), 'w') as f: |
| LOG.info("Save tempest container inspect data") |
| |
| container_inspect = json.dumps(inspect, |
| indent=4, sort_keys=True) |
| f.write(container_inspect) |
| |
| def prepare(self): |
| self.store_runtest_model() |
| cirros_pillar = ("salt-call --out=newline_values_only " |
| "pillar.get " |
| "glance:client:identity:" |
| "admin_identity:image:cirros:location") |
| dpdk_pillar = "linux:network:dpdk:enabled" |
| salt_cmd = "salt -l info --hard-crash --state-output=mixed " |
| salt_call_cmd = "salt-call -l info --hard-crash --state-output=mixed " |
| |
| result = self.__salt_api.get_pillar(tgt=self.compute_name, |
| pillar=dpdk_pillar) |
| |
| dpdk = result[0].get(self.compute_name, False) |
| LOG.info("DPDK enabled: {}".format(bool(dpdk))) |
| |
| commands = [ |
| { |
| 'description': "Sync salt objects for runtest model", |
| 'node_name': self.master_name, |
| 'cmd': ("set -ex;" + |
| salt_cmd + "'*' saltutil.refresh_pillar && " + |
| salt_cmd + "'*' saltutil.sync_all")}, |
| { |
| 'description': ("Install docker.io package and " |
| "enable packets forwarding"), |
| 'node_name': self.target_name, |
| 'cmd': ("set -ex;" + |
| salt_call_cmd + " pkg.install docker.io && " + |
| " iptables --policy FORWARD ACCEPT")}, |
| { |
| 'description': "Install PyPI docker package", |
| 'node_name': self.target_name, |
| 'cmd': ("set -ex;" + |
| salt_call_cmd + " pip.install setuptools && " + |
| salt_call_cmd + " pip.install docker")}, |
| { |
| 'description': "Run salt.minion state for runtest formula", |
| 'node_name': self.master_name, |
| 'cmd': ("set -ex;" + |
| salt_call_cmd + " state.sls salt.minion && " |
| " sleep 20")}, |
| { |
| 'description': "Enforce keystone state for neutronv2", |
| 'node_name': self.master_name, |
| 'cmd': ("set -ex;" + |
| salt_call_cmd + " state.sls keystone.client")}, |
| { |
| 'description': "Create networks for Tempest tests", |
| 'node_name': self.master_name, |
| 'cmd': ("set -ex;" + |
| salt_call_cmd + " state.sls neutron.client")}, |
| { |
| 'description': "Create flavors for Tempest tests", |
| 'node_name': self.master_name, |
| 'cmd': ("set -ex;" + |
| salt_call_cmd + " state.sls nova.client")}, |
| { |
| 'description': "Upload images for Tempest", |
| 'node_name': self.master_name, |
| 'cmd': ("set -ex;" + |
| salt_call_cmd + " state.sls glance.client")}, |
| { |
| 'description': "Generate config for Tempest", |
| 'node_name': self.master_name, |
| 'cmd': ("set -ex;" + |
| salt_call_cmd + " state.sls runtest")}, |
| { |
| 'description': "Upload cirros image", |
| 'node_name': self.master_name, |
| 'cmd': ("set -ex;" |
| "cirros_url=$({}) && {} '{}' cmd.run " |
| "\"wget $cirros_url -O /tmp/TestCirros-0.3.5.img\"" |
| .format(cirros_pillar, salt_cmd, self.target_name))}, |
| ] |
| |
| if dpdk: |
| commands.append({ |
| 'description': "Configure flavor for DPDK", |
| 'node_name': self.control_name, |
| 'cmd': ("set -ex;" + |
| salt_call_cmd + " cmd.run " |
| " '. /root/keystonercv3;" |
| " openstack flavor set m1.extra_tiny_test" |
| " --property hw:mem_page_size=any;" |
| " openstack flavor set m1.tiny_test" |
| " --property hw:mem_page_size=any'")}, |
| ) |
| |
| self.__salt_api.execute_commands(commands=commands, |
| label="Prepare for Tempest") |
| |
| def run_tempest(self, timeout=600): |
| tgt = self.target_name |
| image_nameversion = "{}:{}".format(self.image_name, self.image_version) |
| |
| docker_args = ( |
| " -t " |
| " --name {container_name} " |
| " -e ARGS=\"-r {tempest_pattern} -w {tempest_threads}\"" |
| " -v {cfg_dir}/tempest.conf:/etc/tempest/tempest.conf" |
| " -v /tmp/:/tmp/" |
| " -v {cfg_dir}:/root/tempest" |
| " -v /etc/ssl/certs/:/etc/ssl/certs/" |
| " -d " |
| " {image_nameversion} {run_cmd}" |
| .format( |
| container_name=self.container_name, |
| image_nameversion=image_nameversion, |
| tempest_pattern=self.tempest_pattern, |
| tempest_threads=self.tempest_threads, |
| cfg_dir=TEMPEST_CFG_DIR, |
| run_cmd=self.run_cmd, |
| )) |
| |
| commands = [ |
| { |
| 'description': "Run Tempest tests {0}".format( |
| image_nameversion), |
| 'node_name': self.target_name, |
| 'cmd': ("set -ex;" + |
| " docker rm --force {container_name} || true;" |
| " docker run {docker_args}" |
| .format(container_name=self.container_name, |
| docker_args=docker_args)), |
| 'timeout': timeout}, |
| ] |
| |
| self.__salt_api.execute_commands(commands=commands, |
| label="Run Tempest tests") |
| |
| def wait_status(s): |
| inspect_res = self.salt_api.local(tgt, |
| 'dockerng.inspect', |
| self.container_name) |
| if 'return' in inspect_res: |
| inspect = inspect_res['return'] |
| inspect = inspect[0] |
| inspect = next(inspect.iteritems())[1] |
| status = inspect['State']['Status'] |
| |
| return status.lower() == s.lower() |
| |
| return False |
| |
| helpers.wait(lambda: wait_status('exited'), |
| timeout=timeout, |
| timeout_msg=('Tempest run didnt finished ' |
| 'in {}'.format(timeout))) |
| |
| inspect_res = self.salt_api.local(tgt, |
| 'dockerng.inspect', |
| self.container_name) |
| inspect = inspect_res['return'][0] |
| inspect = next(inspect.iteritems())[1] |
| logs_res = self.salt_api.local(tgt, |
| 'dockerng.logs', |
| self.container_name) |
| logs = logs_res['return'][0] |
| logs = next(logs.iteritems())[1] |
| |
| res = self.salt_api.local(tgt, 'dockerng.rm', self.container_name) |
| LOG.info("Tempest container was removed".format( |
| json.dumps(res, indent=4))) |
| |
| return {'inspect': inspect, |
| 'logs': logs} |
| |
| def prepare_and_run_tempest(self, username='root'): |
| """ |
| Run tempest tests |
| """ |
| tempest_timeout = settings.TEMPEST_TIMEOUT |
| self.prepare() |
| test_res = self.run_tempest(tempest_timeout) |
| self.fetch_arficats(username=username) |
| self.save_runtime_logs(**test_res) |