485 lines
16 KiB
Python
485 lines
16 KiB
Python
#!/usr/bin/python
|
|
# -*- coding: utf-8 -*-
|
|
# Copyright (c) 2020 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
|
|
idx:
|
|
description:
|
|
- TripleO network index number
|
|
type: int
|
|
author:
|
|
- Harald Jensås <hjensas@redhat.com>
|
|
'''
|
|
|
|
RETURN = '''
|
|
'''
|
|
|
|
EXAMPLES = '''
|
|
- name: Create composable networks
|
|
default_network:
|
|
description:
|
|
- Default control plane network
|
|
type: string
|
|
default: ctlplane
|
|
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
|
|
idx: 1
|
|
'''
|
|
|
|
DEFAULT_NETWORK = 'ctlplane'
|
|
DEFAULT_ADMIN_STATE = False
|
|
DEFAULT_SHARED = False
|
|
DEFAULT_DOMAIN = 'localdomain.'
|
|
DEFAULT_NETWORK_TYPE = 'flat'
|
|
DEFAULT_MTU = 1500
|
|
DEFAULT_VLAN_ID = 1
|
|
|
|
|
|
def get_overcloud_domain_name(conn, default_network):
|
|
network = conn.network.find_network(default_network)
|
|
if network is not None and network.dns_domain:
|
|
return network.dns_domain.partition('.')[-1]
|
|
else:
|
|
return DEFAULT_DOMAIN
|
|
|
|
|
|
def build_network_tag_field(net_data, idx):
|
|
tags = ['='.join(['tripleo_network_name', net_data['name']]),
|
|
'='.join(['tripleo_net_idx', str(idx)])]
|
|
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, overcloud_domain_name, idx):
|
|
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(),
|
|
overcloud_domain_name])
|
|
),
|
|
'mtu': net_data.get('mtu', DEFAULT_MTU),
|
|
'name': name_lower,
|
|
'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, idx)})
|
|
|
|
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 (net_spec.pop('provider:physical_network')
|
|
not in [network.provider_physical_network, net_spec['name']]
|
|
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()
|
|
)
|
|
|
|
default_network = module.params.get('default_network', DEFAULT_NETWORK)
|
|
net_data = module.params['net_data']
|
|
idx = module.params['idx']
|
|
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, get_overcloud_domain_name(conn, default_network), idx)
|
|
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()
|