diff options
author | Ross Brattain <ross.b.brattain@intel.com> | 2017-06-22 15:10:21 +0000 |
---|---|---|
committer | Gerrit Code Review <gerrit@opnfv.org> | 2017-06-22 15:10:21 +0000 |
commit | e80c35164e7dfee4fe4a3652b71b8775c1c0857a (patch) | |
tree | b62143208b719c39de73c001a547258ab19bfa98 | |
parent | 6b3ee75dc0b5fc0e66c914d0b72b4396411526fd (diff) | |
parent | 653902770572c780777d1dc7a371794b670585b1 (diff) |
Merge "Acquire NSB specific data from Heat."
-rw-r--r-- | samples/vnf_samples/nsut/ping/tc_external_ping_heat_context.yaml | 61 | ||||
-rw-r--r-- | samples/vnf_samples/nsut/ping/tc_ping_heat_context.yaml | 61 | ||||
-rw-r--r-- | tests/unit/benchmark/contexts/test_heat.py | 8 | ||||
-rw-r--r-- | tests/unit/benchmark/scenarios/networking/test_vnf_generic.py | 150 | ||||
-rw-r--r-- | tests/unit/orchestrator/test_heat.py | 328 | ||||
-rw-r--r-- | yardstick/benchmark/contexts/heat.py | 77 | ||||
-rw-r--r-- | yardstick/benchmark/contexts/model.py | 2 | ||||
-rw-r--r-- | yardstick/benchmark/core/task.py | 3 | ||||
-rw-r--r-- | yardstick/benchmark/scenarios/networking/vnf_generic.py | 130 | ||||
-rw-r--r-- | yardstick/network_services/vnf_generic/vnfdgen.py | 18 | ||||
-rw-r--r-- | yardstick/orchestrator/heat.py | 93 |
11 files changed, 780 insertions, 151 deletions
diff --git a/samples/vnf_samples/nsut/ping/tc_external_ping_heat_context.yaml b/samples/vnf_samples/nsut/ping/tc_external_ping_heat_context.yaml new file mode 100644 index 000000000..8826f539e --- /dev/null +++ b/samples/vnf_samples/nsut/ping/tc_external_ping_heat_context.yaml @@ -0,0 +1,61 @@ +# 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. + +--- +schema: "yardstick:task:0.1" + +scenarios: +- + type: NSPerf + traffic_profile: ../../traffic_profiles/fixed.yaml + topology: ping_tg_topology.yaml + + nodes: + tg__1: trafficgen_1.baremetal + vnf__1: vnf.yardstick + + runner: + type: Duration + duration: 10 + +contexts: + - name: yardstick + image: yardstick-image + flavor: yardstick-flavor + user: ubuntu + + placement_groups: + pgrp1: + policy: "availability" + + servers: + vnf: + floating_ip: true + placement: "pgrp1" + + networks: + mgmt: + cidr: '10.0.1.0/24' + external_network: "yardstick-public" + xe0: + cidr: '10.0.2.0/24' + vld_id: public + + xe1: + cidr: '10.0.3.0/24' + vld_id: private + + - name: baremetal + type: Node + file: baremetal-pod.yaml diff --git a/samples/vnf_samples/nsut/ping/tc_ping_heat_context.yaml b/samples/vnf_samples/nsut/ping/tc_ping_heat_context.yaml new file mode 100644 index 000000000..394523ffa --- /dev/null +++ b/samples/vnf_samples/nsut/ping/tc_ping_heat_context.yaml @@ -0,0 +1,61 @@ +# 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. + +--- +schema: "yardstick:task:0.1" + +scenarios: +- + type: NSPerf + traffic_profile: ../../traffic_profiles/fixed.yaml + topology: ping_tg_topology.yaml + + nodes: + tg__1: trafficgen_1.yardstick + vnf__1: vnf.yardstick + + runner: + type: Duration + duration: 10 + +context: + name: yardstick + image: yardstick-image + flavor: yardstick-flavor + user: ubuntu + + placement_groups: + pgrp1: + policy: "availability" + + servers: + vnf: + floating_ip: true + placement: "pgrp1" + trafficgen_1: + floating_ip: true + placement: "pgrp1" + + networks: + mgmt: + cidr: '10.0.1.0/24' + external_network: "yardstick-public" + xe0: + cidr: '10.0.2.0/24' + vld_id: public + xe1: + cidr: '10.0.3.0/24' + vld_id: private + + diff --git a/tests/unit/benchmark/contexts/test_heat.py b/tests/unit/benchmark/contexts/test_heat.py index d878ebe97..3dadd48eb 100644 --- a/tests/unit/benchmark/contexts/test_heat.py +++ b/tests/unit/benchmark/contexts/test_heat.py @@ -17,6 +17,7 @@ import logging import os import unittest import uuid +from collections import OrderedDict import mock @@ -37,7 +38,7 @@ class HeatContextTestCase(unittest.TestCase): self.assertIsNone(self.test_context.name) self.assertIsNone(self.test_context.stack) - self.assertEqual(self.test_context.networks, []) + self.assertEqual(self.test_context.networks, OrderedDict()) self.assertEqual(self.test_context.servers, []) self.assertEqual(self.test_context.placement_groups, []) self.assertEqual(self.test_context.server_groups, []) @@ -105,7 +106,9 @@ class HeatContextTestCase(unittest.TestCase): self.test_context.key_uuid = "2f2e4997-0a8e-4eb7-9fa4-f3f8fbbc393b" netattrs = {'cidr': '10.0.0.0/24', 'provider': None, 'external_network': 'ext_net'} self.mock_context.name = 'bar' - self.test_context.networks = [model.Network("fool-network", self.mock_context, netattrs)] + self.test_context.networks = OrderedDict( + {"fool-network": model.Network("fool-network", self.mock_context, + netattrs)}) self.test_context._add_resources_to_template(mock_template) mock_template.add_keypair.assert_called_with( @@ -122,6 +125,7 @@ class HeatContextTestCase(unittest.TestCase): self.test_context.name = 'foo' self.test_context.template_file = '/bar/baz/some-heat-file' self.test_context.heat_parameters = {'image': 'cirros'} + self.test_context.heat_timeout = 5 self.test_context.deploy() mock_template.assert_called_with(self.test_context.name, diff --git a/tests/unit/benchmark/scenarios/networking/test_vnf_generic.py b/tests/unit/benchmark/scenarios/networking/test_vnf_generic.py index 4167d6f3b..111e7812e 100644 --- a/tests/unit/benchmark/scenarios/networking/test_vnf_generic.py +++ b/tests/unit/benchmark/scenarios/networking/test_vnf_generic.py @@ -20,12 +20,13 @@ from __future__ import absolute_import import os +import errno import unittest - import mock from yardstick.benchmark.scenarios.networking.vnf_generic import \ - SshManager, NetworkServiceTestCase, IncorrectConfig, IncorrectSetup + SshManager, NetworkServiceTestCase, IncorrectConfig, \ + IncorrectSetup, open_relative_file from yardstick.network_services.collector.subscriber import Collector from yardstick.network_services.vnf_generic.vnf.base import \ GenericTrafficGen, GenericVNF @@ -288,6 +289,7 @@ class TestNetworkServiceTestCase(unittest.TestCase): } self.scenario_cfg = { + 'task_path': "", 'tc_options': {'rfc2544': {'allowed_drop_rate': '0.8 - 1'}}, 'task_id': 'a70bdf4a-8e67-47a3-9dc1-273c14506eb7', 'tc': 'tc_ipv4_1Mflow_64B_packetsize', @@ -350,7 +352,8 @@ class TestNetworkServiceTestCase(unittest.TestCase): vnf = mock.Mock(autospec=GenericVNF) self.s.get_vnf_impl = mock.Mock(return_value=vnf) - self.assertIsNotNone(self.s.load_vnf_models(self.context_cfg)) + self.assertIsNotNone( + self.s.load_vnf_models(self.scenario_cfg, self.context_cfg)) def test_map_topology_to_infrastructure(self): with mock.patch("yardstick.ssh.SSH") as ssh: @@ -488,3 +491,144 @@ class TestNetworkServiceTestCase(unittest.TestCase): self.s.collector.stop = \ mock.Mock(return_value=True) self.assertIsNone(self.s.teardown()) + + SAMPLE_NETDEVS = { + 'enp11s0': { + 'address': '0a:de:ad:be:ef:f5', + 'device': '0x1533', + 'driver': 'igb', + 'ifindex': '2', + 'interface_name': 'enp11s0', + 'operstate': 'down', + 'pci_bus_id': '0000:0b:00.0', + 'subsystem_device': '0x1533', + 'subsystem_vendor': '0x15d9', + 'vendor': '0x8086' + }, + 'lan': { + 'address': '0a:de:ad:be:ef:f4', + 'device': '0x153a', + 'driver': 'e1000e', + 'ifindex': '3', + 'interface_name': 'lan', + 'operstate': 'up', + 'pci_bus_id': '0000:00:19.0', + 'subsystem_device': '0x153a', + 'subsystem_vendor': '0x15d9', + 'vendor': '0x8086' + } + } + SAMPLE_VM_NETDEVS = { + 'eth1': { + 'address': 'fa:de:ad:be:ef:5b', + 'device': '0x0001', + 'driver': 'virtio_net', + 'ifindex': '3', + 'interface_name': 'eth1', + 'operstate': 'down', + 'pci_bus_id': '0000:00:04.0', + 'vendor': '0x1af4' + } + } + + def test_parse_netdev_info(self): + output = """\ +/sys/devices/pci0000:00/0000:00:1c.3/0000:0b:00.0/net/enp11s0/ifindex:2 +/sys/devices/pci0000:00/0000:00:1c.3/0000:0b:00.0/net/enp11s0/address:0a:de:ad:be:ef:f5 +/sys/devices/pci0000:00/0000:00:1c.3/0000:0b:00.0/net/enp11s0/operstate:down +/sys/devices/pci0000:00/0000:00:1c.3/0000:0b:00.0/net/enp11s0/device/vendor:0x8086 +/sys/devices/pci0000:00/0000:00:1c.3/0000:0b:00.0/net/enp11s0/device/device:0x1533 +/sys/devices/pci0000:00/0000:00:1c.3/0000:0b:00.0/net/enp11s0/device/subsystem_vendor:0x15d9 +/sys/devices/pci0000:00/0000:00:1c.3/0000:0b:00.0/net/enp11s0/device/subsystem_device:0x1533 +/sys/devices/pci0000:00/0000:00:1c.3/0000:0b:00.0/net/enp11s0/driver:igb +/sys/devices/pci0000:00/0000:00:1c.3/0000:0b:00.0/net/enp11s0/pci_bus_id:0000:0b:00.0 +/sys/devices/pci0000:00/0000:00:19.0/net/lan/ifindex:3 +/sys/devices/pci0000:00/0000:00:19.0/net/lan/address:0a:de:ad:be:ef:f4 +/sys/devices/pci0000:00/0000:00:19.0/net/lan/operstate:up +/sys/devices/pci0000:00/0000:00:19.0/net/lan/device/vendor:0x8086 +/sys/devices/pci0000:00/0000:00:19.0/net/lan/device/device:0x153a +/sys/devices/pci0000:00/0000:00:19.0/net/lan/device/subsystem_vendor:0x15d9 +/sys/devices/pci0000:00/0000:00:19.0/net/lan/device/subsystem_device:0x153a +/sys/devices/pci0000:00/0000:00:19.0/net/lan/driver:e1000e +/sys/devices/pci0000:00/0000:00:19.0/net/lan/pci_bus_id:0000:00:19.0 +""" + res = NetworkServiceTestCase.parse_netdev_info(output) + assert res == self.SAMPLE_NETDEVS + + def test_parse_netdev_info_virtio(self): + output = """\ +/sys/devices/pci0000:00/0000:00:04.0/virtio1/net/eth1/ifindex:3 +/sys/devices/pci0000:00/0000:00:04.0/virtio1/net/eth1/address:fa:de:ad:be:ef:5b +/sys/devices/pci0000:00/0000:00:04.0/virtio1/net/eth1/operstate:down +/sys/devices/pci0000:00/0000:00:04.0/virtio1/net/eth1/device/vendor:0x1af4 +/sys/devices/pci0000:00/0000:00:04.0/virtio1/net/eth1/device/device:0x0001 +/sys/devices/pci0000:00/0000:00:04.0/virtio1/net/eth1/driver:virtio_net +""" + res = NetworkServiceTestCase.parse_netdev_info(output) + assert res == self.SAMPLE_VM_NETDEVS + + def test_sort_dpdk_port_num(self): + netdevs = self.SAMPLE_NETDEVS.copy() + NetworkServiceTestCase._sort_dpdk_port_num(netdevs) + assert netdevs['lan']['dpdk_port_num'] == 1 + assert netdevs['enp11s0']['dpdk_port_num'] == 2 + + def test_probe_missing_values(self): + netdevs = self.SAMPLE_NETDEVS.copy() + NetworkServiceTestCase._sort_dpdk_port_num(netdevs) + network = {'local_mac': '0a:de:ad:be:ef:f5'} + NetworkServiceTestCase._probe_missing_values(netdevs, network, set()) + assert network['dpdk_port_num'] == 2 + + network = {'local_mac': '0a:de:ad:be:ef:f4'} + NetworkServiceTestCase._probe_missing_values(netdevs, network, set()) + assert network['dpdk_port_num'] == 1 + + def test_open_relative_path(self): + mock_open = mock.mock_open() + mock_open_result = mock_open() + mock_open_call_count = 1 # initial call to get result + + module_name = \ + 'yardstick.benchmark.scenarios.networking.vnf_generic.open' + + # test + with mock.patch(module_name, mock_open, create=True): + self.assertEqual(open_relative_file('foo', 'bar'), mock_open_result) + + mock_open_call_count += 1 # one more call expected + self.assertEqual(mock_open.call_count, mock_open_call_count) + self.assertIn('foo', mock_open.call_args_list[-1][0][0]) + self.assertNotIn('bar', mock_open.call_args_list[-1][0][0]) + + def open_effect(*args, **kwargs): + if kwargs.get('name', args[0]) == os.path.join('bar', 'foo'): + return mock_open_result + raise IOError(errno.ENOENT, 'not found') + + mock_open.side_effect = open_effect + self.assertEqual(open_relative_file('foo', 'bar'), mock_open_result) + + mock_open_call_count += 2 # two more calls expected + self.assertEqual(mock_open.call_count, mock_open_call_count) + self.assertIn('foo', mock_open.call_args_list[-1][0][0]) + self.assertIn('bar', mock_open.call_args_list[-1][0][0]) + + # test an IOError of type ENOENT + mock_open.side_effect = IOError(errno.ENOENT, 'not found') + with self.assertRaises(IOError): + # the second call still raises + open_relative_file('foo', 'bar') + + mock_open_call_count += 2 # two more calls expected + self.assertEqual(mock_open.call_count, mock_open_call_count) + self.assertIn('foo', mock_open.call_args_list[-1][0][0]) + self.assertIn('bar', mock_open.call_args_list[-1][0][0]) + + # test an IOError other than ENOENT + mock_open.side_effect = IOError(errno.EBUSY, 'busy') + with self.assertRaises(IOError): + open_relative_file('foo', 'bar') + + mock_open_call_count += 1 # one more call expected + self.assertEqual(mock_open.call_count, mock_open_call_count) diff --git a/tests/unit/orchestrator/test_heat.py b/tests/unit/orchestrator/test_heat.py index 4892f98f8..3b3873301 100644 --- a/tests/unit/orchestrator/test_heat.py +++ b/tests/unit/orchestrator/test_heat.py @@ -10,16 +10,43 @@ ############################################################################## # Unittest for yardstick.benchmark.orchestrator.heat - +from contextlib import contextmanager from tempfile import NamedTemporaryFile import unittest import uuid +import time import mock from yardstick.benchmark.contexts import node from yardstick.orchestrator import heat +TARGET_MODULE = 'yardstick.orchestrator.heat' + + +def mock_patch_target_module(inner_import): + return mock.patch('.'.join([TARGET_MODULE, inner_import])) + + +@contextmanager +def timer(): + start = time.time() + data = {'start': start} + try: + yield data + finally: + data['end'] = end = time.time() + data['delta'] = end - start + +def get_error_message(error): + try: + # py2 + return error.message + except AttributeError: + # py3 + return next((arg for arg in error.args if isinstance(arg, str)), None) + + class HeatContextTestCase(unittest.TestCase): def test_get_short_key_uuid(self): @@ -70,88 +97,245 @@ class HeatTemplateTestCase(unittest.TestCase): self.assertEqual(self.template.resources['some-server-group']['properties']['policies'], ['anti-affinity']) def test__add_resources_to_template_raw(self): - - self.test_context = node.NodeContext() - self.test_context.name = 'foo' - self.test_context.template_file = '/tmp/some-heat-file' - self.test_context.heat_parameters = {'image': 'cirros'} - self.test_context.key_filename = "/tmp/1234" - self.test_context.keypair_name = "foo-key" - self.test_context.secgroup_name = "foo-secgroup" - self.test_context.key_uuid = "2f2e4997-0a8e-4eb7-9fa4-f3f8fbbc393b" - self._template = { - 'outputs' : {}, - 'resources' : {} - } - - self.heat_object = heat.HeatObject() - self.heat_tmp_object = heat.HeatObject() - - self.heat_stack = heat.HeatStack("tmpStack") - self.heat_stack.stacks_exist() - - self.test_context.tmpfile = NamedTemporaryFile(delete=True, mode='w+t') - self.test_context.tmpfile.write("heat_template_version: 2015-04-30") - self.test_context.tmpfile.flush() - self.test_context.tmpfile.seek(0) - self.heat_tmp_template = heat.HeatTemplate(self.heat_tmp_object, self.test_context.tmpfile.name, - heat_parameters= {"dict1": 1, "dict2": 2}) - - self.heat_template = heat.HeatTemplate(self.heat_object) - self.heat_template.resources = {} - - self.heat_template.add_network("network1") - self.heat_template.add_network("network2") - self.heat_template.add_security_group("sec_group1") - self.heat_template.add_security_group("sec_group2") - self.heat_template.add_subnet("subnet1", "network1", "cidr1") - self.heat_template.add_subnet("subnet2", "network2", "cidr2") - self.heat_template.add_router("router1", "gw1", "subnet1") - self.heat_template.add_router_interface("router_if1", "router1", "subnet1") - self.heat_template.add_port("port1", "network1", "subnet1") - self.heat_template.add_port("port2", "network2", "subnet2", sec_group_id="sec_group1",provider="not-sriov") - self.heat_template.add_port("port3", "network2", "subnet2", sec_group_id="sec_group1",provider="sriov") - self.heat_template.add_floating_ip("floating_ip1", "network1", "port1", "router_if1") - self.heat_template.add_floating_ip("floating_ip2", "network2", "port2", "router_if2", "foo-secgroup") - self.heat_template.add_floating_ip_association("floating_ip1_association", "floating_ip1", "port1") - self.heat_template.add_servergroup("server_grp2", "affinity") - self.heat_template.add_servergroup("server_grp3", "anti-affinity") - self.heat_template.add_security_group("security_group") - self.heat_template.add_server(name="server1", image="image1", flavor="flavor1", flavors=[]) - self.heat_template.add_server_group(name="servergroup", policies=["policy1","policy2"]) - self.heat_template.add_server_group(name="servergroup", policies="policy1") - self.heat_template.add_server(name="server2", image="image1", flavor="flavor1", flavors=[], ports=["port1", "port2"], + test_context = node.NodeContext() + test_context.name = 'foo' + test_context.template_file = '/tmp/some-heat-file' + test_context.heat_parameters = {'image': 'cirros'} + test_context.key_filename = "/tmp/1234" + test_context.keypair_name = "foo-key" + test_context.secgroup_name = "foo-secgroup" + test_context.key_uuid = "2f2e4997-0a8e-4eb7-9fa4-f3f8fbbc393b" + heat_object = heat.HeatObject() + + heat_stack = heat.HeatStack("tmpStack") + self.assertTrue(heat_stack.stacks_exist()) + + test_context.tmpfile = NamedTemporaryFile(delete=True, mode='w+t') + test_context.tmpfile.write("heat_template_version: 2015-04-30") + test_context.tmpfile.flush() + test_context.tmpfile.seek(0) + heat_template = heat.HeatTemplate(heat_object) + heat_template.resources = {} + + heat_template.add_network("network1") + heat_template.add_network("network2") + heat_template.add_security_group("sec_group1") + heat_template.add_security_group("sec_group2") + heat_template.add_subnet("subnet1", "network1", "cidr1") + heat_template.add_subnet("subnet2", "network2", "cidr2") + heat_template.add_router("router1", "gw1", "subnet1") + heat_template.add_router_interface("router_if1", "router1", "subnet1") + heat_template.add_port("port1", "network1", "subnet1") + heat_template.add_port("port2", "network2", "subnet2", sec_group_id="sec_group1",provider="not-sriov") + heat_template.add_port("port3", "network2", "subnet2", sec_group_id="sec_group1",provider="sriov") + heat_template.add_floating_ip("floating_ip1", "network1", "port1", "router_if1") + heat_template.add_floating_ip("floating_ip2", "network2", "port2", "router_if2", "foo-secgroup") + heat_template.add_floating_ip_association("floating_ip1_association", "floating_ip1", "port1") + heat_template.add_servergroup("server_grp2", "affinity") + heat_template.add_servergroup("server_grp3", "anti-affinity") + heat_template.add_security_group("security_group") + heat_template.add_server(name="server1", image="image1", flavor="flavor1", flavors=[]) + heat_template.add_server_group(name="servergroup", policies=["policy1","policy2"]) + heat_template.add_server_group(name="servergroup", policies="policy1") + heat_template.add_server(name="server2", image="image1", flavor="flavor1", flavors=[], ports=["port1", "port2"], networks=["network1", "network2"], scheduler_hints="hints1", user="user1", key_name="foo-key", user_data="user", metadata={"cat": 1, "doc": 2}, additional_properties={"prop1": 1, "prop2": 2}) - self.heat_template.add_server(name="server2", image="image1", flavor="flavor1", flavors=["flavor1", "flavor2"], + heat_template.add_server(name="server2", image="image1", flavor="flavor1", flavors=["flavor1", "flavor2"], ports=["port1", "port2"], networks=["network1", "network2"], scheduler_hints="hints1", user="user1", key_name="foo-key", user_data="user", metadata={"cat": 1, "doc": 2}, additional_properties={"prop1": 1, "prop2": 2} ) - self.heat_template.add_server(name="server2", image="image1", flavor="flavor1", flavors=["flavor3", "flavor4"], + heat_template.add_server(name="server2", image="image1", flavor="flavor1", flavors=["flavor3", "flavor4"], ports=["port1", "port2"], networks=["network1", "network2"], scheduler_hints="hints1", user="user1", key_name="foo-key", user_data="user", metadata={"cat": 1, "doc": 2}, additional_properties={"prop1": 1, "prop2": 2}) - self.heat_template.add_flavor(name="flavor1", vcpus=1, ram=2048, disk=1,extra_specs={"cat": 1, "dog": 2}) - self.heat_template.add_flavor(name=None, vcpus=1, ram=2048) - self.heat_template.add_server(name="server1", - image="image1", - flavor="flavor1", - flavors=[], - ports=["port1", "port2"], - networks=["network1", "network2"], - scheduler_hints="hints1", - user="user1", - key_name="foo-key", - user_data="user", - metadata={"cat": 1, "doc": 2}, - additional_properties= {"prop1": 1, "prop2": 2} ) - self.heat_template.add_network("network1") - - self.heat_template.add_flavor("test") - self.assertEqual(self.heat_template.resources['test']['type'], 'OS::Nova::Flavor') + heat_template.add_flavor(name="flavor1", vcpus=1, ram=2048, disk=1,extra_specs={"cat": 1, "dog": 2}) + heat_template.add_flavor(name=None, vcpus=1, ram=2048) + heat_template.add_server(name="server1", + image="image1", + flavor="flavor1", + flavors=[], + ports=["port1", "port2"], + networks=["network1", "network2"], + scheduler_hints="hints1", + user="user1", + key_name="foo-key", + user_data="user", + metadata={"cat": 1, "doc": 2}, + additional_properties= {"prop1": 1, "prop2": 2} ) + heat_template.add_network("network1") + + heat_template.add_flavor("test") + self.assertEqual(heat_template.resources['test']['type'], 'OS::Nova::Flavor') + + @mock_patch_target_module('op_utils') + @mock_patch_target_module('heatclient.client.Client') + def test_create_negative(self, mock_heat_client_class, mock_op_utils): + self.template.HEAT_WAIT_LOOP_INTERVAL = interval = 0.2 + mock_heat_client = mock_heat_client_class() # get the constructed mock + + # populate attributes of the constructed mock + mock_heat_client.stacks.get().stack_status_reason = 'the reason' + + expected_status_calls = 0 + expected_constructor_calls = 1 # above, to get the instance + expected_create_calls = 0 + expected_op_utils_usage = 0 + + with mock.patch.object(self.template, 'status', return_value=None) as mock_status: + # block with timeout hit + timeout = 2 + with self.assertRaises(RuntimeError) as raised, timer() as time_data: + self.template.create(block=True, timeout=timeout) + + # ensure runtime is approximately the timeout value + expected_time_low = timeout - interval * 0.2 + expected_time_high = timeout + interval * 0.2 + self.assertTrue(expected_time_low < time_data['delta'] < expected_time_high) + + # ensure op_utils was used + expected_op_utils_usage += 1 + self.assertEqual(mock_op_utils.get_session.call_count, expected_op_utils_usage) + self.assertEqual(mock_op_utils.get_endpoint.call_count, expected_op_utils_usage) + self.assertEqual(mock_op_utils.get_heat_api_version.call_count, expected_op_utils_usage) + + # ensure the constructor and instance were used + expected_constructor_calls += 1 + expected_create_calls += 1 + self.assertEqual(mock_heat_client_class.call_count, expected_constructor_calls) + self.assertEqual(mock_heat_client.stacks.create.call_count, expected_create_calls) + + # ensure that the status was used + self.assertGreater(mock_status.call_count, expected_status_calls) + expected_status_calls = mock_status.call_count # synchronize the value + + # ensure the expected exception was raised + error_message = get_error_message(raised.exception) + self.assertIn('timeout', error_message) + self.assertNotIn('the reason', error_message) + + # block with create failed + timeout = 10 + mock_status.side_effect = iter([None, None, u'CREATE_FAILED']) + with self.assertRaises(RuntimeError) as raised, timer() as time_data: + self.template.create(block=True, timeout=timeout) + + # ensure runtime is approximately two intervals + expected_time_low = interval * 1.8 + expected_time_high = interval * 2.2 + self.assertTrue(expected_time_low < time_data['delta'] < expected_time_high) + + # ensure the existing heat_client was used and op_utils was used again + self.assertEqual(mock_op_utils.get_session.call_count, expected_op_utils_usage) + self.assertEqual(mock_op_utils.get_endpoint.call_count, expected_op_utils_usage) + self.assertEqual(mock_op_utils.get_heat_api_version.call_count, expected_op_utils_usage) + + # ensure the constructor was not used but the instance was used + expected_create_calls += 1 + self.assertEqual(mock_heat_client_class.call_count, expected_constructor_calls) + self.assertEqual(mock_heat_client.stacks.create.call_count, expected_create_calls) + + # ensure that the status was used three times + expected_status_calls += 3 + self.assertEqual(mock_status.call_count, expected_status_calls) + + # ensure the expected exception was raised + error_message = get_error_message(raised.exception) + self.assertNotIn('timeout', error_message) + self.assertIn('the reason', error_message) + + @mock_patch_target_module('op_utils') + @mock_patch_target_module('heatclient.client.Client') + def test_create(self, mock_heat_client_class, mock_op_utils): + self.template.HEAT_WAIT_LOOP_INTERVAL = interval = 0.2 + mock_heat_client = mock_heat_client_class() + + # populate attributes of the constructed mock + mock_heat_client.stacks.get().outputs = [ + {'output_key': 'key1', 'output_value': 'value1'}, + {'output_key': 'key2', 'output_value': 'value2'}, + {'output_key': 'key3', 'output_value': 'value3'}, + ] + expected_outputs = { + 'key1': 'value1', + 'key2': 'value2', + 'key3': 'value3', + } + + expected_status_calls = 0 + expected_constructor_calls = 1 # above, to get the instance + expected_create_calls = 0 + expected_op_utils_usage = 0 + + with mock.patch.object(self.template, 'status') as mock_status: + # no block + with timer() as time_data: + self.assertIsInstance(self.template.create(block=False, timeout=2), heat.HeatStack) + + # ensure runtime is much less than one interval + self.assertLess(time_data['delta'], interval * 0.2) + + # ensure op_utils was used + expected_op_utils_usage += 1 + self.assertEqual(mock_op_utils.get_session.call_count, expected_op_utils_usage) + self.assertEqual(mock_op_utils.get_endpoint.call_count, expected_op_utils_usage) + self.assertEqual(mock_op_utils.get_heat_api_version.call_count, expected_op_utils_usage) + + # ensure the constructor and instance were used + expected_constructor_calls += 1 + expected_create_calls += 1 + self.assertEqual(mock_heat_client_class.call_count, expected_constructor_calls) + self.assertEqual(mock_heat_client.stacks.create.call_count, expected_create_calls) + + # ensure that the status was not used + self.assertEqual(mock_status.call_count, expected_status_calls) + + # ensure no outputs because this requires blocking + self.assertEqual(self.template.outputs, {}) + + # block with immediate complete + mock_status.return_value = u'CREATE_COMPLETE' + with timer() as time_data: + self.assertIsInstance(self.template.create(block=True, timeout=2), heat.HeatStack) + + # ensure runtime is less than one interval + self.assertLess(time_data['delta'], interval * 0.2) + + # ensure existing instance was re-used and op_utils was not used + expected_create_calls += 1 + self.assertEqual(mock_heat_client_class.call_count, expected_constructor_calls) + self.assertEqual(mock_heat_client.stacks.create.call_count, expected_create_calls) + + # ensure status was checked once + expected_status_calls += 1 + self.assertEqual(mock_status.call_count, expected_status_calls) + + # ensure the expected outputs are present + self.assertDictEqual(self.template.outputs, expected_outputs) + + # reset template outputs + self.template.outputs = None + + # block with delayed complete + mock_status.side_effect = iter([None, None, u'CREATE_COMPLETE']) + with timer() as time_data: + self.assertIsInstance(self.template.create(block=True, timeout=2), heat.HeatStack) + + # ensure runtime is approximately two intervals + expected_time_low = interval * 1.8 + expected_time_high = interval * 2.2 + self.assertTrue(expected_time_low < time_data['delta'] < expected_time_high) + + # ensure existing instance was re-used and op_utils was not used + expected_create_calls += 1 + self.assertEqual(mock_heat_client_class.call_count, expected_constructor_calls) + self.assertEqual(mock_heat_client.stacks.create.call_count, expected_create_calls) + + # ensure status was checked three more times + expected_status_calls += 3 + self.assertEqual(mock_status.call_count, expected_status_calls) class HeatStackTestCase(unittest.TestCase): diff --git a/yardstick/benchmark/contexts/heat.py b/yardstick/benchmark/contexts/heat.py index b689ac09c..aa134d694 100644 --- a/yardstick/benchmark/contexts/heat.py +++ b/yardstick/benchmark/contexts/heat.py @@ -13,9 +13,10 @@ from __future__ import print_function import collections import logging import os -import sys import uuid +from collections import OrderedDict +import ipaddress import paramiko import pkg_resources @@ -29,6 +30,8 @@ from yardstick.common.constants import YARDSTICK_ROOT_PATH LOG = logging.getLogger(__name__) +DEFAULT_HEAT_TIMEOUT = 3600 + class HeatContext(Context): """Class that represents a context in the logical model""" @@ -38,7 +41,7 @@ class HeatContext(Context): def __init__(self): self.name = None self.stack = None - self.networks = [] + self.networks = OrderedDict() self.servers = [] self.placement_groups = [] self.server_groups = [] @@ -68,6 +71,7 @@ class HeatContext(Context): # no external net defined, assign it to first network usig os.environ if sorted_networks and not have_external_network: sorted_networks[0][1]["external_network"] = external_network + return sorted_networks def init(self, attrs): # pragma: no cover """initializes itself from the supplied arguments""" @@ -87,6 +91,8 @@ class HeatContext(Context): self._flavor = attrs.get("flavor") + self.heat_timeout = attrs.get("timeout", DEFAULT_HEAT_TIMEOUT) + self.placement_groups = [PlacementGroup(name, self, pgattrs["policy"]) for name, pgattrs in attrs.get( "placement_groups", {}).items()] @@ -95,12 +101,15 @@ class HeatContext(Context): for name, sgattrs in attrs.get( "server_groups", {}).items()] - self.assign_external_network(attrs["networks"]) + # we have to do this first, because we are injecting external_network + # into the dict + sorted_networks = self.assign_external_network(attrs["networks"]) - self.networks = [Network(name, self, netattrs) for name, netattrs in - sorted(attrs["networks"].items())] + self.networks = OrderedDict( + (name, Network(name, self, netattrs)) for name, netattrs in + sorted_networks) - for name, serverattrs in attrs["servers"].items(): + for name, serverattrs in sorted(attrs["servers"].items()): server = Server(name, self, serverattrs) self.servers.append(server) self._server_map[server.dn] = server @@ -140,7 +149,7 @@ class HeatContext(Context): template.add_keypair(self.keypair_name, self.key_uuid) template.add_security_group(self.secgroup_name) - for network in self.networks: + for network in self.networks.values(): template.add_network(network.stack_name, network.physical_network, network.provider) @@ -190,17 +199,17 @@ class HeatContext(Context): if not scheduler_hints["different_host"]: scheduler_hints.pop("different_host", None) server.add_to_template(template, - self.networks, + list(self.networks.values()), scheduler_hints) else: scheduler_hints["different_host"] = \ scheduler_hints["different_host"][0] server.add_to_template(template, - self.networks, + list(self.networks.values()), scheduler_hints) else: server.add_to_template(template, - self.networks, + list(self.networks.values()), scheduler_hints) added_servers.append(server.stack_name) @@ -219,7 +228,8 @@ class HeatContext(Context): scheduler_hints = {} for pg in server.placement_groups: update_scheduler_hints(scheduler_hints, added_servers, pg) - server.add_to_template(template, self.networks, scheduler_hints) + server.add_to_template(template, list(self.networks.values()), + scheduler_hints) added_servers.append(server.stack_name) # add server group @@ -236,7 +246,8 @@ class HeatContext(Context): if sg: scheduler_hints["group"] = {'get_resource': sg.name} server.add_to_template(template, - self.networks, scheduler_hints) + list(self.networks.values()), + scheduler_hints) def deploy(self): """deploys template into a stack using cloud""" @@ -249,13 +260,14 @@ class HeatContext(Context): self._add_resources_to_template(heat_template) try: - self.stack = heat_template.create() + self.stack = heat_template.create(block=True, + timeout=self.heat_timeout) except KeyboardInterrupt: - sys.exit("\nStack create interrupted") - except RuntimeError as err: - sys.exit("error: failed to deploy stack: '%s'" % err.args) - except Exception as err: - sys.exit("error: failed to deploy stack: '%s'" % err) + raise SystemExit("\nStack create interrupted") + except: + LOG.exception("stack failed") + raise + # let the other failures happend, we want stack trace # copy some vital stack output into server objects for server in self.servers: @@ -263,6 +275,11 @@ class HeatContext(Context): # TODO(hafe) can only handle one internal network for now port = next(iter(server.ports.values())) server.private_ip = self.stack.outputs[port["stack_name"]] + server.interfaces = {} + for network_name, port in server.ports.items(): + self.make_interface_dict(network_name, port['stack_name'], + server, + self.stack.outputs) if server.floating_ip: server.public_ip = \ @@ -270,6 +287,27 @@ class HeatContext(Context): print("Context '%s' deployed" % self.name) + def make_interface_dict(self, network_name, stack_name, server, outputs): + server.interfaces[network_name] = { + "private_ip": outputs[stack_name], + "subnet_id": outputs[stack_name + "-subnet_id"], + "subnet_cidr": outputs[ + "{}-{}-subnet-cidr".format(self.name, network_name)], + "netmask": str(ipaddress.ip_network( + outputs["{}-{}-subnet-cidr".format(self.name, + network_name)]).netmask), + "gateway_ip": outputs[ + "{}-{}-subnet-gateway_ip".format(self.name, network_name)], + "mac_address": outputs[stack_name + "-mac_address"], + "device_id": outputs[stack_name + "-device_id"], + "network_id": outputs[stack_name + "-network_id"], + "network_name": network_name, + # to match vnf_generic + "local_mac": outputs[stack_name + "-mac_address"], + "local_ip": outputs[stack_name], + "vld_id": self.networks[network_name].vld_id, + } + def undeploy(self): """undeploys stack from cloud""" if self.stack: @@ -324,7 +362,8 @@ class HeatContext(Context): result = { "user": server.context.user, "key_filename": key_filename, - "private_ip": server.private_ip + "private_ip": server.private_ip, + "interfaces": server.interfaces, } # Target server may only have private_ip if server.public_ip: diff --git a/yardstick/benchmark/contexts/model.py b/yardstick/benchmark/contexts/model.py index 546201e9b..1f8c6f11c 100644 --- a/yardstick/benchmark/contexts/model.py +++ b/yardstick/benchmark/contexts/model.py @@ -111,6 +111,7 @@ class Network(Object): if "external_network" in attrs: self.router = Router("router", self.name, context, attrs["external_network"]) + self.vld_id = attrs.get("vld_id", "") Network.list.append(self) @@ -152,6 +153,7 @@ class Server(Object): # pragma: no cover self.public_ip = None self.private_ip = None self.user_data = '' + self.interfaces = {} if attrs is None: attrs = {} diff --git a/yardstick/benchmark/core/task.py b/yardstick/benchmark/core/task.py index c44081b73..5a006f2b2 100644 --- a/yardstick/benchmark/core/task.py +++ b/yardstick/benchmark/core/task.py @@ -400,6 +400,9 @@ class TaskParser(object): # pragma: no cover task_name = os.path.splitext(os.path.basename(self.path))[0] scenario["tc"] = task_name scenario["task_id"] = task_id + # embed task path into scenario so we can load other files + # relative to task path + scenario["task_path"] = os.path.dirname(self.path) change_server_name(scenario, name_suffix) diff --git a/yardstick/benchmark/scenarios/networking/vnf_generic.py b/yardstick/benchmark/scenarios/networking/vnf_generic.py index be179631e..594edeaa8 100644 --- a/yardstick/benchmark/scenarios/networking/vnf_generic.py +++ b/yardstick/benchmark/scenarios/networking/vnf_generic.py @@ -15,6 +15,14 @@ from __future__ import absolute_import import logging + +import errno +import os + +import re +from operator import itemgetter +from collections import defaultdict + import yaml from yardstick.benchmark.scenarios import base @@ -72,6 +80,15 @@ class SshManager(object): self.conn.close() +def open_relative_file(path, task_path): + try: + return open(path) + except IOError as e: + if e.errno == errno.ENOENT: + return open(os.path.join(task_path, path)) + raise + + class NetworkServiceTestCase(base.Scenario): """Class handles Generic framework to do pre-deployment VNF & Network service testing """ @@ -84,8 +101,11 @@ class NetworkServiceTestCase(base.Scenario): self.context_cfg = context_cfg # fixme: create schema to validate all fields have been provided - with open(scenario_cfg["topology"]) as stream: - self.topology = yaml.load(stream)["nsd:nsd-catalog"]["nsd"][0] + with open_relative_file(scenario_cfg["topology"], + scenario_cfg['task_path']) as stream: + topology_yaml = yaml.load(stream) + + self.topology = topology_yaml["nsd:nsd-catalog"]["nsd"][0] self.vnfs = [] self.collector = None self.traffic_profile = None @@ -114,7 +134,8 @@ class NetworkServiceTestCase(base.Scenario): private = {} public = {} try: - with open(scenario_cfg["traffic_profile"]) as infile: + with open_relative_file(scenario_cfg["traffic_profile"], + scenario_cfg["task_path"]) as infile: traffic_profile_tpl = infile.read() except (KeyError, IOError, OSError): @@ -123,8 +144,6 @@ class NetworkServiceTestCase(base.Scenario): return [traffic_profile_tpl, private, public] def _fill_traffic_profile(self, scenario_cfg, context_cfg): - traffic_profile = {} - flow = self._get_traffic_flow(scenario_cfg) imix = self._get_traffic_imix(scenario_cfg) @@ -193,6 +212,26 @@ class NetworkServiceTestCase(base.Scenario): list_idx = self._find_list_index_from_vnf_idx(topology, vnf_idx) nodes[node].update(topology["constituent-vnfd"][list_idx]) + @staticmethod + def _sort_dpdk_port_num(netdevs): + # dpdk_port_num is PCI BUS ID ordering, lowest first + s = sorted(netdevs.values(), key=itemgetter('pci_bus_id')) + for dpdk_port_num, netdev in enumerate(s, 1): + netdev['dpdk_port_num'] = dpdk_port_num + + @classmethod + def _probe_missing_values(cls, netdevs, network, missing): + mac = network['local_mac'] + for netdev in netdevs.values(): + if netdev['address'].lower() == mac.lower(): + network['driver'] = netdev['driver'] + network['vpci'] = netdev['pci_bus_id'] + network['dpdk_port_num'] = netdev['dpdk_port_num'] + network['ifindex'] = netdev['ifindex'] + + TOPOLOGY_REQUIRED_KEYS = frozenset({ + "vpci", "local_ip", "netmask", "local_mac", "driver", "dpdk_port_num"}) + def map_topology_to_infrastructure(self, context_cfg, topology): """ This method should verify if the available resources defined in pod.yaml match the topology.yaml file. @@ -208,21 +247,66 @@ class NetworkServiceTestCase(base.Scenario): exit_status = conn.execute(cmd)[0] if exit_status != 0: raise IncorrectSetup("Node's %s lacks ip tool." % node) - - for interface in node_dict["interfaces"]: - network = node_dict["interfaces"][interface] - keys = ["vpci", "local_ip", "netmask", - "local_mac", "driver", "dpdk_port_num"] - missing = set(keys).difference(network) + exit_status, stdout, _ = conn.execute( + self.FIND_NETDEVICE_STRING) + if exit_status != 0: + raise IncorrectSetup( + "Cannot find netdev info in sysfs" % node) + netdevs = node_dict['netdevs'] = self.parse_netdev_info( + stdout) + self._sort_dpdk_port_num(netdevs) + + for network in node_dict["interfaces"].values(): + missing = self.TOPOLOGY_REQUIRED_KEYS.difference(network) if missing: - raise IncorrectConfig("Require interface fields '%s' " - "not found, topology file " - "corrupted" % ', '.join(missing)) + try: + self._probe_missing_values(netdevs, network, + missing) + except KeyError: + pass + else: + missing = self.TOPOLOGY_REQUIRED_KEYS.difference( + network) + if missing: + raise IncorrectConfig( + "Require interface fields '%s' " + "not found, topology file " + "corrupted" % ', '.join(missing)) # 3. Use topology file to find connections & resolve dest address self._resolve_topology(context_cfg, topology) self._update_context_with_topology(context_cfg, topology) + FIND_NETDEVICE_STRING = r"""find /sys/devices/pci* -type d -name net -exec sh -c '{ grep -sH ^ \ +$1/ifindex $1/address $1/operstate $1/device/vendor $1/device/device \ +$1/device/subsystem_vendor $1/device/subsystem_device ; \ +printf "%s/driver:" $1 ; basename $(readlink -s $1/device/driver); } \ +' sh \{\}/* \; +""" + BASE_ADAPTER_RE = re.compile( + '^/sys/devices/(.*)/net/([^/]*)/([^:]*):(.*)$', re.M) + + @classmethod + def parse_netdev_info(cls, stdout): + network_devices = defaultdict(dict) + matches = cls.BASE_ADAPTER_RE.findall(stdout) + for bus_path, interface_name, name, value in matches: + dirname, bus_id = os.path.split(bus_path) + if 'virtio' in bus_id: + # for some stupid reason VMs include virtio1/ + # in PCI device path + bus_id = os.path.basename(dirname) + # remove extra 'device/' from 'device/vendor, + # device/subsystem_vendor', etc. + if 'device/' in name: + name = name.split('/')[1] + network_devices[interface_name][name] = value + network_devices[interface_name][ + 'interface_name'] = interface_name + network_devices[interface_name]['pci_bus_id'] = bus_id + # convert back to regular dict + return dict(network_devices) + @classmethod def get_vnf_impl(cls, vnf_model): """ Find the implementing class from vnf_model["vnf"]["name"] field @@ -240,21 +324,24 @@ class NetworkServiceTestCase(base.Scenario): except StopIteration: raise IncorrectConfig("No implementation for %s", expected_name) - def load_vnf_models(self, context_cfg): + def load_vnf_models(self, scenario_cfg, context_cfg): """ Create VNF objects based on YAML descriptors + :param scenario_cfg: + :type scenario_cfg: :param context_cfg: :return: """ vnfs = [] - for node in context_cfg["nodes"]: - LOG.debug(context_cfg["nodes"][node]) - with open(context_cfg["nodes"][node]["VNF model"]) as stream: + for node_name, node in context_cfg["nodes"].items(): + LOG.debug(node) + with open_relative_file(node["VNF model"], + scenario_cfg['task_path']) as stream: vnf_model = stream.read() - vnfd = vnfdgen.generate_vnfd(vnf_model, context_cfg["nodes"][node]) + vnfd = vnfdgen.generate_vnfd(vnf_model, node) vnf_impl = self.get_vnf_impl(vnfd["vnfd:vnfd-catalog"]["vnfd"][0]) vnf_instance = vnf_impl(vnfd["vnfd:vnfd-catalog"]["vnfd"][0]) - vnf_instance.name = node + vnf_instance.name = node_name vnfs.append(vnf_instance) return vnfs @@ -264,11 +351,10 @@ class NetworkServiceTestCase(base.Scenario): :return: """ - # 1. Verify if infrastructure mapping can meet topology self.map_topology_to_infrastructure(self.context_cfg, self.topology) # 1a. Load VNF models - self.vnfs = self.load_vnf_models(self.context_cfg) + self.vnfs = self.load_vnf_models(self.scenario_cfg, self.context_cfg) # 1b. Fill traffic profile with information from topology self.traffic_profile = self._fill_traffic_profile(self.scenario_cfg, self.context_cfg) diff --git a/yardstick/network_services/vnf_generic/vnfdgen.py b/yardstick/network_services/vnf_generic/vnfdgen.py index 97dd97198..40cc14a49 100644 --- a/yardstick/network_services/vnf_generic/vnfdgen.py +++ b/yardstick/network_services/vnf_generic/vnfdgen.py @@ -15,9 +15,20 @@ from __future__ import absolute_import import collections + +import jinja2 import yaml -from yardstick.common.task_template import TaskTemplate + +def render(vnf_model, **kwargs): + """Render jinja2 VNF template + + :param vnf_model: string that contains template + :param kwargs: Dict with template arguments + :returns:rendered template str + """ + + return jinja2.Template(vnf_model).render(**kwargs) def generate_vnfd(vnf_model, node): @@ -31,7 +42,10 @@ def generate_vnfd(vnf_model, node): # get is unused as global method inside template node["get"] = get # Set Node details to default if not defined in pod file - rendered_vnfd = TaskTemplate.render(vnf_model, **node) + # we CANNOT use TaskTemplate.render because it does not allow + # for missing variables, we need to allow password for key_filename + # to be undefined + rendered_vnfd = render(vnf_model, **node) # This is done to get rid of issues with serializing node del node["get"] filled_vnfd = yaml.load(rendered_vnfd) diff --git a/yardstick/orchestrator/heat.py b/yardstick/orchestrator/heat.py index 864f1f9ec..a99d4631d 100644 --- a/yardstick/orchestrator/heat.py +++ b/yardstick/orchestrator/heat.py @@ -1,5 +1,5 @@ ############################################################################## -# Copyright (c) 2015 Ericsson AB and others. +# Copyright (c) 2015-2017 Ericsson AB and others. # # All rights reserved. This program and the accompanying materials # are made available under the terms of the Apache License, Version 2.0 @@ -11,6 +11,7 @@ from __future__ import absolute_import from __future__ import print_function +from six.moves import range import collections import datetime @@ -47,7 +48,8 @@ class HeatObject(object): self._heat_client = None self.uuid = None - def _get_heat_client(self): + @property + def heat_client(self): """returns a heat client instance""" if self._heat_client is None: @@ -61,9 +63,9 @@ class HeatObject(object): def status(self): """returns stack state as a string""" - heat = self._get_heat_client() - stack = heat.stacks.get(self.uuid) - return getattr(stack, 'stack_status') + heat_client = self.heat_client + stack = heat_client.stacks.get(self.uuid) + return stack.stack_status class HeatStack(HeatObject): @@ -88,20 +90,18 @@ class HeatStack(HeatObject): return log.info("Deleting stack '%s', uuid:%s", self.name, self.uuid) - heat = self._get_heat_client() + heat = self.heat_client template = heat.stacks.get(self.uuid) start_time = time.time() template.delete() - status = self.status() - while status != u'DELETE_COMPLETE': + for status in iter(self.status, u'DELETE_COMPLETE'): log.debug("stack state %s", status) if status == u'DELETE_FAILED': raise RuntimeError( heat.stacks.get(self.uuid).stack_status_reason) time.sleep(2) - status = self.status() end_time = time.time() log.info("Deleted stack '%s' in %d secs", self.name, @@ -120,15 +120,13 @@ class HeatStack(HeatObject): self._delete() return - i = 0 - while i < retries: + for _ in range(retries): try: self._delete() break except RuntimeError as err: log.warning(err.args) time.sleep(2) - i += 1 # if still not deleted try once more and let it fail everything if self.uuid is not None: @@ -177,7 +175,6 @@ name (i.e. %s).\ self.name = name self.state = "NOT_CREATED" self.keystone_client = None - self.heat_client = None self.heat_parameters = {} # heat_parameters is passed to heat in stack create, empty dict when @@ -279,6 +276,14 @@ name (i.e. %s).\ 'description': 'subnet %s ID' % name, 'value': {'get_resource': name} } + self._template['outputs'][name + "-cidr"] = { + 'description': 'subnet %s cidr' % name, + 'value': {'get_attr': [name, 'cidr']} + } + self._template['outputs'][name + "-gateway_ip"] = { + 'description': 'subnet %s gateway_ip' % name, + 'value': {'get_attr': [name, 'gateway_ip']} + } def add_router(self, name, ext_gw_net, subnet_name): """add to the template a Neutron Router and interface""" @@ -336,6 +341,22 @@ name (i.e. %s).\ 'description': 'Address for interface %s' % name, 'value': {'get_attr': [name, 'fixed_ips', 0, 'ip_address']} } + self._template['outputs'][name + "-subnet_id"] = { + 'description': 'Address for interface %s' % name, + 'value': {'get_attr': [name, 'fixed_ips', 0, 'subnet_id']} + } + self._template['outputs'][name + "-mac_address"] = { + 'description': 'MAC Address for interface %s' % name, + 'value': {'get_attr': [name, 'mac_address']} + } + self._template['outputs'][name + "-device_id"] = { + 'description': 'Device ID for interface %s' % name, + 'value': {'get_attr': [name, 'device_id']} + } + self._template['outputs'][name + "-network_id"] = { + 'description': 'Network ID for interface %s' % name, + 'value': {'get_attr': [name, 'network_id']} + } def add_floating_ip(self, name, network_name, port_name, router_if_name, secgroup_name=None): @@ -508,38 +529,48 @@ name (i.e. %s).\ 'value': {'get_resource': name} } - def create(self, block=True): - """creates a template in the target cloud using heat + HEAT_WAIT_LOOP_INTERVAL = 2 + + def create(self, block=True, timeout=3600): + """ + creates a template in the target cloud using heat returns a dict with the requested output values from the template + + :param block: Wait for Heat create to finish + :type block: bool + :param: timeout: timeout in seconds for Heat create, default 3600s + :type timeout: int """ log.info("Creating stack '%s'", self.name) # create stack early to support cleanup, e.g. ctrl-c while waiting stack = HeatStack(self.name) - heat = self._get_heat_client() + heat_client = self.heat_client start_time = time.time() - stack.uuid = self.uuid = heat.stacks.create( + stack.uuid = self.uuid = heat_client.stacks.create( stack_name=self.name, template=self._template, parameters=self.heat_parameters)['stack']['id'] - status = self.status() - outputs = [] + if not block: + self.outputs = stack.outputs = {} + return stack - if block: - while status != u'CREATE_COMPLETE': - log.debug("stack state %s", status) - if status == u'CREATE_FAILED': - raise RuntimeError(getattr(heat.stacks.get(self.uuid), - 'stack_status_reason')) + time_limit = start_time + timeout + for status in iter(self.status, u'CREATE_COMPLETE'): + log.debug("stack state %s", status) + if status == u'CREATE_FAILED': + raise RuntimeError( + heat_client.stacks.get(self.uuid).stack_status_reason) + if time.time() > time_limit: + raise RuntimeError("Heat stack create timeout") - time.sleep(2) - status = self.status() + time.sleep(self.HEAT_WAIT_LOOP_INTERVAL) - end_time = time.time() - outputs = getattr(heat.stacks.get(self.uuid), 'outputs') - log.info("Created stack '%s' in %d secs", - self.name, end_time - start_time) + end_time = time.time() + outputs = heat_client.stacks.get(self.uuid).outputs + log.info("Created stack '%s' in %d secs", + self.name, end_time - start_time) # keep outputs as unicode self.outputs = {output["output_key"]: output["output_value"] for output |