# 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 neutronclient.common import exceptions as neutron_exceptions from neutronclient.v2_0 import client as clientv20 from oslo_log import log from ironic.common import exception from ironic.common.i18n import _ from ironic.common.i18n import _LE from ironic.common.i18n import _LI from ironic.common.i18n import _LW from ironic.common import keystone from ironic.conf import CONF LOG = log.getLogger(__name__) DEFAULT_NEUTRON_URL = 'http://%s:9696' % CONF.my_ip _NEUTRON_SESSION = None def _get_neutron_session(): global _NEUTRON_SESSION if not _NEUTRON_SESSION: _NEUTRON_SESSION = keystone.get_session('neutron') return _NEUTRON_SESSION def get_client(token=None): params = {'retries': CONF.neutron.retries} url = CONF.neutron.url if CONF.neutron.auth_strategy == 'noauth': params['endpoint_url'] = url or DEFAULT_NEUTRON_URL params['auth_strategy'] = 'noauth' params.update({ 'timeout': CONF.neutron.url_timeout or CONF.neutron.timeout, 'insecure': CONF.neutron.insecure, 'ca_cert': CONF.neutron.cafile}) else: session = _get_neutron_session() if token is None: params['session'] = session # NOTE(pas-ha) endpoint_override==None will auto-discover # endpoint from Keystone catalog. # Region is needed only in this case. # SSL related options are ignored as they are already embedded # in keystoneauth Session object if url: params['endpoint_override'] = url else: params['region_name'] = CONF.keystone.region_name else: params['token'] = token params['endpoint_url'] = url or keystone.get_service_url( session, service_type='network') params.update({ 'timeout': CONF.neutron.url_timeout or CONF.neutron.timeout, 'insecure': CONF.neutron.insecure, 'ca_cert': CONF.neutron.cafile}) return clientv20.Client(**params) def add_ports_to_network(task, network_uuid, is_flat=False): """Create neutron ports to boot the ramdisk. Create neutron ports for each pxe_enabled port on task.node to boot the ramdisk. :param task: a TaskManager instance. :param network_uuid: UUID of a neutron network where ports will be created. :param is_flat: Indicates whether it is a flat network or not. :raises: NetworkError :returns: a dictionary in the form {port.uuid: neutron_port['id']} """ client = get_client(task.context.auth_token) node = task.node 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}) body = { 'port': { 'network_id': network_uuid, 'admin_state_up': True, 'binding:vnic_type': 'baremetal', 'device_owner': 'baremetal:none', } } if not is_flat: # NOTE(vdrok): It seems that change # I437290affd8eb87177d0626bf7935a165859cbdd to neutron broke the # possibility to always bind port. Set binding:host_id only in # case of non flat network. body['port']['binding:host_id'] = node.uuid # Since instance_uuid will not be available during cleaning # operations, we need to check that and populate them only when # available body['port']['device_id'] = node.instance_uuid or node.uuid ports = {} failures = [] portmap = get_node_portmap(task) pxe_enabled_ports = [p for p in task.ports if p.pxe_enabled] for ironic_port in pxe_enabled_ports: body['port']['mac_address'] = ironic_port.address binding_profile = {'local_link_information': [portmap[ironic_port.uuid]]} body['port']['binding:profile'] = binding_profile client_id = ironic_port.extra.get('client-id') if client_id: client_id_opt = {'opt_name': 'client-id', 'opt_value': client_id} extra_dhcp_opts = body['port'].get('extra_dhcp_opts', []) extra_dhcp_opts.append(client_id_opt) body['port']['extra_dhcp_opts'] = extra_dhcp_opts try: port = client.create_port(body) except neutron_exceptions.NeutronClientException as e: rollback_ports(task, network_uuid) msg = (_('Could not create neutron port for ironic port ' '%(ir-port)s on given network %(net)s from node ' '%(node)s. %(exc)s') % {'net': network_uuid, 'node': node.uuid, 'ir-port': ironic_port.uuid, 'exc': e}) LOG.exception(msg) raise exception.NetworkError(msg) try: ports[ironic_port.uuid] = port['port']['id'] except KeyError: failures.append(ironic_port.uuid) if failures: if len(failures) == len(pxe_enabled_ports): raise exception.NetworkError(_( "Failed to update vif_port_id for any PXE enabled port " "on node %s.") % node.uuid) else: LOG.warning(_LW("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(_LI('Successfully created ports for node %(node_uuid)s in ' 'network %(net)s.'), {'node_uuid': node.uuid, 'net': network_uuid}) 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 """ macs = [p.address for p in task.ports if p.pxe_enabled] 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(task.context.auth_token) node_uuid = task.node.uuid try: response = client.list_ports(**params) except neutron_exceptions.NeutronClientException 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) ports = response.get('ports', []) if not ports: LOG.debug('No ports to remove for node %s', node_uuid) return for port in ports: if not port['id']: # TODO(morgabra) client.list_ports() sometimes returns # port objects with null ids. It's unclear why this happens. LOG.warning(_LW("Deleting neutron port failed, missing 'id'. " "Node: %(node)s, neutron port: %(port)s."), {'node': node_uuid, 'port': port}) continue LOG.debug('Deleting neutron port %(vif_port_id)s of node ' '%(node_id)s.', {'vif_port_id': port['id'], 'node_id': node_uuid}) try: client.delete_port(port['id']) except neutron_exceptions.NeutronClientException 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(_LI('Successfully removed node %(node_uuid)s neutron ports.'), {'node_uuid': node_uuid}) def get_node_portmap(task): """Extract the switch port information for the node. :param task: a task containing the Node object. :returns: a dictionary in the form {port.uuid: port.local_link_connection} """ 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 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(_LE( 'Failed to rollback port changes for node %(node)s ' 'on network %(network)s'), {'node': task.node.uuid, 'network': network_uuid})