From 1c858ea9077345f9e5e5a3f5e0f3e9539e9a6950 Mon Sep 17 00:00:00 2001 From: Guillaume Espanel Date: Thu, 17 Sep 2015 12:13:31 +0200 Subject: [PATCH] Add an extract-ports flag When the extract-ports flag is used, flame extracts the neutron ports and connects them (according to the other flags) to : - floating ips - nova:compute instances - security groups - subnets and networks This allows the exported stack to have the same network configuration as its source. When the flag is used in conjunction with --generate-stack-data, the generated stack data also contains the assocation between ports and floating IPs. Change-Id: Id2e406c7aac5457cdef822b78edf7e30bd833a2d --- README.rst | 2 + doc/source/index.rst | 1 + doc/source/limitations.rst | 18 + doc/source/usage.rst | 2 + flameclient/client.py | 5 +- flameclient/cmd.py | 6 +- flameclient/flame.py | 168 ++++++- flameclient/tests/test_flame.py | 114 ++--- flameclient/tests/test_flameports.py | 640 +++++++++++++++++++++++++++ 9 files changed, 878 insertions(+), 78 deletions(-) create mode 100644 doc/source/limitations.rst create mode 100644 flameclient/tests/test_flameports.py diff --git a/README.rst b/README.rst index d2f1656..39ad69d 100644 --- a/README.rst +++ b/README.rst @@ -37,6 +37,7 @@ Usage [--os-auth-token OS_AUTH_TOKEN] [--insecure] [--endpoint_type ENDPOINT_TYPE] [--exclude-servers] [--exclude-volumes] [--exclude-keypairs] [--generate-stack-data] + [--extract-ports] Heat template and data file generator @@ -62,6 +63,7 @@ Usage --generate-stack-data In addition to template, generate Heat stack data file. + --extract-ports Export the tenant network ports Usage example ------------- diff --git a/doc/source/index.rst b/doc/source/index.rst index b0cae16..78cdf86 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -14,6 +14,7 @@ Contents: readme installation usage + limitations contributing Indices and tables diff --git a/doc/source/limitations.rst b/doc/source/limitations.rst new file mode 100644 index 0000000..333682d --- /dev/null +++ b/doc/source/limitations.rst @@ -0,0 +1,18 @@ +=========== +Limitations +=========== + +Subnets created before Kilo can have their DHCP server's IP in the middle of +an allocation pool. This causes the first VMs to be allocated the first +free IP addresses of a pool, for example, with a pool starting at 10.0.0.2 : + + - 10.0.0.2 : vm1 + - 10.0.0.3 : vm2 + - 10.0.0.4 : DHCP server + - 10.0.0.5 : vm3 + +When this stack is imported in Heat, the DHCP server IP is set to the lowest +free IP address of its pool. Depending on the VM creation order, the DHCP +address can either collide with vm1's or vm2's IP. + + diff --git a/doc/source/usage.rst b/doc/source/usage.rst index 5dfc34b..0cab5a8 100644 --- a/doc/source/usage.rst +++ b/doc/source/usage.rst @@ -13,6 +13,7 @@ To use the CLI of flame:: [--os-auth-token OS_AUTH_TOKEN] [--insecure] [--endpoint_type ENDPOINT_TYPE] [--exclude-servers] [--exclude-volumes] [--exclude-keypairs] [--generate-stack-data] + [--extract-ports] Heat template and data file generator @@ -38,6 +39,7 @@ To use the CLI of flame:: --generate-stack-data In addition to template, generate Heat stack data file. + --extract-ports Export the tenant network ports Example diff --git a/flameclient/client.py b/flameclient/client.py index 52112d1..6d514ad 100644 --- a/flameclient/client.py +++ b/flameclient/client.py @@ -34,11 +34,12 @@ class Client(object): **kwargs) def generate(self, exclude_servers, exclude_volumes, exclude_keypairs, - generate_stack_data): + generate_stack_data, extract_ports=False): self.template_generator.extract_vm_details(exclude_servers, exclude_volumes, exclude_keypairs, - generate_stack_data + generate_stack_data, + extract_ports ) self.template_generator.extract_data() return self.template_generator.heat_template_and_data() diff --git a/flameclient/cmd.py b/flameclient/cmd.py index 6c3d997..5cd5990 100644 --- a/flameclient/cmd.py +++ b/flameclient/cmd.py @@ -80,6 +80,9 @@ def main(args=None): default=False, help="In addition to template, generate Heat " "stack data file.") + parser.add_argument('--extract-ports', action='store_true', + default=False, + help="Export the tenant network ports") args = parser.parse_args() flame = client.Client(args.username, args.password, @@ -92,7 +95,8 @@ def main(args=None): template.extract_vm_details(args.exclude_servers, args.exclude_volumes, args.exclude_keypairs, - args.generate_stack_data) + args.generate_stack_data, + args.extract_ports) template.extract_data() print("### Heat Template ###") print(template.heat_template_and_data()) diff --git a/flameclient/flame.py b/flameclient/flame.py index 781faa3..9cf891c 100644 --- a/flameclient/flame.py +++ b/flameclient/flame.py @@ -158,12 +158,25 @@ class TemplateGenerator(object): res_type = future_res[res_available] yield res_type, fetch_map[res_type][1](res) + def order_ports(self): + for i, port in self.ports.values(): + for fixed_ip in port['fixed_ips']: + ip_subnet = self.subnets[fixed_ip['subnet_id']][1] + pools = ip_subnet.get('allocation_pools') + if pools: + pools_starts = [pool['start'] for pool in pools] + if fixed_ip['ip_address'] in pools_starts: + # Its the first port of the subnet + ip_subnet['first_port'] = port + def extract_vm_details(self, exclude_servers, exclude_volumes, - exclude_keypairs, generate_data): + exclude_keypairs, generate_data, + extract_ports=False): self.exclude_servers = exclude_servers self.exclude_volumes = exclude_volumes self.exclude_keypairs = exclude_keypairs self.generate_data = generate_data + self.extract_ports = extract_ports self.external_networks = [] fetch_map = { 'subnets': (self.neutron.subnet_list, self.build_data), @@ -174,6 +187,7 @@ class TemplateGenerator(object): 'floatingips': (self.neutron.floatingip_list, lambda x: x), 'ports': (self.neutron.port_list, self.build_data), } + if not exclude_keypairs: fetch_map['keys'] = (self.nova.keypair_list, lambda l: {key.name: (index, key) for @@ -189,6 +203,7 @@ class TemplateGenerator(object): for res_type, result in self.async_fetch_data(fetch_map): self.__setattr__(res_type, result) + self.order_ports() def build_data(self, data): if not data: @@ -289,6 +304,22 @@ class TemplateGenerator(object): def get_subnet_resource_name(self, subnet_id): return "subnet_%d" % self.subnets[subnet_id][0] + def get_server_resource_name(self, device_id): + return "server_%d" % self.servers[device_id][0] + + def get_router_resource_name(self, device_id): + return "router_%d" % self.routers[device_id][0] + + def get_secgroup_resource_name(self, secgroup_id): + return "security_group_%d" % self.secgroups[secgroup_id][0] + + def get_ports_for_server(self, server_id): + ports = [] + for n, port in self.ports.values(): + if port['device_id'] == server_id: + ports.append("port_%d" % n) + return ports + def _extract_subnets(self): resources = [] for n, subnet in self.subnets.values(): @@ -311,11 +342,56 @@ class TemplateGenerator(object): resources.append(resource) return resources + def _extract_ports(self): + resources = [] + resources_dict = {} + self.dhcp_fixed_ips = {} + for n, port in self.ports.values(): + fixed_ips = [] + for fixed_ip_dict in port['fixed_ips']: + subnet_id = fixed_ip_dict['subnet_id'] + subnet_name = self.get_subnet_resource_name(subnet_id) + fixed_ip_resource = {u'subnet_id': + {'get_resource': subnet_name}, + u'ip_address': fixed_ip_dict['ip_address'] + } + fixed_ips.append(fixed_ip_resource) + + if port['device_owner'] == 'network:dhcp': + # Add the fixed ip to the dhcp_fixed_ips list + dhcp_ips = self.dhcp_fixed_ips.setdefault(subnet_name, []) + dhcp_ips.append(fixed_ip_dict['ip_address']) + if not port['device_owner'].startswith('compute:'): + # It's not a server, skip it! + continue + net_name = self.get_network_resource_name(port['network_id']) + properties = { + 'network_id': {'get_resource': net_name}, + 'admin_state_up': port['admin_state_up'], + 'fixed_ips': fixed_ips, + 'mac_address': port['mac_address'], + 'device_owner': port['device_owner'], + } + if port['name'] != '': + # This port has a name + properties['name'] = port['name'] + resource = Resource("port_%d" % n, 'OS::Neutron::Port', + port['id'], properties) + security_groups = self.build_port_secgroups(resource, port) + properties['security_groups'] = security_groups + + resources.append(resource) + resources_dict[port['id']] = resource + + return resources + def _build_rules(self, rules): brules = [] for rule in rules: if rule['protocol'] == 'any': del rule['protocol'] + del rule['port_range_min'] + del rule['port_range_max'] rg_id = rule['remote_group_id'] if rg_id is not None: rule['remote_mode'] = "remote_group_id" @@ -396,6 +472,31 @@ class TemplateGenerator(object): return security_groups + def build_port_secgroups(self, resource, port): + security_groups = [] + port_secgroups = [self.secgroups[sgid][1] + for sgid in port['security_groups']] + + secgroup_default_parameter = None + for secgr in port_secgroups: + if secgr['name'] == 'default' and self.generate_data: + if not secgroup_default_parameter: + port_res_name = 'port_%d' % self.ports[port['id']][0] + param_name = "%s_default_security_group" % port_res_name + description = ("Default security group for port %s" % + port['name']) + default = secgr['id'] + resource.add_parameter(param_name, description, + default=default) + secgroup_default_parameter = {'get_param': param_name} + security_groups.append(secgroup_default_parameter) + else: + resource_name = ("security_group_%d" % + self.secgroups[secgr['id']][0]) + security_groups.append({'get_resource': resource_name}) + + return security_groups + def build_networks(self, addresses): networks = [] for net_name in addresses: @@ -460,13 +561,19 @@ class TemplateGenerator(object): resource_key = "key_%d" % self.keys[server.key_name][0] properties['key_name'] = {'get_resource': resource_key} - security_groups = self.build_secgroups(resource, server) - if security_groups: - properties['security_groups'] = security_groups + if self.extract_ports: + ports = [{"port": {"get_resource": port}} + for port in self.get_ports_for_server(server.id)] + if ports: + properties['networks'] = ports + else: + security_groups = self.build_secgroups(resource, server) + if security_groups: + properties['security_groups'] = security_groups - networks = self.build_networks(server.addresses) - if networks: - properties['networks'] = networks + networks = self.build_networks(server.addresses) + if networks: + properties['networks'] = networks if server.metadata: properties['metadata'] = server.metadata @@ -526,20 +633,36 @@ class TemplateGenerator(object): default=default) resources.append(resource) - if not self.exclude_servers and ip['port_id']: - device = self.ports[ip['port_id']][1]['device_id'] - if device and self.servers[device]: - server = self.servers[device] - server_resource_name = "server_%d" % server[0] - properties = { - 'floating_ip': {'get_resource': ip_resource_name}, - 'server_id': {'get_resource': server_resource_name} - } - resource = Resource("floatingip_association_%d" % n, - 'OS::Nova::FloatingIPAssociation', - None, - properties) - resources.append(resource) + if self.extract_ports and ip['port_id']: + port_number = self.ports[ip['port_id']][0] + port_resource_name = "port_%d" % port_number + properties = { + 'floatingip_id': {'get_resource': ip_resource_name}, + 'port_id': {'get_resource': port_resource_name} + } + resource_id = ("%s:%s" % + (ip['id'], + ip['port_id'])) + resource = Resource("floatingip_association_%d" % n, + 'OS::Neutron::FloatingIPAssociation', + resource_id, + properties) + resources.append(resource) + else: + if not self.exclude_servers and ip['port_id']: + device = self.ports[ip['port_id']][1]['device_id'] + if device and self.servers[device]: + server = self.servers[device] + server_resource_name = "server_%d" % server[0] + properties = { + 'floating_ip': {'get_resource': ip_resource_name}, + 'server_id': {'get_resource': server_resource_name} + } + resource = Resource("floatingip_association_%d" % n, + 'OS::Nova::FloatingIPAssociation', + None, + properties) + resources.append(resource) return resources def _extract_volumes(self): @@ -596,6 +719,9 @@ class TemplateGenerator(object): def extract_data(self): resources = self._extract_routers() resources += self._extract_networks() + if self.extract_ports: + resources += self._extract_ports() + resources += self._extract_subnets() resources += self._extract_secgroups() resources += self._extract_floating() diff --git a/flameclient/tests/test_flame.py b/flameclient/tests/test_flame.py index bb30e19..4761ec5 100644 --- a/flameclient/tests/test_flame.py +++ b/flameclient/tests/test_flame.py @@ -22,6 +22,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. + import mock import yaml @@ -274,11 +275,12 @@ class BaseTestCase(base.TestCase): super(BaseTestCase, self).tearDown() def get_generator(self, exclude_servers, exclude_volumes, - exclude_keypairs, generate_data): + exclude_keypairs, generate_data, extract_ports): generator = flame.TemplateGenerator('x', 'x', 'x', 'x', True, 'publicURL') generator.extract_vm_details(exclude_servers, exclude_volumes, - exclude_keypairs, generate_data) + exclude_keypairs, generate_data, + extract_ports) return generator def check_stackdata(self, resources, expected_resources): @@ -304,7 +306,7 @@ class BaseTestCase(base.TestCase): class TemplateGenerationTest(BaseTestCase): def test_heat_template_and_data_with_data(self): - generator = self.get_generator(False, False, False, True) + generator = self.get_generator(False, False, False, True, False) generator.extract_data() out = yaml.load(generator.heat_template_and_data()) mandatory_keys = set(('environment', 'template', 'resources', @@ -316,7 +318,7 @@ class TemplateGenerationTest(BaseTestCase): self.assertEqual(generator.stack_data, out) def test_heat_template_and_data_without_data(self): - generator = self.get_generator(False, False, False, False) + generator = self.get_generator(False, False, False, False, False) generator.extract_data() out = yaml.load(generator.heat_template_and_data()) mandatory_keys = {'heat_template_version', 'resources', 'description', @@ -343,7 +345,7 @@ class ClientTest(BaseTestCase): self.assertNotIn('template', parsed_out.keys()) def test_generate_contains_extract(self): - out = self.c.generate(False, False, False, True) + out = self.c.generate(False, False, False, True, False) parsed_out = yaml.load(out) self.assertIsInstance(parsed_out, dict) self.assertIn('template', parsed_out.keys()) @@ -353,7 +355,7 @@ class StackDataTests(BaseTestCase): def test_keypair(self): self.mock_nova.return_value = FakeNovaManager() - generator = self.get_generator(False, False, False, True) + generator = self.get_generator(False, False, False, True, False) expected = { 'key_0': { @@ -370,7 +372,7 @@ class StackDataTests(BaseTestCase): def test_router(self): self.mock_neutron.return_value = FakeNeutronManager() - generator = self.get_generator(False, False, False, True) + generator = self.get_generator(False, False, False, True, False) expected = { 'router_0': { @@ -393,8 +395,10 @@ class StackDataTests(BaseTestCase): 'external_gateway_info': { 'network_id': '8765', 'enable_snat': 'true'}}, ] + # This router has no port + fake.ports = [] self.mock_neutron.return_value = fake - generator = self.get_generator(False, False, False, True) + generator = self.get_generator(False, False, False, True, False) expected = { 'router_0': { @@ -447,7 +451,7 @@ class StackDataTests(BaseTestCase): 'id': '1111'}, ] self.mock_neutron.return_value = fake - generator = self.get_generator(False, False, False, True) + generator = self.get_generator(False, False, False, True, False) expected = { 'router_0': { @@ -473,7 +477,7 @@ class StackDataTests(BaseTestCase): def test_network(self): self.mock_neutron.return_value = FakeNeutronManager() - generator = self.get_generator(False, False, False, True) + generator = self.get_generator(False, False, False, True, False) expected = { 'network_0': { @@ -493,7 +497,7 @@ class StackDataTests(BaseTestCase): fake.networks[0]['router:external'] = True self.mock_neutron.return_value = fake - generator = self.get_generator(False, False, False, True) + generator = self.get_generator(False, False, False, True, False) self.check_stackdata(generator._extract_networks(), {}) @@ -512,7 +516,7 @@ class StackDataTests(BaseTestCase): 'id': '1111'}, ] self.mock_neutron.return_value = fake - generator = self.get_generator(False, False, False, True) + generator = self.get_generator(False, False, False, True, False) expected = { 'subnet_0': { @@ -538,7 +542,7 @@ class StackDataTests(BaseTestCase): 'id': '2222'}, ] self.mock_neutron.return_value = fake - generator = self.get_generator(True, False, False, True) + generator = self.get_generator(True, False, False, True, False) expected = { 'floatingip_0': { @@ -572,7 +576,7 @@ class StackDataTests(BaseTestCase): 'id': '1234'}, ] self.mock_neutron.return_value = fake - generator = self.get_generator(False, False, False, True) + generator = self.get_generator(False, False, False, True, False) expected = { 'security_group_0': { @@ -606,14 +610,14 @@ class StackDataTests(BaseTestCase): 'id': '1234'}, ] self.mock_neutron.return_value = fake - generator = self.get_generator(False, False, False, True) + generator = self.get_generator(False, False, False, True, False) self.check_stackdata(generator._extract_secgroups(), {}) def test_volume(self): self.mock_cinder.return_value = FakeCinderManager() - generator = self.get_generator(False, False, False, True) + generator = self.get_generator(False, False, False, True, False) expected = { 'volume_0': { @@ -631,7 +635,7 @@ class StackDataTests(BaseTestCase): def test_server(self): self.mock_nova.return_value = FakeNovaManager() - generator = self.get_generator(False, False, False, True) + generator = self.get_generator(False, False, False, True, False) expected = { 'server_0': { @@ -658,7 +662,7 @@ class StackDataTests(BaseTestCase): self.mock_neutron.return_value = fake_neutron self.mock_nova.return_value = fake_nova - generator = self.get_generator(False, False, False, True) + generator = self.get_generator(False, False, False, True, False) expected = { 'server_0': { @@ -675,7 +679,7 @@ class StackDataTests(BaseTestCase): def test_servergroup(self): self.mock_nova.return_value = FakeNovaManager() - generator = self.get_generator(False, False, False, True) + generator = self.get_generator(False, False, False, True, False) expected = { 'servergroup_0': { @@ -695,7 +699,7 @@ class NetworkTests(BaseTestCase): def test_keypair(self): self.mock_nova.return_value = FakeNovaManager() - generator = self.get_generator(False, False, False, True) + generator = self.get_generator(False, False, False, True, False) expected = { 'key_0': { @@ -710,7 +714,8 @@ class NetworkTests(BaseTestCase): def test_router(self): self.mock_neutron.return_value = FakeNeutronManager() - generator = self.get_generator(False, False, False, True) + self.mock_neutron.return_value.ports = [] + generator = self.get_generator(False, False, False, True, False) expected = { 'router_0': { @@ -725,6 +730,7 @@ class NetworkTests(BaseTestCase): def test_router_with_external_gateway(self): fake = FakeNeutronManager() + fake.ports = [] fake.routers = [{'name': 'myrouter', 'id': '1234', 'admin_state_up': 'true', @@ -732,7 +738,7 @@ class NetworkTests(BaseTestCase): 'network_id': '8765', 'enable_snat': 'true'}}, ] self.mock_neutron.return_value = fake - generator = self.get_generator(False, False, False, True) + generator = self.get_generator(False, False, False, True, False) expected_parameters = { 'router_0_external_network': { @@ -791,7 +797,7 @@ class NetworkTests(BaseTestCase): 'id': '1111'}, ] self.mock_neutron.return_value = fake - generator = self.get_generator(False, False, False, True) + generator = self.get_generator(False, False, False, True, False) expected = { 'router_0': { @@ -813,7 +819,7 @@ class NetworkTests(BaseTestCase): def test_network(self): self.mock_neutron.return_value = FakeNeutronManager() - generator = self.get_generator(False, False, False, True) + generator = self.get_generator(False, False, False, True, False) expected = { 'network_0': { @@ -832,7 +838,7 @@ class NetworkTests(BaseTestCase): fake.networks[0]['router:external'] = True self.mock_neutron.return_value = fake - generator = self.get_generator(False, False, False, True) + generator = self.get_generator(False, False, False, True, False) self.check_template(generator._extract_networks(), {}) @@ -851,7 +857,7 @@ class NetworkTests(BaseTestCase): 'id': '1111'}, ] self.mock_neutron.return_value = fake - generator = self.get_generator(False, False, False, True) + generator = self.get_generator(False, False, False, True, False) expected = { 'subnet_0': { @@ -882,7 +888,7 @@ class NetworkTests(BaseTestCase): 'id': '2222'}, ] self.mock_neutron.return_value = fake - generator = self.get_generator(True, False, False, False) + generator = self.get_generator(True, False, False, False, False) expected_parameters = { 'external_network_for_floating_ip_0': { @@ -963,7 +969,7 @@ class NetworkTests(BaseTestCase): 'id': '1234'}, ] self.mock_neutron.return_value = fake - generator = self.get_generator(False, False, False, False) + generator = self.get_generator(False, False, False, False, False) expected = { 'security_group_0': { @@ -1073,7 +1079,7 @@ class NetworkTests(BaseTestCase): 'id': '1111'}, ] self.mock_neutron.return_value = fake - generator = self.get_generator(False, False, False, False) + generator = self.get_generator(False, False, False, False, False) expected = { 'security_group_0': { @@ -1229,7 +1235,7 @@ class NetworkTests(BaseTestCase): 'id': '2222'}, ] self.mock_neutron.return_value = fake - generator = self.get_generator(False, False, False, True) + generator = self.get_generator(False, False, False, True, False) expected = { 'security_group_0': { @@ -1317,7 +1323,7 @@ class VolumeTests(BaseTestCase): self.mock_cinder.return_value = self.fake def test_basic(self): - generator = self.get_generator(False, False, False, True) + generator = self.get_generator(False, False, False, True, False) expected_parameters = { 'volume_0_volume_type': { @@ -1342,7 +1348,7 @@ class VolumeTests(BaseTestCase): def test_basic_unnamed(self): self.fake.volumes = [FakeUnnamedVolume(), ] - generator = self.get_generator(False, False, False, True) + generator = self.get_generator(False, False, False, True, False) expected_parameters = { 'volume_0_volume_type': { @@ -1365,7 +1371,7 @@ class VolumeTests(BaseTestCase): def test_source_volid_external(self): self.fake.volumes = [FakeVolume(source_volid=5678), ] - generator = self.get_generator(False, False, False, True) + generator = self.get_generator(False, False, False, True, False) expected_parameters = { 'volume_0_source_volid': { @@ -1395,7 +1401,7 @@ class VolumeTests(BaseTestCase): def test_source_volid_included(self): self.fake.volumes = [FakeVolume(source_volid=5678), FakeVolume(id=5678)] - generator = self.get_generator(False, False, False, True) + generator = self.get_generator(False, False, False, True, False) expected_parameters = { 'volume_0_volume_type': { @@ -1448,7 +1454,7 @@ class VolumeTests(BaseTestCase): 'size': '25'} self.fake.volumes = [FakeVolume(bootable='true', volume_image_metadata=metadata), ] - generator = self.get_generator(False, False, False, True) + generator = self.get_generator(False, False, False, True, False) expected_parameters = { 'volume_0_volume_type': { @@ -1479,7 +1485,7 @@ class VolumeTests(BaseTestCase): def test_snapshot_id(self): self.fake.volumes = [FakeVolume(snapshot_id=5678), ] - generator = self.get_generator(False, False, False, True) + generator = self.get_generator(False, False, False, True, False) expected_parameters = { 'volume_0_snapshot_id': { @@ -1510,7 +1516,7 @@ class VolumeTests(BaseTestCase): def test_volume_type(self): self.fake.volumes = [FakeVolume(volume_type='isci'), ] - generator = self.get_generator(False, False, False, True) + generator = self.get_generator(False, False, False, True, False) expected_parameters = { 'volume_0_volume_type': { @@ -1535,7 +1541,7 @@ class VolumeTests(BaseTestCase): def test_metadata(self): self.fake.volumes = [FakeVolume(metadata={'key': 'value'}), ] - generator = self.get_generator(False, False, False, True) + generator = self.get_generator(False, False, False, True, False) expected_parameters = { 'volume_0_volume_type': { @@ -1568,7 +1574,7 @@ class ServerTests(BaseTestCase): self.mock_nova.return_value = self.fake def test_basic(self): - generator = self.get_generator(False, False, False, True) + generator = self.get_generator(False, False, False, True, False) expected_parameters = { 'server_0_flavor': { @@ -1599,7 +1605,7 @@ class ServerTests(BaseTestCase): def test_keypair(self): self.fake.servers = [FakeServer(key_name='testkey')] - generator = self.get_generator(False, False, False, True) + generator = self.get_generator(False, False, False, True, False) expected_parameters = { 'server_0_flavor': { @@ -1645,7 +1651,7 @@ class ServerTests(BaseTestCase): bootable='true')] self.mock_cinder.return_value = fake_cinder - generator = self.get_generator(False, False, False, True) + generator = self.get_generator(False, False, False, True, False) expected_parameters = { 'server_0_flavor': { @@ -1683,7 +1689,7 @@ class ServerTests(BaseTestCase): server_args = {"id": 777, "os-extended-volumes:volumes_attached": [{'id': 5678}]} self.fake.servers = [FakeServer(**server_args), ] - generator = self.get_generator(False, False, False, True) + generator = self.get_generator(False, False, False, True, False) expected_parameters = { 'server_0_flavor': { @@ -1723,7 +1729,7 @@ class ServerTests(BaseTestCase): "description": "Group"}, ] self.mock_neutron.return_value = fake_neutron self.fake.groups = {'server1': [FakeSecurityGroup(), ]} - generator = self.get_generator(False, False, False, True) + generator = self.get_generator(False, False, False, True, False) expected_parameters = { 'server_0_flavor': { @@ -1760,7 +1766,7 @@ class ServerTests(BaseTestCase): def test_config_drive(self): self.fake.servers = [FakeServer(config_drive="True"), ] - generator = self.get_generator(False, False, False, True) + generator = self.get_generator(False, False, False, True, False) expected_parameters = { 'server_0_flavor': { @@ -1792,7 +1798,7 @@ class ServerTests(BaseTestCase): def test_metadata(self): self.fake.servers = [FakeServer(metadata={"key": "value"}), ] - generator = self.get_generator(False, False, False, True) + generator = self.get_generator(False, False, False, True, False) expected_parameters = { 'server_0_flavor': { @@ -1839,7 +1845,7 @@ class ServerTests(BaseTestCase): self.mock_neutron.return_value = fake_neutron addresses = {"private": [{"addr": "10.0.0.2"}]} self.fake.servers = [FakeServer(addresses=addresses)] - generator = self.get_generator(False, False, False, True) + generator = self.get_generator(False, False, False, True, False) expected_parameters = { 'server_0_flavor': { @@ -1883,7 +1889,7 @@ class ServerTests(BaseTestCase): server_args = {"id": 777, "os-extended-volumes:volumes_attached": [{'id': 5678}]} self.fake.servers = [FakeServer(**server_args), ] - generator = self.get_generator(False, True, False, False) + generator = self.get_generator(False, True, False, False, False) expected_parameters = { 'server_0_flavor': { @@ -1924,7 +1930,7 @@ class ServerTests(BaseTestCase): def test_servergroup(self): self.fake.servers = [FakeServer()] self.fake.servers[0].id = '12345' - generator = self.get_generator(False, False, False, True) + generator = self.get_generator(False, False, False, True, False) expected_parameters = { 'server_0_flavor': { @@ -1966,7 +1972,7 @@ class GenerationTests(BaseTestCase): def test_generation(self): - generator = self.get_generator(False, False, False, True) + generator = self.get_generator(False, False, False, True, False) expected_parameters = { 'server_0_flavor': { @@ -2098,7 +2104,7 @@ class GenerationTests(BaseTestCase): def test_generation_exclude_servers(self): - generator = self.get_generator(True, False, False, True) + generator = self.get_generator(True, False, False, True, False) expected_parameters = { 'volume_0_volume_type': { @@ -2204,7 +2210,7 @@ class GenerationTests(BaseTestCase): def test_generation_exclude_volumes(self): - generator = self.get_generator(False, True, False, True) + generator = self.get_generator(False, True, False, True, False) expected_parameters = { 'server_0_flavor': { @@ -2314,7 +2320,7 @@ class GenerationTests(BaseTestCase): def test_generation_exclude_keypairs(self): - generator = self.get_generator(False, False, True, True) + generator = self.get_generator(False, False, True, True, False) expected_parameters = { 'server_0_flavor': { @@ -2437,7 +2443,7 @@ class GenerationTests(BaseTestCase): def test_generation_exclude_servers_and_volumes(self): - generator = self.get_generator(True, True, False, True) + generator = self.get_generator(True, True, False, True, False) expected_parameters = {} expected_resources = { @@ -2517,7 +2523,7 @@ class GenerationTests(BaseTestCase): def test_generation_exclude_servers_volumes_keypairs(self): - generator = self.get_generator(True, True, True, True) + generator = self.get_generator(True, True, True, True, False) expected_parameters = {} expected_resources = { diff --git a/flameclient/tests/test_flameports.py b/flameclient/tests/test_flameports.py new file mode 100644 index 0000000..00f720b --- /dev/null +++ b/flameclient/tests/test_flameports.py @@ -0,0 +1,640 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2014 Cloudwatt + +# 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 re + +import mock + +from flameclient import flame +from flameclient.tests import base + + +class FakeBase(object): + + def __init__(self, **kwargs): + for key, value in kwargs.items(): + setattr(self, key, value) + + +class FakeVolume(FakeBase): + id = 1234 + size = 1 + source_volid = None + bootable = 'false' + snapshot_id = None + display_name = 'vol1' + display_description = 'Description' + volume_type = 'fast' + metadata = None + + +class FakeServer(FakeBase): + id = '1234' + name = 'server1' + config_drive = None + flavor = {'id': '2'} + image = {'id': '3333', + 'links': [{'href': 'http://p/7777/images/3333', + 'rel': 'bookmark'}]} + key_name = 'testkey' + addresses = [] + metadata = None + + def __init__(self, server_id, **kwargs): + self.id = server_id + kwargs.setdefault('OS-DCF:diskConfig', 'MANUAL') + kwargs.setdefault('os-extended-volumes:volumes_attached', []) + super(FakeServer, self).__init__(**kwargs) + + +class FakeFlavor(FakeBase): + name = 'm1.tiny' + id = '1' + + +class FakeKeypair(FakeBase): + name = 'key' + id = 'key' + public_key = 'ssh-rsa AAAAB3NzaC' + + +class FakeSecurityGroup(FakeBase): + id = '1' + name = 'name' + + +class FakeNeutronManager(object): + + def __init__(self): + self.groups = [{u'description': u'default', + u'id': u'secgorup1', + u'name': u'default', + u'security_group_rules': [ + {u'direction': u'ingress', + u'ethertype': u'IPv4', + u'id': u'secgroup-rule1', + u'port_range_max': 65535, + u'port_range_min': 1, + u'protocol': u'tcp', + u'remote_group_id': None, + u'remote_ip_prefix': u'0.0.0.0/0', + u'security_group_id': u'secgorup1', + u'tenant_id': u'tenant1'}, + ], + u'tenant_id': u'tenant1'}] + + self.routers = [ + {u'admin_state_up': True, + u'external_gateway_info': {u'enable_snat': True, + u'network_id': u'network3'}, + u'id': u'router1', + u'name': u'gw-internal-a', + u'routes': [], + u'status': u'ACTIVE', + u'tenant_id': u'tenant1'}, + ] + + self.ports = [{u'admin_state_up': True, + u'allowed_address_pairs': [], + u'binding:vnic_type': u'normal', + u'device_id': u'router1', + u'device_owner': u'network:router_interface', + u'extra_dhcp_opts': [], + u'fixed_ips': [{u'ip_address': u'192.168.203.1', + u'subnet_id': u'subnet3'}], + u'id': u'port1', + u'mac_address': u'fa:16:3e:fe:c1:b3', + u'name': u'', + u'network_id': u'network1', + u'security_groups': [], + u'status': u'ACTIVE', + u'tenant_id': u'tenant1'}, + {u'admin_state_up': True, + u'allowed_address_pairs': [], + u'binding:vnic_type': u'normal', + u'device_id': u'server3', + u'device_owner': u'compute:nova', + u'extra_dhcp_opts': [], + u'fixed_ips': [{u'ip_address': u'192.168.203.5', + u'subnet_id': u'subnet3'}], + u'id': u'port2', + u'mac_address': u'fa:16:3e:e4:44:7b', + u'name': u'', + u'network_id': u'network1', + u'security_groups': [u'secgorup1'], + u'status': u'ACTIVE', + u'tenant_id': u'tenant1'}, + {u'admin_state_up': True, + u'allowed_address_pairs': [], + u'binding:vnic_type': u'normal', + u'device_id': u'server2', + u'device_owner': u'compute:nova', + u'extra_dhcp_opts': [], + u'fixed_ips': [{u'ip_address': u'192.168.203.4', + u'subnet_id': u'subnet3'}], + u'id': u'port3', + u'mac_address': u'fa:16:3e:e8:e4:e2', + u'name': u'', + u'network_id': u'network1', + u'security_groups': [u'secgorup1'], + u'status': u'ACTIVE', + u'tenant_id': u'tenant1'}, + {u'admin_state_up': True, + u'allowed_address_pairs': [], + u'binding:vnic_type': u'normal', + u'device_id': u'dhcp1-network1', + u'device_owner': u'network:dhcp', + u'extra_dhcp_opts': [], + u'fixed_ips': [{u'ip_address': u'192.168.203.3', + u'subnet_id': u'subnet3'}, + {u'ip_address': u'192.168.204.2', + u'subnet_id': u'subnet4'}], + u'id': u'port4', + u'mac_address': u'fa:16:3e:af:86:30', + u'name': u'', + u'network_id': u'network1', + u'security_groups': [], + u'status': u'ACTIVE', + u'tenant_id': u'tenant1'}, + {u'admin_state_up': True, + u'allowed_address_pairs': [], + u'binding:vnic_type': u'normal', + u'device_id': u'server1', + u'device_owner': u'compute:nova', + u'extra_dhcp_opts': [], + u'fixed_ips': [{u'ip_address': u'192.168.203.2', + u'subnet_id': u'subnet3'}], + u'id': u'port6', + u'mac_address': u'fa:16:3e:b0:9a:e2', + u'name': u'', + u'network_id': u'network1', + u'security_groups': [u'secgorup1'], + u'status': u'ACTIVE', + u'tenant_id': u'tenant1'} + ] + self.subnets = [{u'allocation_pools': [ + {u'end': u'172.19.0.254', u'start': u'172.19.0.2'}], + u'cidr': u'172.19.0.0/24', + u'dns_nameservers': [], + u'enable_dhcp': True, + u'gateway_ip': u'172.19.0.1', + u'host_routes': [], + u'id': u'subnet1', + u'ip_version': 4, + u'name': u'storage', + u'network_id': u'network2', + u'tenant_id': u'tenant1'}, + {u'allocation_pools': [ + {u'end': u'10.8.8.200', + u'start': u'10.8.8.100'}], + u'cidr': u'10.8.8.0/24', + u'dns_nameservers': [], + u'enable_dhcp': False, + u'gateway_ip': u'10.8.8.254', + u'host_routes': [], + u'id': u'subnet2', + u'ip_version': 4, + u'name': u'ext-subnet', + u'network_id': u'network3', + u'tenant_id': u'tenant1'}, + {u'allocation_pools': [{u'end': u'192.168.203.254', + u'start': u'192.168.203.2'}], + u'cidr': u'192.168.203.0/24', + u'dns_nameservers': [], + u'enable_dhcp': True, + u'gateway_ip': u'192.168.203.1', + u'host_routes': [], + u'id': u'subnet3', + u'ip_version': 4, + u'name': u'int-a-1', + u'network_id': u'network1', + u'tenant_id': u'tenant1'}, + {u'allocation_pools': [{u'end': u'192.168.204.254', + u'start': u'192.168.204.2'}], + u'cidr': u'192.168.204.0/24', + u'dns_nameservers': [], + u'enable_dhcp': True, + u'gateway_ip': u'192.168.204.1', + u'host_routes': [], + u'id': u'subnet4', + u'ip_version': 4, + u'name': u'int-a-2', + u'network_id': u'network1', + u'tenant_id': u'tenant1'}] + self.networks = [{u'admin_state_up': True, + u'id': u'network1', + u'name': u'internal', + u'router:external': False, + u'shared': False, + u'status': u'ACTIVE', + u'subnets': [u'subnet3', + u'subnet4'], + u'tenant_id': u'tenant1'}, + {u'admin_state_up': True, + u'id': u'network2', + u'name': u'storage', + u'router:external': False, + u'shared': False, + u'status': u'ACTIVE', + u'subnets': [u'subnet1'], + u'tenant_id': u'tenant1'}, + {u'admin_state_up': True, + u'id': u'network3', + u'name': u'ext-net', + u'router:external': True, + u'shared': True, + u'status': u'ACTIVE', + u'subnets': [u'subnet2'], + u'tenant_id': u'tenant1'}] + + self.floatingips = [{u'fixed_ip_address': None, + u'floating_ip_address': u'10.8.8.102', + u'floating_network_id': u'network3', + u'id': u'floating1', + u'port_id': None, + u'router_id': None, + u'status': u'DOWN', + u'tenant_id': u'tenant1'}, + {u'fixed_ip_address': None, + u'floating_ip_address': u'10.8.8.101', + u'floating_network_id': u'network3', + u'id': u'floating2', + u'port_id': None, + u'router_id': None, + u'status': u'DOWN', + u'tenant_id': u'tenant1'}, + {u'fixed_ip_address': u'192.168.203.4', + u'floating_ip_address': u'10.8.8.168', + u'floating_network_id': u'network3', + u'id': u'floating3', + u'port_id': u'port3', + u'router_id': u'router1', + u'status': u'ACTIVE', + u'tenant_id': u'tenant1'}, + {u'fixed_ip_address': None, + u'floating_ip_address': u'10.8.8.118', + u'floating_network_id': u'network3', + u'id': u'floating4', + u'port_id': None, + u'router_id': None, + u'status': u'DOWN', + u'tenant_id': u'tenant1'}] + + def subnet_list(self): + return self.subnets + + def network_list(self): + return self.networks + + def port_list(self): + return self.ports + + def router_list(self): + return self.routers + + def router_interfaces_list(self, router): + return [port for port in self.ports + if port['device_id'] == router['id']] + + def secgroup_list(self): + return self.groups + + def floatingip_list(self): + return self.floatingips + + +class FakeNovaManager(object): + + def __init__(self): + self.servers = [FakeServer('server1'), + FakeServer('server2'), + FakeServer('server3')] + self.servergroups = [] + self.flavors = [FakeFlavor(id='2', name='m1.small')] + self.groups = {} + self.keypairs = [FakeKeypair(name='testkey', + public_key='ssh-rsa XXXX')] + + def keypair_list(self): + return self.keypairs + + def flavor_list(self): + return self.flavors + + def server_list(self): + return self.servers + + def server_security_group_list(self, server): + return self.groups.get(server.name, []) + + def servergroup_list(self): + return self.servergroups + + +class FakeCinderManager(object): + + def __init__(self): + self.volumes = [FakeVolume(), ] + + def volume_list(self): + return self.volumes + + +class ResourceTestCase(base.TestCase): + + def test_template_resource(self): + resource = flame.Resource('my-name', + 'my-type', + properties='my-properties') + + expected = { + 'my-name': { + 'type': 'my-type', + 'properties': 'my-properties', + } + } + self.assertEqual(expected, resource.template_resource) + + +class BaseTestCase(base.TestCase): + + def setUp(self): + super(BaseTestCase, self).setUp() + self.patch_neutron = mock.patch('flameclient.managers.NeutronManager') + self.mock_neutron = self.patch_neutron.start() + self.patch_nova = mock.patch('flameclient.managers.NovaManager') + self.mock_nova = self.patch_nova.start() + self.patch_cinder = mock.patch('flameclient.managers.CinderManager') + self.mock_cinder = self.patch_cinder.start() + + def tearDown(self): + self.mock_neutron.stop() + self.mock_nova.stop() + self.mock_cinder.stop() + super(BaseTestCase, self).tearDown() + + def get_generator(self, exclude_servers, exclude_volumes, + exclude_keypairs, generate_data, extract_ports): + generator = flame.TemplateGenerator('x', 'x', 'x', 'x', True, + 'publicURL') + generator.extract_vm_details(exclude_servers, exclude_volumes, + exclude_keypairs, generate_data, + extract_ports) + return generator + + def check_stackdata(self, resources, expected_resources): + merged_resources = {} + for resource in resources: + merged_resources.update(resource.stack_resource) + + self.assertEqual(expected_resources, merged_resources) + + def check_template(self, resources, expected_resources, + expected_parameters=None): + + expected_parameters = expected_parameters or {} + merged_resources = {} + merged_parameters = {} + for resource in resources: + merged_resources.update(resource.template_resource) + merged_parameters.update(resource.template_parameter) + + self.assertEqual(expected_resources, merged_resources) + self.assertEqual(expected_parameters, merged_parameters) + + +class StackDataTests(BaseTestCase): + + def setUp(self): + super(StackDataTests, self).setUp() + self.mock_neutron.return_value = FakeNeutronManager() + self.mock_nova.return_value = FakeNovaManager() + self.mock_cinder.return_value = FakeCinderManager() + + def test_routers_presents(self): + generator = self.get_generator(False, False, False, True, True) + extraction = generator._extract_routers() + routers = {r.name: r for r in extraction} + self.assertIn('router_0', routers) + + def test_routers_resource_names(self): + generator = self.get_generator(False, False, False, True, True) + generator_output = generator._extract_routers() + routers = (res for res in generator_output + if res.type == "OS::Neutron::Router") + for n, router in enumerate(routers): + assert(router.name.startswith("router_")) + + def test_ports_presents(self): + generator = self.get_generator(False, False, False, True, True) + extraction = generator._extract_ports() + ports = {r.name: r for r in extraction} + self.assertIn('port_1', ports) + self.assertIn('port_2', ports) + + def test_ports_resource_names_types(self): + generator = self.get_generator(False, False, False, True, True) + extraction = generator._extract_ports() + for n, port in enumerate(extraction): + props = port.properties + assert(extraction[0].name.startswith("port_")) + self.assertEqual("OS::Neutron::Port", port.type) + self.assertIsInstance(props['admin_state_up'], bool) + self.assertIsInstance(props['security_groups'], list) + assert(props['device_owner'].startswith("compute:")) + + def test_port_fixed_ip(self): + reference = [{'ip_address': '192.168.203.2', + 'subnet_id': {'get_resource': 'subnet_2'}}] + generator = self.get_generator(False, False, False, True, True) + extraction = generator._extract_ports() + # Get the right port for the test + port = next((p for p in extraction if + p.properties['mac_address'] == 'fa:16:3e:b0:9a:e2')) + props = port.properties + self.assertIsInstance(props['fixed_ips'], list) + fixed_ips = props['fixed_ips'] + for ref in reference: + self.assertIn(ref, fixed_ips) + + def test_servers_ports_assignations(self): + generator = self.get_generator(False, False, False, True, True) + extraction = generator._extract_servers() + used_ports = [] + for n, server in enumerate(extraction): + props = server.properties + self.assertIsInstance(props['networks'], list) + for network in props['networks']: + port = network['port']['get_resource'] + assert(port.startswith("port_")) + # Port has not been used by another server + self.assertNotIn(port, used_ports) + used_ports.append(port) + + def test_floating_association(self): + generator = self.get_generator(False, False, False, True, True) + extraction = generator._extract_floating() + associations = (res for res in extraction + if res.type == "OS::Neutron::FloatingIPAssociation") + for association in associations: + props = association.properties + assert(props['floatingip_id']['get_resource']. + startswith('floatingip_')) + assert(props['port_id']['get_resource']. + startswith('port_')) + + +class GenerationTests(BaseTestCase): + resource_ref = set(['floatingip_association_2', + 'subnet_2', 'subnet_3', 'subnet_0', + 'port_2', 'port_1', 'port_4', + 'server_2', 'server_1', 'server_0', + 'router_0', + 'router_0_interface_0', + 'router_0_gateway', + 'key_0', + 'network_0', 'network_1', + 'floatingip_0', 'floatingip_1', + 'floatingip_2', 'floatingip_3', + 'volume_0']) + + params_ref = set(['volume_0_volume_type', + 'external_network_for_floating_ip_3', + 'external_network_for_floating_ip_2', + 'external_network_for_floating_ip_1', + 'external_network_for_floating_ip_0', + 'port_4_default_security_group', + 'port_1_default_security_group', + 'port_2_default_security_group', + 'router_0_external_network', + 'server_1_image', + 'server_1_flavor', + 'server_1_key', + 'server_2_image', + 'server_2_flavor', + 'server_2_key', + 'server_0_image', + 'server_0_flavor', + 'server_0_key']) + + data_ref = set(['floatingip_0', 'floatingip_1', 'floatingip_2', + 'floatingip_3', + 'floatingip_association_2', + 'key_0', + 'network_0', 'network_1', + 'port_1', 'port_2', 'port_4', + 'router_0', + 'router_0_gateway', + 'router_0_interface_0', + 'server_0', 'server_1', 'server_2', + 'subnet_0', 'subnet_2', 'subnet_3', + 'volume_0']) + + def filter_set(self, filtered_set, exclude): + excluded_set = set() + for exc in exclude: + excluded_set.update( + set([e for e in filtered_set if re.search(exc, e)]) + ) + return filtered_set.difference(excluded_set) + + def setUp(self): + super(GenerationTests, self).setUp() + self.mock_neutron.return_value = FakeNeutronManager() + self.mock_nova.return_value = FakeNovaManager() + self.mock_cinder.return_value = FakeCinderManager() + + def test_generation(self): + + exclusion_table = [ + {'call_params': (False, False, False, True, True), + 'resource_filter': [], + 'params_filter': ['^server_\d+_key$'], + 'data_filter': []}, + # No server + {'call_params': (True, False, False, True, True), + 'resource_filter': ['^server'], + 'params_filter': ['^server'], + 'data_filter': ['^server']}, + # No volumes + {'call_params': (False, True, False, True, True), + 'resource_filter': ['^volume'], + 'params_filter': [r'^volume_\d+_volume_type$', + '^server_\d+_key$'], + 'data_filter': ['^volume']}, + # No keys + {'call_params': (False, False, True, True, True), + 'resource_filter': ['^key_\d+$'], + 'params_filter': [], + 'data_filter': ['^key', 'server_\d+_key']}, + # No ports + {'call_params': (False, False, False, True, False), + 'resource_filter': ['^port_\d+$'], + 'params_filter': ['^port_\d+_default_security_group$', + 'server_\d+_key$'], + 'data_filter': ['^port_\d+', '^floatingip_association_\d+$']}, + ] + + for exclusion in exclusion_table: + generator = self.get_generator(*exclusion['call_params']) + resource_ref = self.filter_set(self.resource_ref, + exclusion['resource_filter']) + params_ref = self.filter_set(self.params_ref, + exclusion['params_filter']) + data_ref = self.filter_set(self.data_ref, + exclusion['data_filter']) + + generator.extract_data() + # All the resources, params and datas are present + self.assertEqual(resource_ref, + set(generator.template['resources'].keys()), + "Called with : %r" % (exclusion['call_params'],)) + self.assertEqual(params_ref, + set(generator.template['parameters'].keys()), + "Called with : %r" % (exclusion['call_params'],)) + self.assertEqual(data_ref, + set(generator.stack_data['resources'].keys()), + "Called with : %r" % (exclusion['call_params'],)) + + def test_floating_association_data(self): + generator = self.get_generator(False, False, False, True, True) + generator.extract_data() + # Look for floating ips + assoc_name = 'floatingip_association_2' + association_data = generator.stack_data['resources'][assoc_name] + reference = {'action': 'CREATE', + 'metadata': {}, + 'name': 'floatingip_association_2', + 'resource_data': {}, + 'resource_id': u'floating3:port3', + 'status': 'COMPLETE', + 'type': 'OS::Neutron::FloatingIPAssociation'} + self.assertEqual(reference, association_data) + + def test_port_data(self): + generator = self.get_generator(False, False, False, True, True) + generator.extract_data() + # Look for floating ips + assoc_name = 'port_2' + association_data = generator.stack_data['resources'][assoc_name] + reference = {'action': 'CREATE', + 'metadata': {}, + 'name': 'port_2', + 'resource_data': {}, + 'resource_id': u'port3', + 'status': 'COMPLETE', + 'type': 'OS::Neutron::Port'} + self.assertEqual(reference, association_data)