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:
parent
207d3f1c55
commit
6634f86989
|
@ -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')
|
|
@ -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)
|
|
@ -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)}
|
|
@ -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)]
|
|
@ -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"}]}
|
|
@ -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)
|
Loading…
Reference in New Issue