Merge "Module: tripleo_composable_network"
This commit is contained in:
commit
a0ee4a45ef
|
@ -0,0 +1,14 @@
|
|||
===================================
|
||||
Module - tripleo_composable_network
|
||||
===================================
|
||||
|
||||
|
||||
This module provides for the following ansible plugin:
|
||||
|
||||
* tripleo_composable_network
|
||||
|
||||
|
||||
.. ansibleautoplugin::
|
||||
:module: tripleo_ansible/ansible_plugins/modules/tripleo_composable_network.py
|
||||
:documentation: true
|
||||
:examples: true
|
|
@ -0,0 +1,460 @@
|
|||
#!/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
|
||||
try:
|
||||
from ansible.module_utils import network_data_v2
|
||||
except ImportError:
|
||||
from tripleo_ansible.ansible_plugins.module_utils import network_data_v2
|
||||
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_composable_network
|
||||
|
||||
short_description: Create a TripleO Composable network
|
||||
|
||||
version_added: "2.8"
|
||||
|
||||
description:
|
||||
- "Create a TripleO Composable network, a network, one or more segments and one or more subnets"
|
||||
|
||||
options:
|
||||
net_data:
|
||||
description:
|
||||
- Structure describing a TripleO composable network
|
||||
type: dict
|
||||
author:
|
||||
- Harald Jensås <hjensas@redhat.com>
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Create composable networks
|
||||
tripleo_composable_network:
|
||||
net_data:
|
||||
name: Storage
|
||||
name_lower: storage
|
||||
dns_domain: storage.localdomain.
|
||||
mtu: 1442
|
||||
subnets:
|
||||
storage_subnet:
|
||||
ip_subnet: 172.18.0.0/24
|
||||
gateway_ip: 172.18.0.254
|
||||
allocation_pools:
|
||||
- start: 172.18.0.10
|
||||
end: 172.18.0.250
|
||||
routes:
|
||||
- destination: 172.18.1.0/24
|
||||
nexthop: 172.18.0.254
|
||||
vip: true
|
||||
vlan: 20
|
||||
storage_leaf1:
|
||||
ip_subnet: 172.18.1.0/24
|
||||
gateway_ip: 172.18.1.254
|
||||
allocation_pools:
|
||||
- start: 172.18.1.10
|
||||
end: 172.18.1.250
|
||||
routes:
|
||||
- destination: 172.18.0.0/24
|
||||
nexthop: 172.18.1.254
|
||||
vip: false
|
||||
vlan: 21
|
||||
'''
|
||||
|
||||
DEFAULT_ADMIN_STATE = False
|
||||
DEFAULT_SHARED = False
|
||||
DEFAULT_DOMAIN = 'localdomain.'
|
||||
DEFAULT_NETWORK_TYPE = 'flat'
|
||||
DEFAULT_MTU = 1500
|
||||
DEFAULT_VLAN_ID = 1
|
||||
|
||||
|
||||
def build_network_tag_field(net_data):
|
||||
tags = []
|
||||
service_net_map_replace = net_data.get('service_net_map_replace')
|
||||
vip = net_data.get('vip')
|
||||
if service_net_map_replace:
|
||||
tags.append('='.join(['tripleo_service_net_map_replace',
|
||||
service_net_map_replace]))
|
||||
if vip:
|
||||
tags.append('='.join(['tripleo_vip', 'true']))
|
||||
|
||||
return tags
|
||||
|
||||
|
||||
def build_subnet_tag_field(subnet_data):
|
||||
tags = []
|
||||
vlan_id = subnet_data.get('vlan')
|
||||
vlan_id = str(vlan_id) if vlan_id is not None else str(DEFAULT_VLAN_ID)
|
||||
tags.append('='.join(['tripleo_vlan_id', vlan_id]))
|
||||
|
||||
return tags
|
||||
|
||||
|
||||
def create_net_spec(net_data):
|
||||
name_lower = net_data.get('name_lower', net_data['name'].lower())
|
||||
net_spec = {
|
||||
'admin_state_up': net_data.get('admin_state_up', DEFAULT_ADMIN_STATE),
|
||||
'dns_domain': net_data.get(
|
||||
'dns_domain', '.'.join([net_data['name'].lower(), DEFAULT_DOMAIN])
|
||||
),
|
||||
'mtu': net_data.get('mtu', DEFAULT_MTU),
|
||||
'name': net_data['name'],
|
||||
'shared': net_data.get('shared', DEFAULT_SHARED),
|
||||
'provider:physical_network': name_lower,
|
||||
'provider:network_type': DEFAULT_NETWORK_TYPE,
|
||||
}
|
||||
|
||||
net_spec.update({'tags': build_network_tag_field(net_data)})
|
||||
|
||||
return net_spec
|
||||
|
||||
|
||||
def validate_network_update(module, network, net_spec):
|
||||
# Fail if updating read-only attributes
|
||||
if (network.provider_network_type != net_spec.pop(
|
||||
'provider:network_type')
|
||||
and network.provider_network_type is not None):
|
||||
module.fail_json(
|
||||
msg='Cannot update provider:network_type in existing network')
|
||||
# NOTE(hjensas): When a network have multiple segments,
|
||||
# attributes provider:network_type, provider:physical_network is None
|
||||
# for the network.
|
||||
if (network.provider_physical_network != net_spec.pop(
|
||||
'provider:physical_network')
|
||||
and network.provider_physical_network is not None):
|
||||
module.fail_json(
|
||||
msg='Cannot update provider:physical_network in existing network')
|
||||
|
||||
# Remove fields that don't need update from spec
|
||||
if network.is_admin_state_up == net_spec['admin_state_up']:
|
||||
net_spec.pop('admin_state_up')
|
||||
if network.dns_domain == net_spec['dns_domain']:
|
||||
net_spec.pop('dns_domain')
|
||||
if network.mtu == net_spec['mtu']:
|
||||
net_spec.pop('mtu')
|
||||
if network.name == net_spec['name']:
|
||||
net_spec.pop('name')
|
||||
if network.is_shared == net_spec['shared']:
|
||||
net_spec.pop('shared')
|
||||
|
||||
return net_spec
|
||||
|
||||
|
||||
def create_or_update_network(conn, module, net_spec):
|
||||
changed = False
|
||||
|
||||
# Need to use set_tags for the tags ...
|
||||
tags = net_spec.pop('tags')
|
||||
|
||||
network = conn.network.find_network(net_spec['name'])
|
||||
if not network:
|
||||
network = conn.network.create_network(**net_spec)
|
||||
changed = True
|
||||
else:
|
||||
net_spec = validate_network_update(module, network, net_spec)
|
||||
if net_spec:
|
||||
network = conn.network.update_network(network.id, **net_spec)
|
||||
changed = True
|
||||
|
||||
if network.tags != tags:
|
||||
conn.network.set_tags(network, tags)
|
||||
changed = True
|
||||
|
||||
return changed, network
|
||||
|
||||
|
||||
def create_segment_spec(net_id, net_name, subnet_name, physical_network=None):
|
||||
name = '_'.join([net_name, subnet_name])
|
||||
if physical_network is None:
|
||||
physical_network = name
|
||||
else:
|
||||
physical_network = physical_network
|
||||
|
||||
return {'network_id': net_id,
|
||||
'physical_network': physical_network,
|
||||
'name': name,
|
||||
'network_type': DEFAULT_NETWORK_TYPE}
|
||||
|
||||
|
||||
def validate_segment_update(module, segment, segment_spec):
|
||||
# Fail if updating read-only attributes
|
||||
if segment.network_id != segment_spec.pop('network_id'):
|
||||
module.fail_json(
|
||||
msg='Cannot update network_id in existing segment')
|
||||
if segment.network_type != segment_spec.pop('network_type'):
|
||||
module.fail_json(
|
||||
msg='Cannot update network_type in existing segment')
|
||||
if segment.physical_network != segment_spec.pop('physical_network'):
|
||||
module.fail_json(
|
||||
msg='Cannot update physical_network in existing segment')
|
||||
|
||||
# Remove fields that don't need update from spec
|
||||
if segment.name == segment_spec['name']:
|
||||
segment_spec.pop('name')
|
||||
|
||||
return segment_spec
|
||||
|
||||
|
||||
def create_or_update_segment(conn, module, segment_spec, segment_id=None):
|
||||
changed = False
|
||||
|
||||
if segment_id:
|
||||
segment = conn.network.find_segment(segment_id)
|
||||
else:
|
||||
segment = conn.network.find_segment(
|
||||
segment_spec['name'], network_id=segment_spec['network_id'])
|
||||
|
||||
if not segment:
|
||||
segment = conn.network.create_segment(**segment_spec)
|
||||
changed = True
|
||||
else:
|
||||
segment_spec = validate_segment_update(module, segment, segment_spec)
|
||||
if segment_spec:
|
||||
segment = conn.network.update_segment(segment.id, **segment_spec)
|
||||
changed = True
|
||||
|
||||
return changed, segment
|
||||
|
||||
|
||||
def create_subnet_spec(net_id, name, subnet_data):
|
||||
tags = build_subnet_tag_field(subnet_data)
|
||||
subnet_v4_spec = None
|
||||
subnet_v6_spec = None
|
||||
if subnet_data.get('ip_subnet'):
|
||||
subnet_v4_spec = {
|
||||
'ip_version': 4,
|
||||
'name': name,
|
||||
'network_id': net_id,
|
||||
'enable_dhcp': subnet_data.get('enable_dhcp', False),
|
||||
'gateway_ip': subnet_data.get('gateway_ip', None),
|
||||
'cidr': subnet_data['ip_subnet'],
|
||||
'allocation_pools': subnet_data.get('allocation_pools', []),
|
||||
'host_routes': subnet_data.get('routes', []),
|
||||
'tags': tags,
|
||||
}
|
||||
if subnet_data.get('ipv6_subnet'):
|
||||
subnet_v6_spec = {
|
||||
'ip_version': 6,
|
||||
'name': name,
|
||||
'network_id': net_id,
|
||||
'enable_dhcp': subnet_data.get('enable_dhcp', False),
|
||||
'ipv6_address_mode': subnet_data.get('ipv6_address_mode', None),
|
||||
'ipv6_ra_mode': subnet_data.get('ipv6_ra_mode', None),
|
||||
'gateway_ip': subnet_data.get('gateway_ipv6', None),
|
||||
'cidr': subnet_data['ipv6_subnet'],
|
||||
'allocation_pools': subnet_data.get('ipv6_allocation_pools', []),
|
||||
'host_routes': subnet_data.get('routes_ipv6', []),
|
||||
'tags': tags,
|
||||
}
|
||||
|
||||
return subnet_v4_spec, subnet_v6_spec
|
||||
|
||||
|
||||
def validate_subnet_update(module, subnet, subnet_spec):
|
||||
|
||||
# Fail if updating read-only attributes
|
||||
if subnet.ip_version != subnet_spec.pop('ip_version'):
|
||||
module.fail_json(
|
||||
msg='Cannot update ip_version in existing subnet')
|
||||
if subnet.network_id != subnet_spec.pop('network_id'):
|
||||
module.fail_json(
|
||||
msg='Cannot update network_id in existing subnet')
|
||||
if subnet.cidr != subnet_spec.pop('cidr'):
|
||||
module.fail_json(
|
||||
msg='Cannot update cidr in existing subnet')
|
||||
segment_id = subnet_spec.pop('segment_id')
|
||||
if subnet.segment_id != segment_id:
|
||||
module.fail_json(
|
||||
msg='Cannot update segment_id in existing subnet, '
|
||||
'Current segment_id: {} Update segment_id: {}'.format(
|
||||
subnet.segment_id, segment_id))
|
||||
|
||||
# Remove fields that don't need update from spec
|
||||
if subnet.name == subnet_spec['name']:
|
||||
subnet_spec.pop('name')
|
||||
if subnet.is_dhcp_enabled == subnet_spec['enable_dhcp']:
|
||||
subnet_spec.pop('enable_dhcp')
|
||||
if subnet.ipv6_address_mode == subnet_spec.get('ipv6_address_mode'):
|
||||
try:
|
||||
subnet_spec.pop('ipv6_address_mode')
|
||||
except KeyError:
|
||||
pass
|
||||
if subnet.ipv6_ra_mode == subnet_spec.get('ipv6_ra_mode'):
|
||||
try:
|
||||
subnet_spec.pop('ipv6_ra_mode')
|
||||
except KeyError:
|
||||
pass
|
||||
if subnet.gateway_ip == subnet_spec['gateway_ip']:
|
||||
subnet_spec.pop('gateway_ip')
|
||||
if subnet.allocation_pools == subnet_spec['allocation_pools']:
|
||||
subnet_spec.pop('allocation_pools')
|
||||
if subnet.host_routes == subnet_spec['host_routes']:
|
||||
subnet_spec.pop('host_routes')
|
||||
|
||||
return subnet_spec
|
||||
|
||||
|
||||
def create_or_update_subnet(conn, module, subnet_spec):
|
||||
changed = False
|
||||
# Need to use set_tags for the tags ...
|
||||
tags = subnet_spec.pop('tags')
|
||||
|
||||
subnet = conn.network.find_subnet(subnet_spec['name'],
|
||||
network_id=subnet_spec['network_id'])
|
||||
if not subnet:
|
||||
subnet = conn.network.create_subnet(**subnet_spec)
|
||||
changed = True
|
||||
else:
|
||||
subnet_spec = validate_subnet_update(module, subnet, subnet_spec)
|
||||
if subnet_spec:
|
||||
subnet = conn.network.update_subnet(subnet.id, **subnet_spec)
|
||||
changed = True
|
||||
|
||||
if subnet.tags != tags:
|
||||
conn.network.set_tags(subnet, tags)
|
||||
changed = True
|
||||
|
||||
return changed
|
||||
|
||||
|
||||
def adopt_the_implicit_segment(conn, module, segments, subnets, network):
|
||||
changed = False
|
||||
# Check for implicit segment
|
||||
implicit_segment = [s for s in segments if s['name'] is None]
|
||||
if not implicit_segment:
|
||||
return changed
|
||||
|
||||
if len(implicit_segment) > 1:
|
||||
module.fail_json(msg='Multiple segments with no name attribute exist '
|
||||
'on network {}, unable to reliably adopt the '
|
||||
'implicit segment.'.format(network.id))
|
||||
else:
|
||||
implicit_segment = implicit_segment[0]
|
||||
|
||||
if implicit_segment and subnets:
|
||||
subnet_associated = [s for s in subnets
|
||||
if s.segment_id == implicit_segment.id][0]
|
||||
segment_spec = create_segment_spec(
|
||||
network.id, network.name, subnet_associated.name,
|
||||
physical_network=implicit_segment.physical_network)
|
||||
create_or_update_segment(conn, module, segment_spec,
|
||||
segment_id=implicit_segment.id)
|
||||
changed = True
|
||||
|
||||
return changed
|
||||
elif implicit_segment and not subnets:
|
||||
conn.network.delete_segment(implicit_segment.id)
|
||||
changed = True
|
||||
return changed
|
||||
|
||||
module.fail_json(msg='ERROR: Unable to reliably adopt the implicit '
|
||||
'segment.')
|
||||
|
||||
|
||||
def run_module():
|
||||
result = dict(
|
||||
success=False,
|
||||
changed=False,
|
||||
error="",
|
||||
)
|
||||
|
||||
argument_spec = openstack_full_argument_spec(
|
||||
**yaml.safe_load(DOCUMENTATION)['options']
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec,
|
||||
supports_check_mode=False,
|
||||
**openstack_module_kwargs()
|
||||
)
|
||||
|
||||
net_data = module.params['net_data']
|
||||
error_messages = network_data_v2.validate_json_schema(net_data)
|
||||
if error_messages:
|
||||
module.fail_json(msg='\n\n'.join(error_messages))
|
||||
|
||||
try:
|
||||
_, conn = openstack_cloud_from_module(module)
|
||||
|
||||
# Create or update the network
|
||||
net_spec = create_net_spec(net_data)
|
||||
changed, network = create_or_update_network(conn, module, net_spec)
|
||||
result['changed'] = changed if changed else result['changed']
|
||||
|
||||
# Get current segments and subnets on the network
|
||||
segments = list(conn.network.segments(network_id=network.id))
|
||||
subnets = list(conn.network.subnets(network_id=network.id))
|
||||
|
||||
changed = adopt_the_implicit_segment(conn, module, segments,
|
||||
subnets, network)
|
||||
result['changed'] = changed if changed else result['changed']
|
||||
|
||||
for subnet_name, subnet_data in net_data.get('subnets', {}).items():
|
||||
segment_spec = create_segment_spec(
|
||||
network.id, network.name, subnet_name,
|
||||
physical_network=subnet_data.get('physical_network'))
|
||||
subnet_v4_spec, subnet_v6_spec = create_subnet_spec(
|
||||
network.id, subnet_name, subnet_data)
|
||||
|
||||
changed, segment = create_or_update_segment(
|
||||
conn, module, segment_spec)
|
||||
result['changed'] = changed if changed else result['changed']
|
||||
|
||||
if subnet_v4_spec:
|
||||
subnet_v4_spec.update({'segment_id': segment.id})
|
||||
changed = create_or_update_subnet(conn, module, subnet_v4_spec)
|
||||
result['changed'] = changed if changed else result['changed']
|
||||
if subnet_v6_spec:
|
||||
subnet_v6_spec.update({'segment_id': segment.id})
|
||||
changed = create_or_update_subnet(conn, module, subnet_v6_spec)
|
||||
result['changed'] = changed if changed else result['changed']
|
||||
|
||||
result['success'] = True
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
except Exception as err:
|
||||
result['error'] = str(err)
|
||||
result['msg'] = ("Error overcloud network provision failed!")
|
||||
module.fail_json(**result)
|
||||
|
||||
|
||||
def main():
|
||||
run_module()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -0,0 +1,596 @@
|
|||
# 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_composable_network as plugin)
|
||||
from tripleo_ansible.tests import base as tests_base
|
||||
from tripleo_ansible.tests import stubs
|
||||
|
||||
|
||||
class TestTripleoComposableNetwork(tests_base.TestCase):
|
||||
|
||||
def test_build_network_tag_field(self):
|
||||
net_data = {'name': 'foo',
|
||||
'service_net_map_replace': 'replacement',
|
||||
'vip': True}
|
||||
expected = ['tripleo_service_net_map_replace=replacement',
|
||||
'tripleo_vip=true']
|
||||
result = plugin.build_network_tag_field(net_data)
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
net_data = {'name': 'foo'}
|
||||
expected = []
|
||||
result = plugin.build_network_tag_field(net_data)
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
def test_build_subnet_tag_field(self):
|
||||
# Default VLAN id 1
|
||||
subnet_data = {}
|
||||
expected = ["tripleo_vlan_id=1"]
|
||||
result = plugin.build_subnet_tag_field(subnet_data)
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
subnet_data = {'vlan': 100}
|
||||
expected = ["tripleo_vlan_id=100"]
|
||||
result = plugin.build_subnet_tag_field(subnet_data)
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
def test_create_net_spec(self):
|
||||
net_data = {'name': 'NetName'}
|
||||
expected = {
|
||||
'admin_state_up': plugin.DEFAULT_ADMIN_STATE,
|
||||
'dns_domain': '.'.join(['netname', plugin.DEFAULT_DOMAIN]),
|
||||
'mtu': plugin.DEFAULT_MTU,
|
||||
'name': 'NetName',
|
||||
'shared': plugin.DEFAULT_SHARED,
|
||||
'provider:physical_network': 'netname',
|
||||
'provider:network_type': plugin.DEFAULT_NETWORK_TYPE,
|
||||
'tags': [],
|
||||
}
|
||||
result = plugin.create_net_spec(net_data)
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
def test_validate_network_update(self):
|
||||
net_spec = {
|
||||
'admin_state_up': True,
|
||||
'dns_domain': 'netname.localdomain',
|
||||
'mtu': 1450,
|
||||
'name': 'new_name',
|
||||
'shared': True,
|
||||
'provider:physical_network': 'NEWNAME',
|
||||
'provider:network_type': 'vlan',
|
||||
'provider:segmentation_id': 101
|
||||
}
|
||||
fake_network = stubs.FakeNeutronNetwork(**{
|
||||
'is_admin_state_up': False,
|
||||
'mtu': 1500,
|
||||
'is_shared': False,
|
||||
'provider:network_type': 'flat',
|
||||
'provider:physical_network': 'netname',
|
||||
'provider:segmentation_id': 100,
|
||||
'is_admin_state_up': False,
|
||||
'dns_domain': 'netname.localdomain',
|
||||
'name': 'netname'})
|
||||
module = mock.Mock()
|
||||
module.fail_json = mock.Mock()
|
||||
result = plugin.validate_network_update(module, fake_network, net_spec)
|
||||
module.fail_json.assert_has_calls([
|
||||
mock.call(msg=('Cannot update provider:network_type in existing '
|
||||
'network')),
|
||||
mock.call(msg=('Cannot update provider:physical_network in '
|
||||
'existing network'))
|
||||
])
|
||||
expected = {'mtu': 1450,
|
||||
'shared': True,
|
||||
'admin_state_up': True,
|
||||
'name': 'new_name',
|
||||
'provider:segmentation_id': 101}
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
@mock.patch.object(openstack.connection, 'Connection', autospec=True)
|
||||
def test_create_network(self, mock_conn):
|
||||
mock_module = mock.Mock()
|
||||
net_spec = {
|
||||
'admin_state_up': plugin.DEFAULT_ADMIN_STATE,
|
||||
'dns_domain': '.'.join(['netname', plugin.DEFAULT_DOMAIN]),
|
||||
'mtu': plugin.DEFAULT_MTU,
|
||||
'name': 'netname',
|
||||
'shared': plugin.DEFAULT_SHARED,
|
||||
'provider:physical_network': 'netname',
|
||||
'provider:network_type': plugin.DEFAULT_NETWORK_TYPE,
|
||||
'tags': ['tripleo_foo=bar'],
|
||||
}
|
||||
fake_network = stubs.FakeNeutronNetwork(
|
||||
id='foo',
|
||||
name='netname',
|
||||
is_shared=False,
|
||||
dns_domain='.'.join(['netname', plugin.DEFAULT_DOMAIN]),
|
||||
mtu=plugin.DEFAULT_MTU,
|
||||
is_admin_state_up=plugin.DEFAULT_ADMIN_STATE,
|
||||
physical_network='netname',
|
||||
network_type=plugin.DEFAULT_NETWORK_TYPE,
|
||||
tags=[],
|
||||
)
|
||||
mock_conn.network.find_network.return_value = None
|
||||
mock_conn.network.create_network.return_value = fake_network
|
||||
changed, network = plugin.create_or_update_network(
|
||||
mock_conn, mock_module, net_spec)
|
||||
mock_conn.network.create_network.assert_called_once_with(**net_spec)
|
||||
mock_conn.network.set_tags.assert_called_once_with(
|
||||
network, ['tripleo_foo=bar'])
|
||||
self.assertEqual(True, changed)
|
||||
|
||||
@mock.patch.object(openstack.connection, 'Connection', autospec=True)
|
||||
def test_update_network(self, mock_conn):
|
||||
mock_module = mock.Mock()
|
||||
net_spec = {
|
||||
'admin_state_up': plugin.DEFAULT_ADMIN_STATE,
|
||||
'dns_domain': '.'.join(['netname', plugin.DEFAULT_DOMAIN]),
|
||||
'mtu': plugin.DEFAULT_MTU,
|
||||
'name': 'new_name',
|
||||
'shared': plugin.DEFAULT_SHARED,
|
||||
'provider:physical_network': 'netname',
|
||||
'provider:network_type': plugin.DEFAULT_NETWORK_TYPE,
|
||||
'tags': ['tripleo_foo=bar'],
|
||||
}
|
||||
fake_network = stubs.FakeNeutronNetwork(
|
||||
id='foo',
|
||||
name='netname',
|
||||
is_shared=False,
|
||||
dns_domain='.'.join(['netname', plugin.DEFAULT_DOMAIN]),
|
||||
mtu=plugin.DEFAULT_MTU,
|
||||
is_admin_state_up=plugin.DEFAULT_ADMIN_STATE,
|
||||
physical_network='netname',
|
||||
network_type=plugin.DEFAULT_NETWORK_TYPE,
|
||||
tags=[],
|
||||
)
|
||||
mock_conn.network.find_network.return_value = fake_network
|
||||
changed, network = plugin.create_or_update_network(
|
||||
mock_conn, mock_module, net_spec)
|
||||
mock_conn.network.update_network.assert_called_once_with(
|
||||
'foo', **{'name': 'new_name'})
|
||||
mock_conn.network.set_tags.assert_called_once_with(
|
||||
network, ['tripleo_foo=bar'])
|
||||
self.assertEqual(True, changed)
|
||||
|
||||
@mock.patch.object(openstack.connection, 'Connection', autospec=True)
|
||||
def test_update_network_no_change(self, mock_conn):
|
||||
mock_module = mock.Mock()
|
||||
net_spec = {
|
||||
'admin_state_up': plugin.DEFAULT_ADMIN_STATE,
|
||||
'dns_domain': '.'.join(['netname', plugin.DEFAULT_DOMAIN]),
|
||||
'mtu': plugin.DEFAULT_MTU,
|
||||
'name': 'netname',
|
||||
'shared': plugin.DEFAULT_SHARED,
|
||||
'provider:physical_network': 'netname',
|
||||
'provider:network_type': plugin.DEFAULT_NETWORK_TYPE,
|
||||
'tags': ['tripleo_foo=bar'],
|
||||
}
|
||||
fake_network = stubs.FakeNeutronNetwork(
|
||||
id='foo',
|
||||
name='netname',
|
||||
is_shared=False,
|
||||
dns_domain='.'.join(['netname', plugin.DEFAULT_DOMAIN]),
|
||||
mtu=plugin.DEFAULT_MTU,
|
||||
is_admin_state_up=plugin.DEFAULT_ADMIN_STATE,
|
||||
physical_network='netname',
|
||||
network_type=plugin.DEFAULT_NETWORK_TYPE,
|
||||
tags=['tripleo_foo=bar'],
|
||||
)
|
||||
mock_conn.network.find_network.return_value = fake_network
|
||||
changed, network = plugin.create_or_update_network(
|
||||
mock_conn, mock_module, net_spec)
|
||||
mock_conn.network.create_network.assert_not_called()
|
||||
mock_conn.network.update_network.assert_not_called()
|
||||
mock_conn.network.set_tags.assert_not_called()
|
||||
self.assertEqual(False, changed)
|
||||
|
||||
def test_create_segment_spec(self):
|
||||
net_id = 'net_id'
|
||||
net_name = 'net_name'
|
||||
subnet_name = 'subnet_name'
|
||||
expected = {'network_id': 'net_id', 'name': 'net_name_subnet_name',
|
||||
'physical_network': 'net_name_subnet_name',
|
||||
'network_type': plugin.DEFAULT_NETWORK_TYPE}
|
||||
result = plugin.create_segment_spec(net_id, net_name, subnet_name)
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
def test_validate_segment_update(self):
|
||||
segmnet_spec = {
|
||||
'network_id': 'new_net_id',
|
||||
'physical_network': 'new_physical_network',
|
||||
'name': 'new_name',
|
||||
'network_type': 'vlan',
|
||||
}
|
||||
fake_segment = stubs.FakeNeutronSegment(
|
||||
name='net_name_subnet_name',
|
||||
network_id='net_id',
|
||||
network_type=plugin.DEFAULT_NETWORK_TYPE,
|
||||
physical_network='net_name_subnet_name'
|
||||
)
|
||||
|
||||
module = mock.Mock()
|
||||
module.fail_json = mock.Mock()
|
||||
result = plugin.validate_segment_update(
|
||||
module, fake_segment, segmnet_spec)
|
||||
module.fail_json.assert_has_calls([
|
||||
mock.call(msg='Cannot update network_id in existing segment'),
|
||||
mock.call(msg='Cannot update network_type in existing segment'),
|
||||
mock.call(msg='Cannot update physical_network in existing segment')
|
||||
])
|
||||
expected = {'name': 'new_name'}
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
@mock.patch.object(openstack.connection, 'Connection', autospec=True)
|
||||
def test_create_segment(self, mock_conn):
|
||||
mock_module = mock.Mock()
|
||||
segment_spec = {'network_id': 'net_id', 'name': 'net_name_subnet_name',
|
||||
'physical_network': 'net_name_subnet_name',
|
||||
'network_type': plugin.DEFAULT_NETWORK_TYPE}
|
||||
mock_conn.network.find_segment.return_value = None
|
||||
changed, segment = plugin.create_or_update_segment(
|
||||
mock_conn, mock_module, segment_spec)
|
||||
mock_conn.network.create_segment.assert_called_once_with(
|
||||
**segment_spec)
|
||||
self.assertEqual(True, changed)
|
||||
|
||||
@mock.patch.object(openstack.connection, 'Connection', autospec=True)
|
||||
def test_update_segment(self, mock_conn):
|
||||
mock_module = mock.Mock()
|
||||
segment_spec = {'network_id': 'net_id',
|
||||
'physical_network': 'net_name_subnet_name',
|
||||
'name': 'NEW_NAME',
|
||||
'network_type': plugin.DEFAULT_NETWORK_TYPE}
|
||||
fake_segment = stubs.FakeNeutronSegment(
|
||||
id='foo', name='net_name_subnet_name', network_id='net_id',
|
||||
network_type=plugin.DEFAULT_NETWORK_TYPE,
|
||||
physical_network='net_name_subnet_name')
|
||||
mock_conn.network.find_segment.return_value = fake_segment
|
||||
changed, segment = plugin.create_or_update_segment(
|
||||
mock_conn, mock_module, segment_spec, segment_id='foo')
|
||||
mock_conn.network.find_segment.assert_called_once_with('foo')
|
||||
mock_conn.network.update_segment.assert_called_once_with(
|
||||
fake_segment.id, **{'name': 'NEW_NAME'})
|
||||
self.assertEqual(True, changed)
|
||||
|
||||
@mock.patch.object(openstack.connection, 'Connection', autospec=True)
|
||||
def test_update_segment_no_change(self, mock_conn):
|
||||
mock_module = mock.Mock()
|
||||
segment_spec = {'network_id': 'net_id',
|
||||
'physical_network': 'net_name_subnet_name',
|
||||
'name': 'net_name_subnet_name',
|
||||
'network_type': plugin.DEFAULT_NETWORK_TYPE}
|
||||
fake_segment = stubs.FakeNeutronSegment(
|
||||
id='foo', name='net_name_subnet_name', network_id='net_id',
|
||||
network_type=plugin.DEFAULT_NETWORK_TYPE,
|
||||
physical_network='net_name_subnet_name')
|
||||
mock_conn.network.find_segment.return_value = fake_segment
|
||||
changed, segment = plugin.create_or_update_segment(
|
||||
mock_conn, mock_module, segment_spec)
|
||||
mock_conn.network.find_segment.assert_called_once_with(
|
||||
'net_name_subnet_name', network_id='net_id')
|
||||
mock_conn.network.create_segment.assert_not_called()
|
||||
mock_conn.network.update_segment.assert_not_called()
|
||||
self.assertEqual(False, changed)
|
||||
|
||||
def test_create_subnet_spec_ipv4(self):
|
||||
net_id = 'net_id'
|
||||
name = 'subnet0'
|
||||
subnet_data = {
|
||||
'ip_subnet': '192.168.24.0/24',
|
||||
'gateway_ip': '192.168.24.1',
|
||||
'allocation_pools': [
|
||||
{'start': '192.168.24.100', 'end': '192.168.24.200'}
|
||||
],
|
||||
'routes': [
|
||||
{'destination': '192.168.25.0/24', 'nexthop': '192.168.24.1'}
|
||||
],
|
||||
'vlan': 100,
|
||||
}
|
||||
expected = {
|
||||
'ip_version': 4,
|
||||
'name': name,
|
||||
'network_id': net_id,
|
||||
'enable_dhcp': False,
|
||||
'gateway_ip': '192.168.24.1',
|
||||
'cidr': '192.168.24.0/24',
|
||||
'allocation_pools': [
|
||||
{'start': '192.168.24.100', 'end': '192.168.24.200'}
|
||||
],
|
||||
'host_routes': [
|
||||
{'destination': '192.168.25.0/24', 'nexthop': '192.168.24.1'}
|
||||
],
|
||||
'tags': ['tripleo_vlan_id=100'],
|
||||
}
|
||||
subnet_v4_spec, subnet_v6_spec = plugin.create_subnet_spec(
|
||||
net_id, name, subnet_data)
|
||||
self.assertEqual(expected, subnet_v4_spec)
|
||||
self.assertEqual(None, subnet_v6_spec)
|
||||
|
||||
def test_create_subnet_spec_ipv6(self):
|
||||
net_id = 'net_id'
|
||||
name = 'subnet0'
|
||||
subnet_data = {
|
||||
'ipv6_subnet': '2001:db8:a::/64',
|
||||
'gateway_ipv6': '2001:db8:a::1',
|
||||
'vlan': 100,
|
||||
}
|
||||
expected = {
|
||||
'ip_version': 6,
|
||||
'name': name,
|
||||
'network_id': net_id,
|
||||
'enable_dhcp': False,
|
||||
'gateway_ip': '2001:db8:a::1',
|
||||
'cidr': '2001:db8:a::/64',
|
||||
'allocation_pools': [],
|
||||
'host_routes': [],
|
||||
'ipv6_address_mode': None,
|
||||
'ipv6_ra_mode': None,
|
||||
'tags': ['tripleo_vlan_id=100'],
|
||||
}
|
||||
subnet_v4_spec, subnet_v6_spec = plugin.create_subnet_spec(
|
||||
net_id, name, subnet_data)
|
||||
self.assertEqual(None, subnet_v4_spec)
|
||||
self.assertEqual(expected, subnet_v6_spec)
|
||||
|
||||
def test_create_subnet_spec_dual_stack(self):
|
||||
net_id = 'net_id'
|
||||
name = 'subnet0'
|
||||
subnet_data = {
|
||||
'ip_subnet': '192.168.24.0/24',
|
||||
'gateway_ip': '192.168.24.1',
|
||||
'allocation_pools': [
|
||||
{'start': '192.168.24.100', 'end': '192.168.24.200'}],
|
||||
'routes': [
|
||||
{'destination': '192.168.25.0/24', 'nexthop': '192.168.24.1'}],
|
||||
'ipv6_subnet': '2001:db8:a::/64',
|
||||
'gateway_ipv6': '2001:db8:a::1',
|
||||
'ipv6_allocation_pools': [
|
||||
{'start': '2001:db8:a::0010', 'end': '2001:db8:a::fff9'}
|
||||
],
|
||||
'routes_ipv6': [
|
||||
{'destination': '2001:db8:b::/64', 'nexthop': '2001:db8:a::1'}
|
||||
],
|
||||
'vlan': 100,
|
||||
}
|
||||
expected_ipv4 = {
|
||||
'ip_version': 4,
|
||||
'name': name,
|
||||
'network_id': net_id,
|
||||
'enable_dhcp': False,
|
||||
'gateway_ip': '192.168.24.1',
|
||||
'cidr': '192.168.24.0/24',
|
||||
'allocation_pools': [
|
||||
{'start': '192.168.24.100', 'end': '192.168.24.200'}
|
||||
],
|
||||
'host_routes': [
|
||||
{'destination': '192.168.25.0/24', 'nexthop': '192.168.24.1'}
|
||||
],
|
||||
'tags': ['tripleo_vlan_id=100'],
|
||||
}
|
||||
expected_ipv6 = {
|
||||
'ip_version': 6,
|
||||
'name': name,
|
||||
'network_id': net_id,
|
||||
'enable_dhcp': False,
|
||||
'gateway_ip': '2001:db8:a::1',
|
||||
'cidr': '2001:db8:a::/64',
|
||||
'allocation_pools': [
|
||||
{'start': '2001:db8:a::0010', 'end': '2001:db8:a::fff9'}
|
||||
],
|
||||
'host_routes': [
|
||||
{'destination': '2001:db8:b::/64', 'nexthop': '2001:db8:a::1'}
|
||||
],
|
||||
'ipv6_address_mode': None,
|
||||
'ipv6_ra_mode': None,
|
||||
'tags': ['tripleo_vlan_id=100'],
|
||||
}
|
||||
subnet_v4_spec, subnet_v6_spec = plugin.create_subnet_spec(
|
||||
net_id, name, subnet_data)
|
||||
self.assertEqual(expected_ipv4, subnet_v4_spec)
|
||||
self.assertEqual(expected_ipv6, subnet_v6_spec)
|
||||
|
||||
def test_validate_subnet_update(self):
|
||||
module = mock.Mock()
|
||||
module.fail = mock.Mock()
|
||||
subnet_spec = {
|
||||
'ip_version': 6,
|
||||
'network_id': 'new_net_id',
|
||||
'cidr': '192.168.24.0/25',
|
||||
'segment_id': 'new_segment_id',
|
||||
'name': 'new_name',
|
||||
'enable_dhcp': True,
|
||||
'ipv6_address_mode': 'slaac',
|
||||
'ipv6_ra_mode': 'slaac',
|
||||
'gateway_ip': '192.168.24.254',
|
||||
'allocation_pools': [{'start': '192.168.24.100', 'end': '200'}],
|
||||
'host_routes': [
|
||||
{'destination': '192.168.25.0/24', 'nexthop': '192.168.24.254'}
|
||||
],
|
||||
}
|
||||
fake_subnet = stubs.FakeNeutronSubnet(
|
||||
id='foo',
|
||||
name='subnet01',
|
||||
cidr='192.168.24.0/24',
|
||||
gateway_ip='192.168.24.1',
|
||||
allocation_pools=[{'start': '192.168.24.50', 'end': '99'}],
|
||||
host_routes={'destination': '192.168.25.0/24',
|
||||
'nexthop': '192.168.24.1'},
|
||||
ip_version=4,
|
||||
network_id='net_id',
|
||||
segment_id='segment_id',
|
||||
is_dhcp_enabled=False,
|
||||
ipv6_address_mode=None,
|
||||
ipv6_ra_mode=None,
|
||||
|
||||
)
|
||||
result = plugin.validate_subnet_update(
|
||||
module, fake_subnet, subnet_spec)
|
||||
module.fail_json.assert_has_calls([
|
||||
mock.call(msg='Cannot update ip_version in existing subnet'),
|
||||
mock.call(msg='Cannot update network_id in existing subnet'),
|
||||
mock.call(msg='Cannot update cidr in existing subnet'),
|
||||
mock.call(
|
||||
msg='Cannot update segment_id in existing subnet, Current '
|
||||
'segment_id: {} Update segment_id: {}'.format(
|
||||
'segment_id', 'new_segment_id')
|
||||
),
|
||||
])
|
||||
expected_spec = {
|
||||
'name': 'new_name',
|
||||
'enable_dhcp': True,
|
||||
'ipv6_address_mode': 'slaac',
|
||||
'ipv6_ra_mode': 'slaac',
|
||||
'gateway_ip': '192.168.24.254',
|
||||
'allocation_pools': [{'start': '192.168.24.100', 'end': '200'}],
|
||||
'host_routes': [
|
||||
{'destination': '192.168.25.0/24', 'nexthop': '192.168.24.254'}
|
||||
],
|
||||
}
|
||||
self.assertEqual(expected_spec, result)
|
||||
|
||||
@mock.patch.object(openstack.connection, 'Connection', autospec=True)
|
||||
def test_create_subnet(self, mock_conn):
|
||||
mock_module = mock.Mock()
|
||||
subnet_spec = {
|
||||
'ip_version': 4,
|
||||
'name': 'subnet_name',
|
||||
'network_id': 'net_id',
|
||||
'enable_dhcp': False,
|
||||
'gateway_ip': '192.168.24.1',
|
||||
'cidr': '192.168.24.0/24',
|
||||
'allocation_pools': [
|
||||
{'start': '192.168.24.100', 'end': '192.168.24.200'}
|
||||
],
|
||||
'host_routes': [
|
||||
{'destination': '192.168.25.0/24', 'nexthop': '192.168.24.1'}
|
||||
],
|
||||
'tags': ['tripleo_vlan_id=100'],
|
||||
}
|
||||
mock_conn.network.find_subnet.return_value = None
|
||||
changed = plugin.create_or_update_subnet(mock_conn, mock_module,
|
||||
subnet_spec)
|
||||
mock_conn.network.create_subnet.assert_called_once_with(**subnet_spec)
|
||||
mock_conn.network.set_tags.assert_called_once_with(
|
||||
mock.ANY, ['tripleo_vlan_id=100'])
|
||||
self.assertEqual(True, changed)
|
||||
|
||||
@mock.patch.object(openstack.connection, 'Connection', autospec=True)
|
||||
def test_update_subnet(self, mock_conn):
|
||||
mock_module = mock.Mock()
|
||||
subnet_spec = {
|
||||
'ip_version': 4,
|
||||
'name': 'subnet_name',
|
||||
'network_id': 'net_id',
|
||||
'enable_dhcp': False,
|
||||
'gateway_ip': '192.168.24.1',
|
||||
'cidr': '192.168.24.0/24',
|
||||
'allocation_pools': [
|
||||
{'start': '192.168.24.100', 'end': '192.168.24.200'}
|
||||
],
|
||||
'host_routes': [
|
||||
{'destination': '192.168.25.0/24', 'nexthop': '192.168.24.1'}
|
||||
],
|
||||
'tags': ['tripleo_vlan_id=100'],
|
||||
'segment_id': 'segment_id',
|
||||
}
|
||||
fake_subnet = stubs.FakeNeutronSubnet(
|
||||
id='foo',
|
||||
name='subnet_name',
|
||||
network_id='net_id',
|
||||
is_dhcp_enabled=False,
|
||||
gateway_ip='192.168.24.254',
|
||||
cidr='192.168.24.0/24',
|
||||
allocation_pools=[
|
||||
{'start': '192.168.24.100', 'end': '192.168.24.200'}],
|
||||
host_routes=[
|
||||
{'destination': '192.168.25.0/24', 'nexthop': '192.168.24.254'}
|
||||
],
|
||||
tags=['tripleo_vlan_id=100'],
|
||||
segment_id='segment_id'
|
||||
)
|
||||
mock_conn.network.find_subnet.return_value = fake_subnet
|
||||
changed = plugin.create_or_update_subnet(mock_conn, mock_module,
|
||||
subnet_spec)
|
||||
mock_conn.network.find_subnet.ssert_called_once_with(
|
||||
'subnet_name', network_id='net_id')
|
||||
mock_conn.network.create_subnet.assert_not_called()
|
||||
mock_conn.network.update_subnet.assert_called_once_with(
|
||||
'foo', **{'gateway_ip': '192.168.24.1',
|
||||
'host_routes': [{'destination': '192.168.25.0/24',
|
||||
'nexthop': '192.168.24.1'}]}
|
||||
)
|
||||
self.assertTrue(changed)
|
||||
|
||||
@mock.patch.object(plugin, 'create_segment_spec', autospec=True)
|
||||
@mock.patch.object(plugin, 'create_or_update_segment', autospec=True)
|
||||
def test_adopt_the_implicit_segment(self, mock_create_or_update_segment,
|
||||
mock_create_segment_spec):
|
||||
fake_network = stubs.FakeNeutronNetwork(id='net_id', name='net_name')
|
||||
fake_segments = [
|
||||
stubs.FakeNeutronSegment(id='segment_id', name=None,
|
||||
physical_network='physical_net')]
|
||||
fake_subnets = [
|
||||
stubs.FakeNeutronSubnet(id='subnet_id', name='subnet_name',
|
||||
segment_id='segment_id')]
|
||||
|
||||
changed = plugin.adopt_the_implicit_segment(
|
||||
mock.ANY, mock.ANY, fake_segments, fake_subnets, fake_network)
|
||||
|
||||
mock_create_segment_spec.assert_called_once_with(
|
||||
fake_network.id, fake_network.name, fake_subnets[0].name,
|
||||
physical_network=fake_segments[0].physical_network)
|
||||
mock_create_or_update_segment.assert_called_once_with(
|
||||
mock.ANY, mock.ANY, mock.ANY, segment_id=fake_segments[0].id)
|
||||
self.assertTrue(changed)
|
||||
|
||||
def test_implicit_segment_already_adopted(self):
|
||||
fake_segments = [
|
||||
stubs.FakeNeutronSegment(id='segment_id',
|
||||
name='net_name_subnet_name',
|
||||
physical_network='physical_net')]
|
||||
|
||||
changed = plugin.adopt_the_implicit_segment(
|
||||
mock.ANY, mock.ANY, fake_segments, mock.ANY, mock.ANY)
|
||||
self.assertFalse(changed)
|
||||
|
||||
def test_implicit_segment_unable_to_adopt(self):
|
||||
mock_module = mock.Mock()
|
||||
mock_module.fail_json = mock.Mock()
|
||||
fake_network = stubs.FakeNeutronNetwork(id='net_id', name='net_name')
|
||||
fake_segments = [
|
||||
stubs.FakeNeutronSegment(id='segment_id_01',
|
||||
name=None,
|
||||
network_id='net_id',
|
||||
physical_network='physical_net_01'),
|
||||
stubs.FakeNeutronSegment(id='segment_id_02',
|
||||
name=None,
|
||||
network_id='net_id',
|
||||
physical_network='physical_net_02')
|
||||
]
|
||||
fake_subnets = []
|
||||
|
||||
try:
|
||||
plugin.adopt_the_implicit_segment(
|
||||
mock.Mock(), mock_module, fake_segments, fake_subnets,
|
||||
fake_network)
|
||||
except AttributeError:
|
||||
mock_module.fail_json.assert_called_once_with(
|
||||
msg='Multiple segments with no name attribute exist on '
|
||||
'network {}, unable to reliably adopt the implicit '
|
||||
'segment.'.format(fake_network.id))
|
Loading…
Reference in New Issue