From 3d8b423db353f0d2106f8fae420b0e365b25177a Mon Sep 17 00:00:00 2001 From: Bruno Semperlotti Date: Wed, 16 Apr 2014 10:59:22 -0700 Subject: [PATCH] Add nova floating ip management in VM scenario This patch allows VM runcommand scenario attach a floating ip to the instance to perform ssh connection. Blueprint: benchmark-scenarios-vm-floating-ip Change-Id: I2e77d6be34a64d5feb9d3ecc5e35c8a712b10bed --- .../scenarios/vm/boot-runcommand-delete.json | 3 + .../scenarios/vm/boot-runcommand-delete.yaml | 3 + rally-scenarios/rally-neutron.yaml | 18 +++ rally-scenarios/rally.yaml | 41 +++++++ rally/benchmark/scenarios/nova/utils.py | 85 +++++++++++++ rally/benchmark/scenarios/vm/utils.py | 63 +++++++--- rally/benchmark/scenarios/vm/vmtasks.py | 114 +++++++++++++----- rally/benchmark/validation.py | 40 ++++++ rally/exceptions.py | 4 + rally/sshutils.py | 2 +- tests/benchmark/scenarios/nova/test_utils.py | 96 +++++++++++++++ tests/benchmark/scenarios/vm/test_utils.py | 68 ++++++++--- tests/benchmark/scenarios/vm/test_vmtasks.py | 109 +++++++++++++++-- tests/benchmark/test_validation.py | 112 +++++++++++++++++ tests/fakes.py | 11 ++ tests/test_sshutils.py | 2 +- 16 files changed, 698 insertions(+), 73 deletions(-) diff --git a/doc/samples/tasks/scenarios/vm/boot-runcommand-delete.json b/doc/samples/tasks/scenarios/vm/boot-runcommand-delete.json index e4ff505cf9..f2a205e9ad 100644 --- a/doc/samples/tasks/scenarios/vm/boot-runcommand-delete.json +++ b/doc/samples/tasks/scenarios/vm/boot-runcommand-delete.json @@ -8,6 +8,9 @@ "image": { "name": "cirros-0.3.1-x86_64-uec" }, + "fixed_network": "private", + "floating_network": "public", + "use_floatingip": true, "script": "doc/samples/support/instance_dd_test.sh", "interpreter": "/bin/sh", "username": "cirros" diff --git a/doc/samples/tasks/scenarios/vm/boot-runcommand-delete.yaml b/doc/samples/tasks/scenarios/vm/boot-runcommand-delete.yaml index 03e24c99b1..b15751738b 100644 --- a/doc/samples/tasks/scenarios/vm/boot-runcommand-delete.yaml +++ b/doc/samples/tasks/scenarios/vm/boot-runcommand-delete.yaml @@ -6,6 +6,9 @@ name: "m1.nano" image: name: "cirros-0.3.1-x86_64-uec" + fixed_network: "private" + floating_network: "public" + use_floatingip: true script: "doc/samples/support/instance_dd_test.sh" interpreter: "/bin/sh" username: "cirros" diff --git a/rally-scenarios/rally-neutron.yaml b/rally-scenarios/rally-neutron.yaml index cd9a6f12bb..c13e07ec9a 100644 --- a/rally-scenarios/rally-neutron.yaml +++ b/rally-scenarios/rally-neutron.yaml @@ -102,6 +102,24 @@ type: "constant" times: 100 concurrency: 10 + + VMTasks.boot_runcommand_delete: + - + args: + flavor: + name: "m1.tiny" + image: + name: "cirros-0.3.2-x86_64-uec" + fixed_network: "private" + floating_network: "public" + use_floatingip: true + script: "/home/jenkins/.rally/extra/instance_dd_test.sh" + interpreter: "/bin/sh" + username: "cirros" + runner: + type: "constant" + times: 6 + concurrency: 3 context: users: tenants: 1 diff --git a/rally-scenarios/rally.yaml b/rally-scenarios/rally.yaml index 903cda1e20..da5258f11b 100644 --- a/rally-scenarios/rally.yaml +++ b/rally-scenarios/rally.yaml @@ -321,3 +321,44 @@ users: tenants: 3 users_per_tenant: 5 + + VMTasks.boot_runcommand_delete: + - + args: + flavor: + name: "m1.tiny" + image: + name: "cirros-0.3.2-x86_64-uec" + fixed_network: "private" + floating_network: "public" + use_floatingip: true + script: "/home/jenkins/.rally/extra/instance_dd_test.sh" + interpreter: "/bin/sh" + username: "cirros" + runner: + type: "constant" + times: 6 + concurrency: 3 + context: + users: + tenants: 1 + users_per_tenant: 1 + - + args: + flavor: + name: "m1.tiny" + image: + name: "cirros-0.3.2-x86_64-uec" + fixed_network: "private" + use_floatingip: false + script: "/home/jenkins/.rally/extra/instance_dd_test.sh" + interpreter: "/bin/sh" + username: "cirros" + runner: + type: "constant" + times: 6 + concurrency: 3 + context: + users: + tenants: 1 + users_per_tenant: 1 diff --git a/rally/benchmark/scenarios/nova/utils.py b/rally/benchmark/scenarios/nova/utils.py index 6e953963b3..31217bd7eb 100644 --- a/rally/benchmark/scenarios/nova/utils.py +++ b/rally/benchmark/scenarios/nova/utils.py @@ -21,6 +21,7 @@ from rally.benchmark.scenarios import base from rally.benchmark.scenarios import utils as scenario_utils from rally.benchmark import utils as bench_utils + nova_benchmark_opts = [] option_names_and_defaults = [ # action, prepoll delay, timeout, poll interval @@ -93,6 +94,7 @@ class NovaScenario(base.Scenario): server = self.clients("nova").servers.create(server_name, image_id, flavor_id, **kwargs) + time.sleep(CONF.benchmark.nova_server_boot_prepoll_delay) server = bench_utils.wait_for( server, @@ -312,3 +314,86 @@ class NovaScenario(base.Scenario): check_interval=CONF.benchmark.nova_server_boot_poll_interval ) for server in servers] return servers + + @scenario_utils.atomic_action_timer('nova.list_floating_ip_pools') + def _list_floating_ip_pools(self): + """Returns user floating ip pools list.""" + return self.clients("nova").floating_ip_pools.list() + + @scenario_utils.atomic_action_timer('nova.list_floating_ips') + def _list_floating_ips(self): + """Returns user floating ips list.""" + return self.clients("nova").floating_ips.list() + + @scenario_utils.atomic_action_timer('nova.create_floating_ip') + def _create_floating_ip(self, pool): + """Create (allocate) a floating ip from the given pool + + :param pool: Name of the floating ip pool or external network + + :returns: The created floating ip + """ + return self.clients("nova").floating_ips.create(pool) + + @scenario_utils.atomic_action_timer('nova.delete_floating_ip') + def _delete_floating_ip(self, floating_ip): + """Delete (deallocate) a floating ip for a tenant + + :param floating_ip: The floating ip address to delete. + """ + self.clients("nova").floating_ips.delete(floating_ip) + bench_utils.wait_for_delete( + floating_ip, + update_resource=bench_utils.get_from_manager() + ) + + @scenario_utils.atomic_action_timer('nova.associate_floating_ip') + def _associate_floating_ip(self, server, address, fixed_address=None): + """Add floating IP to an instance + + :param server: The :class:`Server` to add an IP to. + :param address: The ip address or FloatingIP to add to the instance + :param fixed_address: The fixedIP address the FloatingIP is to be + associated with (optional) + """ + server.add_floating_ip(address, fixed_address=fixed_address) + bench_utils.wait_for( + server, + is_ready=self.check_ip_address(address), + update_resource=bench_utils.get_from_manager() + ) + # Update server data + server.addresses = server.manager.get(server.id).addresses + + @scenario_utils.atomic_action_timer('nova.dissociate_floating_ip') + def _dissociate_floating_ip(self, server, address): + """Remove floating IP from an instance + + :param server: The :class:`Server` to add an IP to. + :param address: The ip address or FloatingIP to remove + """ + server.remove_floating_ip(address) + bench_utils.wait_for( + server, + is_ready=self.check_ip_address(address, must_exist=False), + update_resource=bench_utils.get_from_manager() + ) + # Update server data + server.addresses = server.manager.get(server.id).addresses + + @staticmethod + def check_ip_address(address, must_exist=True): + ip_to_check = getattr(address, "ip", address) + + def _check_addr(resource): + for network, addr_list in resource.addresses.items(): + for addr in addr_list: + if ip_to_check == addr["addr"]: + return must_exist + return not must_exist + return _check_addr + + @scenario_utils.atomic_action_timer('nova.list_networks') + def _list_networks(self): + """Returns user networks list.""" + return self.clients("nova").networks.list() diff --git a/rally/benchmark/scenarios/vm/utils.py b/rally/benchmark/scenarios/vm/utils.py index 55d21e9b8d..bc5ce7e95e 100644 --- a/rally/benchmark/scenarios/vm/utils.py +++ b/rally/benchmark/scenarios/vm/utils.py @@ -14,8 +14,11 @@ # under the License. +import subprocess + from rally.benchmark.scenarios import base from rally.benchmark.scenarios import utils as scenario_utils +from rally.benchmark import utils as bench_utils from rally import sshutils @@ -29,12 +32,19 @@ class VMScenario(base.Scenario): """ return ssh.execute(interpreter, stdin=open(script, "rb")) - @scenario_utils.atomic_action_timer('vm.wait_for_network') - def wait_for_network(self, ssh): + @scenario_utils.atomic_action_timer('vm.wait_for_ssh') + def wait_for_ssh(self, ssh): ssh.wait() - def run_command(self, server, username, network, port, ip_version, - interpreter, script): + @scenario_utils.atomic_action_timer('vm.wait_for_ping') + def wait_for_ping(self, server_ip): + bench_utils.wait_for( + server_ip, + is_ready=self.ping_ip_address, + timeout=120 + ) + + def run_command(self, server_ip, port, username, interpreter, script): """Run command via SSH on server. Create SSH connection for server, wait for server to become @@ -42,21 +52,38 @@ class VMScenario(base.Scenario): and sshd being available). Then call __run_command to actually execute the command. """ - - if network not in server.addresses: - raise ValueError( - "Can't find cloud network %(network)s, so cannot boot " - "instance for Rally scenario boot-runcommand-delete. " - "Available networks: %(networks)s" % ( - dict(network=network, - networks=server.addresses.keys() - ) - ) - ) - server_ip = [ip for ip in server.addresses[network] if - ip["version"] == ip_version][0]["addr"] + self.wait_for_ping(server_ip) ssh = sshutils.SSH(username, server_ip, port=port, pkey=self.context()["user"]["keypair"]["private"]) - self.wait_for_network(ssh) + self.wait_for_ssh(ssh) return self.run_action(ssh, interpreter, script) + + @staticmethod + def check_network(server, network): + """Check if a server is attached to the specified network. + + :param server: The server object to consider + :param network: The name of the network to search for. + + :raises: `ValueError` if server is not attached to network. + """ + if network not in server.addresses: + raise ValueError( + "Server %(server_name)s is not attached to" + " network %(network)s. " + "Attached networks are: %(networks)s" % { + "server_name": server.name, + "network": network, + "networks": server.addresses.keys() + } + ) + + @staticmethod + def ping_ip_address(host, should_succeed=True): + cmd = ['ping', '-c1', '-w1', host] + proc = subprocess.Popen(cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + proc.wait() + return (proc.returncode == 0) == should_succeed diff --git a/rally/benchmark/scenarios/vm/vmtasks.py b/rally/benchmark/scenarios/vm/vmtasks.py index 103a32e6f6..7a742f8220 100644 --- a/rally/benchmark/scenarios/vm/vmtasks.py +++ b/rally/benchmark/scenarios/vm/vmtasks.py @@ -20,11 +20,7 @@ from rally.benchmark.scenarios.nova import utils as nova_utils from rally.benchmark.scenarios.vm import utils as vm_utils from rally.benchmark import types as types from rally.benchmark import validation -from rally.openstack.common.gettextutils import _ # noqa -from rally.openstack.common import log as logging - - -LOG = logging.getLogger(__name__) +from rally import exceptions class VMTasks(nova_utils.NovaScenario, vm_utils.VMScenario): @@ -38,44 +34,100 @@ class VMTasks(nova_utils.NovaScenario, vm_utils.VMScenario): @validation.add(validation.file_exists("script")) @validation.add(validation.number("port", minval=1, maxval=65535, nullable=True, integer_only=True)) + @validation.add(validation.external_network_exists("floating_network", + "use_floatingip")) @base.scenario(context={"cleanup": ["nova"], "keypair": {}, "allow_ssh": {}}) def boot_runcommand_delete(self, image, flavor, - script, interpreter, network='private', - username='ubuntu', ip_version=4, - port=22, **kwargs): + script, interpreter, username, + fixed_network="private", + floating_network="public", + ip_version=4, port=22, + use_floatingip=True, **kwargs): """Boot server, run a script that outputs JSON, delete server. - Parameters: - script: script to run on the server, must output JSON mapping metric - names to values. See sample script below. - network: Network to choose address to connect to instance from - username: User to SSH to instance as - ip_version: Version of ip protocol to use for connection + :param script: script to run on the server, must output JSON mapping + metric names to values. See sample script below. + :param interpreter: The shell interpreter to use when running script + :param username: User to SSH to instance as + :param fixed_network: Network where instance is part of + :param floating_network: External network used to get floating ip from + :param ip_version: Version of ip protocol to use for connection + :param port: Port to use for SSH connection + :param use_floatingip: Whether to associate a floating ip for + connection - returns: Dictionary containing two keys, data and errors. Data is JSON + :returns: Dictionary containing two keys, data and errors. Data is JSON data output by the script. Errors is raw data from the script's standard error stream. Example Script in doc/samples/support/instance_dd_test.sh """ - server = self._boot_server( - self._generate_random_name("rally_novaserver_"), - image, flavor, key_name='rally_ssh_key', **kwargs) - - code, out, err = self.run_command(server, username, network, port, - ip_version, interpreter, script) - if code: - LOG.error(_("Error running script on instance via SSH. " - "Error: %s") % err) + server = None + floating_ip = None try: - out = json.loads(out) - except ValueError: - LOG.warning(_("Script %s did not output valid JSON.") % script) + server = self._boot_server( + self._generate_random_name("rally_novaserver_"), + image, flavor, key_name='rally_ssh_key', **kwargs) + + self.check_network(server, fixed_network) + + fixed_ip = [ip for ip in server.addresses[fixed_network] if + ip["version"] == ip_version][0]["addr"] + + if use_floatingip: + floating_ip = self._create_floating_ip(floating_network) + self._associate_floating_ip(server, floating_ip) + server_ip = floating_ip.ip + else: + server_ip = fixed_ip + + code, out, err = self.run_command(server_ip, port, + username, interpreter, script) + + if code: + raise exceptions.ScriptError( + "Error running script %(script)s." + "Error %(code)s: %(error)s" % { + "script": script, + "code": code, + "error": err + }) + + try: + out = json.loads(out) + except ValueError as e: + raise exceptions.ScriptError( + "Script %(script)s did not output valid JSON: %(error)s" % + { + "script": script, + "error": str(e) + } + ) + + # Always try to free resources + finally: + if use_floatingip: + self._release_server_floating_ip(server, floating_ip) + if server: + self._delete_server(server) - self._delete_server(server) - LOG.debug("Output streams from in-instance script execution: " - "stdout: %(stdout)s, stderr: $(stderr)s" % dict( - stdout=out, stderr=err)) return {"data": out, "errors": err} + + def _release_server_floating_ip(self, server, floating_ip): + """Release a floating ip associated to a server. + + This method check that the given floating ip is associated with the + specified server and tries to dissociate it. + Once dissociated, release the floating ip to reintegrate + it to the pool of available ips. + + :param server: The server to dissociate the floating ip from + :param floating_ip: The floating ip to release + """ + if floating_ip and server: + if self.check_ip_address(floating_ip)(server): + self._dissociate_floating_ip(server, floating_ip) + if floating_ip: + self._delete_floating_ip(floating_ip) diff --git a/rally/benchmark/validation.py b/rally/benchmark/validation.py index 18bb7817ab..56648de5af 100644 --- a/rally/benchmark/validation.py +++ b/rally/benchmark/validation.py @@ -227,6 +227,46 @@ def image_valid_on_flavor(flavor_name, image_name): return image_valid_on_flavor_validator +def network_exists(network_name): + def network_exists_validator(**kwargs): + network = kwargs.get(network_name, "private") + + networks = [net.label for net in + kwargs["clients"].nova().networks.list()] + if network not in networks: + message = _("Network with name %(network)s not found. " + "Available networks: %(networks)s") % { + "network": network, + "networks": networks + } + return ValidationResult(False, message) + + return ValidationResult() + return network_exists_validator + + +def external_network_exists(network_name, use_external_network): + def external_network_exists_validator(**kwargs): + if not kwargs.get(use_external_network, True): + return ValidationResult() + + ext_network = kwargs.get(network_name, "public") + + networks = [net.name for net in + kwargs["clients"].nova().floating_ip_pools.list()] + if ext_network not in networks: + message = _("External (floating) network with name %(network)s " + "not found. " + "Available networks: %(networks)s") % { + "network": ext_network, + "networks": networks + } + return ValidationResult(False, message) + + return ValidationResult() + return external_network_exists_validator + + def tempest_tests_exists(): """Returns validator for tempest test.""" def tempest_test_exists_validator(**kwargs): diff --git a/rally/exceptions.py b/rally/exceptions.py index c499ef5652..58a1b76e5c 100644 --- a/rally/exceptions.py +++ b/rally/exceptions.py @@ -186,6 +186,10 @@ class SSHError(RallyException): msg_fmt = _("Remote command failed.") +class ScriptError(RallyException): + msg_fmt = _("Script execution failed.") + + class TaskInvalidStatus(RallyException): msg_fmt = _("Task `%(uuid)s` in `%(actual)s` status but `%(require)s` is " "required.") diff --git a/rally/sshutils.py b/rally/sshutils.py index df5dca82fc..33ae022035 100644 --- a/rally/sshutils.py +++ b/rally/sshutils.py @@ -123,7 +123,7 @@ class SSH(object): self._client.connect(self.host, username=self.user, port=self.port, pkey=self.pkey, key_filename=self.key_filename, - password=self.password) + password=self.password, timeout=1) return self._client except Exception as e: message = _("Exception %(exception_type)s was raised " diff --git a/tests/benchmark/scenarios/nova/test_utils.py b/tests/benchmark/scenarios/nova/test_utils.py index 94e06b6102..6df9535b5e 100644 --- a/tests/benchmark/scenarios/nova/test_utils.py +++ b/tests/benchmark/scenarios/nova/test_utils.py @@ -35,6 +35,7 @@ class NovaScenarioTestCase(test.TestCase): super(NovaScenarioTestCase, self).setUp() self.server = mock.Mock() self.server1 = mock.Mock() + self.floating_ip = mock.Mock() self.image = mock.Mock() self.res_is = mockpatch.Patch(BM_UTILS + ".resource_is") self.get_fm = mockpatch.Patch(BM_UTILS + '.get_from_manager') @@ -270,3 +271,98 @@ class NovaScenarioTestCase(test.TestCase): self.res_is.mock.assert_has_calls(mock.call('ACTIVE')) self._test_atomic_action_timer(nova_scenario.atomic_actions(), 'nova.boot_servers') + + @mock.patch(NOVA_UTILS + '.NovaScenario.clients') + def test__list_floating_ip_pools(self, mock_clients): + pools_list = [] + mock_clients("nova").floating_ip_pools.list.return_value = pools_list + nova_scenario = utils.NovaScenario() + return_pools_list = nova_scenario._list_floating_ip_pools() + self.assertEqual(pools_list, return_pools_list) + self._test_atomic_action_timer(nova_scenario.atomic_actions(), + 'nova.list_floating_ip_pools') + + @mock.patch(NOVA_UTILS + '.NovaScenario.clients') + def test__list_floating_ips(self, mock_clients): + floating_ips_list = [] + mock_clients("nova").floating_ips.list.return_value = floating_ips_list + nova_scenario = utils.NovaScenario() + return_floating_ips_list = nova_scenario._list_floating_ips() + self.assertEqual(floating_ips_list, return_floating_ips_list) + self._test_atomic_action_timer(nova_scenario.atomic_actions(), + 'nova.list_floating_ips') + + @mock.patch(NOVA_UTILS + '.NovaScenario.clients') + def test__create_floating_ip(self, mock_clients): + mock_clients("nova").floating_ips.create.return_value = \ + self.floating_ip + nova_scenario = utils.NovaScenario() + return_floating_ip = nova_scenario._create_floating_ip("public") + self.assertEqual(self.floating_ip, return_floating_ip) + self._test_atomic_action_timer(nova_scenario.atomic_actions(), + 'nova.create_floating_ip') + + @mock.patch(NOVA_UTILS + '.NovaScenario.clients') + def test__delete_floating_ip(self, mock_clients): + nova_scenario = utils.NovaScenario() + nova_scenario._delete_floating_ip(self.floating_ip) + mock_clients("nova").floating_ips.delete.assert_called_once_with( + self.floating_ip) + self._test_atomic_action_timer(nova_scenario.atomic_actions(), + 'nova.delete_floating_ip') + + def test__associate_floating_ip(self): + nova_scenario = utils.NovaScenario() + nova_scenario._associate_floating_ip(self.server, self.floating_ip) + self.server.add_floating_ip.assert_called_once_with(self.floating_ip, + fixed_address=None) + self._test_atomic_action_timer(nova_scenario.atomic_actions(), + 'nova.associate_floating_ip') + + def test__dissociate_floating_ip(self): + nova_scenario = utils.NovaScenario() + nova_scenario._dissociate_floating_ip(self.server, self.floating_ip) + self.server.remove_floating_ip.assert_called_once_with( + self.floating_ip) + self._test_atomic_action_timer(nova_scenario.atomic_actions(), + 'nova.dissociate_floating_ip') + + def test__check_ip_address(self): + nova_scenario = utils.NovaScenario() + fake_server = fakes.FakeServerManager().create("test_server", + "image_id_01", + "flavor_id_01") + fake_server.addresses = { + "private": [ + {"version": 4, "addr": "1.2.3.4"}, + ]} + floating_ip = fakes.FakeFloatingIP() + floating_ip.ip = "10.20.30.40" + + # Also test function check_ip_address accept a string as attr + self.assertFalse( + nova_scenario.check_ip_address(floating_ip.ip)(fake_server)) + self.assertTrue( + nova_scenario.check_ip_address(floating_ip.ip, must_exist=False) + (fake_server)) + + fake_server.addresses["private"].append( + {"version": 4, "addr": floating_ip.ip} + ) + # Also test function check_ip_address accept an object with attr ip + self.assertTrue( + nova_scenario.check_ip_address(floating_ip) + (fake_server)) + self.assertFalse( + nova_scenario.check_ip_address(floating_ip, must_exist=False) + (fake_server)) + + @mock.patch(NOVA_UTILS + '.NovaScenario.clients') + def test__list_networks(self, mock_clients): + network_list = [] + mock_clients("nova").networks.list.return_value = network_list + nova_scenario = utils.NovaScenario() + return_network_list = nova_scenario._list_networks() + self.assertEqual(network_list, return_network_list) + self._test_atomic_action_timer(nova_scenario.atomic_actions(), + 'nova.list_networks') diff --git a/tests/benchmark/scenarios/vm/test_utils.py b/tests/benchmark/scenarios/vm/test_utils.py index 1a02ae8900..5943d48b45 100644 --- a/tests/benchmark/scenarios/vm/test_utils.py +++ b/tests/benchmark/scenarios/vm/test_utils.py @@ -14,7 +14,9 @@ # under the License. import mock +import subprocess +from oslotest import mockpatch from rally.benchmark.scenarios.vm import utils from tests import fakes from tests import test @@ -25,6 +27,12 @@ VMTASKS_UTILS = "rally.benchmark.scenarios.vm.utils" class VMScenarioTestCase(test.TestCase): + def setUp(self): + super(VMScenarioTestCase, self).setUp() + self.wait_for = mockpatch.Patch(VMTASKS_UTILS + + ".bench_utils.wait_for") + self.useFixture(self.wait_for) + @mock.patch('__builtin__.open') def test_run_action(self, mock_open): mock_ssh = mock.MagicMock() @@ -35,32 +43,64 @@ class VMScenarioTestCase(test.TestCase): mock_ssh.execute.assert_called_once_with('interpreter', stdin=mock_file_handle) - def test_wait_for_network(self): + def test_wait_for_ssh(self): ssh = mock.MagicMock() vm_scenario = utils.VMScenario() - vm_scenario.wait_for_network(ssh) + vm_scenario.wait_for_ssh(ssh) ssh.wait.assert_called_once_with() + @mock.patch(VMTASKS_UTILS + ".VMScenario.ping_ip_address") + def test_wait_for_ping(self, mock_ping): + mock_ping.return_value = True + vm_scenario = utils.VMScenario() + vm_scenario.wait_for_ping("1.2.3.4") + self.wait_for.mock.assert_called_once_with("1.2.3.4", + is_ready=mock_ping, + timeout=120) + @mock.patch(VMTASKS_UTILS + ".VMScenario.run_action") + @mock.patch(VMTASKS_UTILS + ".VMScenario.wait_for_ping") @mock.patch("rally.sshutils.SSH") - def test_run_command(self, mock_ssh_class, + def test_run_command(self, mock_ssh_class, mock_wait_ping, mock_run_action): mock_ssh_instance = mock.MagicMock() mock_ssh_class.return_value = mock_ssh_instance - fake_server = fakes.FakeServer() - fake_server.addresses = dict( - private=[dict( - version=4, - addr="1.2.3.4" - )] - ) + vm_scenario = utils.VMScenario() vm_scenario._context = {"user": {"keypair": {"private": "ssh"}}} - vm_scenario.run_command(fake_server, "username", "private", 22, 4, - "int", "script") + vm_scenario.run_command("1.2.3.4", 22, "username", "int", "script") - mock_ssh_class.assert_called_once_with("username", "1.2.3.4", - port=22, pkey="ssh") + mock_wait_ping.assert_called_once_with("1.2.3.4") + mock_ssh_class.assert_called_once_with("username", "1.2.3.4", port=22, + pkey="ssh") mock_ssh_instance.wait.assert_called_once_with() mock_run_action.assert_called_once_with(mock_ssh_instance, "int", "script") + + def test_check_network(self): + vm_scenario = utils.VMScenario() + fake_server = fakes.FakeServer() + fake_server.addresses = {} + self.assertRaises(ValueError, + vm_scenario.check_network, fake_server, "private") + fake_server.addresses["private_1"] = { + "version": 4, + "addr": "1.2.3.4" + } + vm_scenario.check_network(fake_server, "private_1") + + @mock.patch("subprocess.Popen") + def test_ping_ip_address(self, mock_subprocess): + + ping_process = mock.MagicMock() + ping_process.returncode = 0 + mock_subprocess.return_value = ping_process + + vm_scenario = utils.VMScenario() + host_ip = "1.2.3.4" + self.assertTrue(vm_scenario.ping_ip_address(host_ip)) + + mock_subprocess.assert_called_once_with( + ['ping', '-c1', '-w1', host_ip], + stderr=subprocess.PIPE, stdout=subprocess.PIPE) + ping_process.wait.assert_called_once_with() diff --git a/tests/benchmark/scenarios/vm/test_vmtasks.py b/tests/benchmark/scenarios/vm/test_vmtasks.py index 0e5d1a33a0..b74beba22c 100644 --- a/tests/benchmark/scenarios/vm/test_vmtasks.py +++ b/tests/benchmark/scenarios/vm/test_vmtasks.py @@ -16,6 +16,7 @@ import mock from rally.benchmark.scenarios.vm import vmtasks +from tests import fakes from tests import test @@ -25,27 +26,119 @@ class VMTasksTestCase(test.TestCase): def test_boot_runcommand_delete(self, mock_json_loads): # Setup mocks scenario = vmtasks.VMTasks() - scenario._boot_server = mock.MagicMock(return_value="fake_server") + fake_server = fakes.FakeServer() + fake_server.addresses = dict( + private=[dict( + version=4, + addr="1.2.3.4" + )] + ) + + scenario._boot_server = mock.MagicMock(return_value=fake_server) + scenario._generate_random_name = mock.MagicMock(return_value="name") + + fake_floating_ip = fakes.FakeFloatingIP() + fake_floating_ip.ip = "4.3.2.1" + scenario._create_floating_ip = mock.MagicMock( + return_value=fake_floating_ip) + scenario._associate_floating_ip = mock.MagicMock() + #scenario._dissociate_floating_ip = mock.MagicMock() + #scenario._delete_floating_ip = mock.MagicMock() + scenario._release_server_floating_ip = mock.MagicMock() + #scenario.check_ip_address = mock.MagicMock( + # return_value=mock.MagicMock(return_value=True)) + + fake_floating_ip_pool = fakes.FakeFloatingIPPool() + fake_floating_ip_pool.name = "public" + scenario._list_floating_ip_pools = mock.MagicMock( + return_value=[fake_floating_ip_pool]) + scenario.run_command = mock.MagicMock() - scenario.run_command.return_value = ('code', 'stdout', 'stderr') + scenario.run_command.return_value = (0, 'stdout', 'stderr') scenario._delete_server = mock.MagicMock() # Run scenario scenario.boot_runcommand_delete( "image_id", "flavour_id", "script_path", "interpreter", - network="network", username="username", ip_version="ip_version", - port="port", fakearg="f") + fixed_network='private', floating_network='public', + username="username", ip_version=4, + port=22, use_floatingip=True, fakearg="f") # Assertions scenario._boot_server.assert_called_once_with( - 'name', 'image_id', "flavour_id", key_name="rally_ssh_key", - fakearg="f") + 'name', 'image_id', "flavour_id", key_name="rally_ssh_key", + fakearg="f") + scenario._create_floating_ip.assert_called_once_with( + fake_floating_ip_pool.name) + scenario._associate_floating_ip.assert_called_once_with( + fake_server, fake_floating_ip) scenario.run_command.assert_called_once_with( - "fake_server", 'username', "network", "port", "ip_version", + fake_floating_ip.ip, 22, 'username', "interpreter", "script_path") mock_json_loads.assert_called_once_with('stdout') - scenario._delete_server.assert_called_once_with("fake_server") + #scenario._dissociate_floating_ip.assert_called_once_with( + # fake_server, fake_floating_ip) + #scenario._delete_floating_ip.assert_called_once_with(fake_floating_ip) + scenario._release_server_floating_ip.assert_called_once_with( + fake_server, fake_floating_ip) + scenario._delete_server.assert_called_once_with(fake_server) + + @mock.patch("json.loads") + def test_boot_runcommand_delete_no_floating_ip(self, mock_json_loads): + # Setup mocks + scenario = vmtasks.VMTasks() + fake_server = fakes.FakeServer() + fake_server.addresses = dict( + private=[dict( + version=4, + addr="1.2.3.4" + )] + ) + + scenario._boot_server = mock.MagicMock(return_value=fake_server) + + scenario._generate_random_name = mock.MagicMock(return_value="name") + + scenario.run_command = mock.MagicMock() + scenario.run_command.return_value = (0, 'stdout', 'stderr') + scenario._delete_server = mock.MagicMock() + + # Run scenario + scenario.boot_runcommand_delete( + "image_id", "flavour_id", "script_path", "interpreter", + fixed_network='private', floating_network='public', + username="username", ip_version=4, + port=22, use_floatingip=False, fakearg="f") + + # Assertions + scenario._boot_server.assert_called_once_with( + 'name', 'image_id', "flavour_id", key_name="rally_ssh_key", + fakearg="f") + + scenario.run_command.assert_called_once_with( + fake_server.addresses['private'][0]['addr'], 22, 'username', + "interpreter", "script_path") + + mock_json_loads.assert_called_once_with('stdout') + + scenario._delete_server.assert_called_once_with(fake_server) + + def test__release_server_floating_ip(self): + scenario = vmtasks.VMTasks() + fake_server = fakes.FakeServer() + fake_floating_ip = fakes.FakeFloatingIP() + + scenario._dissociate_floating_ip = mock.MagicMock() + scenario._delete_floating_ip = mock.MagicMock() + scenario.check_ip_address = mock.MagicMock( + return_value=mock.MagicMock(return_value=True)) + + scenario._release_server_floating_ip(fake_server, fake_floating_ip) + + scenario._dissociate_floating_ip.assert_called_once_with( + fake_server, fake_floating_ip) + scenario._delete_floating_ip.assert_called_once_with(fake_floating_ip) diff --git a/tests/benchmark/test_validation.py b/tests/benchmark/test_validation.py index 963dc72a1e..cd47d48f4b 100644 --- a/tests/benchmark/test_validation.py +++ b/tests/benchmark/test_validation.py @@ -260,6 +260,118 @@ class ValidationUtilsTestCase(test.TestCase): self.assertFalse(result.is_valid) self.assertEqual(result.msg, "Image with id 'test_image_id' not found") + @mock.patch("rally.osclients.Clients") + def test_network_exists(self, mock_osclients): + fakenclient = fakes.FakeNovaClient() + fake_network = fakes.FakeNetwork() + fake_network.label = "private" + fake_network.id = "net_id_1234" + + fakenclient.networks.list = mock.MagicMock( + return_value=[fake_network]) + mock_osclients.nova.return_value = fakenclient + + validator = validation.network_exists("fixed_network") + + network_name = "private" + + result = validator(clients=mock_osclients, + fixed_network=network_name) + + fakenclient.networks.list.assert_called_once_with() + self.assertTrue(result.is_valid) + self.assertIsNone(result.msg) + + @mock.patch("rally.osclients.Clients") + def test_network_exists_fail(self, mock_osclients): + fakenclient = fakes.FakeNovaClient() + fake_network = fakes.FakeNetwork() + fake_network.label = "private" + fake_network.id = "net_id_1234" + + fakenclient.networks.list = mock.MagicMock( + return_value=[fake_network]) + mock_osclients.nova.return_value = fakenclient + + validator = validation.network_exists("fixed_network") + + network_name = "foo" + + result = validator(clients=mock_osclients, + fixed_network=network_name) + + fakenclient.networks.list.assert_called_once_with() + self.assertFalse(result.is_valid) + self.assertEqual(result.msg, + "Network with name foo not found. " + "Available networks: ['private']") + + @mock.patch("rally.osclients.Clients") + def test_external_network_exists(self, mock_osclients): + fakenclient = fakes.FakeNovaClient() + fake_pool = fakes.FakeFloatingIPPool() + fake_pool.name = "floating" + fakenclient.floating_ip_pools.list = mock.MagicMock( + return_value=[fake_pool]) + mock_osclients.nova.return_value = fakenclient + + validator = validation.external_network_exists("floating_network", + "use_floatingip") + + network_name = "floating" + + result = validator(clients=mock_osclients, + floating_network=network_name) + + fakenclient.floating_ip_pools.list.assert_called_once_with() + self.assertTrue(result.is_valid) + self.assertIsNone(result.msg) + + @mock.patch("rally.osclients.Clients") + def test_external_network_exists_ignored(self, mock_osclients): + fakenclient = fakes.FakeNovaClient() + fake_pool = fakes.FakeFloatingIPPool() + fake_pool.name = "floating" + fakenclient.floating_ip_pools.list = mock.MagicMock( + return_value=[fake_pool]) + mock_osclients.nova.return_value = fakenclient + + validator = validation.external_network_exists("floating_network", + "use_floatingip") + + network_name = "not_used" + + result = validator(clients=mock_osclients, + floating_network=network_name, + use_floatingip=False) + + self.assertFalse(fakenclient.floating_ip_pools.list.called) + self.assertTrue(result.is_valid) + self.assertIsNone(result.msg) + + @mock.patch("rally.osclients.Clients") + def test_external_network_exists_fail(self, mock_osclients): + fakenclient = fakes.FakeNovaClient() + fake_pool = fakes.FakeFloatingIPPool() + fake_pool.name = "floating" + fakenclient.floating_ip_pools.list = mock.MagicMock( + return_value=[fake_pool]) + mock_osclients.nova.return_value = fakenclient + + validator = validation.external_network_exists("floating_network", + "use_floatingip") + + network_name = "foo" + + result = validator(clients=mock_osclients, + floating_network=network_name) + + fakenclient.floating_ip_pools.list.assert_called_once_with() + self.assertFalse(result.is_valid) + self.assertEqual(result.msg, "External (floating) network with name " + "foo not found. " + "Available networks: ['floating']") + @mock.patch("rally.osclients.Clients") def test_image_valid_on_flavor_flavor_not_exist(self, mock_osclients): fakegclient = fakes.FakeGlanceClient() diff --git a/tests/fakes.py b/tests/fakes.py index 21da873138..28a1c83f22 100644 --- a/tests/fakes.py +++ b/tests/fakes.py @@ -134,6 +134,10 @@ class FakeFloatingIP(FakeResource): pass +class FakeFloatingIPPool(FakeResource): + pass + + class FakeTenant(FakeResource): def __init__(self, manager, name): @@ -337,6 +341,12 @@ class FakeFloatingIPsManager(FakeManager): return FakeFloatingIP(self) +class FakeFloatingIPPoolsManager(FakeManager): + + def create(self): + return FakeFloatingIPPool(self) + + class FakeTenantsManager(FakeManager): def create(self, name): @@ -565,6 +575,7 @@ class FakeNovaClient(object): else: self.servers = FakeServerManager(self.images) self.floating_ips = FakeFloatingIPsManager() + self.floating_ip_pools = FakeFloatingIPPoolsManager() self.networks = FakeNetworkManager() self.flavors = FakeFlavorManager() self.keypairs = FakeKeypairManager() diff --git a/tests/test_sshutils.py b/tests/test_sshutils.py index 02aecbb4ec..74040c44da 100644 --- a/tests/test_sshutils.py +++ b/tests/test_sshutils.py @@ -104,7 +104,7 @@ class SSHTestCase(test.TestCase): mock.call.set_missing_host_key_policy('autoadd'), mock.call.connect('example.net', username='admin', port=22, pkey='key', key_filename=None, - password=None), + password=None, timeout=1), ] self.assertEqual(client_calls, client.mock_calls)