commit 7cb4304a7656f02a29cb83652ba50bfbc38c2593 Author: Harald Jensås Date: Tue Sep 8 09:45:09 2020 +0200 Extract provisioned networks from stack Extract networks provisioned in stack and dump output to network_data.yaml v2 format. The network_data.yaml v2 format changes: Subnets are nested in each networks 'subnets' dict. Previously the subnets dict was only used for additional subnets in DCN/spine-leaf configurations. New network keys: 'dns_domain', 'shared', 'admin_state_up' New subnet(segment) keys: 'ipv6_address_mode', 'ipv6_ra_mode', 'network_type', 'physical_network', 'segmentation_id'. Depends-On: https://review.opendev.org/752437 Depends-On: https://review.opendev.org/750666 Depends-On: https://review.opendev.org/752041 Change-Id: Ia7cdd36917ed41e114d570bb56cbbea2cc37e865 diff --git a/doc/source/modules/modules_tripleo_overcloud_network_extract.rst b/doc/source/modules/modules_tripleo_overcloud_network_extract.rst new file mode 100644 index 0000000..56f8055 --- /dev/null +++ b/doc/source/modules/modules_tripleo_overcloud_network_extract.rst @@ -0,0 +1,14 @@ +========================================== +Module - tripleo_overcloud_network_extract +========================================== + + +This module provides for the following ansible plugin: + + * tripleo_overcloud_network_extract + + +.. ansibleautoplugin:: + :module: tripleo_ansible/ansible_plugins/modules/tripleo_overcloud_network_extract.py + :documentation: true + :examples: true diff --git a/tripleo_ansible/__init__.py b/tripleo_ansible/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tripleo_ansible/ansible_plugins/modules/tripleo_overcloud_network_extract.py b/tripleo_ansible/ansible_plugins/modules/tripleo_overcloud_network_extract.py new file mode 100644 index 0000000..3c34c16 --- /dev/null +++ b/tripleo_ansible/ansible_plugins/modules/tripleo_overcloud_network_extract.py @@ -0,0 +1,338 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright (c) 2018 OpenStack Foundation +# All Rights Reserved. +# +# 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 yaml + +try: + from ansible.module_utils import tripleo_common_utils as tc +except ImportError: + from tripleo_ansible.ansible_plugins.module_utils import tripleo_common_utils as tc +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.openstack import openstack_full_argument_spec +from ansible.module_utils.openstack import openstack_module_kwargs +from ansible.module_utils.openstack import openstack_cloud_from_module + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community' +} + +DOCUMENTATION = ''' +--- +module: tripleo_overcloud_network_extract + +short_description: Extract information on provisioned overcloud networks + +version_added: "2.8" + +description: + - "Extract information about provisioned network resource in overcloud heat stack." + +options: + stack_name: + description: + - Name of the overcloud heat stack + type: str +author: + - Harald Jensås +''' + +RETURN = ''' +network_data: + description: Overcloud networks data + returned: always + type: list + sample: + - name: Storage + name_lower: storage + mtu: 1440 + dns_domain: storage.localdomain. + vip: true + subnets: + storage: + ip_subnet: '172.18.0.0/24' + allocation_pools: + - {'end': '172.18.0.250', 'start': '172.18.0.10'} + gateway_ip: '172.18.0.254' + ipv6_subnet: 'fd00:fd00:fd00:2000::/64' + ipv6_allocation_pools: + - {'end': 'fd00:fd00:fd00:2000:ffff:ffff:ffff:fffe', 'start': 'fd00:fd00:fd00:2000::10'} + gateway_ipv6: 'fd00:fd00:fd00:2000::1' + routes: + - destination: 172.18.1.0/24 + nexthop: 172.18.0.254 + routes_ipv6: + - destination: 'fd00:fd00:fd00:2001::/64' + nexthop: 'fd00:fd00:fd00:2000::1' + vlan: 10 + physical_network: storage + storage_leaf1: + vlan: 21 + ip_subnet: '172.18.1.0/24' + allocation_pools: + - {'end': '172.18.1.250', 'start': '172.18.1.10'} + gateway_ip: '172.18.1.254' + ipv6_subnet: 'fd00:fd00:fd00:2001::/64' + ipv6_allocation_pools: + - {'end': 'fd00:fd00:fd00:2001:ffff:ffff:ffff:fffe', 'start': 'fd00:fd00:fd00:2001::10'} + gateway_ipv6: 'fd00:fd00:fd00:2001::1' + routes: + - destination: 172.18.0.0/24 + nexthop: 172.18.1.254 + routes_ipv6: + - destination: 'fd00:fd00:fd00:2000::/64' + nexthop: 'fd00:fd00:fd00:2001::1' + vlan: 20 + physical_network: storage_leaf1 +''' + +EXAMPLES = ''' +- name: Get Overcloud networks data + tripleo_overcloud_network_extract: + stack_name: overcloud + register: overcloud_network_data +- name: Write netowork data to output file + copy: + content: "{{ overcloud_network_data.network_data | to_yaml }}" + dest: /path/exported-network-data.yaml +''' + +TYPE_NET = 'OS::Neutron::Net' +TYPE_SUBNET = 'OS::Neutron::Subnet' +TYPE_SEGMENT = 'OS::Neutron::Segment' +RES_ID = 'physical_resource_id' +RES_TYPE = 'resource_type' + +NET_VIP_SUFFIX = '_virtual_ip' + +DEFAULT_NETWORK_MTU = 1500 +DEFAULT_NETWROK_SHARED = False +DEFAULT_NETWORK_ADMIN_STATE_UP = False +DEFAULT_NETWORK_TYPE = 'flat' +DEFAULT_NETWORK_VIP = False +DEFAULT_SUBNET_DHCP_ENABLED = False +DEFAULT_SUBNET_IPV6_ADDRESS_MODE = None +DEFAULT_SUBNET_IPV6_RA_MODE = None + + +def get_overcloud_network_resources(conn, stack_name): + network_resource_dict = dict() + networks = [res for res in conn.orchestration.resources(stack_name) + if res.name == 'Networks'][0] + networks = conn.orchestration.resources(networks.physical_resource_id) + for net in networks: + if net.name == 'NetworkExtraConfig': + continue + network_resource_dict[net.name] = dict() + for res in conn.orchestration.resources(net.physical_resource_id): + if res.resource_type == TYPE_SEGMENT: + continue + network_resource_dict[net.name][res.name] = { + RES_ID: res.physical_resource_id, + RES_TYPE: res.resource_type + } + + return network_resource_dict + + +def tripleo_resource_tags_to_dict(resource_tags): + tag_dict = dict() + for tag in resource_tags: + if not tag.startswith('tripleo_'): + continue + try: + key, value = tag.rsplit('=') + except ValueError: + continue + + tag_dict.update({key: value}) + + return tag_dict + + +def is_vip_network(conn, network_id): + network_name = conn.network.get_network(network_id).name + vip_ports = conn.network.ports(network_id=network_id, + name='{}{}'.format(network_name, + NET_VIP_SUFFIX)) + try: + next(vip_ports) + return True + except StopIteration: + pass + + return False + + +def get_network_info(conn, network_id): + + def pop_defaults(dict): + if dict['mtu'] == DEFAULT_NETWORK_MTU: + dict.pop('mtu') + if dict['shared'] == DEFAULT_NETWROK_SHARED: + dict.pop('shared') + if dict['admin_state_up'] == DEFAULT_NETWORK_ADMIN_STATE_UP: + dict.pop('admin_state_up') + if dict['vip'] == DEFAULT_NETWORK_VIP: + dict.pop('vip') + + network = conn.network.get_network(network_id) + tag_dict = tripleo_resource_tags_to_dict(network.tags) + + net_dict = { + 'name_lower': network.name, + 'dns_domain': network.dns_domain, + 'mtu': network.mtu, + 'shared': network.is_shared, + 'admin_state_up': network.is_admin_state_up, + 'vip': is_vip_network(conn, network.id), + } + + if 'tripleo_service_net_map_replace' in tag_dict: + net_dict.update({ + 'service_net_map_replace': + tag_dict['tripleo_service_net_map_replace'] + }) + + pop_defaults(net_dict) + + return net_dict + + +def get_subnet_info(conn, subnet_id): + + def pop_defaults(dict): + if dict['enable_dhcp'] == DEFAULT_SUBNET_DHCP_ENABLED: + dict.pop('enable_dhcp') + if dict['network_type'] == DEFAULT_NETWORK_TYPE: + dict.pop('network_type') + if dict['vlan'] is None: + dict.pop('vlan') + if dict['segmentation_id'] is None: + dict.pop('segmentation_id') + + try: + if dict['ipv6_address_mode'] == DEFAULT_SUBNET_IPV6_ADDRESS_MODE: + dict.pop('ipv6_address_mode') + except KeyError: + pass + + try: + if dict['ipv6_ra_mode'] == DEFAULT_SUBNET_IPV6_RA_MODE: + dict.pop('ipv6_ra_mode') + except KeyError: + pass + + subnet = conn.network.get_subnet(subnet_id) + segment = conn.network.get_segment(subnet.segment_id) + tag_dict = tripleo_resource_tags_to_dict(subnet.tags) + subnet_name = subnet.name + + subnet_dict = { + 'enable_dhcp': subnet.is_dhcp_enabled, + 'vlan': (int(tag_dict['tripleo_vlan_id']) + if tag_dict.get('tripleo_vlan_id') else None), + 'physical_network': segment.physical_network, + 'network_type': segment.network_type, + 'segmentation_id': segment.segmentation_id, + } + + if subnet.ip_version == 4: + subnet_dict.update({ + 'ip_subnet': subnet.cidr, + 'allocation_pools': subnet.allocation_pools, + 'gateway_ip': subnet.gateway_ip, + 'routes': subnet.host_routes, + }) + + if subnet.ip_version == 6: + subnet_dict.update({ + 'ipv6_subnet': subnet.cidr, + 'ipv6_allocation_pools': subnet.allocation_pools, + 'gateway_ipv6': subnet.gateway_ip, + 'routes_ipv6': subnet.host_routes, + 'ipv6_address_mode': subnet.ipv6_address_mode, + 'ipv6_ra_mode': subnet.ipv6_ra_mode, + }) + + pop_defaults(subnet_dict) + + return subnet_name, subnet_dict + + +def parse_net_resources(conn, net_resources): + network_data = list() + for net in net_resources: + name = net.rpartition('Network')[0] + net_entry = {'name': name, 'subnets': dict()} + for res in net_resources[net]: + res_dict = net_resources[net][res] + if res_dict['resource_type'] == TYPE_NET: + net_dict = get_network_info(conn, res_dict[RES_ID]) + net_entry.update(net_dict) + if res_dict['resource_type'] == TYPE_SUBNET: + subnet_name, subnet_dict = get_subnet_info(conn, + res_dict[RES_ID]) + net_entry['subnets'].update({subnet_name: subnet_dict}) + + network_data.append(net_entry) + + return network_data + + +def run_module(): + result = dict( + success=False, + changed=False, + error="", + network_data=list() + ) + + argument_spec = openstack_full_argument_spec( + **yaml.safe_load(DOCUMENTATION)['options'] + ) + + module = AnsibleModule( + argument_spec, + supports_check_mode=False, + **openstack_module_kwargs() + ) + + stack_name = module.params['stack_name'] + + try: + _, conn = openstack_cloud_from_module(module) + net_resources = get_overcloud_network_resources(conn, stack_name) + result['network_data'] = parse_net_resources(conn, net_resources) + + result['changed'] = True if result['network_data'] else False + module.exit_json(**result) + except Exception as err: + result['error'] = str(err) + result['msg'] = ("Error getting network data from overcloud stack " + "{stack_name}: %{error}".format(stack_name=stack_name, + error=err)) + module.fail_json(**result) + + +def main(): + run_module() + + +if __name__ == '__main__': + main() diff --git a/tripleo_ansible/playbooks/cli-overcloud-network-extract.yaml b/tripleo_ansible/playbooks/cli-overcloud-network-extract.yaml new file mode 100644 index 0000000..b3c8fa8 --- /dev/null +++ b/tripleo_ansible/playbooks/cli-overcloud-network-extract.yaml @@ -0,0 +1,50 @@ +--- +# Copyright 2019 Red Hat, Inc. +# All Rights Reserved. +# +# 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. +# +- name: Overcloud Network Extract Networks + connection: "{{ (tripleo_target_host is defined) | ternary('ssh', 'local') }}" + hosts: "{{ tripleo_target_host | default('localhost') }}" + remote_user: "{{ tripleo_target_user | default(lookup('env', 'USER')) }}" + gather_facts: "{{ (tripleo_target_host is defined) | ternary(true, false) }}" + any_errors_fatal: true + pre_tasks: + - fail: + msg: stack_name is a required input + when: + - stack_name is undefined + - fail: + msg: output is a required input + when: + - output is undefined + - name: Check if output file exists + stat: + path: "{{ output }}" + register: stat_output_file + - fail: + msg: Output file exists + when: + - stat_output_file.stat.exists and not overwrite|bool + + tasks: + + - name: Get network data from overcloud stack + tripleo_overcloud_network_extract: + stack_name: "{{ stack_name }}" + register: overcloud_network_data + - name: Write network data to output file + copy: + content: "{{ overcloud_network_data.network_data | to_nice_yaml(indent=2) }}" + dest: "{{ output }}" diff --git a/tripleo_ansible/tests/modules/test_tripleo_overcloud_network_extract.py b/tripleo_ansible/tests/modules/test_tripleo_overcloud_network_extract.py new file mode 100644 index 0000000..9cc04d8 --- /dev/null +++ b/tripleo_ansible/tests/modules/test_tripleo_overcloud_network_extract.py @@ -0,0 +1,216 @@ +# Copyright 2019 Red Hat, Inc. +# All Rights Reserved. +# +# 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 mock +import openstack + +from tripleo_ansible.ansible_plugins.modules import ( + tripleo_overcloud_network_extract as plugin) +from tripleo_ansible.tests import base as tests_base +from tripleo_ansible.tests import stubs + + +class TestTripleoOvercloudNetworkExtract(tests_base.TestCase): + + def test_tripleo_resource_tags_to_dict(self): + tags = ['foo=bar', 'baz=qux', 'tripleo_foo=bar', 'tripleo_baz=qux'] + expected = {'tripleo_foo': 'bar', 'tripleo_baz': 'qux'} + result = plugin.tripleo_resource_tags_to_dict(tags) + self.assertEqual(expected, result) + + @mock.patch.object(openstack.connection, 'Connection', autospec=True) + def test_is_vip_network_true(self, conn_mock): + net_name = 'external' + net_id = '132f871f-eaec-4fed-9475-0d54465e0f00' + fake_network = stubs.FakeNeutronNetwork(id=net_id, name=net_name) + fake_port = stubs.FakeNeutronPort( + name='{}{}'.format(net_name, plugin.NET_VIP_SUFFIX), + fixed_ips=[{'ip_address': '10.10.10.10', 'subnet_id': 'foo'}] + ) + + conn_mock.network.get_network.return_value = fake_network + conn_mock.network.ports.return_value = (x for x in [fake_port]) + + result = plugin.is_vip_network(conn_mock, net_id) + self.assertEqual(True, result) + + @mock.patch.object(openstack.connection, 'Connection', autospec=True) + def test_is_vip_network_false(self, conn_mock): + net_name = 'external' + net_id = '132f871f-eaec-4fed-9475-0d54465e0f00' + fake_network = stubs.FakeNeutronNetwork(id=net_id, name=net_name) + + conn_mock.network.get_network.return_value = fake_network + conn_mock.network.ports.return_value = (x for x in []) + + result = plugin.is_vip_network(conn_mock, net_id) + self.assertEqual(False, result) + + @mock.patch.object(plugin, 'is_vip_network', autospec=True, + return_value=False) + @mock.patch.object(openstack.connection, 'Connection', autospec=True) + def test_get_network_info(self, conn_mock, is_vip_net_mock): + fake_network = stubs.FakeNeutronNetwork( + id='132f871f-eaec-4fed-9475-0d54465e0f00', + name='public', + dns_domain='public.localdomain.', + mtu=1500, + is_shared=False, + is_admin_state_up=False, + tags=['tripleo_service_net_map_replace=external'] + ) + conn_mock.network.get_network.return_value = fake_network + expected = { + 'name_lower': 'public', + 'dns_domain': 'public.localdomain.', + 'service_net_map_replace': 'external', + } + result = plugin.get_network_info( + conn_mock, '132f871f-eaec-4fed-9475-0d54465e0f00') + self.assertEqual(expected, result) + + @mock.patch.object(openstack.connection, 'Connection', autospec=True) + def test_get_subnet_info_ipv4(self, conn_mock): + fake_subnet = stubs.FakeNeutronSubnet( + name='public_subnet', + is_dhcp_enabled=False, + tags=['tripleo_vlan_id=100'], + ip_version=4, + cidr='10.0.0.0/24', + allocation_pools=[{'start': '10.0.0.10', 'end': '10.0.0.150'}], + gateway_ip='10.0.0.1', + host_routes=[{'destination': '172.17.1.0/24', + 'nexthop': '10.0.0.1'}], + ) + fake_segment = stubs.FakeNeutronSegment( + name='public_subnet', + network_type='flat', + physical_network='public_subnet' + ) + conn_mock.network.get_subnet.return_value = fake_subnet + conn_mock.network.get_segment.return_value = fake_segment + expected = { + 'vlan': 100, + 'ip_subnet': '10.0.0.0/24', + 'allocation_pools': [{'start': '10.0.0.10', 'end': '10.0.0.150'}], + 'gateway_ip': '10.0.0.1', + 'routes': [{'destination': '172.17.1.0/24', + 'nexthop': '10.0.0.1'}], + 'physical_network': 'public_subnet', + } + name, subnet = plugin.get_subnet_info(conn_mock, mock.Mock()) + self.assertEqual(name, 'public_subnet') + self.assertEqual(expected, subnet) + + @mock.patch.object(openstack.connection, 'Connection', autospec=True) + def test_get_subnet_info_ipv6(self, conn_mock): + fake_subnet = stubs.FakeNeutronSubnet( + name='public_subnet', + is_dhcp_enabled=False, + tags=['tripleo_vlan_id=200'], + ip_version=6, + cidr='2001:db8:a::/64', + allocation_pools=[{'start': '2001:db8:a::0010', + 'end': '2001:db8:a::fff9'}], + gateway_ip='2001:db8:a::1', + host_routes=[{'destination': '2001:db8:b::/64', + 'nexthop': '2001:db8:a::1'}], + ipv6_address_mode=None, + ipv6_ra_mode=None, + ) + fake_segment = stubs.FakeNeutronSegment( + name='public_subnet', + network_type='flat', + physical_network='public_subnet' + ) + conn_mock.network.get_subnet.return_value = fake_subnet + conn_mock.network.get_segment.return_value = fake_segment + expected = { + 'vlan': 200, + 'ipv6_subnet': '2001:db8:a::/64', + 'ipv6_allocation_pools': [{'start': '2001:db8:a::0010', + 'end': '2001:db8:a::fff9'}], + 'gateway_ipv6': '2001:db8:a::1', + 'routes_ipv6': [{'destination': '2001:db8:b::/64', + 'nexthop': '2001:db8:a::1'}], + 'physical_network': 'public_subnet', + } + name, subnet = plugin.get_subnet_info(conn_mock, mock.Mock()) + self.assertEqual(name, 'public_subnet') + self.assertEqual(expected, subnet) + + @mock.patch.object(plugin, 'get_subnet_info', auto_spec=True) + @mock.patch.object(plugin, 'get_network_info', auto_spec=True) + @mock.patch.object(openstack.connection, 'Connection', autospec=True) + def test_parse_net_resources(self, conn_mock, mock_get_network, + mock_get_subnet): + net_resources = { + 'StorageNetwork': { + 'StorageNetwork': {'physical_resource_id': 'fake-id', + 'resource_type': plugin.TYPE_NET}, + 'StorageSubnet': {'physical_resource_id': 'fake-id', + 'resource_type': plugin.TYPE_SUBNET}, + 'StorageSubnet_leaf1': {'physical_resource_id': 'fake-id', + 'resource_type': plugin.TYPE_SUBNET} + } + } + + fake_network = { + 'name_lower': 'storage', + 'dns_domain': 'storage.localdomain.', + 'mtu': 1500, + 'shared': False, + 'admin_state_up': False, + 'vip': False, + } + fake_subnet_storage = { + 'enable_dhcp': False, + 'vlan': 100, + 'ip_subnet': '10.0.0.0/24', + 'allocation_pools': [{'start': '10.0.0.10', 'end': '10.0.0.150'}], + 'gateway_ip': '10.0.0.1', + 'routes': [{'destination': '10.1.0.0/24', 'nexthop': '10.0.0.1'}], + 'network_type': 'flat', + 'physical_network': 'storage', + } + fake_subnet_storage_leaf1 = { + 'enable_dhcp': False, + 'vlan': 101, + 'ip_subnet': '10.1.0.0/24', + 'allocation_pools': [{'start': '10.1.0.10', 'end': '10.1.0.150'}], + 'gateway_ip': '10.1.0.1', + 'routes': [{'destination': '10.0.0.0/24', 'nexthop': '10.1.0.1'}], + 'network_type': 'flat', + 'physical_network': 'leaf1', + } + + mock_get_network.return_value = fake_network + mock_get_subnet.side_effect = [ + ('storage', fake_subnet_storage), + ('leaf1', fake_subnet_storage_leaf1)] + + expected = [{'name': 'Storage', + 'mtu': 1500, + 'name_lower': 'storage', + 'dns_domain': 'storage.localdomain.', + 'shared': False, + 'admin_state_up': False, + 'vip': False, + 'subnets': { + 'storage': fake_subnet_storage, + 'leaf1': fake_subnet_storage_leaf1} + }] + result = plugin.parse_net_resources(conn_mock, net_resources) + self.assertEqual(expected, result) diff --git a/tripleo_ansible/tests/stubs.py b/tripleo_ansible/tests/stubs.py new file mode 100644 index 0000000..0e06ab7 --- /dev/null +++ b/tripleo_ansible/tests/stubs.py @@ -0,0 +1,169 @@ +# Copyright 2019 Red Hat, Inc. +# All Rights Reserved. +# +# 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. + +class FakeNeutronNetwork(dict): + def __init__(self, **attrs): + NETWORK_ATTRS = ['id', + 'name', + 'status', + 'tenant_id', + 'is_admin_state_up', + 'mtu', + 'segments', + 'is_shared', + 'subnets', + 'provider:network_type', + 'provider:physical_network', + 'provider:segmentation_id', + 'router:external', + 'availability_zones', + 'availability_zone_hints', + 'is_default', + 'tags'] + + raw = dict.fromkeys(NETWORK_ATTRS) + raw.update(attrs) + raw.update({ + 'provider_physical_network': attrs.get( + 'provider:physical_network', None), + 'provider_network_type': attrs.get( + 'provider:network_type', None), + 'provider_segmentation_id': attrs.get( + 'provider:segmentation_id', None) + }) + super(FakeNeutronNetwork, self).__init__(raw) + + def __getattr__(self, key): + try: + return self[key] + except KeyError: + raise AttributeError(key) + + def __setattr__(self, key, value): + if key in self: + self[key] = value + else: + raise AttributeError(key) + + +class FakeNeutronSegment(dict): + def __init__(self, **attrs): + NETWORK_ATTRS = ['id', + 'name', + 'network_id', + 'description', + 'network_type', + 'physical_network', + 'segmentation_id', + 'tags'] + + raw = dict.fromkeys(NETWORK_ATTRS) + raw.update(attrs) + super(FakeNeutronSegment, self).__init__(raw) + + def __getattr__(self, key): + try: + return self[key] + except KeyError: + raise AttributeError(key) + + def __setattr__(self, key, value): + if key in self: + self[key] = value + else: + raise AttributeError(key) + + +class FakeNeutronPort(dict): + def __init__(self, **attrs): + PORT_ATTRS = ['admin_state_up', + 'allowed_address_pairs', + 'binding:host_id', + 'binding:profile', + 'binding:vif_details', + 'binding:vif_type', + 'binding:vnic_type', + 'data_plane_status', + 'description', + 'device_id', + 'device_owner', + 'dns_assignment', + 'dns_domain', + 'dns_name', + 'extra_dhcp_opts', + 'fixed_ips', + 'id', + 'mac_address', + 'name', 'network_id', + 'port_security_enabled', + 'security_group_ids', + 'status', + 'tenant_id', + 'qos_network_policy_id', + 'qos_policy_id', + 'tags', + 'uplink_status_propagation'] + + raw = dict.fromkeys(PORT_ATTRS) + raw.update(attrs) + super(FakeNeutronPort, self).__init__(raw) + + def __getattr__(self, key): + try: + return self[key] + except KeyError: + raise AttributeError(key) + + def __setattr__(self, key, value): + if key in self: + self[key] = value + else: + raise AttributeError(key) + + +class FakeNeutronSubnet(dict): + def __init__(self, **attrs): + SUBNET_ATTRS = ['id', + 'name', + 'network_id', + 'cidr', + 'tenant_id', + 'is_dhcp_enabled', + 'dns_nameservers', + 'allocation_pools', + 'host_routes', + 'ip_version', + 'gateway_ip', + 'ipv6_address_mode', + 'ipv6_ra_mode', + 'subnetpool_id', + 'segment_id', + 'tags'] + + raw = dict.fromkeys(SUBNET_ATTRS) + raw.update(attrs) + super(FakeNeutronSubnet, self).__init__(raw) + + def __getattr__(self, key): + try: + return self[key] + except KeyError: + raise AttributeError(key) + + def __setattr__(self, key, value): + if key in self: + self[key] = value + else: + raise AttributeError(key)