Performance tests for handling nodes in Fuel Client

For performance testing all invotations to Nailgun
were mocked in order to only measure pure client's
performance.

The patch introduces the following tests for 100 nodes:

- list all nodes;
- assign nodes to an environment;
- list environment with pending nodes;
- upload network parameters to nodes.

Blueprint: 100-nodes-support
Change-Id: I849ecd17ba4a9a48b8d4e6dafa75b095ee919ce6
This commit is contained in:
Roman Prykhodchenko 2014-10-13 12:54:19 +02:00
parent 207d3f1c55
commit 6634f86989
6 changed files with 473 additions and 0 deletions

View File

@ -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')

View File

@ -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)

View File

@ -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, <number>) 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)}

View File

@ -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)]

View File

@ -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"}]}

View File

@ -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)