Source code for ironic.dhcp.neutron

# Copyright 2014 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
#    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 ipaddress
import time

from openstack.connection import exceptions as openstack_exc
from oslo_log import log as logging

from ironic.common import exception
from ironic.common.i18n import _
from ironic.common import network
from ironic.common import neutron
from ironic.conf import CONF
from ironic.dhcp import base
from ironic import objects

LOG = logging.getLogger(__name__)

[docs] class NeutronDHCPApi(base.BaseDHCP): """API for communicating to neutron 2.x API."""
[docs] def update_port_dhcp_opts(self, port_id, dhcp_options, token=None, context=None): """Update a port's attributes. Update one or more DHCP options on the specified port. For the relevant API spec, see :param port_id: designate which port these attributes will be applied to. :param dhcp_options: this will be a list of dicts, e.g. :: [{'opt_name': '67', 'opt_value': 'pxelinux.0', 'ip_version': 4}, {'opt_name': '66', 'opt_value': ''}, 'ip_version': 4}] :param token: optional auth token. Deprecated, use context. :param context: request context :type context: ironic.common.context.RequestContext :raises: FailedToUpdateDHCPOptOnPort """ super(NeutronDHCPApi, self).update_port_dhcp_opts( port_id, dhcp_options, token=token, context=context) try: neutron_client = neutron.get_client(token=token, context=context) fips = [] port = neutron_client.get_port(port_id) if port: # TODO(TheJulia): We need to retool this down the # road so that we handle ports and allow preferences # for multi-address ports with different IP versions # and enable operators to possibly select preferences # for provisionioning operations. # This is compounded by v6 mainly only being available # with UEFI machines, so the support matrix also gets # a little "weird". # Ideally, we should work on this in Victoria. fips = port.get('fixed_ips') update_opts = [] if len(fips) != 0: ip_versions = {ipaddress.ip_address(fip['ip_address']).version for fip in fips} for ip_version in ip_versions: for option in dhcp_options: if option.get('ip_version', 4) == ip_version: update_opts.append(option) else: LOG.error('Requested to update port for port %s, ' 'however port lacks an IP address.', port_id) port_attrs = {'extra_dhcp_opts': update_opts} neutron.update_neutron_port(context, port_id, port_attrs) except openstack_exc.OpenStackCloudException: LOG.exception("Failed to update Neutron port %s.", port_id) raise exception.FailedToUpdateDHCPOptOnPort(port_id=port_id)
[docs] def update_dhcp_opts(self, task, options, vifs=None): """Send or update the DHCP BOOT options for this node. :param task: A TaskManager instance. :param options: this will be a list of dicts, e.g. :: [{'opt_name': '67', 'opt_value': 'pxelinux.0', 'ip_version': 4}, {'opt_name': '66', 'opt_value': '', 'ip_version': 4}] :param vifs: a dict of Neutron port/portgroup dicts to update DHCP options on. The port/portgroup dict key should be Ironic port UUIDs, and the values should be Neutron port UUIDs, e.g. :: {'ports': {'port.uuid':}, 'portgroups': {'portgroup.uuid':}} If the value is None, will get the list of ports/portgroups from the Ironic port/portgroup objects. """ if vifs is None: vifs = network.get_node_vif_ids(task) if not (vifs['ports'] or vifs['portgroups']): raise exception.FailedToUpdateDHCPOptOnPort( _("No VIFs found for node %(node)s when attempting " "to update DHCP BOOT options.") % {'node': task.node.uuid}) failures = [] vif_list = [vif for pdict in vifs.values() for vif in pdict.values()] for vif in vif_list: try: self.update_port_dhcp_opts(vif, options, context=task.context) except exception.FailedToUpdateDHCPOptOnPort: failures.append(vif) if failures: if len(failures) == len(vif_list): raise exception.FailedToUpdateDHCPOptOnPort(_( "Failed to set DHCP BOOT options for any port on node %s.") % task.node.uuid) else: LOG.warning("Some errors were encountered when updating " "the DHCP BOOT options for node %(node)s on " "the following Neutron ports: %(ports)s.", {'node': task.node.uuid, 'ports': failures}) # TODO(adam_g): Hack to workaround bug 1334447 until we have a # mechanism for synchronizing events with Neutron. We need to sleep # only if server gets to PXE faster than Neutron agents have setup # sufficient DHCP config for netboot. It may occur when we are using # VMs or hardware server with fast boot enabled. port_delay = CONF.neutron.port_setup_delay if port_delay != 0: LOG.debug("Waiting %d seconds for Neutron.", port_delay) time.sleep(port_delay)
def _get_fixed_ip_address(self, port_id, client): """Get a Neutron port's fixed ip address. :param port_id: Neutron port id. :param client: Neutron client instance. :returns: Neutron port ip address. :raises: NetworkError :raises: InvalidIPv4Address :raises: FailedToGetIPAddressOnPort """ ip_address = None try: neutron_port = client.get_port(port_id) except openstack_exc.OpenStackCloudException: raise exception.NetworkError( _('Could not retrieve neutron port: %s') % port_id) fixed_ips = neutron_port.get('fixed_ips') # NOTE(faizan) At present only the first fixed_ip assigned to this # neutron port will be used, since nova allocates only one fixed_ip # for the instance. if fixed_ips: ip_address = fixed_ips[0].get('ip_address', None) if ip_address: try: if (ipaddress.ip_address(ip_address).version == 4 or ipaddress.ip_address(ip_address).version == 6): return ip_address else: LOG.error("Neutron returned invalid IP " "address %(ip_address)s on port %(port_id)s.", {'ip_address': ip_address, 'port_id': port_id}) raise exception.InvalidIPv4Address(ip_address=ip_address) except ValueError as exc: LOG.error("An Invalid IP address was supplied and failed " "basic validation: %s", exc) raise exception.InvalidIPAddress(ip_address=ip_address) else: LOG.error("No IP address assigned to Neutron port %s.", port_id) raise exception.FailedToGetIPAddressOnPort(port_id=port_id) def _get_port_ip_address(self, task, p_obj, client): """Get ip address of ironic port/portgroup assigned by Neutron. :param task: a TaskManager instance. :param p_obj: Ironic port or portgroup object. :param client: Neutron client instance. :returns: List of Neutron vif ip address associated with Node's port/portgroup. :raises: FailedToGetIPAddressOnPort :raises: InvalidIPv4Address """ vif =, p_obj) if not vif: obj_name = 'portgroup' if isinstance(p_obj, objects.Port): obj_name = 'port' LOG.warning("No VIFs found for node %(node)s when attempting " "to get IP address for %(obj_name)s: %(obj_id)s.", {'node': task.node.uuid, 'obj_name': obj_name, 'obj_id': p_obj.uuid}) raise exception.FailedToGetIPAddressOnPort(port_id=p_obj.uuid) vif_ip_address = self._get_fixed_ip_address(vif, client) return vif_ip_address def _get_ip_addresses(self, task, pobj_list, client): """Get IP addresses for all ports/portgroups. :param task: a TaskManager instance. :param pobj_list: List of port or portgroup objects. :param client: Neutron client instance. :returns: List of IP addresses associated with task's ports/portgroups. """ failures = [] ip_addresses = [] for obj in pobj_list: try: vif_ip_address = self._get_port_ip_address(task, obj, client) ip_addresses.append(vif_ip_address) except (exception.FailedToGetIPAddressOnPort, exception.InvalidIPv4Address, exception.NetworkError): failures.append(obj.uuid) if failures: obj_name = 'portgroups' if isinstance(pobj_list[0], objects.Port): obj_name = 'ports' LOG.warning( "Some errors were encountered on node %(node)s " "while retrieving IP addresses on the following " "%(obj_name)s: %(failures)s.", {'node': task.node.uuid, 'obj_name': obj_name, 'failures': failures}) return ip_addresses
[docs] def get_ip_addresses(self, task): """Get IP addresses for all ports/portgroups in `task`. :param task: a TaskManager instance. :returns: List of IP addresses associated with task's ports/portgroups. """ client = neutron.get_client(context=task.context) port_ip_addresses = self._get_ip_addresses(task, task.ports, client) portgroup_ip_addresses = self._get_ip_addresses( task, task.portgroups, client) return port_ip_addresses + portgroup_ip_addresses
[docs] def supports_ipxe_tag(self): """Whether the provider will correctly apply the 'ipxe' tag. When iPXE makes a DHCP request, does this provider support adding the tag `ipxe` or `ipxe6` (for IPv6). When the provider returns True, options can be added which filter on these tags. :returns: True """ return True