Network ports module

Ansible module to manage network ports for overcloud
nodes. The main module option 'instances' takes a list
of instances similar to the one created by the
tripleo_baremetal_expand_roles module.

Additionally the 'stack_name' must be specified. Tags
will be added to each port resource created, one
with a stack_name hint and the other with the hostname
and a third with the ironic node uuid. Tags are also added
to metalsmith managed vif ports.

The tags is used to filter for already existing instance
ports on re-run, i.e idempotency. The tags will also be
used by tripleo-ansible-inventory to create an ansible
inventory prior to having a stack created.

On re-run existing ports will be updated, if the definition
changed.

The parameter 'concurrency' controls the maximum threads
to use for parallell processing.

Depends-On: https://review.opendev.org/761845
Depends-On: https://review.opendev.org/760536
Partial-Implements: blueprint network-data-v2-ports
Change-Id: Ie2874190c869abb8f9372acb6a45e93557090b2c
This commit is contained in:
Harald Jensås 2020-11-04 14:30:09 +01:00
parent f1e03a316a
commit c94945ae6b
3 changed files with 1159 additions and 0 deletions

View File

@ -0,0 +1,14 @@
========================================
Module - tripleo_overcloud_network_ports
========================================
This module provides for the following ansible plugin:
* tripleo_overcloud_network_ports
.. ansibleautoplugin::
:module: tripleo_ansible/ansible_plugins/modules/tripleo_overcloud_network_ports.py
:documentation: true
:examples: true

View File

@ -0,0 +1,587 @@
#!/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.
from concurrent import futures
import ipaddress
import metalsmith
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_ports
short_description: Manage composable networks ports for overcloud nodes
version_added: "2.8"
author: Harald Jensås <hjensas@redhat.com>
description:
- "Manage composable networks ports for overcloud nodes."
options:
stack_name:
description:
- Name of the overcloud stack which will be deployed on these instances
default: overcloud
concurrency:
description:
- Maximum number of instances to provision ports for at once. Set to 0
to have no concurrency limit
type: int
default: 0
state:
description:
- The desired provision state, "present" to provision, "absent" to
unprovision
default: present
choices:
- present
- absent
instances:
description:
- Data describing instances, node instances and networks to provision
ports in
type: list
elements: dict
suboptions:
name:
description:
- Mandatory role name
type: str
required: True
hostname:
description:
- Node hostname
type: str
networks:
description:
- List of networks for the role
type: list
elements: dict
suboptions:
network:
description:
- Name of the network
type: str
subnet:
description:
- Name of the subnet on the network
type: str
port:
description:
- Name or ID of a pre-created port
type: str
provisioned_instances:
description:
- List of provisioned instances
required: false
type: list
elements: dict
suboptions:
id:
description:
- Ironic Node UUID
type: str
hostname:
description:
- Node hostname
type: str
default: []
hostname_role_map:
description:
- Mapping of instance hostnames to role name
type: dict
'''
RETURN = '''
node_port_map:
controller-0:
External:
ip_address: 10.0.0.9
ip_subnet: 10.0.0.9/24
ip_address_uri: 10.0.0.9
InternalApi:
ip_address: 172.18.0.9
ip_subnet: 172.18.0.9/24
ip_address_uri: 172.18.0.9
Tenant:
ip_address: 172.19.0.9
ip_subnet: 172.19.0.9/24
ip_address_uri: 172.19.0.9
compute-0:
InternalApi:
ip_address: 172.18.0.15
ip_subnet: 172.18.0.15/24
ip_address_uri: 172.18.0.15
Tenant:
ip_address: 172.19.0.15
ip_subnet: 172.19.0.15/24
ip_address_uri: 172.19.0.15
'''
EXAMPLES = '''
- name: Manage composable networks instance ports
tripleo_overcloud_network_ports:
stack_name: overcloud
concurrency: 20
instances:
- hostname: overcloud-controller-0
networks:
- network: internal_api
subnet: internal_api_subnet
- network: tenant
subnet: tenant_subnet
- hostname: overcloud-novacompute-0
networks:
- network: internal_api
subnet: internal_api_subnet
- network: tenant
subnet: tenant_subnet
- hostname: overcloud-novacompute-1
networks:
- network: internal_api
subnet: internal_api_subnet02
- network: tenant
subnet: tenant_subnet02
provisioned: false
provisioned_instances:
- hostname: overcloud-novacompute-0
id: 1e3685bd-ffbc-4028-8a1c-4e87e45062d0
- hostname: overcloud-controller-0
id: 59cf045a-ef7f-4f2e-be66-accd05dcd1e6
register: overcloud_network_ports
'''
def wrap_ipv6(ip_address):
"""Wrap the address in square brackets if it's an IPv6 address."""
if ipaddress.ip_address(ip_address).version == 6:
return '[{}]'.format(ip_address)
return ip_address
def create_name_id_maps(conn):
net_name_map = {}
net_id_map = {}
cidr_prefix_map = {}
for net in conn.network.networks():
subnets = conn.network.subnets(network_id=net.id)
net_id_map[net.id] = net.name
net_name_map[net.name] = dict(id=net.id)
subnets_map = net_name_map[net.name]['subnets'] = dict()
for s in subnets:
subnets_map[s.name] = s.id
cidr_prefix_map[s.id] = s.cidr.split('/')[-1]
net_maps = dict(by_id=net_id_map,
by_name=net_name_map,
cidr_prefix_map=cidr_prefix_map)
return net_maps
def delete_ports(conn, ports):
for port in ports:
conn.network.delete_port(port.id)
def pre_provisioned_ports(result, conn, net_maps, instance, inst_ports, tags):
for net in instance['networks']:
if net.get('port'):
network_id = net_maps['by_name'][net['network']]['id']
p_obj = conn.network.find_port(net['port'], network_id=network_id)
if p_obj is None:
msg = ("Network port {port} for instance {instance} could not "
"be found.".format(port=net['port'],
instance=instance['hostname']))
raise Exception(msg)
p_tags = set(p_obj.tags)
if not tags.issubset(p_tags):
p_tags.update(tags)
conn.network.set_tags(p_obj, list(p_tags))
inst_ports.append(p_obj)
result['changed'] = True
def fixed_ips_need_update(port_def, port):
number_of_fixed_ips_in_def = len(port_def['fixed_ips'])
number_of_fixed_ips_on_port = len(port.fixed_ips)
if number_of_fixed_ips_in_def != number_of_fixed_ips_on_port:
return True
match_count = 0
for def_fixed_ip in port_def['fixed_ips']:
def_values = set(def_fixed_ip.values())
for port_fixed_ip in port.fixed_ips:
port_values = set(port_fixed_ip.values())
if def_values.issubset(port_values):
match_count += 1
return number_of_fixed_ips_in_def != match_count
def port_need_update(port_def, port):
update_fields = dict()
if fixed_ips_need_update(port_def, port):
update_fields['fixed_ips'] = port_def['fixed_ips']
return update_fields
def update_ports(result, conn, port_defs, inst_ports, tags):
for port_def in port_defs:
for p in inst_ports:
if (p.name == port_def['name']
and p.network_id == port_def['network_id']):
port = p
break
else: # Executed because no break in for
raise Exception(
'Port {name} on network {network} not found.'.format(
name=port_def['name'], network=port_def['network_id']))
update_fields = port_need_update(port_def, port)
if update_fields:
conn.network.update_port(port.id, update_fields)
result['changed'] = True
p_tags = set(port.tags)
if not tags.issubset(p_tags):
p_tags.update(tags)
conn.network.set_tags(port, list(p_tags))
def create_ports(result, conn, port_defs, inst_ports, tags):
ports = conn.network.create_ports(port_defs)
for port in ports:
conn.network.set_tags(port, list(tags))
inst_ports.append(port)
result['changed'] = True
def generate_port_defs(net_maps, instance, inst_ports):
hostname = instance['hostname']
create_port_defs = []
update_port_defs = []
existing_port_names = [port.name for port in inst_ports]
for net in instance['networks']:
net_name = net['network']
if net.get('vif', False):
# VIF port's are managed by metalsmith.
continue
net_id = net_maps['by_name'][net_name]['id']
subnet_name_map = net_maps['by_name'][net_name]['subnets']
if net.get('fixed_ip'):
fixed_ips = [{'ip_address': net['fixed_ip']}]
else:
if net.get('subnet'):
subnet_id = subnet_name_map[net['subnet']]
elif len(net_maps['by_name'][net_name]['subnets']) == 1:
subnet_id = next(iter(subnet_name_map.values()))
else:
raise Exception(
'The "subnet" or "fixed_ip" must be set for the '
'{instance_name} port on the {network_name} network since '
'there are multiple subnets'.format(
instance_name=hostname, network_name=net_name))
fixed_ips = [{'subnet_id': subnet_id}]
port_name = '_'.join([hostname, net_name])
port_def = dict(name=port_name, network_id=net_id, fixed_ips=fixed_ips)
if port_name not in existing_port_names:
create_port_defs.append(port_def)
else:
update_port_defs.append(port_def)
return create_port_defs, update_port_defs
def delete_removed_nets(result, conn, instance, net_maps, inst_ports):
instance_nets = [net['network'] for net in instance['networks']]
ports_by_net = {net_maps['by_id'][port.network_id]: port
for port in inst_ports
# Filter ports managed by metalsmith (vifs)
if 'tripleo_ironic_vif_port=true' not in port.tags}
to_delete = []
for net_name in ports_by_net:
if net_name not in instance_nets:
to_delete.append(ports_by_net[net_name])
if to_delete:
delete_ports(conn, to_delete)
inst_ports[:] = [port for port in inst_ports if port not in to_delete]
result['changed'] = True
def _provision_ports(result, conn, stack, instance, net_maps, ports_by_node,
ironic_uuid, role):
hostname = instance['hostname']
tags = ['tripleo_stack_name={}'.format(stack),
'tripleo_hostname={}'.format(hostname),
'tripleo_role={}'.format(role)]
# TODO(hjensas): This can be moved below the ironic_uuid condition in
# later release when all upgraded deployments has had the
# tripleo_ironic_uuid tag added
inst_ports = list(conn.network.ports(tags=tags))
if ironic_uuid:
tags.append('tripleo_ironic_uuid={}'.format(ironic_uuid))
tags = set(tags)
delete_removed_nets(result, conn, instance, net_maps, inst_ports)
pre_provisioned_ports(result, conn, net_maps, instance, inst_ports, tags)
create_port_defs, update_port_defs = generate_port_defs(net_maps, instance,
inst_ports)
if create_port_defs:
create_ports(result, conn, create_port_defs, inst_ports, tags)
if update_port_defs:
update_ports(result, conn, update_port_defs, inst_ports, tags)
ports_by_node[hostname] = inst_ports
def _unprovision_ports(result, conn, stack, instance, ironic_uuid):
hostname = instance['hostname']
tags = ['tripleo_stack_name={}'.format(stack),
'tripleo_hostname={}'.format(hostname)]
if ironic_uuid:
tags.append('tripleo_ironic_uuid={}'.format(ironic_uuid))
inst_ports = list(conn.network.ports(tags=tags))
# TODO(hjensas): This can be removed in later release when all upgraded
# deployments has had the tripleo_ironic_uuid tag added.
if not inst_ports:
tags = ['tripleo_stack_name={}'.format(stack),
'tripleo_hostname={}'.format(hostname)]
inst_ports = list(conn.network.ports(tags=tags))
if inst_ports:
delete_ports(conn, inst_ports)
result['changed'] = True
def generate_node_port_map(result, net_maps, ports_by_node):
node_port_map = result['node_port_map']
for hostname, ports in ports_by_node.items():
node = node_port_map[hostname] = dict()
for port in ports:
if not port.fixed_ips:
continue
net_name = net_maps['by_id'][port.network_id]
ip_address = port.fixed_ips[0]['ip_address']
subnet_id = port.fixed_ips[0]['subnet_id']
cidr_prefix = net_maps['cidr_prefix_map'][subnet_id]
node_net = node[net_name] = dict()
node_net['ip_address'] = ip_address
node_net['ip_subnet'] = '/'.join([ip_address, cidr_prefix])
node_net['ip_address_uri'] = wrap_ipv6(ip_address)
def validate_instance_nets_in_net_map(instances, net_maps):
for instance in instances:
for net in instance['networks']:
if not net['network'] in net_maps['by_name']:
raise Exception(
'Network {network_name} for instance {instance_name} not '
'found.'.format(network_name=net['network'],
instance_name=instance['hostname']))
def manage_instances_ports(result, conn, stack, instances, concurrency, state,
uuid_by_hostname, hostname_role_map):
if not instances:
return
# no limit on concurrency, create a worker for every instance
if concurrency < 1:
concurrency = len(instances)
net_maps = create_name_id_maps(conn)
validate_instance_nets_in_net_map(instances, net_maps)
ports_by_node = dict()
provision_jobs = []
exceptions = []
with futures.ThreadPoolExecutor(max_workers=concurrency) as p:
for instance in instances:
ironic_uuid = uuid_by_hostname.get(instance['hostname'])
role = hostname_role_map[instance['hostname']]
if state == 'present':
provision_jobs.append(
p.submit(_provision_ports,
result,
conn,
stack,
instance,
net_maps,
ports_by_node,
ironic_uuid,
role)
)
elif state == 'absent':
provision_jobs.append(
p.submit(_unprovision_ports,
result,
conn,
stack,
instance,
ironic_uuid)
)
for job in futures.as_completed(provision_jobs):
e = job.exception()
if e:
exceptions.append(e)
if exceptions:
raise exceptions[0]
generate_node_port_map(result, net_maps, ports_by_node)
def _tag_metalsmith_instance_ports(result, conn, provisioner, uuid, tags):
instance = provisioner.show_instance(uuid)
for nic in instance.nics():
nic_tags = set(nic.tags)
if not tags.issubset(nic_tags):
nic_tags.update(tags)
conn.network.set_tags(nic, list(nic_tags))
result['changed'] = True
def tag_metalsmith_managed_ports(result, conn, concurrency, stack,
uuid_by_hostname, hostname_role_map):
# no limit on concurrency, create a worker for every instance
if concurrency < 1:
concurrency = len(uuid_by_hostname)
provisioner = metalsmith.Provisioner(cloud_region=conn.config)
provision_jobs = []
exceptions = []
with futures.ThreadPoolExecutor(max_workers=concurrency) as p:
for hostname, uuid in uuid_by_hostname.items():
role = hostname_role_map[hostname]
tags = {'tripleo_hostname={}'.format(hostname),
'tripleo_stack_name={}'.format(stack),
'tripleo_ironic_uuid={}'.format(uuid),
'tripleo_role={}'.format(role),
'tripleo_ironic_vif_port=true'}
provision_jobs.append(
p.submit(_tag_metalsmith_instance_ports,
result, conn, provisioner, uuid, tags)
)
for job in futures.as_completed(provision_jobs):
e = job.exception()
if e:
exceptions.append(e)
if exceptions:
raise exceptions[0]
def run_module():
result = dict(
success=False,
changed=False,
error="",
node_port_map=dict(),
)
argument_spec = openstack_full_argument_spec(
**yaml.safe_load(DOCUMENTATION)['options']
)
module = AnsibleModule(
argument_spec,
supports_check_mode=False,
**openstack_module_kwargs()
)
stack = module.params['stack_name']
concurrency = module.params['concurrency']
instances = module.params['instances']
state = module.params['state']
provisioned_instances = module.params['provisioned_instances']
hostname_role_map = module.params['hostname_role_map']
uuid_by_hostname = {i['hostname']: i['id'] for i in provisioned_instances}
try:
_, conn = openstack_cloud_from_module(module)
if state == 'present' and uuid_by_hostname:
tag_metalsmith_managed_ports(result, conn, concurrency, stack,
uuid_by_hostname, hostname_role_map)
manage_instances_ports(result, conn, stack, instances, concurrency,
state, uuid_by_hostname, hostname_role_map)
result['success'] = True
module.exit_json(**result)
except Exception as err:
result['error'] = str(err)
result['msg'] = ("Error managing network ports {}".format(err))
module.fail_json(**result)
def main():
run_module()
if __name__ == '__main__':
main()

View File

@ -0,0 +1,558 @@
# 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 copy
import metalsmith
import mock
import openstack
from tripleo_ansible.ansible_plugins.modules import (
tripleo_overcloud_network_ports as plugin)
from tripleo_ansible.tests import base as tests_base
from tripleo_ansible.tests import stubs
FAKE_INSTANCE = {
'hostname': 'instance0',
'networks': [
{'network': 'ctlplane', 'vif': True},
{'network': 'foo', 'subnet': 'foo_subnet'},
{'network': 'bar', 'subnet': 'bar_subnet'},
],
}
FAKE_NET_NAME_MAP = {
'foo': {
'id': 'foo_id',
'subnets': {
'foo_subnet': 'foo_subnet_id',
}
},
'bar': {
'id': 'bar_id',
'subnets': {
'bar_subnet': 'bar_subnet_id',
}
},
}
FAKE_NET_ID_MAP = {
'foo_id': 'foo',
'bar_id': 'bar',
}
FAKE_CIDR_PREFIX_MAP = {
'foo_id': '24',
'bar_id': '64',
}
FAKE_MAPS = {
'by_name': FAKE_NET_NAME_MAP,
'by_id': FAKE_NET_ID_MAP,
'cidr_prefix_map': FAKE_CIDR_PREFIX_MAP,
}
STACK = 'overcloud'
class TestTripleoOvercloudNetworkPorts(tests_base.TestCase):
def setUp(self):
super(TestTripleoOvercloudNetworkPorts, self).setUp()
# Helper function to convert array to generator
self.a2g = lambda x: (n for n in x)
@mock.patch.object(openstack.connection, 'Connection', autospec=True)
def test_create_name_id_maps(self, conn_mock):
subnet1 = stubs.FakeNeutronSubnet(id='subnet1_id',
name='subnet1',
cidr='192.168.24.0/24')
subnet2 = stubs.FakeNeutronSubnet(id='subnet2_id',
name='subnet2',
cidr='192.168.25.0/25')
subnet3 = stubs.FakeNeutronSubnet(id='subnet3_id',
name='subnet3',
cidr='192.168.26.0/26')
subnet4 = stubs.FakeNeutronSubnet(id='subnet4_id',
name='subnet4',
cidr='192.168.27.0/27')
network1 = stubs.FakeNeutronNetwork(
id='network1_id',
name='network1',
subnet_ids=['subnet1_id', 'subnet2_id']
)
network2 = stubs.FakeNeutronNetwork(
id='network2_id',
name='network2',
subnet_ids=['subnet3_id', 'subnet4_id']
)
conn_mock.network.networks.return_value = self.a2g([network1,
network2])
conn_mock.network.subnets.side_effect = [self.a2g([subnet1, subnet2]),
self.a2g([subnet3, subnet4])]
net_maps = plugin.create_name_id_maps(conn_mock)
expected_by_name_map = {
'network1': {
'id': 'network1_id',
'subnets': {
'subnet1': 'subnet1_id',
'subnet2': 'subnet2_id'
}
},
'network2': {
'id': 'network2_id',
'subnets': {
'subnet3': 'subnet3_id',
'subnet4': 'subnet4_id'
}
}
}
expected_by_id_map = {
'network1_id': 'network1',
'network2_id': 'network2',
}
expected_cidr_prefix_map = {
'subnet1_id': '24',
'subnet2_id': '25',
'subnet3_id': '26',
'subnet4_id': '27',
}
self.assertEqual(expected_by_name_map, net_maps['by_name'])
self.assertEqual(expected_by_id_map, net_maps['by_id'])
self.assertEqual(expected_cidr_prefix_map, net_maps['cidr_prefix_map'])
@mock.patch.object(openstack.connection, 'Connection', autospec=True)
def test_delete_ports(self, mock_conn):
port1 = stubs.FakeNeutronPort(id='port1_id')
port2 = stubs.FakeNeutronPort(id='port2_id')
plugin.delete_ports(mock_conn, [port1, port2])
mock_conn.network.delete_port.assert_has_calls([mock.call('port1_id'),
mock.call('port2_id')])
@mock.patch.object(openstack.connection, 'Connection', autospec=True)
def test_pre_provisioned_ports(self, mock_conn):
result = {'changed': False}
inst_ports = []
tags = set(['tripleo_hostname=instance0',
'tripleo_stack_name=overcloud',
'tripleo_ironic_uuid=ironic_uuid'])
fake_instance = copy.deepcopy(FAKE_INSTANCE)
fake_instance['networks'] = [{'network': 'foo', 'port': 'some_port'}]
some_port = stubs.FakeNeutronPort(name='some_port',
id='some_port_id',
tags=[])
mock_conn.network.find_port.return_value = some_port
plugin.pre_provisioned_ports(result, mock_conn, FAKE_MAPS,
fake_instance, inst_ports, tags)
mock_conn.network.find_port.assert_called_with(
'some_port', network_id=FAKE_NET_NAME_MAP['foo']['id'])
mock_conn.network.set_tags.assrt_called_with(some_port, mock.ANY)
set_tags_args = mock_conn.network.set_tags.call_args.args
self.assertTrue(tags == set(set_tags_args[1]))
self.assertEqual([some_port], inst_ports)
self.assertTrue(result['changed'])
def test_generate_port_defs_create(self):
inst_ports = []
create_port_defs, update_port_defs = plugin.generate_port_defs(
FAKE_MAPS, FAKE_INSTANCE, inst_ports)
self.assertEqual([
{'name': 'instance0_foo',
'network_id': 'foo_id',
'fixed_ips': [{'subnet_id': 'foo_subnet_id'}]},
{'name': 'instance0_bar',
'network_id': 'bar_id',
'fixed_ips': [{'subnet_id': 'bar_subnet_id'}]},
], create_port_defs)
self.assertEqual([], update_port_defs)
def test_generate_port_defs_update(self):
port_foo = stubs.FakeNeutronPort(
name='instance0_foo', network_id='foo_id',
fixed_ips=[{'subnet_id': 'foo_subnet_id'}])
port_bar = stubs.FakeNeutronPort(
name='instance0_bar', network_id='bar_id',
fixed_ips=[{'subnet_id': 'bar_subnet_id'}])
inst_ports = [port_foo, port_bar]
create_port_defs, update_port_defs = plugin.generate_port_defs(
FAKE_MAPS, FAKE_INSTANCE, inst_ports)
self.assertEqual([], create_port_defs)
self.assertEqual([
{'name': 'instance0_foo',
'network_id': 'foo_id',
'fixed_ips': [{'subnet_id': 'foo_subnet_id'}]},
{'name': 'instance0_bar',
'network_id': 'bar_id',
'fixed_ips': [{'subnet_id': 'bar_subnet_id'}]}
], update_port_defs)
def test_generate_port_defs_create_and_update(self):
port_foo = stubs.FakeNeutronPort(
name='instance0_foo', network_id='foo_id',
fixed_ips=[{'subnet_id': 'foo_subnet_id'}])
inst_ports = [port_foo]
create_port_defs, update_port_defs = plugin.generate_port_defs(
FAKE_MAPS, FAKE_INSTANCE, inst_ports)
self.assertEqual([
{'name': 'instance0_bar',
'network_id': 'bar_id',
'fixed_ips': [{'subnet_id': 'bar_subnet_id'}]},
], create_port_defs)
self.assertEqual([
{'name': 'instance0_foo',
'network_id': 'foo_id',
'fixed_ips': [{'subnet_id': 'foo_subnet_id'}]},
], update_port_defs)
def test_generate_port_defs_subnet_not_set(self):
inst_ports = []
instance = copy.deepcopy(FAKE_INSTANCE)
del instance['networks'][1]['subnet']
del instance['networks'][2]['subnet']
create_port_defs, update_port_defs = plugin.generate_port_defs(
FAKE_MAPS, instance, inst_ports)
self.assertEqual([
{'name': 'instance0_foo',
'network_id': 'foo_id',
'fixed_ips': [{'subnet_id': 'foo_subnet_id'}]},
{'name': 'instance0_bar',
'network_id': 'bar_id',
'fixed_ips': [{'subnet_id': 'bar_subnet_id'}]},
], create_port_defs)
self.assertEqual([], update_port_defs)
def test_generate_port_defs_multi_subnet_raise_if_subnet_not_set(self):
inst_ports = []
instance = copy.deepcopy(FAKE_INSTANCE)
del instance['networks'][1]['subnet']
del instance['networks'][2]['subnet']
maps = copy.deepcopy(FAKE_MAPS)
maps['by_name']['foo']['subnets'].update(
{'bas_subnet': 'baz_subnet_id'})
msg = ('The "subnet" or "fixed_ip" must be set for the instance0 port '
'on the foo network since there are multiple subnets')
self.assertRaisesRegex(Exception, msg,
plugin.generate_port_defs,
maps, instance, inst_ports)
def test_generate_port_defs_multi_subnet_fixed_ip(self):
inst_ports = []
instance = copy.deepcopy(FAKE_INSTANCE)
del instance['networks'][1]['subnet']
del instance['networks'][2]['subnet']
instance['networks'][1]['fixed_ip'] = 'baz_fixed_ip'
instance['networks'][2]['fixed_ip'] = 'bar_fixed_ip'
maps = copy.deepcopy(FAKE_MAPS)
maps['by_name']['foo']['subnets'].update(
{'bas_subnet': 'baz_subnet_id'})
create_port_defs, update_port_defs = plugin.generate_port_defs(
maps, instance, inst_ports)
self.assertEqual([
{'name': 'instance0_foo',
'network_id': 'foo_id',
'fixed_ips': [{'ip_address': 'baz_fixed_ip'}]},
{'name': 'instance0_bar',
'network_id': 'bar_id',
'fixed_ips': [{'ip_address': 'bar_fixed_ip'}]},
], create_port_defs)
self.assertEqual([], update_port_defs)
def test_fixed_ips_need_update(self):
fake_port = stubs.FakeNeutronPort(
fixed_ips=[{'ip_address': '192.168.24.24', 'subnet_id': 'foo_id'}])
port_def = {'fixed_ips': [{'ip_address': '192.168.24.24'}]}
self.assertFalse(plugin.fixed_ips_need_update(port_def, fake_port))
port_def = {'fixed_ips': [{'subnet_id': 'foo_id'}]}
self.assertFalse(plugin.fixed_ips_need_update(port_def, fake_port))
port_def = {'fixed_ips': [{'subnet_id': 'bar_id'}]}
self.assertTrue(plugin.fixed_ips_need_update(port_def, fake_port))
port_def = {'fixed_ips': [{'subnet_id': 'foo_id'},
{'ip_address': '192.168.25.24'}]}
self.assertTrue(plugin.fixed_ips_need_update(port_def, fake_port))
@mock.patch.object(plugin, 'fixed_ips_need_update', autospec=True)
def test_port_need_update(self, mock_fixed_ips_need_update):
port_def = {'name': 'foo', 'network_id': 'foo_id', 'fixed_ips': []}
mock_fixed_ips_need_update.return_value = True
self.assertEqual({'fixed_ips': []},
plugin.port_need_update(port_def, mock.ANY))
mock_fixed_ips_need_update.return_value = False
self.assertEqual({}, plugin.port_need_update(port_def, mock.ANY))
@mock.patch.object(openstack.connection, 'Connection', autospec=True)
def test_create_ports(self, mock_conn):
result = {'changed': False}
inst_ports = []
tags = set(['tripleo_hostname=instance0',
'tripleo_stack_name=overcloud',
'tripleo_ironic_uuid=ironic_uuid'])
port_foo = stubs.FakeNeutronPort(
name='instance0_foo', network_id='foo_id',
fixed_ips=[{'subnet_id': 'foo_subnet_id'}])
port_bar = stubs.FakeNeutronPort(
name='instance0_bar', network_id='bar_id',
fixed_ips=[{'subnet_id': 'bar_subnet_id'}])
create_port_defs = [
dict(name='instance0_foo', network_id='foo_id',
fixed_ips=[{'subnet_id': 'foo_subnet_id'}]),
dict(name='instance0_bar', network_id='bar_id',
fixed_ips=[{'subnet_id': 'bar_subnet_id'}]),
]
mock_conn.network.create_ports.return_value = self.a2g(
[port_foo, port_bar])
plugin.create_ports(result, mock_conn, create_port_defs, inst_ports,
tags)
mock_conn.network.create_ports.assert_has_calls([
mock.call([
{'name': 'instance0_foo',
'network_id': 'foo_id',
'fixed_ips': [{'subnet_id': 'foo_subnet_id'}]},
{'name': 'instance0_bar',
'network_id': 'bar_id',
'fixed_ips': [{'subnet_id': 'bar_subnet_id'}]}
])
])
mock_conn.network.set_tags.assert_has_calls([
mock.call(port_foo, mock.ANY),
mock.call(port_bar, mock.ANY)
])
set_tag_args = mock_conn.network.set_tags.call_args_list
self.assertTrue(set(set_tag_args[1][0][1]) == tags)
self.assertTrue(set(set_tag_args[1][0][1]) == tags)
self.assertEqual([port_foo, port_bar], inst_ports)
self.assertTrue(result['changed'])
@mock.patch.object(plugin, 'update_ports', autospec=True)
@mock.patch.object(plugin, 'create_ports', autospec=True)
@mock.patch.object(plugin, 'pre_provisioned_ports', autospec=True)
@mock.patch.object(openstack.connection, 'Connection', autospec=True)
def test__provision_ports_create(self, mock_conn, mock_pre_provisioned,
mock_create_ports, mock_update_ports):
create_port_defs = [
dict(name='instance0_foo', network_id='foo_id',
fixed_ips=[{'subnet_id': 'foo_subnet_id'}]),
dict(name='instance0_bar', network_id='bar_id',
fixed_ips=[{'subnet_id': 'bar_subnet_id'}]),
]
mock_conn.network.ports.return_value = self.a2g([])
expected_tags = {'tripleo_hostname=instance0',
'tripleo_ironic_uuid=ironic_uuid',
'tripleo_role=role',
'tripleo_stack_name=overcloud'}
plugin._provision_ports({}, mock_conn, STACK, FAKE_INSTANCE,
FAKE_MAPS, {}, 'ironic_uuid', 'role')
mock_pre_provisioned.assert_called_with(mock.ANY, mock_conn, FAKE_MAPS,
FAKE_INSTANCE, mock.ANY,
expected_tags)
mock_create_ports.assert_called_with(mock.ANY, mock_conn,
create_port_defs,
mock.ANY, expected_tags)
mock_update_ports.assert_not_called()
@mock.patch.object(plugin, 'update_ports', autospec=True)
@mock.patch.object(plugin, 'create_ports', autospec=True)
@mock.patch.object(plugin, 'pre_provisioned_ports', autospec=True)
@mock.patch.object(openstack.connection, 'Connection', autospec=True)
def test__provision_ports_update(self, mock_conn, mock_pre_provisioned,
mock_create_ports, mock_update_ports):
port_foo = stubs.FakeNeutronPort(
name='instance0_foo', network_id='foo_id',
fixed_ips=[{'subnet_id': 'foo_subnet_id'}],
tags=[])
port_bar = stubs.FakeNeutronPort(
name='instance0_bar', network_id='bar_id',
fixed_ips=[{'subnet_id': 'bar_subnet_id'}],
tags=[])
update_port_defs = [
dict(name='instance0_foo', network_id='foo_id',
fixed_ips=[{'subnet_id': 'foo_subnet_id'}]),
dict(name='instance0_bar', network_id='bar_id',
fixed_ips=[{'subnet_id': 'bar_subnet_id'}]),
]
expected_tags = {'tripleo_hostname=instance0',
'tripleo_ironic_uuid=ironic_uuid',
'tripleo_role=role',
'tripleo_stack_name=overcloud'}
mock_conn.network.ports.return_value = self.a2g([port_foo, port_bar])
plugin._provision_ports({}, mock_conn, STACK, FAKE_INSTANCE,
FAKE_MAPS, {}, 'ironic_uuid', 'role')
mock_pre_provisioned.assert_called_with(mock.ANY, mock_conn,
FAKE_MAPS, FAKE_INSTANCE,
mock.ANY, expected_tags)
mock_create_ports.assert_not_called()
mock_update_ports.assert_called_with(mock.ANY, mock_conn,
update_port_defs,
[port_foo, port_bar],
expected_tags)
@mock.patch.object(plugin, 'update_ports', autospec=True)
@mock.patch.object(plugin, 'create_ports', autospec=True)
@mock.patch.object(plugin, 'pre_provisioned_ports', autospec=True)
@mock.patch.object(openstack.connection, 'Connection', autospec=True)
def test__provision_ports_create_and_update(self, mock_conn,
mock_pre_provisioned,
mock_create_ports,
mock_update_ports):
port_foo = stubs.FakeNeutronPort(
name='instance0_foo', network_id='foo_id',
fixed_ips=[{'subnet_id': 'foo_subnet_id'}],
tags=[])
create_port_defs = [
dict(name='instance0_bar', network_id='bar_id',
fixed_ips=[{'subnet_id': 'bar_subnet_id'}]),
]
update_port_defs = [
dict(name='instance0_foo', network_id='foo_id',
fixed_ips=[{'subnet_id': 'foo_subnet_id'}]),
]
mock_conn.network.ports.return_value = self.a2g([port_foo])
expected_tags = {'tripleo_hostname=instance0',
'tripleo_ironic_uuid=ironic_uuid',
'tripleo_role=role',
'tripleo_stack_name=overcloud'}
plugin._provision_ports({}, mock_conn, STACK, FAKE_INSTANCE,
FAKE_MAPS, {}, 'ironic_uuid', 'role')
mock_pre_provisioned.assert_called_with(mock.ANY, mock_conn,
FAKE_MAPS, FAKE_INSTANCE,
mock.ANY, expected_tags)
mock_create_ports.assert_called_with(mock.ANY, mock_conn,
create_port_defs, mock.ANY,
expected_tags)
mock_update_ports.assert_called_with(mock.ANY, mock_conn,
update_port_defs, [port_foo],
expected_tags)
@mock.patch.object(plugin, 'delete_ports', autospec=True)
@mock.patch.object(openstack.connection, 'Connection', autospec=True)
def test__unprovision_ports(self, mock_conn, mock_delete_ports):
result = {'changed': False, 'instance_port_map': {}}
port_foo = stubs.FakeNeutronPort(
name='instance_foo', network_id='foo_id',
fixed_ips=[{'subnet_id': 'foo_subnet_id'}])
port_bar = stubs.FakeNeutronPort(
name='instance_bar', network_id='bar_id',
fixed_ips=[{'subnet_id': 'bar_subnet_id'}])
mock_conn.network.ports.return_value = self.a2g([port_foo, port_bar])
plugin._unprovision_ports(result, mock_conn, STACK, FAKE_INSTANCE,
None)
mock_delete_ports.assert_called_with(mock_conn, [port_foo, port_bar])
self.assertTrue(result['changed'])
def test_generate_node_port_map(self):
result = dict(node_port_map=dict())
ports_by_node = dict(
node01=[
stubs.FakeNeutronPort(
network_id='foo_id',
fixed_ips=[{'ip_address': '192.168.24.1',
'subnet_id': 'foo_id'}]),
stubs.FakeNeutronPort(
network_id='bar_id',
fixed_ips=[{'ip_address': '2001:DB8:1::1',
'subnet_id': 'bar_id'}])],
node02=[
stubs.FakeNeutronPort(
network_id='foo_id',
fixed_ips=[{'ip_address': '192.168.24.1',
'subnet_id': 'foo_id'}]),
stubs.FakeNeutronPort(
network_id='bar_id',
fixed_ips=[{'ip_address': '2001:DB8:1::2',
'subnet_id': 'bar_id'}])]
)
plugin.generate_node_port_map(result, FAKE_MAPS, ports_by_node)
self.assertEqual(
{'node01': {'bar': {'ip_address': '2001:DB8:1::1',
'ip_address_uri': '[2001:DB8:1::1]',
'ip_subnet': '2001:DB8:1::1/64'},
'foo': {'ip_address': '192.168.24.1',
'ip_address_uri': '192.168.24.1',
'ip_subnet': '192.168.24.1/24'}},
'node02': {'bar': {'ip_address': '2001:DB8:1::2',
'ip_address_uri': '[2001:DB8:1::2]',
'ip_subnet': '2001:DB8:1::2/64'},
'foo': {'ip_address': '192.168.24.1',
'ip_address_uri': '192.168.24.1',
'ip_subnet': '192.168.24.1/24'}}},
result['node_port_map'])
def test_validate_instance_nets_in_net_map(self):
instances = [FAKE_INSTANCE]
msg = 'Network ctlplane for instance {} not found.'.format(
FAKE_INSTANCE['hostname'])
self.assertRaisesRegex(Exception, msg,
plugin.validate_instance_nets_in_net_map,
instances, FAKE_MAPS)
@mock.patch.object(openstack.connection, 'Connection', autospec=True)
@mock.patch.object(metalsmith, 'Provisioner', autospec=True)
def test__tag_metalsmith_instance_ports(self, mock_provisioner, mock_conn):
result = {'changed': False}
tags = {'tripleo_hostname=hostname',
'tripleo_stack_name={}'.format(STACK),
'tripleo_ironic_uuid=ironic_uuid',
'tripleo_role=role',
'tripleo_ironic_vif_port=true'}
fake_nic = stubs.FakeNeutronPort(name='hostname-ctlplane',
id='port_uuid',
tags=[])
fake_instance = mock.Mock()
fake_instance.nics.return_value = [fake_nic]
mock_provisioner.show_instance.return_value = fake_instance
plugin._tag_metalsmith_instance_ports(result, mock_conn,
mock_provisioner, 'ironic_uuid',
tags)
mock_conn.network.set_tags.assert_called_with(fake_nic, mock.ANY)
set_tags_args = mock_conn.network.set_tags.call_args.args
self.assertTrue(set(tags) == set(set_tags_args[1]))
self.assertTrue(result['changed'])
@mock.patch.object(openstack.connection, 'Connection', autospec=True)
@mock.patch.object(metalsmith, 'Provisioner', autospec=True)
def test__tag_metalsmith_instance_ports_tags_already_set(
self, mock_provisioner, mock_conn):
result = {'changed': False}
tags = {'tripleo_hostname=hostname',
'tripleo_stack_name={}'.format(STACK),
'tripleo_ironic_uuid=ironic_uuid',
'tripleo_role=role',
'tripleo_ironic_vif_port=true'}
fake_nic = stubs.FakeNeutronPort(
name='hostname-ctlplane', id='port_uuid',
tags=['tripleo_hostname=hostname',
'tripleo_stack_name={}'.format(STACK),
'tripleo_ironic_uuid=ironic_uuid',
'tripleo_role=role',
'tripleo_ironic_vif_port=true'])
fake_instance = mock.Mock()
fake_instance.nics.return_value = [fake_nic]
mock_provisioner.show_instance.return_value = fake_instance
plugin._tag_metalsmith_instance_ports(result, mock_conn,
mock_provisioner, 'ironic_uuid',
tags)
mock_conn.network.set_tags.assert_not_called()
self.assertFalse(result['changed'])