diff --git a/fuelclient/tests/test_performance.py b/fuelclient/tests/test_performance.py new file mode 100644 index 0000000..c8609ed --- /dev/null +++ b/fuelclient/tests/test_performance.py @@ -0,0 +1,160 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2014 Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json +import os +import shutil +import tarfile +import time + +import mock +import nose +import requests +from six import moves as six_moves + +from fuelclient import client +from fuelclient import fuelclient_settings +from fuelclient.objects import node +from fuelclient import profiler +from fuelclient.tests import base +from fuelclient.tests import utils + + +class ClientPerfTest(base.UnitTestCase): + + NUMBER_OF_NODES = 100 + + @classmethod + def setUpClass(cls): + super(ClientPerfTest, cls).setUpClass() + + if not profiler.profiling_enabled(): + raise nose.SkipTest('Performance profiling tests are not ' + 'enabled in settings.yaml') + + cls.nodes = cls.get_random_nodes(cls.NUMBER_OF_NODES) + settings = fuelclient_settings.get_settings() + test_base = settings.PERF_TESTS_PATHS['perf_tests_base'] + + if os.path.exists(test_base): + shutil.rmtree(test_base) + + os.makedirs(test_base) + + @classmethod + def tearDownClass(cls): + """Packs all the files from the profiling.""" + + settings = fuelclient_settings.get_settings() + test_base = settings.PERF_TESTS_PATHS['perf_tests_base'] + test_results = settings.PERF_TESTS_PATHS['perf_tests_results'] + + if not os.path.exists(test_results): + os.makedirs(test_results) + + if os.path.exists(test_base): + test_result_name = os.path.join( + test_results, + '{name:s}_{timestamp}.tar.gz'.format(name=cls.__name__, + timestamp=time.time())) + tar = tarfile.open(test_result_name, "w:gz") + tar.add(test_base) + tar.close() + + shutil.rmtree(test_base) + + def setUp(self): + super(ClientPerfTest, self).setUp() + + req_patcher = mock.patch.object(requests.api, 'request') + token_patcher = mock.patch.object(client.Client, 'auth_token', + new_callable=mock.PropertyMock) + + self.mock_request = req_patcher.start() + self.mock_auth_token = token_patcher.start() + + def tearDown(self): + super(ClientPerfTest, self).tearDown() + + self.mock_request.stop() + self.mock_auth_token.stop() + + @classmethod + def get_random_nodes(cls, number): + """Returns specified number of random fake nodes.""" + + return [utils.get_fake_node() for i in six_moves.range(number)] + + def _invoke_client(self, *args): + """Invokes Fuel Client with the specified arguments.""" + + args = ['fuelclient'] + list(args) + self.execute(args) + + def mock_nailgun_response(self, *responses): + """Mocks network requests in order to return specified content.""" + + m_responses = [] + + for resp in responses: + m_resp = requests.models.Response() + m_resp.encoding = 'utf8' + m_resp._content = resp + + m_responses.append(m_resp) + + self.mock_request.side_effect = m_responses + + def test_list_nodes(self): + nodes_text = json.dumps(self.nodes) + self.mock_nailgun_response(nodes_text) + + self._invoke_client('node', 'list') + + def test_assign_nodes(self): + node_ids = ','.join([str(n['id']) for n in self.nodes]) + + self.mock_nailgun_response('{}') + self._invoke_client('--env', '42', 'node', 'set', '--node', + node_ids, '--role', 'compute') + + def test_list_environment(self): + # NOTE(romcheg): After 100 nodes were added to an environment + # they are listed as pending changes so that may potentially + # affect the performance. + env = [utils.get_fake_env(changes_number=self.NUMBER_OF_NODES)] + resp_text = json.dumps(env) + + self.mock_nailgun_response(resp_text) + + self._invoke_client('env', '--list') + + @mock.patch.object(node, 'exit_with_error', new_callable=mock.MagicMock) + @mock.patch('__builtin__.open', create=True) + def test_upload_node_settings(self, m_open, m_exit): + node_configs = [json.dumps(utils.get_fake_network_config(3)) + for i in six_moves.range(self.NUMBER_OF_NODES)] + + node_ids = ','.join([str(n['id']) for n in self.nodes]) + + m_open.return_value = mock.MagicMock(spec=file) + m_file = m_open.return_value.__enter__.return_value + m_file.read.side_effect = node_configs + + self.mock_nailgun_response(*node_configs) + + self._invoke_client('--json', 'node', '--node-id', node_ids, + '--network', '--upload', '--dir', '/fake/dir') diff --git a/fuelclient/tests/utils/__init__.py b/fuelclient/tests/utils/__init__.py new file mode 100644 index 0000000..210f49b --- /dev/null +++ b/fuelclient/tests/utils/__init__.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2014 Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from fuelclient.tests.utils.random_data import random_string +from fuelclient.tests.utils.fake_net_conf import get_fake_interface_config +from fuelclient.tests.utils.fake_net_conf import get_fake_network_config +from fuelclient.tests.utils.fake_node import get_fake_node +from fuelclient.tests.utils.fake_env import get_fake_env + + +__all__ = (get_fake_env, + get_fake_node, + random_string, + get_fake_interface_config, + get_fake_network_config) diff --git a/fuelclient/tests/utils/fake_env.py b/fuelclient/tests/utils/fake_env.py new file mode 100644 index 0000000..2988dd8 --- /dev/null +++ b/fuelclient/tests/utils/fake_env.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2014 Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import random + + +def _generate_changes(number=None, node_ids=None): + """Generates specified number of changes + + :param number: Number of changes to create. + :param node_ids: IDs of the nodes which are changed. If None, a linear + sequense [0, ) will be used. Number of IDs must + match the number of changes. + :return: list of dict + + """ + if (number is None) and (node_ids is None): + raise ValueError("Either number of changes or Nodes' IDs is requied.") + + if node_ids is None: + node_ids = range(number) + + change_types = ["networks", "interfaces", "disks", "attributes"] + + return [{"node_id": n_id, "name": random.choice(change_types)} + for n_id in node_ids] + + +def get_fake_env(name=None, status=None, release_id=None, fuel_version=None, + pending_release=None, env_id=None, changes_number=None): + """Create a random fake environment + + Returns the serialized and parametrized representation of a dumped Fuel + environment. Represents the average amount of data. + + """ + return {"status": status or "new", + "is_customized": False, + "release_id": release_id or 1, + "name": name or "fake_env", + "grouping": "roles", + "net_provider": "nova_network", + "fuel_version": fuel_version or "5.1", + "pending_release_id": pending_release, + "id": env_id or 1, + "mode": "multinode", + "changes": _generate_changes(changes_number)} diff --git a/fuelclient/tests/utils/fake_net_conf.py b/fuelclient/tests/utils/fake_net_conf.py new file mode 100644 index 0000000..1cb6c85 --- /dev/null +++ b/fuelclient/tests/utils/fake_net_conf.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2014 Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import random + +from six import moves as six_moves + + +def get_fake_interface_config(iface=None, iface_id=None, state=None, mac=None, + iface_type=None, networks=None): + """Create a random fake interface configuration + + Returns the serialized and parametrized representation of a node's + interface configuration. Represents the average amount of data. + + """ + + return {"name": iface or "eth0", + "id": iface_id or random.randint(0, 1000), + "state": state or "unknown", + "mac": mac or "08:00:27:a4:01:6b", + "max_speed": 100, + "type": iface_type or "ether", + "current_speed": 100, + "assigned_networks": networks or [{"id": 1, + "name": "fuelweb_admin"}, + {"id": 3, + "name": "management"}, + {"id": 4, + "name": "storage"}, + {"id": 5, + "name": "fixed"}]} + + +def get_fake_network_config(iface_number): + """Creates a fake network configuration for a single node.""" + + return [get_fake_interface_config(iface='eth{0}'.format(i)) + for i in six_moves.range(iface_number)] diff --git a/fuelclient/tests/utils/fake_node.py b/fuelclient/tests/utils/fake_node.py new file mode 100644 index 0000000..b80b2a7 --- /dev/null +++ b/fuelclient/tests/utils/fake_node.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2014 Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import random + +from fuelclient import tests + + +def get_fake_node(cluster=None, hostname=None, node_id=None, cpu_model=None, + roles=None, mac=None, memory_b=None, os_platform=None, + status=None, node_name=None, group_id=None): + """Creates a fake random node + + Returns the serialized and parametrized representation of a dumped Fuel + environment. Represents the average amount of data. + + """ + host_name = hostname or tests.utils.random_string(15, prefix='fake-node-') + + return {"name": node_name or host_name, + "error_type": None, + "cluster": cluster or 1, + "id": node_id or random.randint(1, 10000), + "ip": "10.20.0.4", + "kernel_params": None, + "group_id": group_id or 1, + "mac": mac or "d6:11:3f:b0:f1:43", + "manufacturer": "VirtualBox", + "online": True, + "os_platform": os_platform or "centos", + "pending_addition": False, + "pending_deletion": False, + "pending_roles": [], + "platform_name": None, + "progress": 100, + "roles": roles or ["compute"], + "status": status or "ready", + "fqdn": "{hostname}.example.com".format(hostname=host_name), + + "meta": {"cpu": {"real": 0, + "spec": [{"frequency": 2553, + "model": cpu_model or "Random CPU"}], + "total": 1}, + + "disks": [{"disk": "disk/by-path/pci:00:0d.0-scsi-2:0:0", + "extra": ["disk/by-id/scsi-SATA_VBOX_aef0bb5c", + "disk/by-id/ata-VBOX_HARDDISK_VB37"], + "model": "VBOX HARDDISK", + "name": "sdc", + "removable": "0", + "size": 68718428160}, + + {"disk": "disk/by-path/pci:0:0d.0-scsi-1:0:0:0", + "extra": ["disk/by-id/scsi-SATA_VBOX_30fbc3bb", + "disk/by-id/ata-VBOX_HARDD30fbc3bb"], + "model": "VBOX HARDDISK", + "name": "sdb", + "removable": "0", + "size": 68718428160}, + + {"disk": "disk/by-path/pci:00:d.0-scsi-0:0:0:0", + "extra": ["disk/by-id/scsi-SATA_VBOX-17e33653", + "disk/by-id/ata-VBOX_HARDD17e33653"], + "model": "VBOX HARDDISK", + "name": "sda", + "removable": "0", + "size": 68718428160}], + + "interfaces": [{"name": "eth2", + "current_speed": 100, + "mac": "08:00:27:88:9C:46", + "max_speed": 100, + "state": "unknown"}, + + {"name": "eth1", + "current_speed": 100, + "mac": "08:00:27:24:BD:6D", + "max_speed": 100, + "state": "unknown"}, + + {"name": "eth0", + "current_speed": 100, + "mac": "08:00:27:C1:C5:72", + "max_speed": 100, + "state": "unknown"}], + "memory": {"total": memory_b or 1968627712}, + + "system": {"family": "Virtual Machine", + "fqdn": host_name, + "manufacturer": "VirtualBox", + "serial": "0", + "version": "1.2"}}, + "network_data": [{"brd": "192.168.0.255", + "dev": "eth0", + "gateway": None, + "ip": "192.168.0.2/24", + "name": "management", + "netmask": "255.255.255.0", + "vlan": 101}, + + {"brd": "192.168.1.255", + "dev": "eth0", + "gateway": None, + "ip": "192.168.1.2/24", + "name": "storage", + "netmask": "255.255.255.0", + "vlan": 102}, + + {"brd": "172.16.0.255", + "dev": "eth1", + "gateway": "172.16.0.1", + "ip": "172.16.0.3/24", + "name": "public", + "netmask": "255.255.255.0", + "vlan": None}, + + {"dev": "eth0", + "name": "admin"}]} diff --git a/fuelclient/tests/utils/random_data.py b/fuelclient/tests/utils/random_data.py new file mode 100644 index 0000000..b828a76 --- /dev/null +++ b/fuelclient/tests/utils/random_data.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2014 Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import random +import string + + +def random_string(lenght, prefix='', postfix='', charset=None): + """Returns a random string of the specified length + + :param length: The length of the resulting string. + :type lenght: int. + :param prefix: Prefix for the random string. + :type prefix: str, default: ''. + :param postfix: Postfix for the random string. + :type postfix: str, default ''. + :param charset: A set of characters to use for building random strings. + :type chartet: Iterable object. Default: ASCII letters and digits. + :return: str + + """ + charset = charset or string.ascii_letters + string.digits + base_length = lenght - (len(prefix) + len(postfix)) + + base = ''.join([str(random.choice(charset)) for i in xrange(base_length)]) + + return '{prefix}{base}{postfix}'.format(prefix=prefix, + postfix=postfix, + base=base)