diff --git a/requirements.txt b/requirements.txt index e9505d5..ed0b838 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,3 +15,5 @@ pycurl>=7.43.0 PyYAML==3.11 Pygments>=2.1.3 -e git+https://github.com/mike01/pypacker@master#egg=pypacker +python-etcd>=0.4.3 +python-k8sclient>=0.3.0 diff --git a/taf/plugins/pytest_sut_monitor.py b/taf/plugins/pytest_sut_monitor.py index 3d1d666..bee245c 100644 --- a/taf/plugins/pytest_sut_monitor.py +++ b/taf/plugins/pytest_sut_monitor.py @@ -32,7 +32,7 @@ from . import loggers from .pytest_onsenv import setup_scope -from testlib.custom_exceptions import CLISSHException, UICmdException +from testlib.custom_exceptions import UICmdException from testlib import clissh from testlib import rrdtool_graph from testlib import multicall @@ -42,12 +42,12 @@ PATH_TO_RRD = '/var/lib/collectd/rrd' -INCLUDES = {'rr'} +INCLUDES = {'rr', 'linux_host', 'generic'} SUPPORTED_GRAPHS = { 'MEMORY': 'memory', 'CPU': 'cpu', -# 'INTERFACE': 'interface-', + # 'INTERFACE': 'interface-', 'INTERFACE_BYTES': 'interface-', 'LOAD': 'load', 'DISK': 'disk-' @@ -96,12 +96,13 @@ def __init__(self, env): @param env: TAF environment instance @type env: testlib.common3.Environment """ + super().__init__() self.env = env # Initialize start and stop time values self.start_time = time.time() self.end_time = time.time() # Initialize test name value - self.test = 'Underfined' + self.test = 'Undefined' # Store collectd folders in dict self.devices = {} # Store created graphs in list @@ -197,7 +198,8 @@ def multicall(self, commands): # convert to CmdStatus objects if cmd_status.stdout: results.extend( - (result[0], CmdStatus(*result[1:])) for result in json.loads(cmd_status.stdout)) + (result[0], CmdStatus(*result[1:])) + for result in json.loads(cmd_status.stdout)) return [x[1].stdout for x in results] def copy_file(self, remote_file, local_file): @@ -209,7 +211,7 @@ def copy_file(self, remote_file, local_file): @type local_file: str """ self.class_logger.debug("Copy file {0} to the local file {1}".format(remote_file, - local_file)) + local_file)) self.server.ssh.get_file(remote_file, local_file) def configure(self): @@ -227,12 +229,14 @@ def list_rrd_folders(self): for dev_name in self.devices: collectd_folder = os.path.join(PATH_TO_RRD, dev_name) # List all RRD folder related to specific device - folders = self.exec_command('find {}/*' - ' -maxdepth 1 -type d -print0'.format(collectd_folder)).split('\0') + folders = self.exec_command( + 'find {}/*' ' -maxdepth 1 -type d -print0'.format(collectd_folder)).split('\0') # Filter folder by supported graphs # Store results in dict {graph_type: list_of_folders} - folder_dict = {_key: [x for x in folders if x.split(os.path.sep)[-1].startswith(_value)] - for _key, _value in list(SUPPORTED_GRAPHS.items())} + folder_dict = { + _key: [x for x in folders if x.split(os.path.sep)[-1].startswith(_value)] + for _key, _value in list(SUPPORTED_GRAPHS.items()) + } # Update device info self.devices[dev_name]['folders'] = folder_dict @@ -240,7 +244,8 @@ def item_teardown(self): """ @brief Create RRD graphs on test teardown """ - self.class_logger.debug("PROFILING: SutMonitor start time {}".format(time.time())) + self.class_logger.info("Generating graphs...") + self.class_logger.debug("PROFILING: SutMonitor start time %d", time.time()) # Store rrdtool commands in list commands = [] # Store graphs names in list @@ -265,17 +270,18 @@ def item_teardown(self): rrd_folder = os.path.join(PATH_TO_RRD, name, folder) # Exclude empty graphs if self.is_not_empty(rrd_folder, - int(self.start_time - time.time()), - int(self.end_time - time.time()), + int(self.start_time), + int(self.end_time), gtype): # Generate graph name as deviceName_RRDFolderName file_name = "{0}_{1}.png".format(name, folder.split(os.path.sep)[-1]) file_names.append(file_name) # Generate command for graph creation and append to commands list # Store graph on Collectd server host in /tmp/ directory - commands.append(rrdtool_graph.get_graph_command(rrd_folder, - int(self.start_time - time.time()), - int(self.end_time - time.time()), + commands.append(rrdtool_graph.get_graph_command( + rrd_folder, + int(self.start_time), + int(self.end_time), gtype=gtype, destination=os.path.join('/tmp', file_name))) # Create graphs on Collectd server host @@ -323,6 +329,7 @@ def is_not_empty(self, folder, start, end, gtype): # for res in results: # for val in res.splitlines(): # values.extend(list(map(self.convert, val.split()[1:]))) + def convert(value): try: return int(float(value)) @@ -348,6 +355,10 @@ class SutMonitorPlugin(object): @brief SutMonitorPlugin implementation. """ + def __init__(self): + super().__init__() + self.sut_monitor = None + @pytest.fixture(autouse=True, scope='session') def monitor_init(self, env_init): """ @@ -359,7 +370,7 @@ def monitor_init(self, env_init): return self.sut_monitor @pytest.fixture(scope=setup_scope(), autouse=True) - def monitor(self, request, env_main, monitor_init): + def monitor(self, request, env_main, monitor_init): # pylint: disable=W0613 """ @brief Start Collectd service on devices """ @@ -369,7 +380,7 @@ def monitor(self, request, env_main, monitor_init): return monitor_init @pytest.fixture(autouse=True) - def test_monitor(self, request, env, monitor): + def test_monitor(self, request, env, monitor): # pylint: disable=W0613 """ @brief Gather collectd info for certain test case @param request: pytest request object @@ -383,7 +394,7 @@ def test_monitor(self, request, env, monitor): request.addfinalizer(monitor.item_teardown) @pytest.hookimpl(tryfirst=True, hookwrapper=True) - def pytest_runtest_makereport(self, item, call): + def pytest_runtest_makereport(self, item, call): # pylint: disable=W0613 """ @brief Add generated graphs to the pytest report in order to access from reporting plugin """ diff --git a/taf/testlib/helpers.py b/taf/testlib/helpers.py index 8201800..1e98e88 100644 --- a/taf/testlib/helpers.py +++ b/taf/testlib/helpers.py @@ -25,6 +25,7 @@ import itertools from xmlrpc.client import Fault as XMLRPCFault from collections import OrderedDict +import functools import py.code # pylint: disable=no-name-in-module import pytest @@ -1963,3 +1964,9 @@ def merge_dicts(*dict_args): for d in dict_args: result.update(d) return result + + +def apply_action_and_add_finalizer(request, targets, action, reaction): + for a_target in targets: + action(a_target) + request.addfinalizer(functools.partial(reaction, a_target)) diff --git a/taf/testlib/linux/bench.py b/taf/testlib/linux/bench.py new file mode 100644 index 0000000..2482ebe --- /dev/null +++ b/taf/testlib/linux/bench.py @@ -0,0 +1,305 @@ +""" +@copyright Copyright (c) 2016 - 2017, Intel Corporation. + +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. + +@file: bench.py + +""" + +import os +from collections import ChainMap +import time +import json +from contextlib import suppress +from abc import ABC, abstractmethod + +from taf.testlib.linux.etcd_helper import EtcdHelper +from plugins import loggers +from testlib.linux.utils import TimerContext, create_directory, recursive_format +from utils.ab_parser import AbParser, AbAggregator # pylint: disable=no-name-in-module + +from testlib.linux.kubernetes import Kubernetes +from testlib.linux.utils import wait_for + + +class IterableMetaclass(type): + def __iter__(cls): + for key, value in cls.__dict__.items(): + if not key.startswith('__'): + yield value + + +class Constants(metaclass=IterableMetaclass): + pass + + +class Labels(Constants): + REMOTE_SERVER = 'remote-srv' + REMOTE_CLIENT = 'remote-cl' + LOCAL_SERVER = 'local-srv' + LOCAL_CLIENT = 'local-cl' + SINGLE_VM = 'single-vm' + + +class Modes(Constants): + SINGLE_VM = 'single_vm' + LOCAL = 'local' + REMOTE = 'remote' + + +MODE_MAPPING = { + Modes.SINGLE_VM: { + 'client': Labels.SINGLE_VM, + 'server': Labels.SINGLE_VM, + }, + Modes.LOCAL: { + 'client': Labels.LOCAL_CLIENT, + 'server': Labels.LOCAL_SERVER, + }, + Modes.REMOTE: { + 'client': Labels.REMOTE_CLIENT, + 'server': Labels.REMOTE_SERVER, + }, +} + + +class BenchmarkException(Exception): + pass + + +def set_node_selector(cfg, selector): + cfg['spec']['nodeSelector'] = { + selector: 'true', + } + + +class Config(object): + + _ATTRIBUTES = { + 'debug': False, # Additional debug messages TODO: seems it's unused + 'kubernetes_endpoint': None, # Kubernetes endpoint for the KubernetesBenchmark + 'etcd_endpoint': None, # Etcd endpoint + 'docker_registry': None, # Registry where to pull the onp_benchmark image from + 'create': None, # Should the pods be created or not TODO: taken from the original + # Shannon implementation, but I don't see a point here + 'numpairs': 1, # number of client-server pairs + 'perf_server_file': None, # json file defining the server and its parameters + 'perf_client_file': None, # json file defining the client and its parameters + 'kube_server_file': None, # kubernetes json file defining the server + 'kube_client_file': None, # kubernetes json file defining the client + 'env': None, # environment -- information about VMs for the vm-vm test + 'test_type': None, # VM-2-VM or Kubernetes test + 'remote': None, # clients on different hosts than servers + 'local': None, # clients on the same hosts as servers + 'clean': True, # delete everything that was created on the hosts by the + # previous tests (TODO: seems unused) + 'start_timeout': 300, # how long to wait for entities start + 'number_of_nodes': 1, # number of Kubernetes nodes or VMs in case of VM-2-VM + 'countdown_time': 15, # how long to wait before the test is started + 'wait_for_results_timeout': 600, # how long to wait for the test results + } + + CLASS_LOGGER = loggers.ClassLogger() + + def __getattr__(self, key): + with suppress(KeyError): + return self._config[key] + raise AttributeError(key) + + def __setattr__(self, key, value=None): + if key == '_config': + super(Config, self).__setattr__(key, value) + elif key in self._ATTRIBUTES: + self._config[key] = value + else: + raise AttributeError + + def load(self, config): + if not set(config).issubset(self._ATTRIBUTES): + raise AttributeError + self._config = ChainMap(config, self._ATTRIBUTES) + + def set_labels(self, mode): + mapping = MODE_MAPPING[mode] + set_node_selector(self.kube_server_file, mapping['server']) + set_node_selector(self.kube_client_file, mapping['client']) + + def __init__(self, config=None, master_ip=None): + super().__init__() + self._config = {} + self.load(config) + if master_ip is not None: + self.kubernetes_endpoint = self.kubernetes_endpoint.format(master_ip=master_ip) + self.etcd_endpoint = self.etcd_endpoint.format(master_ip=master_ip) + + +class Test(ABC): + + PARSERS = { + 'ab': AbParser, + } + + AGGREGATORS = { + 'ab': AbAggregator, + } + + CLASS_LOGGER = loggers.ClassLogger() + + @abstractmethod + def clean_up(self): + pass + + def __init__(self, config=None): + super().__init__() + self.config = config + self._test_type = None + + self.etcd = EtcdHelper(config.etcd_endpoint) + + self.id = self.etcd.latest_id + 1 + self.CLASS_LOGGER.debug("Creating a new test id = %d", self.id) + self.etcd.change_dir('test-{0.id}'.format(self)) + + self.root_dir = 'test-{0.id}' + create_directory(self.root_dir) + + @abstractmethod + def _start_servers(self): + pass + + @abstractmethod + def _start_clients(self): + pass + + def _start_and_wait_for_entity(self, timer, thing, start_method, key): + with timer: + timer.thing = thing + start_method() + self.CLASS_LOGGER.info("Waiting for %s", thing) + self.etcd.wait_for_key_count(key, self.config.numpairs, self.config.start_timeout) + + def prepare(self): + if not self.config.create: + return + + self.etcd.rootvalue__latest = self.id + self.etcd.rootvalue__numpairs = self.config.numpairs + + self.etcd.value__inputdata__server = json.dumps(self.config.perf_server_file) + self.etcd.value__inputdata__client = json.dumps(self.config.perf_client_file) + self.etcd.value__inputdata__start = "0" + time.sleep(1) + + def log_time(context): + self.CLASS_LOGGER.info( + "Time to create {0.thing} was {0.delta} seconds".format(context)) + + timer = TimerContext(log_time) + + self._start_and_wait_for_entity(timer, 'servers', self._start_servers, + self.etcd.key__outputdata__server) + self._start_and_wait_for_entity(timer, 'clients', self._start_clients, + self.etcd.key__outputdata__state) + + def run(self): + + self.etcd.value__inputdata__starttime = int(time.time()) + self.config.countdown_time + self.etcd.value__inputdata__start = "1" + + self.CLASS_LOGGER.info("Starting test in %d seconds", self.config.countdown_time) + time.sleep(self.config.countdown_time) + + def log_time(context): + self.CLASS_LOGGER.info("Test time was %d seconds", context.delta) + + with TimerContext(log_time): + self.CLASS_LOGGER.info("Waiting for results.") + self.etcd.wait_for_key_count(self.etcd.key__outputdata__result, + self.config.numpairs, + timeout=self.config.wait_for_results_timeout) + + @property + def test_type(self): + if self._test_type is not None: + return self._test_type + client_file = self.etcd.value__inputdata__client + client_file = json.loads(client_file.value) # pylint: disable=no-member + self._test_type = next(iter(client_file)) + if self._test_type not in self.PARSERS: + raise BenchmarkException('{}: Unknown test type'.format(self._test_type)) + return self._test_type + + def collect(self): + try: + parser_type = self.PARSERS[self.test_type] + aggregate_type = self.AGGREGATORS[self.test_type] + except KeyError: + raise BenchmarkException('{}: Unknown test type'.format(self.test_type)) + else: + parser = parser_type() + aggregator = aggregate_type() + + for a_result in self.etcd.value__outputdata__result.leaves: + fname = "{}/raw_output_{}.txt".format(self.root_dir, os.path.basename(a_result.key)) + with open(fname, 'w') as stream: + stream.write(a_result.value) + aggregator += parser.parse(a_result.value) + + return aggregator + + +class KubernetesBenchmark(Test): + def clean_up(self): + if not self.config.clean: + return + + def log_time(context): + self.CLASS_LOGGER.debug("Cleaning took %d seconds", context.delta) + + self.CLASS_LOGGER.info("Cleaning pods...") + with TimerContext(log_time): + self.kubernetes_client.deletecollection_namespaced_pod(namespace='default') + wait_for(iter(self.kubernetes_client.helper.get_number_of_pods, 0), timeout=300) + + def __init__(self, config): + super().__init__(config) + self.kubernetes_client = Kubernetes(self.config.kubernetes_endpoint) + self.clean_up() + self.fmt_obj = { + 'etcd_ip': self.etcd.etcd_config['host'], + 'etcd_port': self.etcd.etcd_config['port'], + 'docker_registry': self.config.docker_registry, + } + + def _start_pod(self, body): + self.kubernetes_client.create_namespaced_pod( + body=body, + namespace='default') + + def _recursive_format(self, container): + return recursive_format(container, self.fmt_obj) + + def _start_entity(self, entity_type, kube_file): + for index in range(1, int(self.config.numpairs) + 1): + self.fmt_obj['id_num'] = index + manifest = self._recursive_format(kube_file) + self.CLASS_LOGGER.debug("Starting %s: %s", entity_type, manifest) + self._start_pod(manifest) + time.sleep(0.25) + + def _start_servers(self): + self._start_entity("server", self.config.kube_server_file) + + def _start_clients(self): + self._start_entity("client", self.config.kube_client_file) diff --git a/taf/testlib/linux/collectd.py b/taf/testlib/linux/collectd.py index 10eab16..93a2dfa 100644 --- a/taf/testlib/linux/collectd.py +++ b/taf/testlib/linux/collectd.py @@ -51,7 +51,7 @@ PLUGINS = ("python", "csv", "dpdkstat", "dpdkevents", "hugepages", "intel_rdt", "mcelog", "ovs_stats", "ovs_events", - "snmp_agent", "syslog", "exec", "ipmi") + "snmp_agent", "syslog", "exec", "ipmi", "cpu", "memory", "network", "interface") GLOBAL_PLUGIN_LOAD_BOILERPLATE = """ @@ -70,7 +70,7 @@ ACTIONS = {'enable': {'cmd': [r"printf '{0}' >> {{collectd_conf}}".format(LOAD_PLUGIN_WITH_PARAM_BOILERPLATE)], 'kwargs_required': True}, 'enable_default': {'cmd': [r"sed -i '/[^<]LoadPlugin {plugin}/s/^\(#\)\+//gw /dev/stdout' {collectd_conf}", - r"sed -i '//,/<\/Plugin>/s/^\(#\)\+//w /dev/stdout' {collectd_conf}"], + r"sed -i '/#/,/#<\/Plugin>/s/^\(#\)\+//w /dev/stdout' {collectd_conf}"], 'kwargs_required': False}, 'enable_global': {'cmd': [r"printf '{0}' >> {{collectd_conf}}".format(GLOBAL_PLUGIN_LOAD_BOILERPLATE)], 'kwargs_required': True}, @@ -161,7 +161,7 @@ def __init__(self, cli_send_command, cli_set_command, collectd_conf=None): self.send_command = cli_send_command self.cli_set_command = cli_set_command self.collectd_conf = collectd_conf if collectd_conf else self.DEFAULT_COLLECTD_CONF - self.service_manager = service_lib.specific_service_manager_factory(self.SERVICE, self.send_command) + self.service_manager = service_lib.SpecificServiceManager(self.SERVICE, self.send_command) for action in ACTIONS: setattr(self, action, collectd_conf_action(action, self.cli_set_command, self.collectd_conf)) @@ -178,12 +178,18 @@ def stop(self): """ return self.service_manager.stop() + def status(self): + return self.service_manager.status(expected_rcs={0, 3}) + def restart(self): """ @brief Restart collectd service """ return self.service_manager.restart() + def reconfiguration(self): + return service_lib.ServiceConfigChangeContext(self.service_manager) + def add_globals(self, **kwargs): """ @brief Add global collectd variables in collectd.conf diff --git a/taf/testlib/linux/dcrpd/dcrpd.py b/taf/testlib/linux/dcrpd/dcrpd.py index 0ab1f34..b693d3e 100644 --- a/taf/testlib/linux/dcrpd/dcrpd.py +++ b/taf/testlib/linux/dcrpd/dcrpd.py @@ -39,8 +39,7 @@ def __init__(self, run_command, switch): self.run_command = run_command self.switch = switch self.switch_driver = SwitchDriver(self, switch) - self.service_manager = service_lib.specific_service_manager_factory( - self.SERVICE, self.run_command) + self.service_manager = service_lib.SpecificServiceManager(self.SERVICE, self.run_command) def start(self): """ diff --git a/taf/testlib/linux/etcd_helper.py b/taf/testlib/linux/etcd_helper.py new file mode 100644 index 0000000..aab25ad --- /dev/null +++ b/taf/testlib/linux/etcd_helper.py @@ -0,0 +1,110 @@ +""" +@copyright Copyright (c) 2016 - 2017, Intel Corporation. + +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. + +@file: etcd_helper.py + +""" +import itertools +from contextlib import suppress +import time + +import etcd +from plugins import loggers +from testlib.linux.utils import wait_for + + +ROOT_KEY = '/intel.com/tests' + + +class EtcdHelperException(Exception): + pass + + +class EtcdHelper(object): + + CLASS_LOGGER = loggers.ClassLogger() + + def __init__(self, endpoint): + if isinstance(endpoint, str): + etcd_protocol, address_port = endpoint.split('://') + etcd_address, etcd_port = address_port.split(':') + self.etcd_config = { + 'host': etcd_address, + 'port': int(etcd_port), + 'protocol': etcd_protocol + } + elif isinstance(endpoint, dict): + self.etcd_config = endpoint + + self.etcd = etcd.Client(**self.etcd_config) + + self._root_key = ROOT_KEY + self._cwd = ROOT_KEY + self._latest_id_key = '/'.join([ROOT_KEY, 'latest']) + + def init_etcd(self): + self.CLASS_LOGGER.debug("Initializing etcd test entries") + self.etcd.write(self._latest_id_key, "0") + + def change_dir(self, directory): + self._cwd = '/'.join([self._root_key, directory]) + + def _get_key(self, item): + return '/'.join(itertools.chain([self._cwd], item.split('__')[1:])) + + def _get_root_key(self, item): + return '/'.join(itertools.chain([self._root_key], item.split('__')[1:])) + + def __getattr__(self, item): + if item.startswith('key__'): + return self._get_key(item) + elif item.startswith('rootvalue__'): + return self.etcd.read(self._get_root_key(item)) + elif item.startswith('value__'): + return self.etcd.read(self._get_key(item)) + raise AttributeError('Unknown attribute {}'.format(item)) + + def __setattr__(self, item, value): + if item.startswith('rootvalue__'): + self.etcd.write(self._get_root_key(item), value) + elif item.startswith('value__'): + self.etcd.write(self._get_key(item), value) + else: + super().__setattr__(item, value) + + @property + def latest_id(self): + with suppress(AttributeError): + return self._latest_id + for _ in range(2): + with suppress(etcd.EtcdKeyNotFound): + self._latest_id = int(self.etcd.read(self._latest_id_key).value) # pylint: disable=no-member + return self._latest_id + self.init_etcd() + raise EtcdHelperException("Failed to find test_id") + + + def read_list(self, key): + return self.etcd.read(key).leaves + + def wait_for_key_count(self, key, count, timeout=15): + def get_key_count(): + with suppress(etcd.EtcdKeyNotFound): + return len(list(self.read_list(key))) + return 0 + + self.CLASS_LOGGER.info('Waiting for %s to give %d. Timeout is %d.', key, count, timeout) + wait_for(iter(get_key_count, count), timeout) + self.CLASS_LOGGER.debug('%s gave %d', key, count) diff --git a/taf/testlib/linux/kubernetes.py b/taf/testlib/linux/kubernetes.py new file mode 100644 index 0000000..d992339 --- /dev/null +++ b/taf/testlib/linux/kubernetes.py @@ -0,0 +1,53 @@ +""" +@copyright Copyright (c) 2016 - 2017, Intel Corporation. + +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. + +@file: kubernetes.py + +""" +from k8sclient.client import api_client +from k8sclient.client.apis import apiv_api + + +class KubernetesHelper(object): + + def __init__(self, kubernetes): + super().__init__() + self.kubernetes = kubernetes + + def label_node(self, name, labels): + return self.kubernetes.api.patch_namespaced_node(body={'metadata': {'labels': labels}}, + name=name) + + def get_number_of_pods(self): + return len(self.kubernetes.api.list_namespaced_pod( + namespace='default').items) + + def clear_labels(self, nodes, labels): + for a_node in nodes: + self.label_node(labels={key: None for key in labels}, name=a_node) + + +class Kubernetes(object): + + def __init__(self, endpoint): + super().__init__() + self.client = api_client.ApiClient(endpoint) + self.api = apiv_api.ApivApi(self.client) + self.helper = KubernetesHelper(self) + + def __getattr__(self, name): + attr = getattr(self.api, name) + setattr(self, name, attr) + return attr diff --git a/taf/testlib/linux/networkd.py b/taf/testlib/linux/networkd.py index 443f32d..455205d 100644 --- a/taf/testlib/linux/networkd.py +++ b/taf/testlib/linux/networkd.py @@ -36,8 +36,7 @@ def __init__(self, run_command, mgmt_ports): super(NetworkD, self).__init__() self.run_command = run_command self.mgmt_ports = mgmt_ports - self.service_manager = service_lib.specific_service_manager_factory( - self.SERVICE, self.run_command) + self.service_manager = service_lib.SpecificServiceManager(self.SERVICE, self.run_command) def restart(self): """ diff --git a/taf/testlib/linux/openvswitch.py b/taf/testlib/linux/openvswitch.py index 5f96725..a58227c 100644 --- a/taf/testlib/linux/openvswitch.py +++ b/taf/testlib/linux/openvswitch.py @@ -44,7 +44,7 @@ def __init__(self, cli_send_command, switch_map, name_to_switchid_map): self.cli_send_command = cli_send_command self.switch_map = switch_map self.name_to_switchid_map = name_to_switchid_map - self.service_manager = service_lib.specific_service_manager_factory(self.SERVICE, self.cli_send_command) + self.service_manager = service_lib.SpecificServiceManager(self.SERVICE, self.cli_send_command) def update_map(self, iface_name, delete=False): """ @@ -186,7 +186,8 @@ def get_interface_statistic(self, iface_name): @return: Output of OVS interface statistics """ data = self.get_interface_info(iface_name) - return dict(re.findall(r'{?(\S+)=(\d+)', data["statistics"])) + # return dict(re.findall(r'{?\"*(\S+)\"*=(\d+)', data["statistics"])) + return dict(re.findall(r'{?"*(\w+)"*=(\d+)', data["statistics"])) def get_existing_bridges_interfaces(self): """ @@ -238,3 +239,16 @@ def set_bridge_port_interface(self, inst_type, name, **kwargs): options = ''.join(map(lambda x: ' {}={}'.format(*x), kwargs.items())) command = "ovs-vsctl set {0} {1}{2}".format(inst_type, name, options) self.cli_send_command(command) + + def get_interface_statistic_counter(self, iface_name, counter_name): + """ + @brief Get ovs interface statistic from ovsdb + @param iface_name: name of ovs interface + @type iface_name: str + @param counter_name: name of ovs interface counter + @type counter_name: str + @rtype: dict + @return: Output of OVS interface statistics + """ + output = self.cli_send_command("ovs-vsctl get Interface {0} statistics:{1}".format(iface_name, counter_name)) + return int(output.stdout.strip()) diff --git a/taf/testlib/linux/service_lib.py b/taf/testlib/linux/service_lib.py index 0a8459c..8a07c9e 100644 --- a/taf/testlib/linux/service_lib.py +++ b/taf/testlib/linux/service_lib.py @@ -29,7 +29,7 @@ cmd = " ".join(systemd_cmd_gen.start("networkd")) # to directly call cli_send_command but still accepts kwargs for cli_send_command -networkd = service_lib.specific_service_manager_factory("networkd", self.cli_send_command) +networkd = service_lib.SpecificServiceManager("networkd", self.cli_send_command) networkd.stop(expected_rcs={0, 3}) networkd.start(expected_rcs={1}) @@ -104,24 +104,25 @@ """ -def systemd_command_generator(command): +class ReturnCodes(object): + SUCCESS = 0 + RUNNING = 0 + STOPPED = 3 + UNKNOWN = None - command_name = "systemctl" - if command == "is_enabled": - command = "is-enabled" - elif command == "is_active": - command = "is-active" - elif command == "list": - # noinspection PyUnusedLocal - def list_command(service_name): - return [command_name, "list-unit-files", "--type=service"] - return list_command - def method(service_name): - return [command_name, command, "%s.service" % service_name] - return method +class SystemdReturnCodes(ReturnCodes): + pass + + +REPLACE_COMMAND_LIST = { + 'is_enabled', + 'is_active', + 'daemon_reload', +} -COMMANDS = ( + +COMMANDS = { "start", "stop", "reload", @@ -133,71 +134,133 @@ def method(service_name): "is_enabled", "is_active", "list", -) + "daemon_reload", +} + + +def systemd_command_generator(command): + + command_name = "systemctl" + if command in REPLACE_COMMAND_LIST: + command = command.replace('_', '-') + + if command == "list": + # noinspection PyUnusedLocal + def list_command(_): + return [command_name, "list-unit-files", "--type=service"] + return list_command + elif command == "daemon-reload": + def daemon_reload_command(*_): + return [command_name, command, ''] + return daemon_reload_command + + def method(service_name): + return [command_name, command, "{}.service".format(service_name)] + return method class ServiceCommandGenerator(object): - def __init__(self, command_generator, command_list=COMMANDS): + def __getattr__(self, name): + if name not in self: + raise AttributeError(name) + command = self.command_generator(name) + setattr(self, name, command) + return command + + def __iter__(self): + return iter(self.commands) + + def __contains__(self, value): + return value in self.commands + + def __init__(self, command_generator, return_codes=ReturnCodes, command_list=None): super(ServiceCommandGenerator, self).__init__() + if command_list is None: + command_list = COMMANDS self.commands = command_list - for command in self.commands: - setattr(self, command, command_generator(command)) + self.command_generator = command_generator + self.return_codes = return_codes -class SpecificServiceManager(object): +class GenericServiceManager(object): + def __init__(self, run_func, command_list=None): + super().__init__() + if command_list is None: + command_list = COMMANDS + self.service_command_generator = ServiceCommandGenerator(systemd_command_generator, + SystemdReturnCodes, + command_list) + + self.return_codes = SystemdReturnCodes + self.run_func = run_func + + def __getattr__(self, name): + def run(service='', **kwargs): + return self.run_func(' '.join(command(service)), **kwargs) + command = getattr(self.service_command_generator, name) + setattr(self, name, run) + return run - def __init__(self, service_name, service_command_generator, run): - super(SpecificServiceManager, self).__init__() - for cmd in service_command_generator.commands: - setattr(self, cmd, - self.generate_run_function(run, getattr(service_command_generator, cmd), service_name)) + def _get_running_status(self, service=''): + return self.status(service=service, expected_rcs={self.return_codes.RUNNING, + self.return_codes.STOPPED}) - @staticmethod - def generate_run_function(run_func, command, service_name): - def run(**kwargs): - return run_func(" ".join(command(service_name)), **kwargs) - return run + def is_running(self, service=''): + rv = self._get_running_status(service) + return rv.rc == self.return_codes.RUNNING + def is_stopped(self, service=''): + rv = self._get_running_status(service) + return rv.rc == self.return_codes.STOPPED -class GenericServiceManager(object): - def __init__(self, service_command_generator, run): - super(GenericServiceManager, self).__init__() - for cmd in service_command_generator.commands: - setattr(self, cmd, - self.generate_run_function(run, getattr(service_command_generator, cmd))) +class SpecificServiceManager(GenericServiceManager): + def __init__(self, service_name, run_func): + command_list = [c for c in COMMANDS if c != "list"] + super().__init__(run_func, command_list) + self.service_name = service_name - @staticmethod - def generate_run_function(run_func, command): - def run(service="", **kwargs): - return run_func(" ".join(command(service)), **kwargs) + def __getattr__(self, name): + def run(**kwargs): + kwargs.pop('service', None) # remove any value associated with the service key + return self.run_func(command, **kwargs) + command = getattr(self.service_command_generator, name) + command = ' '.join(command(self.service_name)) + setattr(self, name, run) return run class SystemdServiceManager(GenericServiceManager): - def __init__(self, service_command_generator, run): - super(SystemdServiceManager, self).__init__(service_command_generator, run) + def __init__(self, run): + super().__init__(run) @staticmethod def change_default_runlevel(runlevel='multi-user.target'): # atomic symlinking, symlink and then rename tmp_symlink = mktemp(dir="/etc/systemd/system") - os.symlink("/usr/lib/systemd/system/%s" % runlevel, tmp_symlink) + os.symlink("/usr/lib/systemd/system/{}".format(runlevel), tmp_symlink) os.rename(tmp_symlink, "/etc/systemd/system/default.target") -_command_generators = {"systemd": systemd_command_generator} - -_service_managers = {"systemd": SystemdServiceManager} - +class ServiceConfigChangeContext(object): + """ + Context manager suitable for service configuration + """ -def specific_service_manager_factory(service_name, run_func): - command_list = [c for c in COMMANDS if c != "list"] - service_command_generator = ServiceCommandGenerator(systemd_command_generator, command_list) - return SpecificServiceManager(service_name, service_command_generator, run_func) + def __init__(self, specific_service_manager): + super().__init__() + self.rcs = specific_service_manager.return_codes + self.was_running = None + self.specific_service_manager = specific_service_manager + def __enter__(self): + self.was_running = self.specific_service_manager.is_running() + if self.was_running: + self.specific_service_manager.stop() -def systemd_manager_factory(run_func): - return SystemdServiceManager(ServiceCommandGenerator(systemd_command_generator), run_func) + def __exit__(self, exc_type, exc, exc_tb): + self.specific_service_manager.daemon_reload() + if self.was_running: + self.specific_service_manager.start() diff --git a/taf/testlib/linux/tool_general.py b/taf/testlib/linux/tool_general.py index 2d9c847..8826704 100644 --- a/taf/testlib/linux/tool_general.py +++ b/taf/testlib/linux/tool_general.py @@ -91,9 +91,7 @@ def start(self, command, prefix=None, timeout=None, tool_name=None, tool_instanc 'command': cmd_str, 'instance_id': tool_instance_id, 'service_name': service_name, - 'service_manager': service_lib.specific_service_manager_factory( - service_name, - self.run_command) + 'service_manager': service_lib.SpecificServiceManager(service_name, self.run_command), } # Wait for tool instance to start self.is_active(tool_instance_id) diff --git a/taf/testlib/linux/utils.py b/taf/testlib/linux/utils.py new file mode 100644 index 0000000..1c8120f --- /dev/null +++ b/taf/testlib/linux/utils.py @@ -0,0 +1,74 @@ +""" +@copyright Copyright (c) 2016 - 2017, Intel Corporation. + +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. + +@file: utils.py + +""" +import time +import os +from contextlib import suppress + + +class TimerContext(object): + + def __init__(self, func=None): + self.start = None + self.end = None + self.delta = None + self.func = func + + def __enter__(self): + self.start = time.time() + return self + + def __exit__(self, *args, **kwargs): + self.end = time.time() + self.delta = self.end - self.start + if self.func: + self.func(self) + + def __str__(self): + return str(self.delta) + + +def create_directory(path): + with suppress(FileExistsError): + os.makedirs(path) + + +def recursive_format(container, kwargs): + + if isinstance(container, str): + return container.format(**kwargs) + + if isinstance(container, list): + return [recursive_format(item, kwargs) for item in container] + + if isinstance(container, dict): + return {recursive_format(k, kwargs): recursive_format(v, kwargs) + for k, v in container.items()} + + return container + + +class TimeoutExceeded(Exception): + pass + + +def wait_for(iterator, timeout): + for index in iterator: + if index > timeout: + raise TimeoutExceeded + time.sleep(1) diff --git a/taf/testlib/linux_host_bash.py b/taf/testlib/linux_host_bash.py index b6a31f4..b29fcc3 100644 --- a/taf/testlib/linux_host_bash.py +++ b/taf/testlib/linux_host_bash.py @@ -730,6 +730,8 @@ def modify_ports(self, ports, expected_rcs=frozenset({0}), **kwargs): commands.append("ip link set dev {0} address {1}".format(port, kwargs['macAddress'])) if 'speed' in kwargs: commands.append("ethtool -s {0} speed {1}".format(port, kwargs['speed'])) + if 'autoneg' in kwargs: + commands.append("ethtool -s {0} autoneg {1}".format(port, kwargs['autoneg'])) if 'ipAddr' in kwargs: if not kwargs['ipAddr']: diff --git a/taf/testlib/magnum.py b/taf/testlib/magnum.py new file mode 100644 index 0000000..41e24b3 --- /dev/null +++ b/taf/testlib/magnum.py @@ -0,0 +1,164 @@ +""" +@copyright Copyright (c) 2015-2016, Intel Corporation. + +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. + +@file magnum.py + +@summary Support for magnum feature of Openstack. Uses magnum tempest client + that was taken form the upstream magnum repository + +When using magnum clients, the environment.json file should contain some additional entries: + +""" +############################################################################### +# Requires the following fiels in environment json in addition to what's needed +# for the virtual_env: +# { +# ... +# "dns_nameserver": "10.248.2.1", +# "http_proxy": "http://proxy.example.com" +# "https_proxy": "https://proxy.example.com" +# "no_proxy": "10.0.0.1,10.0.0.2" +# "insecure_registry": "20.0.0.1:4000" +# "ntp_server": "ntp.example.com", +# ... +# } +# +############################################################################### + +from testlib.tempest_clients.magnum.clients.cluster_client import ClusterClient +from testlib.tempest_clients.magnum.clients.cluster_template_client import ClusterTemplateClient +from testlib.tempest_clients.magnum.clients.magnum_service_client import MagnumServiceClient +import testlib.tempest_clients.magnum.models +from plugins import loggers +import pprint + + +DISTRO_METADATA = { + 'fedora-atomic': { + 'ssh_user': 'fedora', + 'ssh_port': 22, + }, +} + + +CLUSTER_TEMPLATE_DEFAULTS = { + 'coe': 'kubernetes', + 'network_driver': 'flannel', + 'docker_volume_size': 5, + 'labels': {'flannel_backend': 'vxlan'}, + 'flavor_id': 'm1.small', + 'master_flavor_id': 'm1.medium', + 'tls_disabled': 'True', +} + + +CLUSTER_CONSTS = { + 'os_distro': 'fedora-atomic', +} + + +class Magnum(object): + + CLASS_LOGGER = loggers.ClassLogger() + + def __init__(self, venv): + self.venv = venv + self.config = venv.config + self.admin_manager = venv.handle.admin_manager + self.manager = venv.handle.manager + self.magnum_models = testlib.tempest_clients.magnum.models + + try: + self.manager.cluster_client = ClusterClient(self.manager.auth_provider) + self.admin_manager.cluster_client = ClusterClient(self.admin_manager.auth_provider) + + self.manager.cluster_template_client = \ + ClusterTemplateClient(self.manager.auth_provider) + self.admin_manager.cluster_template_client = \ + ClusterTemplateClient(self.admin_manager.auth_provider) + + self.manager.magnum_service_client = MagnumServiceClient(self.manager.auth_provider) + self.admin_manager.magnum_service_client = \ + MagnumServiceClient(self.admin_manager.auth_provider) + + except: + self.CLASS_LOGGER.error('Error adding magnum clients.') + raise + + def delete_cluster(self, uuid, wait=True): + + self.CLASS_LOGGER.info("Deleting the cluster (id=%s).", uuid) + + client = self.manager.cluster_client + client.delete_cluster(uuid) + if wait: + client.wait_for_cluster_to_delete(uuid) + + def create_cluster(self, wait=True, **kwargs): + + client = self.manager.cluster_client + + kwargs.setdefault('name', + self.venv.tempest_lib.common.utils.data_utils.rand_name('onp_cluster')) + kwargs.setdefault('discovery_url', None) + + if loggers.LOG_LEVEL == 'DEBUG': + self.CLASS_LOGGER.debug("Cluster dict:\n%s", pprint.pformat(kwargs)) + + model = self.magnum_models.cluster_model.ClusterEntity.from_dict(kwargs) + + self.CLASS_LOGGER.info("Creating a cluster.") + resp, cluster = client.post_cluster(model) + assert resp['status'] == '202' + + if wait: + client.wait_for_created_cluster(cluster.uuid) + _, cluster = client.get_cluster(cluster.uuid) + assert cluster.status == 'CREATE_COMPLETE' + + self.venv.handle.addCleanup(self.delete_cluster, cluster.uuid) + return cluster + + def delete_cluster_template(self, uuid): + self.CLASS_LOGGER.info("Deleting the cluster template(id=%s).", uuid) + client = self.manager.cluster_template_client + client.delete_cluster_template(uuid) + + def create_cluster_template(self, template, **kwargs): + + client = self.manager.cluster_template_client + + template.setdefault('external_network_id', self.config.network.public_network_id) + template.setdefault('keypair_id', self.venv.key['name']) + for key, default_value in CLUSTER_TEMPLATE_DEFAULTS.items(): + template.setdefault(key, default_value) + + for setting in ['dns_nameserver', 'http_proxy', 'https_proxy', 'no_proxy']: + template.setdefault(setting, self.venv.env_settings.get(setting)) + + template['image_id'] = self.venv.get_image_by_name(template['image_id'])['id'] + metadata = {'os_distro': kwargs.setdefault('os_distro', CLUSTER_CONSTS['os_distro'])} + + self.manager.compute_images_client.set_image_metadata(template['image_id'], metadata) + + if loggers.LOG_LEVEL == 'DEBUG': + self.CLASS_LOGGER.debug("Template dict:\n%s", pprint.pformat(template)) + + self.CLASS_LOGGER.info("Creating a cluster template.") + model = self.magnum_models.cluster_template_model.ClusterTemplateEntity.from_dict(template) + resp, cluster_template = client.post_cluster_template(model) + assert resp['status'] == '201' + self.venv.handle.addCleanup(self.delete_cluster_template, cluster_template.uuid) + return cluster_template diff --git a/taf/testlib/rrdtool_graph.py b/taf/testlib/rrdtool_graph.py index 753efd9..0c9d5a8 100644 --- a/taf/testlib/rrdtool_graph.py +++ b/taf/testlib/rrdtool_graph.py @@ -19,10 +19,8 @@ """ import os -import sys from collections import namedtuple -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../..'))) from utils.pyrrd.graph import CalculationDefinition, ColorAttributes # pylint: disable=no-name-in-module from utils.pyrrd.graph import VariableDefinition, DataDefinition # pylint: disable=no-name-in-module from utils.pyrrd.graph import Area, Graph, GraphPrint, Line # pylint: disable=no-name-in-module @@ -72,8 +70,8 @@ # Graph properties for different RRD files TYPES = {'CPU': {'vertical_label': '"CPU usage [jiffies]"', - 'rigid': True, - 'y_grid': '10:5', + 'rigid': True, + 'y_grid': '10:5', 'upper_limit': 110}, 'MEMORY': {'units_exponent': 9, 'vertical_label': '"Memory usage [Gigabytes]"'}, 'INTERFACE': {'vertical_label': '"Network traffic [bits/sec]"', @@ -82,8 +80,7 @@ 'logarithmic': False, 'hrule': True}, 'LOAD': {'vertical_label': '"System load"'}, 'DISK': {'logarithmic': False, - 'vertical_label': '"Disk traffic [bytes/sec]"'} - } + 'vertical_label': '"Disk traffic [bytes/sec]"'}} # RRD cdef CDEF = ('MIN', 'AVERAGE', 'MAX') @@ -97,31 +94,31 @@ # FileInfo('cpu-idle.rrd', 'idle', 'value'), # FileInfo('cpu-interrupt.rrd', 'interrupt', 'value'), # FileInfo('cpu-nice.rrd', 'nice', 'value'), - ], + ], 'MEMORY': [ FileInfo('memory-used.rrd', 'used', 'value'), FileInfo('memory-buffered.rrd', 'buffered', 'value'), FileInfo('memory-cached.rrd', 'cached', 'value'), FileInfo('memory-free.rrd', 'free', 'value'), - ], + ], 'INTERFACE': [ FileInfo('if_octets.rrd', 'incoming', 'rx'), - FileInfo('if_octets.rrd', 'outgoing', 'tx') - ], + FileInfo('if_octets.rrd', 'outgoing', 'tx'), + ], 'INTERFACE_BYTES': [ FileInfo('if_octets.rrd', 'incoming', 'rx'), - FileInfo('if_octets.rrd', 'outgoing', 'tx') - ], + FileInfo('if_octets.rrd', 'outgoing', 'tx'), + ], 'LOAD': [ FileInfo('load.rrd', 'short', 'shortterm'), FileInfo('load.rrd', 'mid', 'midterm'), - FileInfo('load.rrd', 'long', 'longterm') - ], + FileInfo('load.rrd', 'long', 'longterm'), + ], 'DISK': [ FileInfo('disk_octets.rrd', 'read', 'read'), - FileInfo('disk_octets.rrd', 'write', 'write') - ] - } + FileInfo('disk_octets.rrd', 'write', 'write'), + ], +} # Graphs calculations CALCULATIONS = { @@ -129,8 +126,8 @@ Calculation('user_sys', [(FILES['CPU'][0].vname, CDEF[1]), (FILES['CPU'][1].vname, CDEF[1])], - '{0},{1},+') - ], + '{0},{1},+'), + ], 'MEMORY': [ Calculation('user_buffered', [(FILES['MEMORY'][0].vname, CDEF[2]), @@ -147,7 +144,7 @@ (FILES['MEMORY'][2].vname, CDEF[2]), (FILES['MEMORY'][3].vname, CDEF[2])], '{0},{1},+,{2},+,{3},+'), - ], + ], # For bits/sec representation 'INTERFACE': [ Calculation('rx_min_bits', @@ -170,59 +167,59 @@ '8,{0},*'), Calculation('tx_max_bits_neg', [(FILES['INTERFACE'][1].vname, CDEF[1])], - '-8,{0},*') - ], + '-8,{0},*'), + ], # For bytes/sec representation 'INTERFACE_BYTES': [ Calculation('tx_max_bytes_neg', [(FILES['INTERFACE_BYTES'][1].vname, CDEF[1])], - '-1,{0},*') - ], + '-1,{0},*'), + ], 'LOAD': [], - 'DISK': [] - } + 'DISK': [], +} # Graph's lines and areas DISPLAY = { 'CPU': [ LineDef(CALCULATIONS['CPU'][0].vname, None, True, True, 'User\:'), LineDef(FILES['CPU'][1].vname, CDEF[1], True, True, 'System\:'), - LineDef(FILES['CPU'][2].vname, CDEF[1], True, True, 'Wait-IO\:') - ], + LineDef(FILES['CPU'][2].vname, CDEF[1], True, True, 'Wait-IO\:'), + ], 'MEMORY': [ LineDef(CALCULATIONS['MEMORY'][2].vname, None, True, True, 'Free\:',), LineDef(CALCULATIONS['MEMORY'][1].vname, None, True, True, 'Page cache\:'), LineDef(CALCULATIONS['MEMORY'][0].vname, None, True, True, 'Buffer cache\:'), LineDef(FILES['MEMORY'][0].vname, CDEF[2], True, True, 'Used\:'), - ], + ], # For bits/sec representation 'INTERFACE': [ - LineDef(CALCULATIONS['INTERFACE'][2].vname, None, True, True, 'Incoming:',), - LineDef(CALCULATIONS['INTERFACE'][6].vname, None, True, True, 'Outgoing\:') - ], + LineDef(CALCULATIONS['INTERFACE'][2].vname, None, True, True, 'Incoming\:',), + LineDef(CALCULATIONS['INTERFACE'][6].vname, None, True, True, 'Outgoing\:'), + ], # For bytes/sec representation 'INTERFACE_BYTES': [ - LineDef(FILES['INTERFACE_BYTES'][0].vname, CDEF[2], True, True, 'Incoming:',), - LineDef(CALCULATIONS['INTERFACE_BYTES'][0].vname, None, True, True, 'Outgoing\:') - ], + LineDef(FILES['INTERFACE_BYTES'][0].vname, CDEF[2], True, True, 'Incoming\:',), + LineDef(CALCULATIONS['INTERFACE_BYTES'][0].vname, None, True, True, 'Outgoing\:'), + ], 'LOAD': [ LineDef(FILES['LOAD'][0].vname, CDEF[1], True, False, '1 minute average\:'), LineDef(FILES['LOAD'][1].vname, CDEF[1], True, False, '5 minute average\:'), - LineDef(FILES['LOAD'][2].vname, CDEF[1], True, False, '15 minute average\:') - ], + LineDef(FILES['LOAD'][2].vname, CDEF[1], True, False, '15 minute average\:'), + ], 'DISK': [ LineDef(FILES['DISK'][0].vname, CDEF[2], True, False, 'Read\:'), LineDef(FILES['DISK'][1].vname, CDEF[2], True, False, 'Written\:'), - ] - } + ], +} VARS = [ Variable('MINIMUM', 'Min',), Variable('AVERAGE', 'Avg,'), Variable('MAXIMUM', 'Max,'), - Variable('LAST', 'Last\l') - ] + Variable('LAST', 'Last\l'), +] # Graph's prints @@ -239,8 +236,8 @@ Print([(FILES['CPU'][2].vname, CDEF[0], VARS[0]), (FILES['CPU'][2].vname, CDEF[1], VARS[1]), (FILES['CPU'][2].vname, CDEF[2], VARS[2]), - (FILES['CPU'][2].vname, CDEF[1], VARS[3])], '%8.1lf {}') - ], + (FILES['CPU'][2].vname, CDEF[1], VARS[3])], '%8.1lf {}'), + ], 'MEMORY': [ Print([(FILES['MEMORY'][3].vname, CDEF[0], VARS[0]), (FILES['MEMORY'][3].vname, CDEF[1], VARS[1]), @@ -257,8 +254,8 @@ Print([(FILES['MEMORY'][0].vname, CDEF[0], VARS[0]), (FILES['MEMORY'][0].vname, CDEF[1], VARS[1]), (FILES['MEMORY'][0].vname, CDEF[2], VARS[2]), - (FILES['MEMORY'][0].vname, CDEF[1], VARS[3])], '%8.1lf%S {}') - ], + (FILES['MEMORY'][0].vname, CDEF[1], VARS[3])], '%8.1lf%S {}'), + ], # For bits/sec representation 'INTERFACE': [ Print([(CALCULATIONS['INTERFACE'][0].vname, None, VARS[0]), @@ -268,8 +265,8 @@ Print([(CALCULATIONS['INTERFACE'][3].vname, None, VARS[0]), (CALCULATIONS['INTERFACE'][4].vname, None, VARS[1]), (CALCULATIONS['INTERFACE'][5].vname, None, VARS[2]), - (CALCULATIONS['INTERFACE'][4].vname, None, VARS[3])], '%8.1lf%S {}') - ], + (CALCULATIONS['INTERFACE'][4].vname, None, VARS[3])], '%8.1lf%S {}'), + ], # For bytes/sec representation 'INTERFACE_BYTES': [ Print([(FILES['INTERFACE_BYTES'][0].vname, CDEF[0], VARS[0]), @@ -279,8 +276,8 @@ Print([(FILES['INTERFACE_BYTES'][1].vname, CDEF[0], VARS[0]), (FILES['INTERFACE_BYTES'][1].vname, CDEF[1], VARS[1]), (FILES['INTERFACE_BYTES'][1].vname, CDEF[2], VARS[2]), - (FILES['INTERFACE_BYTES'][1].vname, CDEF[1], VARS[3])], '%8.1lf%S {}') - ], + (FILES['INTERFACE_BYTES'][1].vname, CDEF[1], VARS[3])], '%8.1lf%S {}'), + ], 'LOAD': [ Print([(FILES['LOAD'][0].vname, CDEF[0], VARS[0]), (FILES['LOAD'][0].vname, CDEF[1], VARS[1]), @@ -294,7 +291,7 @@ (FILES['LOAD'][2].vname, CDEF[1], VARS[1]), (FILES['LOAD'][2].vname, CDEF[2], VARS[2]), (FILES['LOAD'][2].vname, CDEF[1], VARS[3])], '%8.1lf {}'), - ], + ], 'DISK': [ Print([(FILES['DISK'][0].vname, CDEF[0], VARS[0]), (FILES['DISK'][0].vname, CDEF[1], VARS[1]), @@ -303,9 +300,9 @@ Print([(FILES['DISK'][1].vname, CDEF[0], VARS[0]), (FILES['DISK'][1].vname, CDEF[1], VARS[1]), (FILES['DISK'][1].vname, CDEF[2], VARS[2]), - (FILES['DISK'][1].vname, CDEF[1], VARS[3])], '%8.1lf%S {}') - ] - } + (FILES['DISK'][1].vname, CDEF[1], VARS[3])], '%8.1lf%S {}'), + ], +} # Graph's xgrid info depending on time period @@ -316,8 +313,8 @@ XGrid(901, 3600, 'MINUTE:5:MINUTE:20:MINUTE:10:0:%R'), XGrid(3601, 18000, 'MINUTE:10:HOUR:1:MINUTE:30:0:%R'), XGrid(18001, 172800, 'MINUTE:30:HOUR:2:MINUTE:120:0:%R'), - XGrid(172801, None, 'HOUR:2:HOUR:8:HOUR:8:0:%R') - ] + XGrid(172801, None, 'HOUR:2:HOUR:8:HOUR:8:0:%R'), +] class GraphHrule(object): @@ -448,8 +445,7 @@ def get_graph_command(plugin_dir, start, end, gtype='CPU', destination='/tmp/rrd var_def_obj = graph_datadefs[_data_def][_cdef].vname else: var_def_obj = graph_calculations[_data_def].vname - var_def = VariableDefinition(var_name, - rpn="{0},{1}".format(var_def_obj, _var.name)) + var_def = VariableDefinition(var_name, rpn="{0},{1}".format(var_def_obj, _var.name)) graph_vars.append(var_def) graph_prints.append(GraphPrint(var_def, prints.fstring.format(_var.label))) diff --git a/taf/testlib/tempest_clients/__init__.py b/taf/testlib/tempest_clients/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/taf/testlib/tempest_clients/magnum/__init__.py b/taf/testlib/tempest_clients/magnum/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/taf/testlib/tempest_clients/magnum/clients/__init__.py b/taf/testlib/tempest_clients/magnum/clients/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/taf/testlib/tempest_clients/magnum/clients/cert_client.py b/taf/testlib/tempest_clients/magnum/clients/cert_client.py new file mode 100644 index 0000000..8ff89ba --- /dev/null +++ b/taf/testlib/tempest_clients/magnum/clients/cert_client.py @@ -0,0 +1,58 @@ +# 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. +# +# Based on OpenStack Magnum (https://github.com/openstack/magnum.git) + +from testlib.tempest_clients.magnum.models import cert_model +from testlib.tempest_clients.magnum.clients import client + + +class CertClient(client.MagnumClient): + """Encapsulates REST calls and maps JSON to/from models""" + + url = "/certificates" + + @classmethod + def cert_uri(cls, cluster_id): + """Construct cluster uri + + :param cluster_id: cluster uuid or name + :returns: url string + """ + + return "{0}/{1}".format(cls.url, cluster_id) + + def get_cert(self, cluster_id, **kwargs): + """Makes GET /certificates/cluster_id request and returns CertEntity + + Abstracts REST call to return a single cert based on uuid or name + + :param cluster_id: cluster uuid or name + :returns: response object and ClusterCollection object + """ + + resp, body = self.get(self.cert_uri(cluster_id)) + return self.deserialize(resp, body, cert_model.CertEntity) + + def post_cert(self, model, **kwargs): + """Makes POST /certificates request and returns CertEntity + + Abstracts REST call to sign new certificate + + :param model: CertEntity + :returns: response object and CertEntity object + """ + + resp, body = self.post( + CertClient.url, + body=model.to_json(), **kwargs) + return self.deserialize(resp, body, cert_model.CertEntity) diff --git a/taf/testlib/tempest_clients/magnum/clients/client.py b/taf/testlib/tempest_clients/magnum/clients/client.py new file mode 100644 index 0000000..5029382 --- /dev/null +++ b/taf/testlib/tempest_clients/magnum/clients/client.py @@ -0,0 +1,54 @@ +# 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 abc + +import six +from six.moves.urllib import parse +from tempest.lib.common import rest_client + +# from magnum.tests.functional.common import config +import tempest + + +@six.add_metaclass(abc.ABCMeta) +class MagnumClient(rest_client.RestClient): + """Abstract class responsible for setting up auth provider""" + + def __init__(self, auth_provider): + config = tempest.config.CONF + super(MagnumClient, self).__init__( + auth_provider=auth_provider, + service='container-infra', + region=config.network.region or config.identity.region, + disable_ssl_certificate_validation=True + ) + + @classmethod + def deserialize(cls, resp, body, model_type): + if isinstance(body, bytes): + body = body.decode() + return resp, model_type.from_json(body) + + @property + def tenant_id(self): + return self.client.tenant_id + + @classmethod + def add_filters(cls, url, filters): + """add_filters adds dict values (filters) to url as query parameters + + :param url: base URL for the request + :param filters: dict with var:val pairs to add as parameters to URL + :returns: url string + """ + return url + "?" + parse(filters) diff --git a/taf/testlib/tempest_clients/magnum/clients/cluster_client.py b/taf/testlib/tempest_clients/magnum/clients/cluster_client.py new file mode 100644 index 0000000..90da2a3 --- /dev/null +++ b/taf/testlib/tempest_clients/magnum/clients/cluster_client.py @@ -0,0 +1,175 @@ +# 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. +# +# Based on OpenStack Magnum (https://github.com/openstack/magnum.git) + +from oslo_log import log as logging +from tempest.lib import exceptions + +from testlib.tempest_clients.magnum.models import cluster_id_model +from testlib.tempest_clients.magnum.models import cluster_model +from testlib.tempest_clients.magnum.clients import client +from testlib.tempest_clients.magnum.common.utils import wait_for_condition + + +class ClusterClient(client.MagnumClient): + """Encapsulates REST calls and maps JSON to/from models""" + + LOG = logging.getLogger(__name__) + + @classmethod + def clusters_uri(cls, filters=None): + """Construct clusters uri with optional filters + + :param filters: Optional k:v dict that's converted to url query + :returns: url string + """ + + url = "/clusters" + if filters: + url = cls.add_filters(url, filters) + return url + + @classmethod + def cluster_uri(cls, cluster_id): + """Construct cluster uri + + :param cluster_id: cluster uuid or name + :returns: url string + """ + + return "{0}/{1}".format(cls.clusters_uri(), cluster_id) + + def list_clusters(self, filters=None, **kwargs): + """Makes GET /clusters request and returns ClusterCollection + + Abstracts REST call to return all clusters + + :param filters: Optional k:v dict that's converted to url query + :returns: response object and ClusterCollection object + """ + + resp, body = self.get(self.clusters_uri(filters), **kwargs) + return self.deserialize(resp, body, cluster_model.ClusterCollection) + + def get_cluster(self, cluster_id, **kwargs): + """Makes GET /cluster request and returns ClusterEntity + + Abstracts REST call to return a single cluster based on uuid or name + + :param cluster_id: cluster uuid or name + :returns: response object and ClusterCollection object + """ + + resp, body = self.get(self.cluster_uri(cluster_id)) + return self.deserialize(resp, body, cluster_model.ClusterEntity) + + def post_cluster(self, model, **kwargs): + """Makes POST /cluster request and returns ClusterIdEntity + + Abstracts REST call to create new cluster + + :param model: ClusterEntity + :returns: response object and ClusterIdEntity object + """ + + resp, body = self.post( + self.clusters_uri(), + body=model.to_json(), **kwargs) + return self.deserialize(resp, body, cluster_id_model.ClusterIdEntity) + + def patch_cluster(self, cluster_id, clusterpatch_listmodel, **kwargs): + """Makes PATCH /cluster request and returns ClusterIdEntity + + Abstracts REST call to update cluster attributes + + :param cluster_id: UUID of cluster + :param clusterpatch_listmodel: ClusterPatchCollection + :returns: response object and ClusterIdEntity object + """ + + resp, body = self.patch( + self.cluster_uri(cluster_id), + body=clusterpatch_listmodel.to_json(), **kwargs) + return self.deserialize(resp, body, cluster_id_model.ClusterIdEntity) + + def delete_cluster(self, cluster_id, **kwargs): + """Makes DELETE /cluster request and returns response object + + Abstracts REST call to delete cluster based on uuid or name + + :param cluster_id: UUID or name of cluster + :returns: response object + """ + + return self.delete(self.cluster_uri(cluster_id), **kwargs) + + def wait_for_cluster_to_delete(self, cluster_id): + wait_for_condition( + lambda: self.does_cluster_not_exist(cluster_id), 10, 600) + + def wait_for_created_cluster(self, cluster_id, delete_on_error=True): + try: + wait_for_condition( + lambda: self.does_cluster_exist(cluster_id), 10, 1800) + except Exception: + # In error state. Clean up the cluster id if desired + self.LOG.error('Cluster %s entered an exception state.' % + cluster_id) + if delete_on_error: + self.LOG.error('We will attempt to delete clusters now.') + self.delete_cluster(cluster_id) + self.wait_for_cluster_to_delete(cluster_id) + raise + + def wait_for_final_state(self, cluster_id): + wait_for_condition( + lambda: self.is_cluster_in_final_state(cluster_id), 10, 1800) + + def is_cluster_in_final_state(self, cluster_id): + try: + resp, model = self.get_cluster(cluster_id) + if model.status in ['CREATED', 'CREATE_COMPLETE', + 'ERROR', 'CREATE_FAILED']: + self.LOG.info('Cluster %s succeeded.' % cluster_id) + return True + else: + return False + except exceptions.NotFound: + self.LOG.warning('Cluster %s is not found.' % cluster_id) + return False + + def does_cluster_exist(self, cluster_id): + try: + resp, model = self.get_cluster(cluster_id) + if model.status in ['CREATED', 'CREATE_COMPLETE']: + self.LOG.info('Cluster %s is created.' % cluster_id) + return True + elif model.status in ['ERROR', 'CREATE_FAILED']: + self.LOG.error('Cluster %s is in fail state.' % + cluster_id) + raise exceptions.ServerFault( + "Got into an error condition: %s for %s" % + (model.status, cluster_id)) + else: + return False + except exceptions.NotFound: + self.LOG.warning('Cluster %s is not found.' % cluster_id) + return False + + def does_cluster_not_exist(self, cluster_id): + try: + self.get_cluster(cluster_id) + except exceptions.NotFound: + self.LOG.warning('Cluster %s is not found.' % cluster_id) + return True + return False diff --git a/taf/testlib/tempest_clients/magnum/clients/cluster_template_client.py b/taf/testlib/tempest_clients/magnum/clients/cluster_template_client.py new file mode 100644 index 0000000..0a5e7e1 --- /dev/null +++ b/taf/testlib/tempest_clients/magnum/clients/cluster_template_client.py @@ -0,0 +1,115 @@ +# 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. +# +# Based on OpenStack Magnum (https://github.com/openstack/magnum.git) + +from testlib.tempest_clients.magnum.models import cluster_template_model +from testlib.tempest_clients.magnum.clients import client + + +class ClusterTemplateClient(client.MagnumClient): + """Encapsulates REST calls and maps JSON to/from models""" + + @classmethod + def cluster_templates_uri(cls, filters=None): + """Construct clustertemplates uri with optional filters + + :param filters: Optional k:v dict that's converted to url query + :returns: url string + """ + + url = "/clustertemplates" + if filters: + url = cls.add_filters(url, filters) + return url + + @classmethod + def cluster_template_uri(cls, cluster_template_id): + """Construct cluster_template uri + + :param cluster_template_id: cluster_template uuid or name + :returns: url string + """ + + return "{0}/{1}".format(cls.cluster_templates_uri(), + cluster_template_id) + + def list_cluster_templates(self, filters=None, **kwargs): + """Makes GET /clustertemplates request + + Abstracts REST call to return all clustertemplates + + :param filters: Optional k:v dict that's converted to url query + :returns: response object and ClusterTemplateCollection object + """ + + resp, body = self.get(self.cluster_templates_uri(filters), **kwargs) + collection = cluster_template_model.ClusterTemplateCollection + return self.deserialize(resp, body, collection) + + def get_cluster_template(self, cluster_template_id, **kwargs): + """Makes GET /clustertemplate request and returns ClusterTemplateEntity + + Abstracts REST call to return a single clustertempalte based on uuid + or name + + :param cluster_template_id: clustertempalte uuid or name + :returns: response object and ClusterTemplateCollection object + """ + + resp, body = self.get(self.cluster_template_uri(cluster_template_id)) + return self.deserialize(resp, body, + cluster_template_model.ClusterTemplateEntity) + + def post_cluster_template(self, model, **kwargs): + """Makes POST /clustertemplate request + + Abstracts REST call to create new clustertemplate + + :param model: ClusterTemplateEntity + :returns: response object and ClusterTemplateEntity object + """ + + resp, body = self.post( + self.cluster_templates_uri(), + body=model.to_json(), **kwargs) + entity = cluster_template_model.ClusterTemplateEntity + return self.deserialize(resp, body, entity) + + def patch_cluster_template(self, cluster_template_id, + cluster_templatepatch_listmodel, **kwargs): + """Makes PATCH /clustertemplate and returns ClusterTemplateEntity + + Abstracts REST call to update clustertemplate attributes + + :param cluster_template_id: UUID of clustertemplate + :param cluster_templatepatch_listmodel: ClusterTemplatePatchCollection + :returns: response object and ClusterTemplateEntity object + """ + + resp, body = self.patch( + self.cluster_template_uri(cluster_template_id), + body=cluster_templatepatch_listmodel.to_json(), **kwargs) + return self.deserialize(resp, body, + cluster_template_model.ClusterTemplateEntity) + + def delete_cluster_template(self, cluster_template_id, **kwargs): + """Makes DELETE /clustertemplate request and returns response object + + Abstracts REST call to delete clustertemplate based on uuid or name + + :param cluster_template_id: UUID or name of clustertemplate + :returns: response object + """ + + return self.delete(self.cluster_template_uri(cluster_template_id), + **kwargs) diff --git a/taf/testlib/tempest_clients/magnum/clients/magnum_service_client.py b/taf/testlib/tempest_clients/magnum/clients/magnum_service_client.py new file mode 100644 index 0000000..152b641 --- /dev/null +++ b/taf/testlib/tempest_clients/magnum/clients/magnum_service_client.py @@ -0,0 +1,46 @@ +# 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. +# +# Based on OpenStack Magnum (https://github.com/openstack/magnum.git) + +from testlib.tempest_clients.magnum.models import magnum_service_model +from testlib.tempest_clients.magnum.clients import client + + +class MagnumServiceClient(client.MagnumClient): + """Encapsulates REST calls and maps JSON to/from models""" + + @classmethod + def magnum_service_uri(cls, filters=None): + """Construct magnum services uri with optional filters + + :param filters: Optional k:v dict that's converted to url query + :returns: url string + """ + + url = "/mservices" + if filters: + url = cls.add_filters(url, filters) + return url + + def magnum_service_list(self, filters=None, **kwargs): + """Makes GET /mservices request and returns MagnumServiceCollection + + Abstracts REST call to return all magnum services. + + :param filters: Optional k:v dict that's converted to url query + :returns: response object and MagnumServiceCollection object + """ + + resp, body = self.get(self.magnum_service_uri(filters), **kwargs) + return self.deserialize(resp, body, + magnum_service_model.MagnumServiceCollection) diff --git a/taf/testlib/tempest_clients/magnum/common/__init__.py b/taf/testlib/tempest_clients/magnum/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/taf/testlib/tempest_clients/magnum/common/utils.py b/taf/testlib/tempest_clients/magnum/common/utils.py new file mode 100644 index 0000000..83cd08e --- /dev/null +++ b/taf/testlib/tempest_clients/magnum/common/utils.py @@ -0,0 +1,111 @@ +# 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 collections +import functools +import inspect +import time +import types + + +def def_method(f, *args, **kwargs): + @functools.wraps(f) + def new_method(self): + return f(self, *args, **kwargs) + return new_method + + +def parameterized_class(cls): + """A class decorator for running parameterized test cases. + + Mark your class with @parameterized_class. + Mark your test cases with @parameterized. + """ + test_functions = inspect.getmembers(cls, predicate=inspect.ismethod) + for (name, f) in test_functions: + if name.startswith('test_') and not hasattr(f, '_test_data'): + continue + + # remove the original test function from the class + delattr(cls, name) + + # add a new test function to the class for each entry in f._test_data + for tag, args in f._test_data.items(): + new_name = "{0}_{1}".format(f.__name__, tag) + if hasattr(cls, new_name): + raise Exception( + "Parameterized test case '{0}.{1}' created from '{0}.{2}' " + "already exists".format(cls.__name__, new_name, name)) + + # Using `def new_method(self): f(self, **args)` is not sufficient + # (all new_methods use the same args value due to late binding). + # Instead, use this factory function. + new_method = def_method(f, **args) + + # To add a method to a class, available for all instances: + # MyClass.method = types.MethodType(f, None, MyClass) + setattr(cls, new_name, types.MethodType(new_method, None, cls)) + return cls + + +def parameterized(data): + """A function decorator for parameterized test cases. + + Example: + + @parameterized({ + 'zero': dict(val=0), + 'one': dict(val=1), + }) + def test_val(self, val): + self.assertEqual(val, self.get_val()) + + The above will generate two test cases: + `test_val_zero` which runs with val=0 + `test_val_one` which runs with val=1 + + :param data: A dictionary that looks like {tag: {arg1: val1, ...}} + """ + def wrapped(f): + f._test_data = data + return f + return wrapped + + +def wait_for_condition(condition, interval=1, timeout=40): + start_time = time.time() + end_time = time.time() + timeout + while time.time() < end_time: + result = condition() + if result: + return result + time.sleep(interval) + raise Exception(("Timed out after %s seconds. Started " + + "on %s and ended on %s") % (timeout, start_time, end_time)) + + +def memoized(func): + """A decorator to cache function's return value""" + cache = {} + + @functools.wraps(func) + def wrapper(*args): + if not isinstance(args, collections.Hashable): + # args is not cacheable. just call the function. + return func(*args) + if args in cache: + return cache[args] + else: + value = func(*args) + cache[args] = value + return value + return wrapper diff --git a/taf/testlib/tempest_clients/magnum/models/__init__.py b/taf/testlib/tempest_clients/magnum/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/taf/testlib/tempest_clients/magnum/models/cert_model.py b/taf/testlib/tempest_clients/magnum/models/cert_model.py new file mode 100644 index 0000000..e3ce2e6 --- /dev/null +++ b/taf/testlib/tempest_clients/magnum/models/cert_model.py @@ -0,0 +1,24 @@ +# 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. + +from testlib.tempest_clients.magnum.models import models + + +class CertData(models.BaseModel): + """Data that encapsulates cert attributes""" + pass + + +class CertEntity(models.EntityModel): + """Entity Model that represents a single instance of CertData""" + ENTITY_NAME = 'certificate' + MODEL_TYPE = CertData diff --git a/taf/testlib/tempest_clients/magnum/models/cluster_id_model.py b/taf/testlib/tempest_clients/magnum/models/cluster_id_model.py new file mode 100644 index 0000000..ca1856b --- /dev/null +++ b/taf/testlib/tempest_clients/magnum/models/cluster_id_model.py @@ -0,0 +1,24 @@ +# 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. + +from testlib.tempest_clients.magnum.models import models + + +class ClusterIdData(models.BaseModel): + """Data that encapsulates ClusterId attributes""" + pass + + +class ClusterIdEntity(models.EntityModel): + """Entity Model that represents a single instance of CertData""" + ENTITY_NAME = 'clusterid' + MODEL_TYPE = ClusterIdData diff --git a/taf/testlib/tempest_clients/magnum/models/cluster_model.py b/taf/testlib/tempest_clients/magnum/models/cluster_model.py new file mode 100644 index 0000000..8db2349 --- /dev/null +++ b/taf/testlib/tempest_clients/magnum/models/cluster_model.py @@ -0,0 +1,30 @@ +# 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. + +from testlib.tempest_clients.magnum.models import models + + +class ClusterData(models.BaseModel): + """Data that encapsulates cluster attributes""" + pass + + +class ClusterEntity(models.EntityModel): + """Entity Model that represents a single instance of ClusterData""" + ENTITY_NAME = 'cluster' + MODEL_TYPE = ClusterData + + +class ClusterCollection(models.CollectionModel): + """Collection Model that represents a list of ClusterData objects""" + COLLECTION_NAME = 'clusterlists' + MODEL_TYPE = ClusterData diff --git a/taf/testlib/tempest_clients/magnum/models/cluster_template_model.py b/taf/testlib/tempest_clients/magnum/models/cluster_template_model.py new file mode 100644 index 0000000..495a6fc --- /dev/null +++ b/taf/testlib/tempest_clients/magnum/models/cluster_template_model.py @@ -0,0 +1,30 @@ +# 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. + +from testlib.tempest_clients.magnum.models import models + + +class ClusterTemplateData(models.BaseModel): + """Data that encapsulates clustertemplate attributes""" + pass + + +class ClusterTemplateEntity(models.EntityModel): + """Entity Model that represents a single instance of ClusterTemplateData""" + ENTITY_NAME = 'clustertemplate' + MODEL_TYPE = ClusterTemplateData + + +class ClusterTemplateCollection(models.CollectionModel): + """Collection that represents a list of ClusterTemplateData objects""" + COLLECTION_NAME = 'clustertemplatelists' + MODEL_TYPE = ClusterTemplateData diff --git a/taf/testlib/tempest_clients/magnum/models/cluster_templatepatch_model.py b/taf/testlib/tempest_clients/magnum/models/cluster_templatepatch_model.py new file mode 100644 index 0000000..50380c8 --- /dev/null +++ b/taf/testlib/tempest_clients/magnum/models/cluster_templatepatch_model.py @@ -0,0 +1,77 @@ +# 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 + +from testlib.tempest_clients.magnum.models import models + + +class ClusterTemplatePatchData(models.BaseModel): + """Data that encapsulates clustertemplatepatch attributes""" + pass + + +class ClusterTemplatePatchEntity(models.EntityModel): + """Model that represents a single instance of ClusterTemplatePatchData""" + ENTITY_NAME = 'clustertemplatepatch' + MODEL_TYPE = ClusterTemplatePatchData + + +class ClusterTemplatePatchCollection(models.CollectionModel): + """Model that represents a list of ClusterTemplatePatchData objects""" + MODEL_TYPE = ClusterTemplatePatchData + COLLECTION_NAME = 'clustertemplatepatchlist' + + def to_json(self): + """Converts ClusterTemplatePatchCollection to json + + Retrieves list from COLLECTION_NAME attribute and converts each object + to dict, appending it to a list. Then converts the entire list to + json + + This is required due to COLLECTION_NAME holding a list of objects that + needed to be converted to dict individually + + :returns: json object + """ + + data = getattr(self, ClusterTemplatePatchCollection.COLLECTION_NAME) + collection = [] + for d in data: + collection.append(d.to_dict()) + return json.dumps(collection) + + @classmethod + def from_dict(cls, data): + """Converts dict to ClusterTemplatePatchData + + Converts data dict to list of ClusterTemplatePatchData objects and + stores it in COLLECTION_NAME + + Example of dict data: + + [{ + "path": "/name", + "value": "myname", + "op": "replace" + }] + + :param data: dict of patch data + :returns: json object + """ + + model = cls() + collection = [] + for d in data: + collection.append(cls.MODEL_TYPE.from_dict(d)) + setattr(model, cls.COLLECTION_NAME, collection) + return model diff --git a/taf/testlib/tempest_clients/magnum/models/clusterpatch_model.py b/taf/testlib/tempest_clients/magnum/models/clusterpatch_model.py new file mode 100644 index 0000000..a9047e1 --- /dev/null +++ b/taf/testlib/tempest_clients/magnum/models/clusterpatch_model.py @@ -0,0 +1,76 @@ +# 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 + +from testlib.tempest_clients.magnum.models import models + + +class ClusterPatchData(models.BaseModel): + """Data that encapsulates clusterpatch attributes""" + pass + + +class ClusterPatchEntity(models.EntityModel): + """Entity Model that represents a single instance of ClusterPatchData""" + ENTITY_NAME = 'clusterpatch' + MODEL_TYPE = ClusterPatchData + + +class ClusterPatchCollection(models.CollectionModel): + """Collection Model that represents a list of ClusterPatchData objects""" + MODEL_TYPE = ClusterPatchData + COLLECTION_NAME = 'clusterpatchlist' + + def to_json(self): + """Converts ClusterPatchCollection to json + + Retrieves list from COLLECTION_NAME attribute and converts each object + to dict, appending it to a list. Then converts the entire list to json + + This is required due to COLLECTION_NAME holding a list of objects that + needed to be converted to dict individually + + :returns: json object + """ + + data = getattr(self, ClusterPatchCollection.COLLECTION_NAME) + collection = [] + for d in data: + collection.append(d.to_dict()) + return json.dumps(collection) + + @classmethod + def from_dict(cls, data): + """Converts dict to ClusterPatchData + + Converts data dict to list of ClusterPatchData objects and stores it + in COLLECTION_NAME + + Example of dict data: + + [{ + "path": "/name", + "value": "myname", + "op": "replace" + }] + + :param data: dict of patch data + :returns: json object + """ + + model = cls() + collection = [] + for d in data: + collection.append(cls.MODEL_TYPE.from_dict(d)) + setattr(model, cls.COLLECTION_NAME, collection) + return model diff --git a/taf/testlib/tempest_clients/magnum/models/magnum_service_model.py b/taf/testlib/tempest_clients/magnum/models/magnum_service_model.py new file mode 100644 index 0000000..1fcc793 --- /dev/null +++ b/taf/testlib/tempest_clients/magnum/models/magnum_service_model.py @@ -0,0 +1,30 @@ +# 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. + +from testlib.tempest_clients.magnum.models import models + + +class MagnumServiceData(models.BaseModel): + """Data that encapsulates magnum_service attributes""" + pass + + +class MagnumServiceEntity(models.EntityModel): + """Entity Model that represents a single instance of MagnumServiceData""" + ENTITY_NAME = 'mservice' + MODEL_TYPE = MagnumServiceData + + +class MagnumServiceCollection(models.CollectionModel): + """Collection Model that represents a list of MagnumServiceData objects""" + COLLECTION_NAME = 'mservicelists' + MODEL_TYPE = MagnumServiceData diff --git a/taf/testlib/tempest_clients/magnum/models/models.py b/taf/testlib/tempest_clients/magnum/models/models.py new file mode 100644 index 0000000..3f8bdd8 --- /dev/null +++ b/taf/testlib/tempest_clients/magnum/models/models.py @@ -0,0 +1,78 @@ +# 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. + +# pylint: disable=E1101 + +import json + + +class BaseModel(object): + """Superclass Responsible for converting json data to/from model""" + + MODEL_TYPE = object + + @classmethod + def from_json(cls, json_str): + return cls.from_dict(json.loads(json_str)) + + def to_json(self): + return json.dumps(self.to_dict()) + + @classmethod + def from_dict(cls, data): + model = cls() + for key in data: + setattr(model, key, data.get(key)) + return model + + def to_dict(self): + result = {} + for key in self.__dict__: + result[key] = getattr(self, key) + if isinstance(result[key], BaseModel): + result[key] = result[key].to_dict() + return result + + def __str__(self): + return "%s" % self.to_dict() + + +class EntityModel(BaseModel): + """Superclass resposible from converting dict to instance of model""" + + ENTITY_NAME = "" + + @classmethod + def from_dict(cls, data): + model = super(EntityModel, cls).from_dict(data) + if hasattr(model, cls.ENTITY_NAME): + val = getattr(model, cls.ENTITY_NAME) + setattr(model, cls.ENTITY_NAME, cls.MODEL_TYPE.from_dict(val)) + return model + + +class CollectionModel(BaseModel): + """Superclass resposible from converting dict to list of models""" + + COLLECTION_NAME = "" + + @classmethod + def from_dict(cls, data): + model = super(CollectionModel, cls).from_dict(data) + + collection = [] + if hasattr(model, cls.COLLECTION_NAME): + for d in getattr(model, cls.COLLECTION_NAME): + collection.append(cls.MODEL_TYPE.from_dict(d)) + setattr(model, cls.COLLECTION_NAME, collection) + + return model diff --git a/taf/testlib/sfc_client.py b/taf/testlib/tempest_clients/sfc_client.py similarity index 99% rename from taf/testlib/sfc_client.py rename to taf/testlib/tempest_clients/sfc_client.py index 1a08b82..89dfc61 100644 --- a/taf/testlib/sfc_client.py +++ b/taf/testlib/tempest_clients/sfc_client.py @@ -28,6 +28,7 @@ # by OpenStack. It is has very limited scope - only inside one line function. # pylint: disable=W0622,C0103 + class SfcClient(base.BaseNetworkClient): base_uri = '/sfc' diff --git a/taf/testlib/ui_onpss_jsonrpc.py b/taf/testlib/ui_onpss_jsonrpc.py index bb375e1..58f649a 100755 --- a/taf/testlib/ui_onpss_jsonrpc.py +++ b/taf/testlib/ui_onpss_jsonrpc.py @@ -60,10 +60,10 @@ def __init__(self, switch): # GAMI API doesn't support creating LAG without members # Initialize LAG map self.lags = [] - self.rest_server_service = service_lib.specific_service_manager_factory( - "psme-rest-server", self.cli_send_command) - self.network_agent_service = service_lib.specific_service_manager_factory( - "psme-network", self.cli_send_command) + self.rest_server_service = service_lib.SpecificServiceManager("psme-rest-server", + self.cli_send_command) + self.network_agent_service = service_lib.SpecificServiceManager("psme-network", + self.cli_send_command) def _get_subcomponents_uuid(self): """ @@ -549,7 +549,7 @@ def get_table_ports(self, ports=None, all_params=False): "pvpt": None, "master": None, "type": port["portClass"]} - except KeyError as err: + except KeyError: raise UIException("Command 'getEthernetSwitchPortInfo' returned incorrect reply: {0}".format(port)) else: port_table.append(port_attr) diff --git a/taf/testlib/ui_onpss_shell/ui_onpss_shell.py b/taf/testlib/ui_onpss_shell/ui_onpss_shell.py index b87e8d0..8c8835f 100755 --- a/taf/testlib/ui_onpss_shell/ui_onpss_shell.py +++ b/taf/testlib/ui_onpss_shell/ui_onpss_shell.py @@ -1274,8 +1274,7 @@ def start_dcrp_with_given_mesh_ports(self, mesh_ports=None, timeout=30, mlag_con self.cli_send_command(cpp_set_cmd) self.cli_send_command(port_set_cmd) - dcrp_srvc_manager = service_lib.specific_service_manager_factory( - self.DCRP_SRVC, self.cli_send_command) + dcrp_srvc_manager = service_lib.SpecificServiceManager(self.DCRP_SRVC, self.cli_send_command) dcrp_srvc_manager.restart(expected_rcs={0}) # wait timeout until all daemons are running @@ -1293,7 +1292,7 @@ def stop_dcrp(self): @brief Stopping DCRP service @return: None """ - dcrp_srvc_manager = service_lib.specific_service_manager_factory( + dcrp_srvc_manager = service_lib.SpecificServiceManager( self.DCRP_SRVC, self.cli_send_command) dcrp_srvc_manager.stop(expected_rcs={0}) diff --git a/taf/testlib/virtual_env.py b/taf/testlib/virtual_env.py index 78e0847..03e9f9b 100644 --- a/taf/testlib/virtual_env.py +++ b/taf/testlib/virtual_env.py @@ -40,6 +40,7 @@ import pprint import itertools from functools import wraps +import traceback import netaddr import pytest @@ -69,6 +70,19 @@ def wrapper(self, *args, **kwargs): return decorator +def only_with_service(service): + def decorator(func): + @wraps(func) + def wrapper(self, *args, **kwargs): + if self.has_service(service): + return func(self, *args, **kwargs) + else: + self.class_logger.error(('Function %s called that expects %s service ' + ' which is not available.'), func.__name__, service) + return wrapper + return decorator + + class VirtualEnv(object): """ @description Main class of all test virtual environment using tempest. @@ -90,25 +104,26 @@ class VirtualEnv(object): OVS_FLAVOR_SPEC = merge_dicts( _DEFAULT_FLAVOR_SPEC, - {'name': 'venv-ovs-flavor'} + {'name': 'venv-ovs-flavor'}, ) DPDK_FLAVOR_SPEC = merge_dicts( _DEFAULT_FLAVOR_SPEC, DPDK_EXTRA_SPECS, - {'name': 'venv-dpdk-flavor'} + {'name': 'venv-dpdk-flavor'}, ) # Example: my_vIPS_image-fedora-bare.qcow2 IMAGE_NAME_PATTERN = r'\S*{0}\S*-(?P\w+)-(?P\w+)\.(?P\w+)' - def __init__(self, opts=None): + def __init__(self, opts=None, external_router=True): super(VirtualEnv, self).__init__() self.class_logger.info('Initializing virtual environment...') self.opts = opts - self.settings = self._get_settings(self.opts.env) + self.env_settings = self._get_settings(self.opts.env) self.tempest_path = self.opts.tempest_path self.reuse_venv = self.opts.reuse_venv self.neutron_extensions = None + self.services = None import tempest from tempest.scenario.manager import NetworkScenarioTest @@ -134,13 +149,16 @@ def __init__(self, opts=None): if self.has_neutron_extension('sfc'): self._add_sfc_client() - self.other_config = self.settings.get('other_configs', {}) + if self.has_service('magnum'): + self._add_magnum_clients() + + self.other_config = self.env_settings.get('other_configs', {}) self.ovs_type = self.other_config.get('ovs_type') # create initial parameters required by all instances - self.images_path = self.settings.get("images_share_path") + self.images_path = self.env_settings.get("images_share_path") self.create_loginable_secgroup_rule() - self.key = self.handle.create_keypair() + self.ensure_keypair() self.tenant_id = self.handle.networks_client.tenant_id _default_spec = self.DPDK_FLAVOR_SPEC if self.is_DPDK() else self.OVS_FLAVOR_SPEC @@ -150,7 +168,8 @@ def __init__(self, opts=None): public_access_kwargs = { 'try_reuse': self.reuse_venv, 'name': self.tempest_lib.common.utils.data_utils.rand_name('tempest-public-net'), - 'tenant_id': self.tenant_id + 'tenant_id': self.tenant_id, + 'create_external_router': external_router, } assert self.ensure_public_access(**public_access_kwargs) @@ -162,20 +181,63 @@ def __init__(self, opts=None): entry_name = custom_classes[entry_type]['NAME'] setattr(self, entry_name, getattr(self.env, entry_name)) + def ensure_keypair(self): + generate = False + key = {} + try: + keyfile = self.env_settings['ssh_key'] + pubkeyfile = self.env_settings['ssh_pubkey'] + except KeyError: + self.class_logger.debug("ssh key not provided, a new will be generated") + generate = True + else: + self.class_logger.debug("Reading private key from %s", keyfile) + self.class_logger.debug("Reading public key from %s", pubkeyfile) + try: + with open(keyfile, 'r') as stream: + key['private_key'] = stream.read() + with open(pubkeyfile, 'r') as stream: + key['public_key'] = stream.read() + except IOError as e: + self.class_logger.info("ssh key provided but can't be used! (%s)", format(e)) + generate = True + else: + client = self.handle.manager.keypairs_client + name = self.tempest_lib.common.utils.data_utils.rand_name('tempest-key') + self.key = client.create_keypair(name=name, + public_key=key['public_key'])['keypair'] + self.key['private_key'] = key['private_key'] + + if generate: + self.key = self.handle.create_keypair() + + assert 'private_key' in self.key + assert 'public_key' in self.key + def _get_neutron_extensions(self): if self.neutron_extensions is None: client = self.handle.admin_manager.network_extensions_client self.neutron_extensions = client.list_extensions()['extensions'] return self.neutron_extensions + def _get_services(self): + if self.services is None: + client = self.handle.admin_manager.identity_services_client + self.services = client.list_services()['OS-KSADM:services'] + return self.services + + def has_service(self, service): + services = self._get_services() + return service in [s['name'] for s in services] + def has_neutron_extension(self, extension): extensions = self._get_neutron_extensions() return extension in [e['alias'] for e in extensions] def _add_sfc_client(self): - from testlib.sfc_client import SfcClient + from testlib.tempest_clients.sfc_client import SfcClient - self.class_logger.debug('Adding SfcClient.') + self.class_logger.info('Adding SfcClient.') try: # FIXME: do I need admin_manager? @@ -188,7 +250,12 @@ def _add_sfc_client(self): build_timeout=self.config.network.build_timeout, **self.handle.admin_manager.default_params) except Exception: - self.class_logger.warning('Could not create sfc client!') + self.class_logger.exception('Could not create sfc client!') + + def _add_magnum_clients(self): + from .magnum import Magnum + self.class_logger.info('Adding Magnum clients.') + self.magnum = Magnum(self) def _get_settings(self, file_name=None): """ @@ -249,7 +316,7 @@ def wait_for_server_status(self, vm_id, status): return waiters.wait_for_server_status(self.handle.servers_client, vm_id, status) def ensure_public_access(self, try_reuse=False, networks_client=None, routers_client=None, - name=None, tenant_id=None): + name=None, tenant_id=None, create_external_router=True): """Create or reuse public/external router & network. :param bool try_reuse: attempt at resusing the public router/network or delete it @@ -271,7 +338,7 @@ def ensure_public_access(self, try_reuse=False, networks_client=None, routers_cl tenant_id = self.tenant_id _net_cfg = self.config.network - _mgmt_ip_cidr = self.settings.get('mgmt_ip_cidr') + _mgmt_ip_cidr = self.env_settings.get('mgmt_ip_cidr') assert _mgmt_ip_cidr net_ip = netaddr.IPNetwork(_mgmt_ip_cidr) @@ -287,7 +354,7 @@ def ensure_public_access(self, try_reuse=False, networks_client=None, routers_cl 'networks_client': networks_client, 'delete_external': True, 'name': name, - 'tenant_id': tenant_id + 'tenant_id': tenant_id, } public_network = self.create_public_network(**public_network_kwargs) assert public_network @@ -304,26 +371,24 @@ def ensure_public_access(self, try_reuse=False, networks_client=None, routers_cl 'start': '{}.100'.format(allocation_prefix), 'end': '{}.254'.format(allocation_prefix)}], 'gateway_ip': net_ip.ip, - 'enable_dhcp': False + 'enable_dhcp': False, } self._create_subnet(public_network['id'], **subnet_kwargs) _net_cfg.public_network_id = public_network['id'] - router_kwargs = { - 'routers_client': routers_client, - 'network_id': _net_cfg.public_network_id, - 'tenant_id': tenant_id, - 'enable_snat': True - } - public_router = self.create_router(**router_kwargs) - assert public_router - _net_cfg.public_router_id = public_router['id'] - - if _net_cfg.public_network_id and _net_cfg.public_router_id: - return True + if create_external_router: + router_kwargs = { + 'routers_client': routers_client, + 'network_id': _net_cfg.public_network_id, + 'tenant_id': tenant_id, + 'enable_snat': True, + } + public_router = self.create_router(**router_kwargs) + assert public_router + _net_cfg.public_router_id = public_router['id'] - return False + return _net_cfg.public_network_id and (not create_external_router or _net_cfg.public_router_id) def _get_external_elements(self, routers_client=None): if not routers_client: @@ -410,8 +475,8 @@ def create_router(self, routers_client=None, name=None, network_id=None, tenant_ { 'name': name, 'tenant_id': tenant_id, - 'external_gateway_info': ext_gw_info - } + 'external_gateway_info': ext_gw_info, + }, ) router_resp = routers_client.create_router(**router_kwargs) @@ -440,7 +505,7 @@ def delete_router(self, router_id, routers_client=None, ports_client=None): clients = { 'routers_client': routers_client, - 'ports_client': ports_client + 'ports_client': ports_client, } self._remove_router_interfaces(router_id, **clients) @@ -499,13 +564,13 @@ def create_network(self, with_router=False, with_subnet=False, name=None, tenant kwargs, { 'name': name, - 'tenant_id': tenant_id - } + 'tenant_id': tenant_id, + }, ) subnet_kwargs = { 'name': self.tempest_lib.common.utils.data_utils.rand_name('tempest-subnet'), 'tenant_id': tenant_id, - 'enable_dhcp': True + 'enable_dhcp': True, } subnet = None @@ -538,8 +603,8 @@ def create_port(self, network_id, name=None, ports_client=None, tenant_id=None, { 'name': name, 'tenant_id': tenant_id, - 'network_id': network_id - } + 'network_id': network_id, + }, ) port = ports_client.create_port(**port_kwargs)['port'] @@ -577,7 +642,7 @@ def create_public_network(self, routers_client=None, networks_client=None, ports del_external_kwargs = { 'routers_client': routers_client, 'networks_client': networks_client, - 'ports_client': ports_client + 'ports_client': ports_client, } self._delete_external_elements(**del_external_kwargs) @@ -610,7 +675,7 @@ def _create_bare_network(self, networks_client=None, name=None, tenant_id=None, { 'name': name, 'tenant_id': tenant_id, - } + }, ) network_resp = networks_client.create_network(**network_kwargs) network = network_resp['network'] @@ -686,7 +751,7 @@ def alloc_pools(cidr, start, end): if hosts: pool = { 'start': hosts[0], - 'end': hosts[-1] + 'end': hosts[-1], } allocation_pools.update({'allocation_pools': [pool]}) @@ -699,7 +764,7 @@ def alloc_pools(cidr, start, end): 'name': name, 'tenant_id': tenant_id, 'ip_version': 4, - } + }, ) from tempest.lib import exceptions as lib_exc try: @@ -733,7 +798,7 @@ def create_image(self, name, fmt, path, disk_format=None): params = { 'name': name, 'container_format': fmt, - 'disk_format': disk_format if disk_format else fmt + 'disk_format': disk_format if disk_format else fmt, } image = image_client.create_image(**params) assert image['status'] == "queued" @@ -788,7 +853,7 @@ def create_flavor(self, name, ram=64, disk=0, vcpus=1, **kwargs): 'name': name, 'ram': ram, 'disk': disk, - 'vcpus': vcpus + 'vcpus': vcpus, } flavor_id = kwargs.pop('flavor_id', '') @@ -921,7 +986,7 @@ def allow_forwarding(self, server_id): kwargs = { 'security_groups': [], - 'port_security_enabled': False + 'port_security_enabled': False, } for a_port_id in port_ids: @@ -1041,7 +1106,7 @@ def create_aggregate(self, name="aggr", availability_zone="zone"): zone_name = "{}-{}".format(availability_zone, int(time.time())) kwargs = { 'name': aggregate_name, - 'availability_zone': zone_name + 'availability_zone': zone_name, } self.class_logger.info("Creating new aggregate %s.", aggregate_name) @@ -1115,7 +1180,7 @@ def create_server(self, nets=None, ports=None, zone=None, image=None, flavor=Non 'instance_type': 'instance', 'ipaddr': None, 'ssh_port': image.get('ssh_port', 22), - 'ssh_user': image.get('ssh_user', 'root') + 'ssh_user': image.get('ssh_user', 'root'), } _ssh_pass = image.get('ssh_pass') if _ssh_pass: @@ -1235,3 +1300,40 @@ def create_security_group_rule(self, **kwargs): def delete_security_group_rule(self, rule_id): return self.handle.manager.security_group_rules_client.delete_security_group_rule(rule_id) + + def get_all_instances(self): + client = self.handle.manager.servers_client + + instances = [self.get_nova_instance(i['id']) + for i in client.list_servers()['servers']] + + return instances + + def get_host_instance_dict(self, instances=None): + """ + gets a dictionary of details of all instances in the current project grouped by hostId. + """ + if instances is None: + instances = self.get_all_instances() + + res = {} + for x in instances: + res.setdefault(x['hostId'], []).append(x) + + return res + + def get_services(self, service_filter=None): + client = self.handle.admin_manager.services_client + services = client.list_services()['services'] + if service_filter is None: + return services + return filter(service_filter, services) # pylint: disable=bad-builtin + + def enable_service(self, service): + client = self.handle.admin_manager.services_client + client.enable_service(host=service['host'], binary=service['binary']) + + def disable_service(self, service): + client = self.handle.admin_manager.services_client + client.disable_service(host=service['host'], binary=service['binary']) + self.handle.addCleanup(self.enable_service, service) diff --git a/unittests/linux/test_tool_general.py b/unittests/linux/test_tool_general.py index 15c1ce2..72cf392 100644 --- a/unittests/linux/test_tool_general.py +++ b/unittests/linux/test_tool_general.py @@ -177,7 +177,7 @@ def gen_tool(request, lh): @pytest.fixture def systemctl(gen_tool): - service_factory = service_lib.specific_service_manager_factory + service_factory = service_lib.SpecificServiceManager systemctl = service_factory(SERVICE_NAME, gen_tool.run_command) return systemctl diff --git a/unittests/test_ab_parser.py b/unittests/test_ab_parser.py new file mode 100644 index 0000000..8f04ea1 --- /dev/null +++ b/unittests/test_ab_parser.py @@ -0,0 +1,191 @@ +""" +@copyright Copyright (c) 2016 - 2017, Intel Corporation. + +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. + +@file: test_ab_parser.py + +""" +from utils.ab_parser import AbParser, AbAggregator + +APACHEBENCH_OUTPUT_1 = """ +This is ApacheBench, Version 2.3 <$Revision: 1748469 $> +Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ +Licensed to The Apache Software Foundation, http://www.apache.org/ + +Benchmarking 10.100.90.2 (be patient) + + +Server Software: nginx/1.8.1 +Server Hostname: 10.100.90.2 +Server Port: 80 + +Document Path: /4_mb_file +Document Length: 4158056 bytes + +Concurrency Level: 10 +Time taken for tests: 43.941 seconds +Complete requests: 999 +Failed requests: 0 +Total transferred: 4154151690 bytes +HTML transferred: 4153897944 bytes +Requests per second: 22.74 [#/sec] (mean) +Time per request: 439.848 [ms] (mean) +Time per request: 43.985 [ms] (mean, across all concurrent requests) +Transfer rate: 92323.99 [Kbytes/sec] received + +Connection Times (ms) + min mean[+/-sd] median max +Connect: 1 5 2.3 4 36 +Processing: 211 434 77.6 428 975 +Waiting: 1 5 5.8 4 55 +Total: 213 438 78.2 433 983 + +Percentage of the requests served within a certain time (ms) + 50% 433 + 66% 455 + 75% 471 + 80% 482 + 90% 526 + 95% 564 + 98% 622 + 99% 712 + 100% 983 (longest request) +""" + +APACHEBENCH_OUTPUT_2 = """ +This is ApacheBench, Version 2.3 <$Revision: 1748469 $> +Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ +Licensed to The Apache Software Foundation, http://www.apache.org/ + +Benchmarking 10.100.71.2 (be patient) + + +Server Software: nginx/1.8.1 +Server Hostname: 10.100.71.2 +Server Port: 80 + +Document Path: /4_mb_file +Document Length: 4158056 bytes + +Concurrency Level: 10 +Time taken for tests: 24.851 seconds +Complete requests: 999 +Failed requests: 0 +Total transferred: 4154151690 bytes +HTML transferred: 4153897944 bytes +Requests per second: 40.20 [#/sec] (mean) +Time per request: 248.762 [ms] (mean) +Time per request: 24.876 [ms] (mean, across all concurrent requests) +Transfer rate: 163242.62 [Kbytes/sec] received + +Connection Times (ms) + min mean[+/-sd] median max +Connect: 1 4 1.2 4 10 +Processing: 115 244 42.1 240 462 +Waiting: 1 5 3.0 4 38 +Total: 119 248 42.0 243 467 + +Percentage of the requests served within a certain time (ms) + 50% 243 + 66% 261 + 75% 272 + 80% 279 + 90% 298 + 95% 313 + 98% 349 + 99% 374 + 100% 467 (longest request) +""" + + +class TestAbParser(object): + + def test_parsing_of_single_ab_output(self): + ab_parser = AbParser() + + ab_parsed = ab_parser.parse(APACHEBENCH_OUTPUT_1) + + assert ab_parsed == { + 'complete_requests': '999', + 'concurency_level': '10', + 'connect_times': { + 'max': '36', + 'mean': '5', + 'median': '4', + 'min': '1', + 'sd': '2.3', + }, + 'document_length': '4158056', + 'document_path': '/4_mb_file', + 'failed_requests': '0', + 'html_transferred': '4153897944', + 'processing_times': { + 'max': '975', + 'mean': '434', + 'median': '428', + 'min': '211', + 'sd': '77.6', + }, + 'requests_per_second': '22.74', + 'server_hostname': '10.100.90.2', + 'server_port': '80', + 'server_software': 'nginx/1.8.1', + 'time_per_request_mean': '439.848', + 'time_per_request_mean_all': '43.985', + 'time_taken_for_tests': '43.941', + 'total_times': { + 'max': '983', + 'mean': '438', + 'median': '433', + 'min': '213', + 'sd': '78.2', + }, + 'total_transferred': '4154151690', + 'transfer_rate': '92323.99', + 'waiting_times': { + 'max': '55', + 'mean': '5', + 'median': '4', + 'min': '1', + 'sd': '5.8', + }, + } + + def test_aggregation(self): + parser = AbParser() + aggregator = AbAggregator() + aggregator += parser.parse(APACHEBENCH_OUTPUT_1) + aggregator += parser.parse(APACHEBENCH_OUTPUT_2) + + expected_result = { + 'complete_requests': 1998, + 'concurency_level': 20, + 'connect_times': {'max': 36.0, 'min': 1.0}, + 'document_length': '4158056', + 'document_path': '/4_mb_file', + 'failed_requests': 0, + 'html_transferred': 8307795888, + 'number_of_clients': 2, + 'processing_times': {'max': 975.0, 'min': 115.0}, + 'requests_per_second': 62.94, + 'server_port': '80', + 'server_software': 'nginx/1.8.1', + 'time_taken_for_tests': 68.792, + 'total_times': {'max': 983.0, 'min': 119.0}, + 'total_transferred': 8308303380, + 'transfer_rate': 255566.61, + 'waiting_times': {'max': 55.0, 'min': 1.0}, + } + + assert aggregator == expected_result diff --git a/unittests/test_service_lib.py b/unittests/test_service_lib.py index a9786bc..483ac3f 100644 --- a/unittests/test_service_lib.py +++ b/unittests/test_service_lib.py @@ -30,41 +30,31 @@ class TestSystemd(unittest.TestCase): def setUp(self): self.service_name = "fake_service" - init_name = "systemd" - command_generator = service_lib._command_generators[init_name] + return_codes = service_lib.SystemdReturnCodes self.service_command_generator = service_lib.ServiceCommandGenerator( - command_generator) + service_lib.systemd_command_generator, + return_codes + ) def test_all_commands(self): - for cmd in (c for c in self.service_command_generator.commands if c != "list"): + for cmd in (c for c in self.service_command_generator.commands + if c not in ["list", "daemon_reload"]): ret = getattr( self.service_command_generator, cmd)(self.service_name) - if cmd == "is_enabled": - cmd = "is-enabled" - elif cmd == "is_active": - cmd = "is-active" - assert ret == ["systemctl", cmd, "%s.service" % self.service_name] + assert ret == ["systemctl", cmd.replace('_', '-'), "%s.service" % self.service_name] class TestSpecificServiceManager(unittest.TestCase): def setUp(self): self.run_mock = MagicMock() - self.init_name = "systemd" - command_generator = service_lib.systemd_command_generator - command_list = [c for c in service_lib.COMMANDS if c != "list"] - service_command_generator = service_lib.ServiceCommandGenerator( - command_generator, command_list) - self.service_manager = service_lib.SpecificServiceManager("lldpad", - service_command_generator, - self.run_mock) + self.service_manager = service_lib.SpecificServiceManager("lldpad", self.run_mock) def test_start(self): service = "lldpad" # should really use --generated-members, but start() is too generic self.service_manager.start() # pylint: disable=no-member - assert self.run_mock.call_args[0][ - 0] == "systemctl start %s.service" % service + assert self.run_mock.call_args[0][0] == "systemctl start %s.service" % service def test_stop_with_args(self): service = "lldpad" @@ -73,7 +63,7 @@ def test_stop_with_args(self): 0] == "systemctl stop %s.service" % service assert self.run_mock.call_args[1] == {'ignore_status': True} - def test_list_is_not_present_in_SpecifcServiceManager(self): + def test_list_is_not_present_in_SpecificServiceManager(self): assert not hasattr(self.service_manager, "list") @@ -81,13 +71,7 @@ class TestSystemdServiceManager(unittest.TestCase): def setUp(self): self.run_mock = MagicMock() - self.init_name = "systemd" - command_generator = service_lib.systemd_command_generator - service_manager = service_lib.SystemdServiceManager - service_command_generator = service_lib.ServiceCommandGenerator( - command_generator) - self.service_manager = service_manager( - service_command_generator, self.run_mock) + self.service_manager = service_lib.SystemdServiceManager(self.run_mock) def test_start(self): service = "lldpad" diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/ab_parser.py b/utils/ab_parser.py new file mode 100644 index 0000000..4ec0f49 --- /dev/null +++ b/utils/ab_parser.py @@ -0,0 +1,227 @@ +""" +@copyright Copyright (c) 2016 - 2017, Intel Corporation. + +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. + +@file: ab_parser.py + +""" + +import io +import re +from collections import defaultdict, ChainMap +from pprint import pformat +from testlib.helpers import merge_dicts + + +class Ab(object): + + TIMES_FIELDS = ('min', 'mean', 'sd', 'median', 'max') + + @classmethod + def store_connection_times(cls, dictionary, key, match): + dictionary[key] = {} + for subkey, value in zip(cls.TIMES_FIELDS, match.groups()): + dictionary[key][subkey] = value + + +class AbParser(object): + NOT_WHITE_SPACE = r'\S+' + WHITE_SPACE = r'\s+' + FLOAT_NUMBER = r'\S+' + EOL = r'$' + + SECONDS = r'seconds' + RECEIVED = r'received' + BYTES = r'bytes' + NUMBER_PER_SEC = r'\[\#/sec\]' + KILOBYTES_PER_SEC = r'\[Kbytes/sec\]' + MILISECONDS = r'\[ms\]' + MEAN = r'\(mean\)' + MEAN_ALL = r'\(mean, across all concurrent requests\)' + + GROUP_VALUE = r'({})'.format(NOT_WHITE_SPACE) + + LINE_FRAME = r'^{keyword}:{cls.WHITE_SPACE}{regexp}{cls.EOL}' + VALUE_BYTES = WHITE_SPACE.join([GROUP_VALUE, BYTES]) + + GROUP_FLOAT_NUMBER = r'({})'.format(FLOAT_NUMBER) + CONNECTION_TIMES_GROUPS = ((GROUP_FLOAT_NUMBER + WHITE_SPACE) * 4 + GROUP_FLOAT_NUMBER) + EOL + + KEYWORD_VALUE_MAPPING = ( + # tag, regexp, dict key, setter function + ('Server Software', GROUP_VALUE, 'server_software', None), + ('Server Hostname', GROUP_VALUE, 'server_hostname', None), + ('Server Port', GROUP_VALUE, 'server_port', None), + ('Document Path', GROUP_VALUE, 'document_path', None), + ('Concurrency Level', GROUP_VALUE, 'concurency_level', None), + ('Complete requests', GROUP_VALUE, 'complete_requests', None), + ('Failed requests', GROUP_VALUE, 'failed_requests', None), + ('Non-2xx responses', GROUP_VALUE, 'non-2xx_responses', None), + ('Document Length', VALUE_BYTES, 'document_length', None), + ('Total transferred', VALUE_BYTES, 'total_transferred', None), + ('HTML transferred', VALUE_BYTES, 'html_transferred', None), + ('Time taken for tests', WHITE_SPACE.join([GROUP_VALUE, SECONDS]), 'time_taken_for_tests', None), + ('Requests per second', WHITE_SPACE.join([GROUP_VALUE, NUMBER_PER_SEC, MEAN]), 'requests_per_second', None), + ('Time per request', WHITE_SPACE.join([GROUP_VALUE, MILISECONDS, MEAN]), 'time_per_request_mean', None), + ('Time per request', WHITE_SPACE.join([GROUP_VALUE, MILISECONDS, MEAN_ALL]), 'time_per_request_mean_all', None), + ('Transfer rate', WHITE_SPACE.join([GROUP_VALUE, KILOBYTES_PER_SEC, RECEIVED]), 'transfer_rate', None), + ('Connect', CONNECTION_TIMES_GROUPS, 'connect_times', Ab.store_connection_times), + ('Processing', CONNECTION_TIMES_GROUPS, 'processing_times', Ab.store_connection_times), + ('Waiting', CONNECTION_TIMES_GROUPS, 'waiting_times', Ab.store_connection_times), + ('Total', CONNECTION_TIMES_GROUPS, 'total_times', Ab.store_connection_times), + ) + + TOKENS = [] + + @classmethod + def _set_class_attrs(cls): + for keyword, regexp, key, func in cls.KEYWORD_VALUE_MAPPING: + cls.TOKENS.append( + [ + re.compile(cls.LINE_FRAME.format(cls=cls, keyword=keyword, regexp=regexp)), + key, + func + ] + ) + + def __init__(self): + super().__init__() + AbParser._set_class_attrs() + self.ab_output = {} + + def parse(self, input_buffer): + string_io = io.StringIO(input_buffer) + + self.ab_output = {} + for line in iter(string_io.readline, ''): + for regexp, key, setter in self.TOKENS: + matches = regexp.match(line) + if not matches: + continue + + if setter is None: + self.ab_output[key] = matches.group(1) + else: + setter(self.ab_output, key, matches) + + break + + return self.ab_output + + +def num(s): + try: + return int(s) + except ValueError: + return float(s) + + +class AbAggregator(object): + + FIX_KEYS = [ + 'server_software', + 'server_port', + 'document_path', + 'document_length', + 'server_hostname', + ] + + KEYS_TO_ADD = [ + 'number_of_clients', + 'complete_requests', + 'failed_requests', + 'non-2xx_responses', + 'total_transferred', + 'html_transferred', + 'requests_per_second', + 'concurency_level', + 'time_taken_for_tests', + 'transfer_rate', + ] + + TIMES_KEYS = [ + 'connect_times', + 'processing_times', + 'waiting_times', + 'total_times', + ] + + IGNORE = [ + 'time_per_request_mean', + 'time_per_request_mean_all', + 'server_hostname', + ] + + KEY_FUNC_MAPPING = {} + + @staticmethod + def min_max_default(): + return {'min': float('inf'), 'max': 0} + + @classmethod + def _set_mappings(cls): + for key in cls.FIX_KEYS: + cls.KEY_FUNC_MAPPING[key] = cls._normal_assignment + for key in cls.KEYS_TO_ADD: + cls.KEY_FUNC_MAPPING[key] = cls._int_addition + for key in cls.TIMES_KEYS: + cls.KEY_FUNC_MAPPING[key] = cls._min_max_eval + for key in cls.IGNORE: + cls.KEY_FUNC_MAPPING[key] = cls._no_op + + def __init__(self): + super().__init__() + AbAggregator._set_mappings() + self._status = {} + self._add_status = defaultdict(int) + self._min_max_status = defaultdict(self.min_max_default) + self.status = ChainMap(self._status, + self._add_status, + self._min_max_status) + + def _int_addition(self, key, value): + self._add_status[key] += num(value) + + def _min_max_eval(self, key, value): + status = self._min_max_status[key] + status['min'] = min(status['min'], float(value['min'])) + status['max'] = max(status['max'], float(value['max'])) + + def _normal_assignment(self, key, value): + self._status[key] = value + + def _no_op(self, key, value): + pass + + def __iadd__(self, item): + self._int_addition('number_of_clients', 1) + for key, value in item.items(): + self.KEY_FUNC_MAPPING[key](self, key, value) + return self + + @property + def dictionary(self): + return merge_dicts(*self.status.maps) + + def __eq__(self, other): + return self.dictionary == other + + def to_str(self): + return pformat(self.dictionary) + + def __str__(self): + return self.to_str() + + def __repr__(self): + return self.to_str() diff --git a/utils/images/onp_benchmark/10_mb_file b/utils/images/onp_benchmark/10_mb_file new file mode 100644 index 0000000..860f4a9 Binary files /dev/null and b/utils/images/onp_benchmark/10_mb_file differ diff --git a/utils/images/onp_benchmark/2_mb_file b/utils/images/onp_benchmark/2_mb_file new file mode 100644 index 0000000..0e5488a Binary files /dev/null and b/utils/images/onp_benchmark/2_mb_file differ diff --git a/utils/images/onp_benchmark/4_mb_file b/utils/images/onp_benchmark/4_mb_file new file mode 100644 index 0000000..40d5dbc Binary files /dev/null and b/utils/images/onp_benchmark/4_mb_file differ diff --git a/utils/images/onp_benchmark/Dockerfile b/utils/images/onp_benchmark/Dockerfile new file mode 100644 index 0000000..80ae131 --- /dev/null +++ b/utils/images/onp_benchmark/Dockerfile @@ -0,0 +1,22 @@ +FROM fedora/nginx +MAINTAINER intel.com +ENV http_proxy $http_proxy +RUN echo "proxy=$http_proxy" >> /etc/dnf/dnf.conf +RUN dnf -y install iperf3 \ + etcd \ + python3-pip \ + nmap \ + net-tools \ + htop \ + procps-ng\ + tcpdump\ + strace\ + httpd-tools\ + python3-ipython-console\ + && dnf clean all +RUN pip3 --proxy $http_proxy install python-etcd +COPY ./invoke_test.py /home/invoke_test.py +COPY ./10_mb_file /usr/share/nginx/html/10_mb_file +COPY ./2_mb_file /usr/share/nginx/html/2_mb_file +COPY ./4_mb_file /usr/share/nginx/html/4_mb_file +COPY ./nginx.conf /etc/nginx/nginx.conf diff --git a/utils/images/onp_benchmark/create-image.sh b/utils/images/onp_benchmark/create-image.sh new file mode 100755 index 0000000..60f73aa --- /dev/null +++ b/utils/images/onp_benchmark/create-image.sh @@ -0,0 +1,19 @@ +#!/bin/bash +if [ -z "$KTEST_DOCKER_REGISTRY" ]; then + echo "source the SOURCE file" + exit 2 +fi + +IMAGE_NAME=onp-bench +IMAGE_VERSION=4 + + +image_name=$IMAGE_NAME:$IMAGE_VERSION + +docker build -t $image_name . +docker images +docker tag $image_name $KTEST_DOCKER_REGISTRY/$image_name +docker tag $KTEST_DOCKER_REGISTRY/$image_name $KTEST_DOCKER_REGISTRY/$IMAGE_NAME +docker push $KTEST_DOCKER_REGISTRY/$image_name +docker push $KTEST_DOCKER_REGISTRY/$IMAGE_NAME +docker images | grep "" | awk '{print $3}' | xargs docker rmi -f >/dev/null 2>&1 diff --git a/utils/images/onp_benchmark/invoke_test.py b/utils/images/onp_benchmark/invoke_test.py new file mode 100644 index 0000000..71d7e8c --- /dev/null +++ b/utils/images/onp_benchmark/invoke_test.py @@ -0,0 +1,552 @@ +""" +@copyright Copyright (c) 2016 - 2017, Intel Corporation. + +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. + +@file: invoke_test.py + +""" +import time +import json +import os +import subprocess +import etcd +import socket +import fcntl +import struct +import re +from contextlib import closing, suppress +from functools import wraps +from io import StringIO + + +_LOCAL_DEFAULT = object() + + +class InvokeError(Exception): + + def __init__(self, message=None): + super(InvokeError, self).__init__(message if message is not None else 'Unknown error') + + +def print_and_return_value(value, attr_name=_LOCAL_DEFAULT, key_name=_LOCAL_DEFAULT, + call_name=_LOCAL_DEFAULT, *args, **kwargs): + if attr_name is not _LOCAL_DEFAULT: + print(getattr(value, str(attr_name))) + elif key_name is not _LOCAL_DEFAULT: + print(value[key_name]) + elif call_name is not _LOCAL_DEFAULT: + print(getattr(value, str(call_name))(*args, **kwargs)) + else: + print(value) + return value + + +class PrintAndReturnValueWrapper(object): + + def __init__(self, attr_name=_LOCAL_DEFAULT, key_name=_LOCAL_DEFAULT, call_name=_LOCAL_DEFAULT, + *args, **kwargs): + super(PrintAndReturnValueWrapper, self).__init__() + self.attr_name = attr_name + self.key_name = key_name + self.call_name = call_name + self.args = args + self.kwargs = kwargs + + def __call__(self, func): + @wraps(func) + def inner(*args, **kwargs): + return print_and_return_value(func(*args, **kwargs), self.attr_name, self.key_name, + self.call_name, *self.args, **self.kwargs) + return inner + + +value_print_wrapper = PrintAndReturnValueWrapper() +value_attr_print_wrapper = PrintAndReturnValueWrapper('value') + + +class CommandExecution(object): + + CONNECTION_ATTEMPTS = 3 + BAD_OUTPUT_STRING_LIST = [] + + def __init__(self, json_dict, caller): + super().__init__() + self.name = '' + self.toggle_flags = [] + self.special_keys = [] + self.json_dict = json_dict + self.caller = caller + self.command = None + self.process = None + self.output = None + self.error_output = None + + def prepare(self): + self.command = self._parse_json() + + def _is_special_key(self, key): + return key in self.special_keys + + def _is_toggle_key(self, key, value): + return key in self.toggle_flags + + def _inject_after_binary(self, str_buffer): + pass + + def _append_to_command(self, str_buffer): + pass + + @value_print_wrapper + def _parse_json(self): + str_buffer = StringIO() + str_buffer.write(self.name) + str_buffer.write(' ') + + self._inject_after_binary(str_buffer) + for key, value in self.json_dict.items(): + if self._is_special_key(key): + continue + str_buffer.write(' -') + str_buffer.write(key) + if not self._is_toggle_key(key, value): + str_buffer.write(' ') + str_buffer.write(str(value)) + + self._append_to_command(str_buffer) + return str_buffer.getvalue() + + def got_good_output(self): + if not self.caller.server_ip: + # server processes never fail + return True + + if self.process.returncode != 0: + return False + + return not any(bad_string in self.output for bad_string in self.BAD_OUTPUT_STRING_LIST) + + def run(self): + for try_count in range(self.CONNECTION_ATTEMPTS): + self.process = subprocess.Popen(self.command, shell=True, stdout=subprocess.PIPE) + self.output, self.error_output = self.process.communicate() + + if self.got_good_output(): + self.caller.write_final_result(self.output, self.error_output) + return self.output, self.error_output + + self.caller.write_bad_result(try_count, self.output) + + +class IperfExecution(CommandExecution): + + BAD_OUTPUT_STRING_LIST = [ + 'error', + ] + + def __init__(self): + super().__init__() + self.name = "iperf3 " + self.toggle_flags = [ + 'J', + 's', + '4', + '6', + 'u', + ] + + def _append_to_command(self, str_buffer): + if self.caller.server_ip: + str_buffer.write('-c ') + str_buffer.write(self.caller.server_ip) + + +class NetperfExecution(CommandExecution): + + BAD_OUTPUT_STRING_LIST = [ + "could not establish the control connection", + "errno", + ] + + def __init__(self): + super().__init__() + if self.caller.server_ip: + self.name = "netperf" + else: + self.name = "netserver" + + self.toggle_list = [ + 'D', + 'f', + '4', + '6', + ] + + def _inject_after_binary(self, str_buffer): + if self.caller.server_ip: + str_buffer.write('-H ') + str_buffer.write(self.caller.server_ip) + + +class NginxExecution(CommandExecution): + + NGINX_CONF_FILE_PATH = "/etc/nginx/nginx.conf" + + def __init__(self): + super().__init__() + self.name = "nginx" + + def _is_special_key(self, key): + return True + + def _inject_after_binary(self, str_buffer): + self._rewrite_nginx_conf_file() + + def _make_conf_line(self, key, value): + if self._is_toggle_key(key, value): + return '\t{0};\n'.format(key) + return '\t{0} {1};\n'.format(key, value) + + def _rewrite_nginx_conf_file(self): + if not self.json_dict: + return + + new_events = [self._make_conf_line(key, value) for key, value in self.json_dict.items()] + with open(self.NGINX_CONF_FILE_PATH) as handle: + lines_list = handle.readlines() + + lines_iter = iter(enumerate(lines_list)) + # Find where events tag starts + events_start_index = next((index for index, line in lines_iter + if line.startswith('events {')), 0) + # Find where events tag end + events_end_index = next((index for index, line in lines_iter if line.startswith('}')), 0) + + with open(self.NGINX_CONF_FILE_PATH, 'w') as handle: + handle.writelines(lines_list[0:events_start_index + 1]) + # Insert and replace parameter into events{} of the .conf file + handle.writelines(new_events) + handle.writelines(lines_list[events_end_index:]) + + +class ApacheBenchExecution(CommandExecution): + + def __init__(self): + super().__init__() + self.name = 'ab' + self.special_keys = ['custom_path'] + + def _is_toggle_key(self, key, value): + return isinstance(value, bool) + + def _append_to_command(self, str_buffer): + if not self.caller.server_ip: + return + str_buffer.write(' ') + str_buffer.write('http://') + str_buffer.write(self.caller.server_ip) + str_buffer.write('/') + str_buffer.write(self.json_dict.get('custom_path', '')) + + +def get_ip_address(ifname): + """ + Adapted from + http://code.activestate.com/recipes/439094-get-the-ip-address-associated-with-a-network-inter/ + :author: Paul Cannon http://code.activestate.com/recipes/users/2551140/ + :license: PSF + :param ifname: interface name + :type ifname: str + :return: IP address + :rtype: str + """ + with closing(socket.socket(socket.AF_INET, socket.SOCK_DGRAM)) as s: + return socket.inet_ntoa(fcntl.ioctl( + s.fileno(), + 0x8915, # SIOCGIFADDR + struct.pack('256s', ifname[:15]) + )[20:24]) + + +def try_int(s): + """"Convert to integer if possible.""" + with suppress(ValueError, TypeError): + return int(s) + return s + + +def natural_sort_key(s): + """Used internally to get a tuple by which s is sorted.""" + return tuple(map(try_int, re.findall(r'(\d+|\D+)', s))) + + +def natural_case_key(s): + return natural_sort_key(s.key.lower()) + + +class ExecutionPlace(object): + + HELP_OUTPUT = ("server usage: docker run -ti -e INPUT_DATA=host:port:key_path " + "fedora/test python /home/parse_data_03.py\n" + "client usage: docker run -ti -e INPUT_DATA=host:port:key_path " + "-e CLIENT_ID=123 fedora/test python /home/parse_data_03.py") + + WRITE_ATTEMPTS = 3 + READ_ATTEMPTS = WRITE_ATTEMPTS + + LATEST_BOILERPLATE = "{0.base_path}/latest" + TEST_PATH_BOILERPLATE = "{0.base_path}/test-{1}/" + CONFIG_DATA_SUB_KEY_BOILERPLATE = '' + + READY_SUB_KEY_BOILERPLATE = '{0.name}' + CONNECTION_ERROR_SUB_KEY_BOILERPLATE = ('result-establish-connection-error{1}/' + 'client-{0.client_id}') + RESULT_ERROR_SUB_KEY_BOILERPLATE = 'result-getting-results-error/client-{0.client_id}' + RESULT_SUB_KEY_BOILERPLATE = 'result/client-{0.client_id}' + + @classmethod + def parse_input_data(cls, input_data): + with suppress(TypeError, IndexError): + input_words = input_data.split(":") + return input_words[0], input_words[1], input_words[2] + print(cls.HELP_OUTPUT) + raise InvokeError("Invalid INPUT_DATA") + + def __init__(self, client_id=None, input_data=None, *args, **kwargs): + super().__init__() + + self.executor = None + self.ip = None + self.name = 'UNKNOWN' + self.client_id = client_id + self.server_id = None + self.server_ip = None + + host, port, self.base_path = self.parse_input_data(input_data) + self.etcd_client = etcd.Client(host=host, port=int(port), allow_reconnect=True) + + base_data = print_and_return_value( + self.etcd_read_data(self.LATEST_BOILERPLATE.format(self)), + attr_name='value') + self.test_path = print_and_return_value( + self.TEST_PATH_BOILERPLATE.format( + self, + base_data.value # pylint: disable=no-member + ) + ) + + @value_attr_print_wrapper + def _get_config_data(self): + data_path = self.make_input_key(self.CONFIG_DATA_SUB_KEY_BOILERPLATE) + + for try_count in range(self.READ_ATTEMPTS): + with suppress(etcd.EtcdKeyNotFound): + return self.etcd_read_data(data_path, True) + time.sleep(1) + + raise InvokeError('Unable to read data path') + + def parse_json(self): + data = self._get_config_data() + + try: + json_data = json.loads(data.value) # pylint: disable=no-member + except ValueError: + raise InvokeError('Failed to parse JSON data') + + # simple ordered dict + command_execution_pairs = [ + ('ab', ApacheBenchExecution), + ('nginx', NginxExecution), + ('netperf', NetperfExecution), + ('iperf', IperfExecution), + ] + + for command_type, exec_class in command_execution_pairs: + with suppress(KeyError): + return exec_class(json_data[command_type], self) + + raise InvokeError('No command found to parse') + + def etcd_read_data(self, path, read_list=False): + for try_count in range(self.READ_ATTEMPTS): + with suppress(etcd.EtcdKeyNotFound): + return self.etcd_client.read(path, recursive=read_list, sorted=read_list) + time.sleep(1) + + raise InvokeError("etcd_client.read failed {}".format(path)) + + def etcd_wait_for_data(self, path, timeout): + while True: + with suppress(Exception): + data = self.etcd_client.read(path) + return data + time.sleep(timeout) + + def etcd_write_data(self, path, value): + for try_count in range(self.WRITE_ATTEMPTS): + with suppress(Exception): + self.etcd_client.write(path, value) + return + time.sleep(1) + + raise InvokeError("unable to write {}".format(path)) + + def prepare(self): + raise NotImplementedError() + + def run(self): + print('Running test command') + self._run() + + def _run(self): + raise NotImplementedError() + + @value_print_wrapper + def make_test_path_key(self, sub_key, *args, **kwargs): + return ''.join(['{0.test_path}', sub_key]).format(self, *args, **kwargs) + + def make_input_key(self, sub_key, *args, **kwargs): + return self.make_test_path_key(''.join(['inputdata/', sub_key]), *args, **kwargs) + + def make_output_key(self, sub_key, *args, **kwargs): + return self.make_test_path_key(''.join(['outputdata/', sub_key]), *args, **kwargs) + + def write_bad_result(self, count, output): + self.etcd_write_data(self.make_output_key( + self.CONNECTION_ERROR_SUB_KEY_BOILERPLATE.format(self, count)), output) + + def write_final_result(self, output, error_output): + self.etcd_write_data( + self.make_output_key(self.RESULT_SUB_KEY_BOILERPLATE.format(self)), + output) + self.etcd_write_data( + self.make_output_key(self.RESULT_ERROR_SUB_KEY_BOILERPLATE.format(self)), + error_output) + + +class ExecutionServer(ExecutionPlace): + + CONFIG_DATA_SUB_KEY_BOILERPLATE = "server" + + SERVER_IP_OUTPUT_PATH_BOILERPLATE = 'server/{0.name}' + + @staticmethod + @value_print_wrapper + def _get_name(): + with open('/etc/hostname', 'r') as file_handle: + return next(file_handle, None) + + @staticmethod + @value_print_wrapper + def _get_ip(): + return get_ip_address(b'eth0') + + def prepare(self): + self.executor = self.parse_json() + self.ip = self._get_ip() + self.name = self._get_name().strip() + self.etcd_write_data(self.make_output_key(self.SERVER_IP_OUTPUT_PATH_BOILERPLATE), self.ip) + self.executor.prepare() + + def _run(self): + self.executor.run() + print('This is a server so no need to save any statistics') + + +class ExecutionClient(ExecutionPlace): + + CONFIG_DATA_SUB_KEY_BOILERPLATE = "client" + + SERVER_DATA_SUB_KEY_BOILERPLATE = 'server/' + SERVER_START_SUB_KEY_BOILERPLATE = 'start' + SERVER_START_TIME_SUB_KEY_BOILERPLATE = 'starttime' + + READY_SUB_KEY_BOILERPLATE = 'state/client-{0.client_id}-is-ready-{0.server_ip}' + CONNECTION_ERROR_SUB_KEY_BOILERPLATE = ('result-establish-connection-error{1}/' + 'client-{0.client_id}') + RESULT_ERROR_SUB_KEY_BOILERPLATE = 'result-getting-results-error/client-{0.client_id}' + RESULT_SUB_KEY_BOILERPLATE = 'result/client-{0.client_id}' + + def __init__(self, client_id=None, input_data=None, *args, **kwargs): + super(ExecutionClient, self).__init__(int(client_id), input_data, *args, **kwargs) + + def prepare(self): + master = self.etcd_read_data( + self.make_output_key(self.SERVER_DATA_SUB_KEY_BOILERPLATE), + True) + server_list = list(master.children) + server_count = len(server_list) + print(server_count) + + if server_count < self.client_id: + raise InvokeError("len(servers) < client_id") + elif self.client_id < 1: + raise InvokeError("client_id should positive value") + + server_list.sort(key=natural_case_key) + server = server_list[self.client_id - 1] + self.server_id = server.key + self.server_ip = server.value + print("Client {0.client_id} connects to {0.server_id} (IP: {0.server_ip})".format(self)) + + self.executor = self.parse_json() + + self.executor.prepare() + + server_path = self.make_output_key(self.READY_SUB_KEY_BOILERPLATE).format(self.client_id, + self.server_ip) + self.etcd_write_data(server_path, self.server_id) + + self.etcd_wait_for_data(self.make_input_key(self.SERVER_START_SUB_KEY_BOILERPLATE), 0.5) + start_time = int(self.etcd_wait_for_data( + self.make_input_key(self.SERVER_START_TIME_SUB_KEY_BOILERPLATE), 1).value.strip()) + now = int(time.time()) + + if start_time > now: + sleep_time = start_time - now + print("Sleeping for {} seconds".format(sleep_time)) + time.sleep(sleep_time) + elif not os.environ.get('DEBUG_SLEEP', 0): + # for debugging purposes it is good to start even if it is late + # (happens in debugger) + raise InvokeError("Too late to start the executor.") + + def _run(self): + if self.server_id is None: + raise InvokeError('Client run before prepared or there was an undetected error ' + 'during prepare') + self.executor.run() + + +def make_executor(): + input_data = os.environ.get('INPUT_DATA') + with suppress(KeyError, TypeError, ValueError): + return ExecutionClient(os.environ['CLIENT_ID'], input_data) + return ExecutionServer(input_data=input_data) + + +def main(): + executor = make_executor() + executor.prepare() + executor.run() + print('The test is finished') + + +if __name__ == '__main__': + try: + main() + except Exception as e: + print(e) + debug_sleep_time = os.environ.get('DEBUG_SLEEP', 0) + time.sleep(int(debug_sleep_time)) diff --git a/utils/images/onp_benchmark/netperf-2.7.0-23.fc23.x86_64.rpm b/utils/images/onp_benchmark/netperf-2.7.0-23.fc23.x86_64.rpm new file mode 100644 index 0000000..3344c30 Binary files /dev/null and b/utils/images/onp_benchmark/netperf-2.7.0-23.fc23.x86_64.rpm differ diff --git a/utils/images/onp_benchmark/nginx.conf b/utils/images/onp_benchmark/nginx.conf new file mode 100644 index 0000000..1be195c --- /dev/null +++ b/utils/images/onp_benchmark/nginx.conf @@ -0,0 +1,104 @@ +""" +@copyright Copyright (c) 2015-2016, Intel Corporation. + +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. +""" +# For more information on configuration, see: +# * Official English Documentation: http://nginx.org/en/docs/ +# * Official Russian Documentation: http://nginx.org/ru/docs/ + +user nginx; +worker_processes auto; +error_log /var/log/nginx/error.log; +pid /run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 1000; + types_hash_max_size 2048; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Load modular configuration files from the /etc/nginx/conf.d directory. + # See http://nginx.org/en/docs/ngx_core_module.html#include + # for more information. + include /etc/nginx/conf.d/*.conf; + + server { + listen 80 default_server; + listen [::]:80 default_server; + server_name _; + root /usr/share/nginx/html; + + # Load configuration files for the default server block. + include /etc/nginx/default.d/*.conf; + + location / { + } + + error_page 404 /404.html; + location = /40x.html { + } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + } + } + +# Settings for a TLS enabled server. +# +# server { +# listen 443 ssl; +# listen [::]:443 ssl; +# server_name _; +# root /usr/share/nginx/html; +# +# ssl_certificate "/etc/pki/nginx/server.crt"; +# ssl_certificate_key "/etc/pki/nginx/private/server.key"; +# ssl_session_cache shared:SSL:1m; +# ssl_session_timeout 10m; +# ssl_ciphers PROFILE=SYSTEM; +# ssl_prefer_server_ciphers on; +# +# # Load configuration files for the default server block. +# include /etc/nginx/default.d/*.conf; +# +# location / { +# } +# +# error_page 404 /404.html; +# location = /40x.html { +# } +# +# error_page 500 502 503 504 /50x.html; +# location = /50x.html { +# } +# } + +} + +daemon off; +