From f1adedde7584d7ba5fdd37ed7ee45a0d415f2af0 Mon Sep 17 00:00:00 2001 From: Ellen Hui Date: Tue, 5 Aug 2014 23:50:56 +0000 Subject: Make DHCP provider pluggable Previously, all dhcp was done explicitly through Neutron. This commit adds an interface for dhcp providers, currently implemented by a Neutron provider and a noop provider. The provider can be set in the dhcp_provider config option, and defaults to neutron. Co-Authored-By: Jim Rollenhagen Co-Authored-By: Lucas Alvares Gomes Implements: blueprint support-external-dhcp Closes-Bug: #1282168 Change-Id: I7eea490c573914c6cd2a73056fe3a0ec91065dc0 --- ironic/common/dhcp_factory.py | 94 +++++++++++++++++++++ ironic/common/exception.py | 4 + ironic/common/network.py | 30 +++++++ ironic/common/neutron.py | 187 ------------------------------------------ 4 files changed, 128 insertions(+), 187 deletions(-) create mode 100644 ironic/common/dhcp_factory.py create mode 100644 ironic/common/network.py delete mode 100644 ironic/common/neutron.py (limited to 'ironic/common') diff --git a/ironic/common/dhcp_factory.py b/ironic/common/dhcp_factory.py new file mode 100644 index 000000000..bca51989d --- /dev/null +++ b/ironic/common/dhcp_factory.py @@ -0,0 +1,94 @@ +# Copyright 2014 Rackspace, Inc. +# +# 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 stevedore + +from oslo.config import cfg + +from ironic.common import exception +from ironic.openstack.common import lockutils + + +dhcp_provider_opts = [ + cfg.StrOpt('dhcp_provider', + default='neutron', + help='DHCP provider to use. "neutron" uses Neutron, and ' + '"none" uses a no-op provider.' + ), +] + +CONF = cfg.CONF +CONF.register_opts(dhcp_provider_opts, group='dhcp') + +_dhcp_provider = None + +EM_SEMAPHORE = 'dhcp_provider' + + +class DHCPFactory(object): + + # NOTE(lucasagomes): Instantiate a stevedore.driver.DriverManager + # only once, the first time DHCPFactory.__init__ + # is called. + _dhcp_provider = None + + def __init__(self, **kwargs): + if not DHCPFactory._dhcp_provider: + DHCPFactory._set_dhcp_provider(**kwargs) + + # NOTE(lucasagomes): Use lockutils to avoid a potential race in eventlet + # that might try to create two dhcp factories. + @classmethod + @lockutils.synchronized(EM_SEMAPHORE, 'ironic-') + def _set_dhcp_provider(cls, **kwargs): + """Initialize the dhcp provider + + :raises: DHCPNotFound if the dhcp_provider cannot be loaded. + """ + + # NOTE(lucasagomes): In case multiple greenthreads queue up on + # this lock before _dhcp_provider is initialized, + # prevent creation of multiple DriverManager. + if cls._dhcp_provider: + return + + dhcp_provider_name = CONF.dhcp.dhcp_provider + try: + _extension_manager = stevedore.driver.DriverManager( + 'ironic.dhcp', + dhcp_provider_name, + invoke_kwds=kwargs, + invoke_on_load=True) + except RuntimeError: + raise exception.DHCPNotFound(dhcp_provider_name=dhcp_provider_name) + + cls._dhcp_provider = _extension_manager.driver + + def update_dhcp(self, task, dhcp_opts): + """Send or update the DHCP BOOT options for this node. + + :param task: A TaskManager instance. + :param dhcp_opts: this will be a list of dicts, e.g. + [{'opt_name': 'bootfile-name', + 'opt_value': 'pxelinux.0'}, + {'opt_name': 'server-ip-address', + 'opt_value': '123.123.123.456'}, + {'opt_name': 'tftp-server', + 'opt_value': '123.123.123.123'}] + """ + self.provider.update_dhcp_opts(task, dhcp_opts) + + @property + def provider(self): + return self._dhcp_provider diff --git a/ironic/common/exception.py b/ironic/common/exception.py index 76cfd9f32..735f1ce1c 100644 --- a/ironic/common/exception.py +++ b/ironic/common/exception.py @@ -190,6 +190,10 @@ class NotFound(IronicException): code = 404 +class DHCPNotFound(NotFound): + message = _("Failed to load DHCP provider %(dhcp_provider_name)s.") + + class DriverNotFound(NotFound): message = _("Failed to load driver %(driver_name)s.") diff --git a/ironic/common/network.py b/ironic/common/network.py new file mode 100644 index 000000000..de5597905 --- /dev/null +++ b/ironic/common/network.py @@ -0,0 +1,30 @@ +# Copyright 2014 Rackspace, Inc. +# +# 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. + + +def get_node_vif_ids(task): + """Get all VIF ids for a node. + + This function does not handle multi node operations. + + :param task: a TaskManager instance. + :returns: A dict of the Node's port UUIDs and their associated VIFs + + """ + port_vifs = {} + for port in task.ports: + vif = port.extra.get('vif_port_id') + if vif: + port_vifs[port.uuid] = vif + return port_vifs diff --git a/ironic/common/neutron.py b/ironic/common/neutron.py deleted file mode 100644 index 621267b17..000000000 --- a/ironic/common/neutron.py +++ /dev/null @@ -1,187 +0,0 @@ -# -# 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 -# -# 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 time - -from neutronclient.common import exceptions as neutron_client_exc -from neutronclient.v2_0 import client as clientv20 -from oslo.config import cfg - -from ironic.common import exception -from ironic.common import keystone -from ironic.drivers.modules import ssh -from ironic.openstack.common import log as logging - - -neutron_opts = [ - cfg.StrOpt('url', - default='http://$my_ip:9696', - help='URL for connecting to neutron.'), - cfg.IntOpt('url_timeout', - default=30, - help='Timeout value for connecting to neutron in seconds.'), - cfg.StrOpt('auth_strategy', - default='keystone', - help='Default authentication strategy to use when connecting ' - 'to neutron. Can be either "keystone" or "noauth". ' - 'Running neutron in noauth mode (related to but not ' - 'affected by this setting) is insecure and should only be ' - 'used for testing.') - ] - -CONF = cfg.CONF -CONF.import_opt('my_ip', 'ironic.netconf') -CONF.register_opts(neutron_opts, group='neutron') -LOG = logging.getLogger(__name__) - - -class NeutronAPI(object): - """API for communicating to neutron 2.x API.""" - - def __init__(self, context): - self.context = context - self.client = None - params = { - 'timeout': CONF.neutron.url_timeout, - 'insecure': CONF.keystone_authtoken.insecure, - 'ca_cert': CONF.keystone_authtoken.certfile, - } - - if CONF.neutron.auth_strategy not in ['noauth', 'keystone']: - raise exception.ConfigInvalid(_('Neutron auth_strategy should be ' - 'either "noauth" or "keystone".')) - - if CONF.neutron.auth_strategy == 'noauth': - params['endpoint_url'] = CONF.neutron.url - params['auth_strategy'] = 'noauth' - elif (CONF.neutron.auth_strategy == 'keystone' and - context.auth_token is None): - params['endpoint_url'] = (CONF.neutron.url or - keystone.get_service_url('neutron')) - params['username'] = CONF.keystone_authtoken.admin_user - params['tenant_name'] = CONF.keystone_authtoken.admin_tenant_name - params['password'] = CONF.keystone_authtoken.admin_password - params['auth_url'] = (CONF.keystone_authtoken.auth_uri or '') - else: - params['token'] = context.auth_token - params['endpoint_url'] = CONF.neutron.url - params['auth_strategy'] = None - - self.client = clientv20.Client(**params) - - def update_port_dhcp_opts(self, port_id, dhcp_options): - """Update a port's attributes. - - Update one or more DHCP options on the specified port. - For the relevant API spec, see - http://docs.openstack.org/api/openstack-network/2.0/content/extra-dhc-opt-ext-update.html # noqa - - :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': 'bootfile-name', - 'opt_value': 'pxelinux.0'}, - {'opt_name': 'server-ip-address', - 'opt_value': '123.123.123.456'}, - {'opt_name': 'tftp-server', - 'opt_value': '123.123.123.123'}] - - :raises: FailedToUpdateDHCPOptOnPort - """ - port_req_body = {'port': {'extra_dhcp_opts': dhcp_options}} - try: - self.client.update_port(port_id, port_req_body) - except neutron_client_exc.NeutronClientException: - LOG.exception(_("Failed to update Neutron port %s."), port_id) - raise exception.FailedToUpdateDHCPOptOnPort(port_id=port_id) - - def update_port_address(self, port_id, address): - """Update a port's mac address. - - :param port_id: Neutron port id. - :param address: new MAC address. - :raises: FailedToUpdateMacOnPort - """ - port_req_body = {'port': {'mac_address': address}} - try: - self.client.update_port(port_id, port_req_body) - except neutron_client_exc.NeutronClientException: - LOG.exception(_("Failed to update MAC address on Neutron port %s." - ), port_id) - raise exception.FailedToUpdateMacOnPort(port_id=port_id) - - -def get_node_vif_ids(task): - """Get all Neutron VIF ids for a node. - - This function does not handle multi node operations. - - :param task: a TaskManager instance. - :returns: A dict of the Node's port UUIDs and their associated VIFs - - """ - port_vifs = {} - for port in task.ports: - vif = port.extra.get('vif_port_id') - if vif: - port_vifs[port.uuid] = vif - return port_vifs - - -def update_neutron(task, options): - """Send or update the DHCP BOOT options to Neutron for this node.""" - vifs = get_node_vif_ids(task) - if not vifs: - LOG.warning(_("No VIFs found for node %(node)s when attempting to " - "update Neutron DHCP BOOT options."), - {'node': task.node.uuid}) - return - - # TODO(deva): decouple instantiation of NeutronAPI from task.context. - # Try to use the user's task.context.auth_token, but if it - # is not present, fall back to a server-generated context. - # We don't need to recreate this in every method call. - api = NeutronAPI(task.context) - failures = [] - for port_id, port_vif in vifs.iteritems(): - try: - api.update_port_dhcp_opts(port_vif, options) - except exception.FailedToUpdateDHCPOptOnPort: - failures.append(port_id) - - if failures: - if len(failures) == len(vifs): - 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 ports: %(ports)s."), - {'node': task.node.uuid, 'ports': failures}) - - _wait_for_neutron_update(task) - - -def _wait_for_neutron_update(task): - """Wait for Neutron agents to process all requested changes if required.""" - # TODO(adam_g): Hack to workaround bug 1334447 until we have a mechanism - # for synchronizing events with Neutron. We need to sleep only if we are - # booting VMs, which is implied by SSHPower, to ensure they do not boot - # before Neutron agents have setup sufficent DHCP config for netboot. - if isinstance(task.driver.power, ssh.SSHPower): - LOG.debug(_("Waiting 15 seconds for Neutron.")) - time.sleep(15) -- cgit v1.2.1