ff00047b4a
This patch removes the dependency from ironicclient to communicate with neutron in favor of openstacksdk. Also: * Use import keystoneauth1.loading as ks_loading accross the project. * Refactor to have one 'get_client' function, removing the '_get_config_client' method. Setting config_client=True when calling 'get_client' returns a client using auth options values from conf parameters. Depends-On: https://review.opendev.org/735601 Change-Id: Ib6c0fa2acfc33deb9c5b36ae724d5d8304d1dd29
980 lines
36 KiB
Python
980 lines
36 KiB
Python
# 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 ipaddress
|
|
|
|
import openstack
|
|
from openstack.connection import exceptions as openstack_exc
|
|
from oslo_log import log
|
|
import retrying
|
|
|
|
from ironic.api.controllers.v1 import types
|
|
from ironic.common import context as ironic_context
|
|
from ironic.common import exception
|
|
from ironic.common.i18n import _
|
|
from ironic.common import keystone
|
|
from ironic.common.pxe_utils import DHCP_CLIENT_ID
|
|
from ironic.conf import CONF
|
|
from ironic import objects
|
|
|
|
LOG = log.getLogger(__name__)
|
|
|
|
# TODO(pas-ha) remove in Rocky, until then it is a default
|
|
# for CONF.neutron.url in noauth case when endpoint_override is not set
|
|
DEFAULT_NEUTRON_URL = 'http://%s:9696' % CONF.my_ip
|
|
|
|
_NEUTRON_SESSION = None
|
|
|
|
VNIC_BAREMETAL = 'baremetal'
|
|
VNIC_SMARTNIC = 'smart-nic'
|
|
|
|
PHYSNET_PARAM_NAME = 'provider:physical_network'
|
|
"""Name of the neutron network API physical network parameter."""
|
|
|
|
|
|
def _get_neutron_session():
|
|
global _NEUTRON_SESSION
|
|
if not _NEUTRON_SESSION:
|
|
_NEUTRON_SESSION = keystone.get_session(
|
|
'neutron',
|
|
timeout=CONF.neutron.timeout)
|
|
return _NEUTRON_SESSION
|
|
|
|
|
|
def get_client(token=None, context=None, auth_from_config=False):
|
|
"""Retrieve a neutron client connection.
|
|
|
|
:param context: request context,
|
|
instance of ironic.common.context.RequestContext
|
|
:param auth_from_config: (boolean) When True, use auth values from
|
|
conf parameters
|
|
:returns: A neutron client.
|
|
"""
|
|
if not context:
|
|
context = ironic_context.RequestContext(auth_token=token)
|
|
session = _get_neutron_session()
|
|
service_auth = keystone.get_auth('neutron')
|
|
endpoint = keystone.get_endpoint('neutron', session=session,
|
|
auth=service_auth)
|
|
|
|
user_auth = None
|
|
if (not auth_from_config and CONF.neutron.auth_type != 'none'
|
|
and context.auth_token):
|
|
user_auth = keystone.get_service_auth(context, endpoint, service_auth)
|
|
|
|
sess = keystone.get_session('neutron', timeout=CONF.neutron.timeout,
|
|
auth=user_auth or service_auth)
|
|
conn = openstack.connection.Connection(session=sess, oslo_conf=CONF)
|
|
|
|
return conn.global_request(context.global_id).network
|
|
|
|
|
|
def update_neutron_port(context, port_id, attrs, client=None):
|
|
"""Undate a neutron port
|
|
|
|
Uses neutron client from conf client to update a neutron client
|
|
an unbound state.
|
|
|
|
:param context: request context,
|
|
instance of ironic.common.context.RequestContext
|
|
:param port_id: Neutron port ID.
|
|
:param attrs: The attributes to update on the port
|
|
:param client: Optional Neutron client
|
|
"""
|
|
if not client:
|
|
# verify that user can see the port before updating it
|
|
get_client(context=context).get_port(port_id)
|
|
# Set user_auth=False to ensure auth values from ironic.conf is used
|
|
# prevents issues where a non-admin user is not allowed to manage
|
|
# Neutron ports.
|
|
client = get_client(context=context, auth_from_config=True)
|
|
|
|
attrs = attrs.get('port', attrs)
|
|
|
|
return client.update_port(port_id, **attrs)
|
|
|
|
|
|
def unbind_neutron_port(port_id, client=None, context=None):
|
|
"""Unbind a neutron port
|
|
|
|
Remove a neutron port's binding profile and host ID so that it returns to
|
|
an unbound state.
|
|
|
|
:param port_id: Neutron port ID.
|
|
:param client: Optional a Neutron client object.
|
|
:param context: request context
|
|
:type context: ironic.common.context.RequestContext
|
|
:raises: NetworkError
|
|
"""
|
|
|
|
attrs_unbind = {'binding:host_id': '', 'binding:profile': {}}
|
|
attrs_reset_mac = {'mac_address': None}
|
|
|
|
try:
|
|
update_neutron_port(context, port_id, attrs_unbind, client)
|
|
# NOTE(hjensas): We need to reset the mac address in a separate step.
|
|
# Exception PortBound will be raised by neutron as it refuses to
|
|
# update the mac address of a bound port if we attempt to unbind and
|
|
# reset the mac in the same call.
|
|
update_neutron_port(context, port_id, attrs_reset_mac, client)
|
|
# NOTE(vsaienko): Ignore if port was deleted before calling vif detach.
|
|
except openstack_exc.ResourceNotFound:
|
|
LOG.info('Port %s was not found while unbinding.', port_id)
|
|
except openstack_exc.OpenStackCloudException as e:
|
|
msg = (_('Unable to clear binding profile for '
|
|
'neutron port %(port_id)s. Error: '
|
|
'%(err)s') % {'port_id': port_id, 'err': e})
|
|
LOG.exception(msg)
|
|
raise exception.NetworkError(msg)
|
|
|
|
|
|
def update_port_address(port_id, address, context=None):
|
|
"""Update a port's mac address.
|
|
|
|
:param port_id: Neutron port id.
|
|
:param address: new MAC address.
|
|
:param context: request context
|
|
:type context: ironic.common.context.RequestContext
|
|
:raises: FailedToUpdateMacOnPort
|
|
"""
|
|
client = get_client(context=context)
|
|
port_attrs = {'mac_address': address}
|
|
|
|
try:
|
|
msg = (_("Failed to get the current binding on Neutron "
|
|
"port %s.") % port_id)
|
|
port = client.get_port(port_id)
|
|
binding_host_id = port.get('binding:host_id')
|
|
binding_profile = port.get('binding:profile')
|
|
|
|
if binding_host_id:
|
|
# Unbind port before we update it's mac address, because you can't
|
|
# change a bound port's mac address.
|
|
msg = (_("Failed to remove the current binding from "
|
|
"Neutron port %s, while updating its MAC "
|
|
"address.") % port_id)
|
|
unbind_neutron_port(port_id, context=context)
|
|
|
|
msg = (_("Failed to update MAC address on Neutron port %s.") % port_id)
|
|
update_neutron_port(context, port_id, port_attrs)
|
|
|
|
# Restore original binding:profile and host_id
|
|
if binding_host_id:
|
|
msg = (_("Failed to update binding:host_id and profile on Neutron "
|
|
"port %s.") % port_id)
|
|
port_attrs = {'binding:host_id': binding_host_id,
|
|
'binding:profile': binding_profile}
|
|
|
|
update_neutron_port(context, port_id, port_attrs)
|
|
except (openstack_exc.OpenStackCloudException, exception.NetworkError):
|
|
LOG.exception(msg)
|
|
raise exception.FailedToUpdateMacOnPort(port_id=port_id)
|
|
|
|
|
|
def _verify_security_groups(security_groups, client):
|
|
"""Verify that the security groups exist.
|
|
|
|
:param security_groups: a list of security group UUIDs; may be None or
|
|
empty
|
|
:param client: Neutron client
|
|
:raises: NetworkError
|
|
"""
|
|
|
|
if not security_groups:
|
|
return
|
|
try:
|
|
neutron_sec_groups = set(
|
|
x.id for x in client.security_groups(id=security_groups))
|
|
except openstack_exc.OpenStackCloudException as e:
|
|
msg = (_("Could not retrieve security groups from neutron: %(exc)s") %
|
|
{'exc': e})
|
|
LOG.exception(msg)
|
|
raise exception.NetworkError(msg)
|
|
|
|
if set(security_groups).issubset(neutron_sec_groups):
|
|
return
|
|
|
|
missing_sec_groups = set(security_groups).difference(neutron_sec_groups)
|
|
msg = (_('Could not find these security groups (specified via ironic '
|
|
'config) in neutron: %(ir-sg)s')
|
|
% {'ir-sg': list(missing_sec_groups)})
|
|
LOG.error(msg)
|
|
raise exception.NetworkError(msg)
|
|
|
|
|
|
def _add_ip_addresses_for_ipv6_stateful(context, port, client):
|
|
"""Add additional IP addresses to the ipv6 stateful neutron port
|
|
|
|
When network booting with DHCPv6-stateful we cannot control the CLID/IAID
|
|
used by the different clients, UEFI, iPXE, Ironic IPA etc. Multiple
|
|
IP address reservation is required in the DHCPv6 server to avoid
|
|
NoAddrsAvail issues.
|
|
|
|
:param port: A neutron port
|
|
:param client: Neutron client
|
|
"""
|
|
fixed_ips = port.fixed_ips
|
|
if (not fixed_ips
|
|
or ipaddress.ip_address(fixed_ips[0]['ip_address']).version != 6):
|
|
return
|
|
subnet = client.get_subnet(fixed_ips[0]['subnet_id'])
|
|
if subnet and subnet.ipv6_address_mode == 'dhcpv6-stateful':
|
|
for i in range(1, CONF.neutron.dhcpv6_stateful_address_count):
|
|
fixed_ips.append({'subnet_id': subnet['id']})
|
|
|
|
attrs = {'fixed_ips': fixed_ips}
|
|
update_neutron_port(context, port.id, attrs, client=client)
|
|
|
|
|
|
def add_ports_to_network(task, network_uuid, security_groups=None):
|
|
"""Create neutron ports to boot the ramdisk.
|
|
|
|
Create neutron ports for each pxe_enabled port on task.node to boot
|
|
the ramdisk.
|
|
|
|
If the config option 'neutron.add_all_ports' is set, neutron ports
|
|
for non-pxe-enabled ports are also created -- these neutron ports
|
|
will not have any assigned IP addresses.
|
|
|
|
:param task: a TaskManager instance.
|
|
:param network_uuid: UUID of a neutron network where ports will be
|
|
created.
|
|
:param security_groups: List of Security Groups UUIDs to be used for
|
|
network.
|
|
:raises: NetworkError
|
|
:returns: a dictionary in the form {port.uuid: neutron_port['id']}
|
|
"""
|
|
client = get_client(context=task.context)
|
|
node = task.node
|
|
add_all_ports = CONF.neutron.add_all_ports
|
|
|
|
# If Security Groups are specified, verify that they exist
|
|
_verify_security_groups(security_groups, client)
|
|
|
|
LOG.debug('For node %(node)s, creating neutron ports on network '
|
|
'%(network_uuid)s using %(net_iface)s network interface.',
|
|
{'net_iface': task.driver.network.__class__.__name__,
|
|
'node': node.uuid, 'network_uuid': network_uuid})
|
|
|
|
attrs = {'network_id': network_uuid,
|
|
'admin_state_up': True,
|
|
'binding:vnic_type': VNIC_BAREMETAL,
|
|
}
|
|
# separate out fields that can only be updated by admins
|
|
update_attrs = {'binding:host_id': node.uuid,
|
|
'device_owner': 'baremetal:none',
|
|
}
|
|
|
|
if security_groups:
|
|
attrs['security_groups'] = security_groups
|
|
|
|
# Since instance_uuid will not be available during cleaning
|
|
# operations, we need to check that and populate them only when
|
|
# available
|
|
attrs['device_id'] = node.instance_uuid or node.uuid
|
|
|
|
ports = {}
|
|
failures = []
|
|
portmap = get_node_portmap(task)
|
|
|
|
if not add_all_ports:
|
|
ports_to_create = [p for p in task.ports if p.pxe_enabled]
|
|
else:
|
|
ports_to_create = task.ports
|
|
if not ports_to_create:
|
|
pxe_enabled = 'PXE-enabled ' if not add_all_ports else ''
|
|
raise exception.NetworkError(_(
|
|
"No available %(enabled)sports on node %(node)s.") %
|
|
{'enabled': pxe_enabled, 'node': node.uuid})
|
|
|
|
for ironic_port in ports_to_create:
|
|
# Start with a clean state for each port
|
|
port_attrs = copy.deepcopy(attrs)
|
|
update_port_attrs = copy.deepcopy(update_attrs)
|
|
|
|
# Skip ports that are missing required information for deploy.
|
|
if not validate_port_info(node, ironic_port):
|
|
failures.append(ironic_port.uuid)
|
|
continue
|
|
|
|
update_port_attrs['mac_address'] = ironic_port.address
|
|
binding_profile = {'local_link_information':
|
|
[portmap[ironic_port.uuid]]}
|
|
update_port_attrs['binding:profile'] = binding_profile
|
|
|
|
if not ironic_port.pxe_enabled:
|
|
LOG.debug("Adding port %(port)s to network %(net)s for "
|
|
"provisioning without an IP allocation.",
|
|
{'port': ironic_port.uuid, 'net': network_uuid})
|
|
port_attrs['fixed_ips'] = []
|
|
|
|
is_smart_nic = is_smartnic_port(ironic_port)
|
|
if is_smart_nic:
|
|
link_info = binding_profile['local_link_information'][0]
|
|
LOG.debug('Setting hostname as host_id in case of Smart NIC, '
|
|
'port %(port_id)s, hostname %(hostname)s',
|
|
{'port_id': ironic_port.uuid,
|
|
'hostname': link_info['hostname']})
|
|
update_port_attrs['binding:host_id'] = link_info['hostname']
|
|
|
|
# TODO(hamdyk): use portbindings.VNIC_SMARTNIC from neutron-lib
|
|
port_attrs['binding:vnic_type'] = VNIC_SMARTNIC
|
|
|
|
client_id = ironic_port.extra.get('client-id')
|
|
if client_id:
|
|
extra_dhcp_opts = port_attrs.get('extra_dhcp_opts', [])
|
|
extra_dhcp_opts.append(
|
|
{'opt_name': DHCP_CLIENT_ID, 'opt_value': client_id})
|
|
port_attrs['extra_dhcp_opts'] = extra_dhcp_opts
|
|
|
|
try:
|
|
if is_smart_nic:
|
|
wait_for_host_agent(
|
|
client, update_port_attrs['binding:host_id'])
|
|
port = client.create_port(**port_attrs)
|
|
update_neutron_port(task.context, port.id, update_port_attrs)
|
|
if CONF.neutron.dhcpv6_stateful_address_count > 1:
|
|
_add_ip_addresses_for_ipv6_stateful(task.context, port, client)
|
|
if is_smart_nic:
|
|
wait_for_port_status(client, port.id, 'ACTIVE')
|
|
except openstack_exc.OpenStackCloudException as e:
|
|
failures.append(ironic_port.uuid)
|
|
LOG.warning("Could not create neutron port for node's "
|
|
"%(node)s port %(ir-port)s on the neutron "
|
|
"network %(net)s. %(exc)s",
|
|
{'net': network_uuid, 'node': node.uuid,
|
|
'ir-port': ironic_port.uuid, 'exc': e})
|
|
else:
|
|
ports[ironic_port.uuid] = port.id
|
|
|
|
if failures:
|
|
if len(failures) == len(ports_to_create):
|
|
rollback_ports(task, network_uuid)
|
|
raise exception.NetworkError(_(
|
|
"Failed to create neutron ports for node's %(node)s ports "
|
|
"%(ports)s.") % {'node': node.uuid, 'ports': ports_to_create})
|
|
else:
|
|
LOG.warning("Some errors were encountered when updating "
|
|
"vif_port_id for node %(node)s on "
|
|
"the following ports: %(ports)s.",
|
|
{'node': node.uuid, 'ports': failures})
|
|
else:
|
|
LOG.info('For node %(node_uuid)s in network %(net)s, successfully '
|
|
'created ports (ironic ID: neutron ID): %(ports)s.',
|
|
{'node_uuid': node.uuid, 'net': network_uuid, 'ports': ports})
|
|
|
|
return ports
|
|
|
|
|
|
def remove_ports_from_network(task, network_uuid):
|
|
"""Deletes the neutron ports created for booting the ramdisk.
|
|
|
|
:param task: a TaskManager instance.
|
|
:param network_uuid: UUID of a neutron network ports will be deleted from.
|
|
:raises: NetworkError
|
|
"""
|
|
add_all_ports = CONF.neutron.add_all_ports
|
|
if not add_all_ports:
|
|
macs = [p.address for p in task.ports if p.pxe_enabled]
|
|
else:
|
|
macs = [p.address for p in task.ports]
|
|
if macs:
|
|
params = {
|
|
'network_id': network_uuid,
|
|
'mac_address': macs,
|
|
}
|
|
LOG.debug("Removing ports on network %(net)s on node %(node)s.",
|
|
{'net': network_uuid, 'node': task.node.uuid})
|
|
|
|
remove_neutron_ports(task, params)
|
|
|
|
|
|
def remove_neutron_ports(task, params):
|
|
"""Deletes the neutron ports matched by params.
|
|
|
|
:param task: a TaskManager instance.
|
|
:param params: Dict of params to filter ports.
|
|
:raises: NetworkError
|
|
"""
|
|
client = get_client(context=task.context)
|
|
node_uuid = task.node.uuid
|
|
|
|
try:
|
|
ports = client.ports(**params)
|
|
except openstack_exc.OpenStackCloudException as e:
|
|
msg = (_('Could not get given network VIF for %(node)s '
|
|
'from neutron, possible network issue. %(exc)s') %
|
|
{'node': node_uuid, 'exc': e})
|
|
LOG.exception(msg)
|
|
raise exception.NetworkError(msg)
|
|
|
|
if not ports:
|
|
LOG.debug('No ports to remove for node %s', node_uuid)
|
|
return
|
|
|
|
for port in ports:
|
|
LOG.debug('Deleting neutron port %(vif_port_id)s of node '
|
|
'%(node_id)s.',
|
|
{'vif_port_id': port['id'], 'node_id': node_uuid})
|
|
|
|
if is_smartnic_port(port):
|
|
wait_for_host_agent(client, port['binding:host_id'])
|
|
try:
|
|
client.delete_port(port)
|
|
# NOTE(mgoddard): Ignore if the port was deleted by nova.
|
|
except openstack_exc.ResourceNotFound:
|
|
LOG.info('Port %s was not found while deleting.', port.id)
|
|
except openstack_exc.OpenStackCloudException as e:
|
|
msg = (_('Could not remove VIF %(vif)s of node %(node)s, possibly '
|
|
'a network issue: %(exc)s') %
|
|
{'vif': port.id, 'node': node_uuid, 'exc': e})
|
|
LOG.exception(msg)
|
|
raise exception.NetworkError(msg)
|
|
|
|
LOG.info('Successfully removed node %(node_uuid)s neutron ports.',
|
|
{'node_uuid': node_uuid})
|
|
|
|
|
|
def _uncidr(cidr, ipv6=False):
|
|
"""Convert CIDR network representation into network/netmask form
|
|
|
|
:param cidr: network in CIDR form
|
|
:param ipv6: if `True`, consider `cidr` being IPv6
|
|
:returns: a tuple of network/host number in dotted
|
|
decimal notation, netmask in dotted decimal notation
|
|
|
|
"""
|
|
net = ipaddress.ip_interface(cidr).network
|
|
return str(net.network_address), str(net.netmask)
|
|
|
|
|
|
def get_neutron_port_data(port_id, vif_id, client=None, context=None):
|
|
"""Gather Neutron port and network configuration
|
|
|
|
Query Neutron for port and network configuration, return whatever
|
|
is available.
|
|
|
|
:param port_id: ironic port/portgroup ID.
|
|
:param vif_id: Neutron port ID.
|
|
:param client: Optional a Neutron client object.
|
|
:param context: request context
|
|
:type context: ironic.common.context.RequestContext
|
|
:raises: NetworkError
|
|
:returns: a dict holding network configuration information
|
|
associated with this ironic or Neutron port.
|
|
"""
|
|
|
|
if not client:
|
|
client = get_client(context=context)
|
|
|
|
try:
|
|
port_config = client.get_port(vif_id)
|
|
|
|
except openstack_exc.OpenStackCloudException as e:
|
|
msg = (_('Unable to get port info for %(port_id)s. Error: '
|
|
'%(err)s') % {'port_id': vif_id, 'err': e})
|
|
LOG.exception(msg)
|
|
raise exception.NetworkError(msg)
|
|
|
|
LOG.debug('Received port %(port)s data: %(info)s',
|
|
{'port': vif_id, 'info': port_config})
|
|
|
|
port_id = port_config['name'] or port_id
|
|
|
|
network_id = port_config.network_id
|
|
|
|
try:
|
|
network_config = client.get_network(network_id)
|
|
|
|
except openstack_exc.OpenStackCloudException as e:
|
|
msg = (_('Unable to get network info for %(network_id)s. Error: '
|
|
'%(err)s') % {'network_id': network_id, 'err': e})
|
|
LOG.exception(msg)
|
|
raise exception.NetworkError(msg)
|
|
|
|
LOG.debug('Received network %(network)s data: %(info)s',
|
|
{'network': network_id, 'info': network_config})
|
|
|
|
subnets_config = {}
|
|
|
|
network_data = {
|
|
'links': [
|
|
{
|
|
'id': port_id,
|
|
'type': 'vif',
|
|
'ethernet_mac_address': port_config['mac_address'],
|
|
'vif_id': port_config['id'],
|
|
'mtu': network_config['mtu']
|
|
}
|
|
],
|
|
'networks': [
|
|
|
|
]
|
|
}
|
|
|
|
for fixed_ip in port_config.get('fixed_ips', []):
|
|
subnet_id = fixed_ip['subnet_id']
|
|
|
|
try:
|
|
subnet_config = client.get_subnet(subnet_id)
|
|
|
|
LOG.debug('Received subnet %(subnet)s data: %(info)s',
|
|
{'subnet': subnet_id, 'info': subnet_config})
|
|
|
|
subnets_config[subnet_id] = subnet_config
|
|
|
|
except openstack_exc.OpenStackCloudException as e:
|
|
msg = (_('Unable to get subnet info for %(subnet_id)s. Error: '
|
|
'%(err)s') % {'subnet_id': subnet_id, 'err': e})
|
|
LOG.exception(msg)
|
|
raise exception.NetworkError(msg)
|
|
|
|
subnet_config = subnets_config[subnet_id]
|
|
|
|
subnet_network, netmask = _uncidr(
|
|
subnet_config.cidr, subnet_config.ip_version == 6)
|
|
|
|
network = {
|
|
'id': fixed_ip['subnet_id'],
|
|
'network_id': port_config['network_id'],
|
|
'type': 'ipv%s' % subnet_config['ip_version'],
|
|
'link': port_id,
|
|
'ip_address': fixed_ip['ip_address'],
|
|
'netmask': netmask,
|
|
'routes': [
|
|
|
|
]
|
|
}
|
|
|
|
# TODO(etingof): Adding default route if gateway is present.
|
|
# This is a hack, Neutron should have given us a route.
|
|
|
|
if subnet_config['gateway_ip']:
|
|
zero_addr = ('::0' if subnet_config['ip_version'] == 6
|
|
else '0.0.0.0')
|
|
|
|
route = {
|
|
'network': zero_addr,
|
|
'netmask': zero_addr,
|
|
'gateway': subnet_config['gateway_ip']
|
|
}
|
|
|
|
network['routes'].append(route)
|
|
|
|
for host_config in subnet_config['host_routes']:
|
|
subnet_network, netmask = _uncidr(
|
|
host_config['destination'],
|
|
subnet_config['ip_version'] == 6)
|
|
|
|
route = {
|
|
'network': subnet_network,
|
|
'netmask': netmask,
|
|
'gateway': host_config['nexthop']
|
|
}
|
|
|
|
network['routes'].append(route)
|
|
|
|
network_data['networks'].append(network)
|
|
|
|
return network_data
|
|
|
|
|
|
def get_node_portmap(task):
|
|
"""Extract the switch port information for the node.
|
|
|
|
The information is returned in the form of::
|
|
|
|
{
|
|
port.uuid: {
|
|
'switch_id': 'abc',
|
|
'port_id': 'Po0/1',
|
|
'other_llc_key': 'val'
|
|
}
|
|
}
|
|
|
|
:param task: a task containing the Node object.
|
|
:returns: port information as a dict
|
|
"""
|
|
|
|
portmap = {}
|
|
for port in task.ports:
|
|
portmap[port.uuid] = port.local_link_connection
|
|
return portmap
|
|
# TODO(jroll) raise InvalidParameterValue if a port doesn't have the
|
|
# necessary info? (probably)
|
|
|
|
|
|
def get_local_group_information(task, portgroup):
|
|
"""Extract the portgroup information.
|
|
|
|
The information is returned in the form of::
|
|
|
|
{
|
|
'id': portgroup.uuid,
|
|
'name': portgroup.name,
|
|
'bond_mode': portgroup.mode,
|
|
'bond_properties': {
|
|
'bond_propertyA': 'valueA',
|
|
'bond_propertyB': 'valueB',
|
|
}
|
|
}
|
|
|
|
:param task: a task containing the Node object.
|
|
:param portgroup: Ironic portgroup object to extract data for.
|
|
:returns: port group information as a dict
|
|
"""
|
|
|
|
portgroup_properties = {}
|
|
for prop, value in portgroup.properties.items():
|
|
# These properties are the bonding driver options described
|
|
# at https://www.kernel.org/doc/Documentation/networking/bonding.txt .
|
|
# cloud-init checks the same way, parameter name has to start with
|
|
# 'bond'. Keep this structure when passing properties to neutron ML2
|
|
# drivers.
|
|
key = prop if prop.startswith('bond') else 'bond_%s' % prop
|
|
portgroup_properties[key] = value
|
|
|
|
return {
|
|
'id': portgroup.uuid,
|
|
'name': portgroup.name,
|
|
'bond_mode': portgroup.mode,
|
|
'bond_properties': portgroup_properties
|
|
}
|
|
|
|
|
|
def rollback_ports(task, network_uuid):
|
|
"""Attempts to delete any ports created by cleaning/provisioning
|
|
|
|
Purposefully will not raise any exceptions so error handling can
|
|
continue.
|
|
|
|
:param task: a TaskManager instance.
|
|
:param network_uuid: UUID of a neutron network.
|
|
"""
|
|
try:
|
|
remove_ports_from_network(task, network_uuid)
|
|
except exception.NetworkError:
|
|
# Only log the error
|
|
LOG.exception('Failed to rollback port changes for '
|
|
'node %(node)s on network %(network)s',
|
|
{'node': task.node.uuid, 'network': network_uuid})
|
|
|
|
|
|
def validate_network(uuid_or_name, net_type=_('network'), context=None):
|
|
"""Check that the given network is present.
|
|
|
|
:param uuid_or_name: network UUID or name
|
|
:param net_type: human-readable network type for error messages
|
|
:param context: request context
|
|
:type context: ironic.common.context.RequestContext
|
|
:return: network UUID
|
|
:raises: MissingParameterValue if uuid_or_name is empty
|
|
:raises: NetworkError on failure to contact Neutron
|
|
:raises: InvalidParameterValue for missing or duplicated network
|
|
"""
|
|
if not uuid_or_name:
|
|
raise exception.MissingParameterValue(
|
|
_('UUID or name of %s is not set in configuration') % net_type)
|
|
|
|
client = get_client(context=context)
|
|
network = _get_network_by_uuid_or_name(client, uuid_or_name,
|
|
net_type=net_type)
|
|
|
|
return network.id
|
|
|
|
|
|
def validate_port_info(node, port):
|
|
"""Check that port contains enough information for deploy.
|
|
|
|
Neutron network interface requires that local_link_information field is
|
|
filled before we can use this port.
|
|
|
|
:param node: Ironic node object.
|
|
:param port: Ironic port object.
|
|
:returns: True if port info is valid, False otherwise.
|
|
"""
|
|
# Note(moshele): client-id in the port extra field indicates an InfiniBand
|
|
# port. In this case we don't require local_link_connection to be
|
|
# populated because the network topology is discoverable by the Infiniband
|
|
# Subnet Manager.
|
|
if port.extra.get('client-id'):
|
|
return True
|
|
if (node.network_interface == 'neutron'
|
|
and not port.local_link_connection):
|
|
LOG.warning("The local_link_connection is required for "
|
|
"'neutron' network interface and is not present "
|
|
"in the nodes %(node)s port %(port)s",
|
|
{'node': node.uuid, 'port': port.uuid})
|
|
return False
|
|
if (port.is_smartnic and not types.locallinkconnectiontype
|
|
.validate_for_smart_nic(port.local_link_connection)):
|
|
LOG.error("Smart NIC port must have port_id and hostname in "
|
|
"local_link_connection, port: %s", port['id'])
|
|
return False
|
|
if (not port.is_smartnic and types.locallinkconnectiontype
|
|
.validate_for_smart_nic(port.local_link_connection)):
|
|
LOG.error("Only Smart NIC ports can have port_id and hostname "
|
|
"in local_link_connection, port: %s", port['id'])
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def _validate_agent(client, **kwargs):
|
|
"""Check that the given neutron agent is alive
|
|
|
|
:param client: Neutron client
|
|
:param kwargs: Additional parameters to pass to the neutron client
|
|
list_agents method.
|
|
:returns: A boolean to describe the agent status, if more than one agent
|
|
returns by the client then return True if at least one of them is
|
|
alive.
|
|
:raises: NetworkError in case of failure contacting Neutron.
|
|
"""
|
|
try:
|
|
agents = client.agents(**kwargs)
|
|
for agent in agents:
|
|
if agent.is_alive:
|
|
return True
|
|
return False
|
|
except openstack_exc.OpenStackCloudException:
|
|
raise exception.NetworkError('Failed to contact Neutron server')
|
|
|
|
|
|
def is_smartnic_port(port_data):
|
|
"""Check that the port is Smart NIC port
|
|
|
|
:param port_data: an instance of ironic.objects.port.Port
|
|
or port data as dict.
|
|
:returns: A boolean to indicate port as Smart NIC port.
|
|
"""
|
|
if isinstance(port_data, objects.Port):
|
|
return port_data.supports_is_smartnic() and port_data.is_smartnic
|
|
|
|
if isinstance(port_data, dict):
|
|
return port_data.get('is_smartnic', False)
|
|
|
|
LOG.warning('Unknown port data type: %(type)s', {'type': type(port_data)})
|
|
return False
|
|
|
|
|
|
def _get_network_by_uuid_or_name(client, uuid_or_name, net_type=_('network')):
|
|
"""Return a neutron network by UUID or name.
|
|
|
|
:param client: A Neutron client object.
|
|
:param uuid_or_name: network UUID or name
|
|
:param net_type: human-readable network type for error messages
|
|
:returns: A dict describing the neutron network.
|
|
:raises: NetworkError on failure to contact Neutron
|
|
:raises: InvalidParameterValue for missing or duplicated network
|
|
"""
|
|
|
|
try:
|
|
network = client.find_network(uuid_or_name, ignore_missing=False)
|
|
except openstack_exc.DuplicateResource:
|
|
network_ids = [net.id for net in client.networks(name=uuid_or_name)]
|
|
raise exception.InvalidParameterValue(
|
|
_('More than one %(type)s was found for name %(name)s: %(nets)s') %
|
|
{'name': uuid_or_name, 'nets': ', '.join(network_ids),
|
|
'type': net_type})
|
|
except openstack_exc.ResourceNotFound:
|
|
raise exception.InvalidParameterValue(
|
|
_('%(type)s with name or UUID %(uuid_or_name)s was not found') %
|
|
{'type': net_type, 'uuid_or_name': uuid_or_name})
|
|
except openstack_exc.OpenStackCloudException as exc:
|
|
raise exception.NetworkError(_('Could not retrieve network: %s') % exc)
|
|
|
|
LOG.debug('Got network matching %(uuid_or_name)s: %(result)s',
|
|
{'uuid_or_name': uuid_or_name, 'result': network})
|
|
|
|
return network
|
|
|
|
|
|
def _get_port_by_uuid(client, port_uuid):
|
|
"""Return a neutron port by UUID.
|
|
|
|
:param client: A Neutron client object.
|
|
:param port_uuid: UUID of a Neutron port to query.
|
|
:returns: A dict describing the neutron port.
|
|
:raises: InvalidParameterValue if the port does not exist.
|
|
:raises: NetworkError on failure to contact Neutron.
|
|
"""
|
|
try:
|
|
port = client.get_port(port_uuid)
|
|
except openstack_exc.ResourceNotFound:
|
|
raise exception.InvalidParameterValue(
|
|
_('Neutron port %(port_uuid)s was not found') %
|
|
{'port_uuid': port_uuid})
|
|
except openstack_exc.OpenStackCloudException as exc:
|
|
raise exception.NetworkError(_('Could not retrieve neutron port: %s') %
|
|
exc)
|
|
|
|
return port
|
|
|
|
|
|
def get_physnets_by_port_uuid(client, port_uuid):
|
|
"""Return the set of physical networks associated with a neutron port.
|
|
|
|
Query the network to which the port is attached and return the set of
|
|
physical networks associated with the segments in that network.
|
|
|
|
:param client: A Neutron client object.
|
|
:param port_uuid: UUID of a Neutron port to query.
|
|
:returns: A set of physical networks.
|
|
:raises: NetworkError if the network query fails.
|
|
:raises: InvalidParameterValue for missing network.
|
|
"""
|
|
port = _get_port_by_uuid(client, port_uuid)
|
|
network_uuid = port.network_id
|
|
|
|
network = _get_network_by_uuid_or_name(client, network_uuid)
|
|
|
|
if network.segments is not None:
|
|
# A network with multiple segments will have a 'segments' parameter
|
|
# which will contain a list of segments. Each segment should have a
|
|
# 'provider:physical_network' parameter which contains the physical
|
|
# network of the segment.
|
|
return set(segment[PHYSNET_PARAM_NAME]
|
|
for segment in network.segments
|
|
if segment[PHYSNET_PARAM_NAME])
|
|
else:
|
|
# A network with a single segment will have a
|
|
# 'provider:physical_network' parameter which contains the network's
|
|
# physical network.
|
|
return (set([network.provider_physical_network])
|
|
if network.provider_physical_network else set())
|
|
|
|
|
|
@retrying.retry(
|
|
stop_max_attempt_number=CONF.agent.neutron_agent_max_attempts,
|
|
retry_on_exception=lambda e: isinstance(e, exception.NetworkError),
|
|
wait_fixed=CONF.agent.neutron_agent_status_retry_interval * 1000
|
|
)
|
|
def wait_for_host_agent(client, host_id, target_state='up'):
|
|
"""Wait for neutron agent to become target state
|
|
|
|
:param client: A Neutron client object.
|
|
:param host_id: Agent host_id
|
|
:param target_state: up: wait for up status,
|
|
down: wait for down status
|
|
:returns: boolean indicates the agent state matches
|
|
param value target_state_up.
|
|
:raises: exception.Invalid if 'target_state' is not valid.
|
|
:raises: exception.NetworkError if host status didn't match the required
|
|
status after max retry attempts.
|
|
"""
|
|
if target_state not in ['up', 'down']:
|
|
raise exception.Invalid(
|
|
'Invalid requested agent state to validate, accepted values: '
|
|
'up, down. Requested state: %(target_state)s' % {
|
|
'target_state': target_state})
|
|
|
|
LOG.debug('Validating host %(host_id)s agent is %(status)s',
|
|
{'host_id': host_id,
|
|
'status': target_state})
|
|
is_alive = _validate_agent(client, host=host_id)
|
|
LOG.debug('Agent on host %(host_id)s is %(status)s',
|
|
{'host_id': host_id,
|
|
'status': 'up' if is_alive else 'down'})
|
|
if ((target_state == 'up' and is_alive)
|
|
or (target_state == 'down' and not is_alive)):
|
|
return True
|
|
raise exception.NetworkError(
|
|
'Agent on host %(host)s failed to reach state %(state)s' % {
|
|
'host': host_id, 'state': target_state})
|
|
|
|
|
|
@retrying.retry(
|
|
stop_max_attempt_number=CONF.agent.neutron_agent_max_attempts,
|
|
retry_on_exception=lambda e: isinstance(e, exception.NetworkError),
|
|
wait_fixed=CONF.agent.neutron_agent_status_retry_interval * 1000
|
|
)
|
|
def wait_for_port_status(client, port_id, status):
|
|
"""Wait for port status to be the desired status
|
|
|
|
:param client: A Neutron client object.
|
|
:param port_id: Neutron port_id
|
|
:param status: Port's target status, can be ACTIVE, DOWN ... etc.
|
|
:returns: boolean indicates that the port status matches the
|
|
required value passed by param status.
|
|
:raises: InvalidParameterValue if the port does not exist.
|
|
:raises: exception.NetworkError if port status didn't match
|
|
the required status after max retry attempts.
|
|
"""
|
|
LOG.debug('Validating Port %(port_id)s status is %(status)s',
|
|
{'port_id': port_id, 'status': status})
|
|
port = _get_port_by_uuid(client, port_id)
|
|
LOG.debug('Port %(port_id)s status is: %(status)s',
|
|
{'port_id': port_id, 'status': port.status})
|
|
if port.status == status:
|
|
return True
|
|
raise exception.NetworkError(
|
|
'Port %(port_id)s failed to reach status %(status)s' % {
|
|
'port_id': port_id, 'status': status})
|
|
|
|
|
|
class NeutronNetworkInterfaceMixin(object):
|
|
|
|
def get_cleaning_network_uuid(self, task):
|
|
cleaning_network = (
|
|
task.node.driver_info.get('cleaning_network')
|
|
or CONF.neutron.cleaning_network
|
|
)
|
|
return validate_network(
|
|
cleaning_network, _('cleaning network'),
|
|
context=task.context)
|
|
|
|
def get_provisioning_network_uuid(self, task):
|
|
provisioning_network = (
|
|
task.node.driver_info.get('provisioning_network')
|
|
or CONF.neutron.provisioning_network
|
|
)
|
|
return validate_network(
|
|
provisioning_network, _('provisioning network'),
|
|
context=task.context)
|
|
|
|
# TODO(stendulker): FlatNetwork should not use this method.
|
|
# FlatNetwork uses tenant network for rescue operation.
|
|
def get_rescuing_network_uuid(self, task):
|
|
rescuing_network = (
|
|
task.node.driver_info.get('rescuing_network')
|
|
or CONF.neutron.rescuing_network
|
|
)
|
|
return validate_network(
|
|
rescuing_network, _('rescuing network'),
|
|
context=task.context)
|
|
|
|
def get_inspection_network_uuid(self, task):
|
|
inspection_network = (
|
|
task.node.driver_info.get('inspection_network')
|
|
or CONF.neutron.inspection_network
|
|
)
|
|
return validate_network(
|
|
inspection_network, _('inspection network'),
|
|
context=task.context)
|
|
|
|
def validate_inspection(self, task):
|
|
"""Validate that the node has required properties for inspection.
|
|
|
|
:param task: A TaskManager instance with the node being checked
|
|
:raises: MissingParameterValue if node is missing one or more required
|
|
parameters
|
|
:raises: UnsupportedDriverExtension
|
|
"""
|
|
try:
|
|
self.get_inspection_network_uuid(task)
|
|
except exception.MissingParameterValue:
|
|
# Fall back to non-managed in-band inspection
|
|
raise exception.UnsupportedDriverExtension(
|
|
driver=task.node.driver, extension='inspection')
|