diff --git a/etc/nimble/nimble.conf.sample b/etc/nimble/nimble.conf.sample index d57e45a7..e5e6acd1 100644 --- a/etc/nimble/nimble.conf.sample +++ b/etc/nimble/nimble.conf.sample @@ -248,6 +248,13 @@ # Deprecated group/name - [DEFAULT]/rpc_zmq_serialization #rpc_zmq_serialization = json +# This option configures round-robin mode in zmq socket. True +# means not keeping a queue when server side disconnects. +# False means to keep queue and messages even if server is +# disconnected, when the server appears we send all +# accumulated messages to it. (boolean value) +#zmq_immediate = false + # Size of executor thread pool. (integer value) # Deprecated group/name - [DEFAULT]/rpc_thread_pool_size #executor_thread_pool_size = 64 @@ -565,6 +572,82 @@ #sync_node_resource_interval = 60 +[ironic] + +# +# From nimble +# + +# URL for the Ironic API endpoint (string value) +#api_endpoint = http://ironic.example.org:6385/ + +# Ironic keystone admin username (string value) +#admin_username = + +# Ironic keystone admin password (string value) +#admin_password = + +# DEPRECATED: +# Ironic keystone auth token. This option is deprecated and +# admin_username, admin_password and admin_tenant_name options +# are used for authorization. +# (string value) +# This option is deprecated for removal. +# Its value may be silently ignored in the future. +#admin_auth_token = + +# Keystone public API endpoint (string value) +#admin_url = + +# +# Path to the PEM encoded Certificate Authority file to be +# used when verifying +# HTTPs connections with the Ironic driver. By default this +# option is not used. +# +# Possible values: +# +# * None - Default +# * Path to the CA file +# (string value) +#cafile = + +# Ironic keystone tenant name (string value) +#admin_tenant_name = + +# +# The number of times to retry when a request conflicts. +# If set to 0, only try once, no retries. +# +# Related options: +# +# * api_retry_interval +# (integer value) +# Minimum value: 0 +#api_max_retries = 60 + +# +# The number of seconds to wait before retrying the request. +# +# Related options: +# +# * api_max_retries +# (integer value) +# Minimum value: 0 +#api_retry_interval = 2 + + +[keystone] + +# +# From nimble +# + +# The region used for getting endpoints of OpenStack services. +# (string value) +#region_name = + + [matchmaker_redis] # @@ -606,16 +689,34 @@ # Time in ms to wait between connection attempts. (integer # value) -#wait_timeout = 5000 +#wait_timeout = 2000 # Time in ms to wait before the transaction is killed. # (integer value) -#check_timeout = 60000 +#check_timeout = 20000 # Timeout in ms on blocking socket operations (integer value) #socket_timeout = 10000 +[neutron] + +# +# From nimble +# + +# URL for connecting to neutron. (string value) +#url = + +# Timeout value for connecting to neutron in seconds. (integer +# value) +#url_timeout = 30 + +# Client retries in the case of a failed request. (integer +# value) +#retries = 3 + + [oslo_concurrency] # @@ -641,22 +742,8 @@ # From oslo.messaging # -# address prefix used when sending to a specific server -# (string value) -# Deprecated group/name - [amqp1]/server_request_prefix -#server_request_prefix = exclusive - -# address prefix used when broadcasting to all servers (string -# value) -# Deprecated group/name - [amqp1]/broadcast_prefix -#broadcast_prefix = broadcast - -# address prefix when sending to any server in group (string -# value) -# Deprecated group/name - [amqp1]/group_request_prefix -#group_request_prefix = unicast - -# Name for the AMQP container (string value) +# Name for the AMQP container. must be globally unique. +# Defaults to a generated UUID (string value) # Deprecated group/name - [amqp1]/container_name #container_name = @@ -716,6 +803,122 @@ # Deprecated group/name - [amqp1]/password #password = +# Seconds to pause before attempting to re-connect. (integer +# value) +# Minimum value: 1 +#connection_retry_interval = 1 + +# Increase the connection_retry_interval by this many seconds +# after each unsuccessful failover attempt. (integer value) +# Minimum value: 0 +#connection_retry_backoff = 2 + +# Maximum limit for connection_retry_interval + +# connection_retry_backoff (integer value) +# Minimum value: 1 +#connection_retry_interval_max = 30 + +# Time to pause between re-connecting an AMQP 1.0 link that +# failed due to a recoverable error. (integer value) +# Minimum value: 1 +#link_retry_delay = 10 + +# The deadline for an rpc reply message delivery. Only used +# when caller does not provide a timeout expiry. (integer +# value) +# Minimum value: 5 +#default_reply_timeout = 30 + +# The deadline for an rpc cast or call message delivery. Only +# used when caller does not provide a timeout expiry. (integer +# value) +# Minimum value: 5 +#default_send_timeout = 30 + +# The deadline for a sent notification message delivery. Only +# used when caller does not provide a timeout expiry. (integer +# value) +# Minimum value: 5 +#default_notify_timeout = 30 + +# Indicates the addressing mode used by the driver. +# Permitted values: +# 'legacy' - use legacy non-routable addressing +# 'routable' - use routable addresses +# 'dynamic' - use legacy addresses if the message bus does +# not support routing otherwise use routable addressing +# (string value) +#addressing_mode = dynamic + +# address prefix used when sending to a specific server +# (string value) +# Deprecated group/name - [amqp1]/server_request_prefix +#server_request_prefix = exclusive + +# address prefix used when broadcasting to all servers (string +# value) +# Deprecated group/name - [amqp1]/broadcast_prefix +#broadcast_prefix = broadcast + +# address prefix when sending to any server in group (string +# value) +# Deprecated group/name - [amqp1]/group_request_prefix +#group_request_prefix = unicast + +# Address prefix for all generated RPC addresses (string +# value) +#rpc_address_prefix = openstack.org/om/rpc + +# Address prefix for all generated Notification addresses +# (string value) +#notify_address_prefix = openstack.org/om/notify + +# Appended to the address prefix when sending a fanout +# message. Used by the message bus to identify fanout +# messages. (string value) +#multicast_address = multicast + +# Appended to the address prefix when sending to a particular +# RPC/Notification server. Used by the message bus to identify +# messages sent to a single destination. (string value) +#unicast_address = unicast + +# Appended to the address prefix when sending to a group of +# consumers. Used by the message bus to identify messages that +# should be delivered in a round-robin fashion across +# consumers. (string value) +#anycast_address = anycast + +# Exchange name used in notification addresses. +# Exchange name resolution precedence: +# Target.exchange if set +# else default_notification_exchange if set +# else control_exchange if set +# else 'notify' (string value) +#default_notification_exchange = + +# Exchange name used in RPC addresses. +# Exchange name resolution precedence: +# Target.exchange if set +# else default_rpc_exchange if set +# else control_exchange if set +# else 'rpc' (string value) +#default_rpc_exchange = + +# Window size for incoming RPC Reply messages. (integer value) +# Minimum value: 1 +#reply_link_credit = 200 + +# Window size for incoming RPC Request messages (integer +# value) +# Minimum value: 1 +#rpc_server_credit = 100 + +# Window size for incoming Notification messages (integer +# value) +# Minimum value: 1 +#notify_server_credit = 100 + [oslo_messaging_notifications] @@ -781,7 +984,7 @@ #kombu_reconnect_delay = 1.0 # EXPERIMENTAL: Possible values are: gzip, bz2. If not set -# compression will not be used. This option may notbe +# compression will not be used. This option may not be # available in future versions. (string value) #kombu_compression = @@ -1113,6 +1316,13 @@ # Deprecated group/name - [DEFAULT]/rpc_zmq_serialization #rpc_zmq_serialization = json +# This option configures round-robin mode in zmq socket. True +# means not keeping a queue when server side disconnects. +# False means to keep queue and messages even if server is +# disconnected, when the server appears we send all +# accumulated messages to it. (boolean value) +#zmq_immediate = false + [oslo_policy] diff --git a/nimble/api/controllers/v1/instance.py b/nimble/api/controllers/v1/instance.py index 3fbe9ca5..46ce64b8 100644 --- a/nimble/api/controllers/v1/instance.py +++ b/nimble/api/controllers/v1/instance.py @@ -33,6 +33,8 @@ class Instance(base.APIBase): between the internal object model and the API representation of a instance. """ + id = wsme.wsattr(wtypes.IntegerType(minimum=1)) + """The ID of the instance""" uuid = types.uuid """The UUID of the instance""" @@ -55,6 +57,15 @@ class Instance(base.APIBase): availability_zone = wtypes.text """The availability zone of the instance""" + instance_type_id = wsme.wsattr(wtypes.IntegerType(minimum=1)) + """The instance type ID of the instance""" + + image_uuid = types.uuid + """The image UUID of the instance""" + + network_uuid = types.uuid + """The network UUID of the instance""" + links = wsme.wsattr([link.Link], readonly=True) """A list containing a self link""" @@ -129,9 +140,11 @@ class InstanceController(rest.RestController): # Set the HTTP Location Header pecan.response.location = link.build_url('instance', instance_obj.uuid) - new_instance = pecan.request.rpcapi.create_instance( - pecan.request.context, instance_obj) - return Instance.convert_with_links(new_instance) + pecan.request.rpcapi.create_instance(pecan.request.context, + instance_obj) + instance_obj.status = 'building' + instance_obj.save() + return Instance.convert_with_links(instance_obj) @expose.expose(None, types.uuid, status_code=http_client.NO_CONTENT) def delete(self, instance_uuid): @@ -141,4 +154,5 @@ class InstanceController(rest.RestController): """ rpc_instance = objects.Instance.get(pecan.request.context, instance_uuid) - rpc_instance.destroy() + pecan.request.rpcapi.delete_instance(pecan.request.context, + rpc_instance) diff --git a/nimble/common/exception.py b/nimble/common/exception.py index a3ae030e..f4560978 100644 --- a/nimble/common/exception.py +++ b/nimble/common/exception.py @@ -155,6 +155,10 @@ class InstanceNotFound(NotFound): msg_fmt = _("Instance %(instance)s could not be found.") +class InstanceDeployFailure(Invalid): + msg_fmt = _("Failed to deploy instance: %(reason)s") + + class NoFreeEngineWorker(TemporaryFailure): _msg_fmt = _('Requested action cannot be performed due to lack of free ' 'engine workers.') diff --git a/nimble/common/ironic.py b/nimble/common/ironic.py index df3e2847..250a3f94 100644 --- a/nimble/common/ironic.py +++ b/nimble/common/ironic.py @@ -1,61 +1,148 @@ -# 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 +# Copyright 2016 Huawei Technologies Co.,LTD. +# All Rights Reserved. # -# http://www.apache.org/licenses/LICENSE-2.0 +# 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. +# 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 ironicclient import client from oslo_config import cfg +from oslo_log import log as logging +from oslo_utils import importutils -from nimble.common import keystone +from nimble.common import exception +from nimble.common.i18n import _ + +LOG = logging.getLogger(__name__) CONF = cfg.CONF -# 1.11 is API version, which support 'enroll' state -DEFAULT_IRONIC_API_VERSION = '1.11' +ironic = None -IRONIC_GROUP = 'ironic' - -IRONIC_SESSION = None -LEGACY_MAP = { - 'auth_url': 'os_auth_url', - 'username': 'os_username', - 'password': 'os_password', - 'tenant_name': 'os_tenant_name' -} +# The API version required by the Ironic driver +IRONIC_API_VERSION = (1, 21) -def get_client(token=None, - api_version=DEFAULT_IRONIC_API_VERSION): # pragma: no cover - """Get Ironic client instance.""" - # NOTE: To support standalone ironic without keystone - if CONF.ironic.auth_strategy == 'noauth': - args = {'token': 'noauth', - 'endpoint': CONF.ironic.ironic_url} - else: - global IRONIC_SESSION - if not IRONIC_SESSION: - IRONIC_SESSION = keystone.get_session( - IRONIC_GROUP, legacy_mapping=LEGACY_MAP) - if token is None: - args = {'session': IRONIC_SESSION, - 'region_name': CONF.ironic.os_region} +class IronicClientWrapper(object): + """Ironic client wrapper class that encapsulates authentication logic.""" + + def __init__(self): + """Initialise the IronicClientWrapper for use. + + Initialise IronicClientWrapper by loading ironicclient + dynamically so that ironicclient is not a dependency for + Nimble. + """ + global ironic + if ironic is None: + ironic = importutils.import_module('ironicclient') + # NOTE(deva): work around a lack of symbols in the current version. + if not hasattr(ironic, 'exc'): + ironic.exc = importutils.import_module('ironicclient.exc') + if not hasattr(ironic, 'client'): + ironic.client = importutils.import_module( + 'ironicclient.client') + self._cached_client = None + + def _invalidate_cached_client(self): + """Tell the wrapper to invalidate the cached ironic-client.""" + self._cached_client = None + + def _get_client(self, retry_on_conflict=True): + max_retries = CONF.ironic.api_max_retries if retry_on_conflict else 1 + retry_interval = (CONF.ironic.api_retry_interval + if retry_on_conflict else 0) + + # If we've already constructed a valid, authed client, just return + # that. + if retry_on_conflict and self._cached_client is not None: + return self._cached_client + + auth_token = CONF.ironic.admin_auth_token + if auth_token is None: + kwargs = {'os_username': CONF.ironic.admin_username, + 'os_password': CONF.ironic.admin_password, + 'os_auth_url': CONF.ironic.admin_url, + 'os_tenant_name': CONF.ironic.admin_tenant_name, + 'os_service_type': 'baremetal', + 'os_endpoint_type': 'public', + 'ironic_url': CONF.ironic.api_endpoint} else: - ironic_url = IRONIC_SESSION.get_endpoint( - service_type=CONF.ironic.os_service_type, - endpoint_type=CONF.ironic.os_endpoint_type, - region_name=CONF.ironic.os_region - ) - args = {'token': token, - 'endpoint': ironic_url} - args['os_ironic_api_version'] = api_version - args['max_retries'] = CONF.ironic.max_retries - args['retry_interval'] = CONF.ironic.retry_interval - return client.Client(1, **args) + kwargs = {'os_auth_token': auth_token, + 'ironic_url': CONF.ironic.api_endpoint} + + if CONF.ironic.cafile: + kwargs['os_cacert'] = CONF.ironic.cafile + # Set the old option for compat with old clients + kwargs['ca_file'] = CONF.ironic.cafile + + # Retries for Conflict exception + kwargs['max_retries'] = max_retries + kwargs['retry_interval'] = retry_interval + kwargs['os_ironic_api_version'] = '%d.%d' % IRONIC_API_VERSION + try: + cli = ironic.client.get_client(IRONIC_API_VERSION[0], **kwargs) + # Cache the client so we don't have to reconstruct and + # reauthenticate it every time we need it. + if retry_on_conflict: + self._cached_client = cli + + except ironic.exc.Unauthorized: + msg = _("Unable to authenticate Ironic client.") + LOG.error(msg) + raise exception.NimbleException(msg) + + return cli + + def _multi_getattr(self, obj, attr): + """Support nested attribute path for getattr(). + + :param obj: Root object. + :param attr: Path of final attribute to get. E.g., "a.b.c.d" + + :returns: The value of the final named attribute. + :raises: AttributeError will be raised if the path is invalid. + """ + for attribute in attr.split("."): + obj = getattr(obj, attribute) + return obj + + def call(self, method, *args, **kwargs): + """Call an Ironic client method and retry on stale token. + + :param method: Name of the client method to call as a string. + :param args: Client method arguments. + :param kwargs: Client method keyword arguments. + :param retry_on_conflict: Boolean value. Whether the request should be + retried in case of a conflict error + (HTTP 409) or not. If retry_on_conflict is + False the cached instance of the client + won't be used. Defaults to True. + """ + retry_on_conflict = kwargs.pop('retry_on_conflict', True) + + # NOTE(dtantsur): allow for authentication retry, other retries are + # handled by ironicclient starting with 0.8.0 + for attempt in range(2): + client = self._get_client(retry_on_conflict=retry_on_conflict) + + try: + return self._multi_getattr(client, method)(*args, **kwargs) + except ironic.exc.Unauthorized: + # In this case, the authorization token of the cached + # ironic-client probably expired. So invalidate the cached + # client and the next try will start with a fresh one. + if not attempt: + self._invalidate_cached_client() + LOG.debug("The Ironic client became unauthorized. " + "Will attempt to reauthorize and try again.") + else: + # This code should be unreachable actually + raise diff --git a/nimble/common/neutron.py b/nimble/common/neutron.py index 1546f3d6..964b0e5b 100644 --- a/nimble/common/neutron.py +++ b/nimble/common/neutron.py @@ -11,11 +11,12 @@ # under the License. from neutronclient.v2_0 import client as clientv20 +from oslo_log import log as logging from nimble.common import keystone from nimble.conf import CONF -DEFAULT_NEUTRON_URL = 'http://%s:9696' % CONF.my_ip +LOG = logging.getLogger(__name__) _NEUTRON_SESSION = None @@ -30,33 +31,41 @@ def _get_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' + 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, + 'timeout': CONF.neutron.url_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 create_ports(context, network_uuid, macs): + """Create neutron port.""" + + client = get_client(context.auth_token) + body = { + 'port': { + 'network_id': network_uuid, + 'mac_address': macs + } + } + + port = client.create_port(body) + + return port diff --git a/nimble/conf/__init__.py b/nimble/conf/__init__.py index 3587b2c7..41c1ea56 100644 --- a/nimble/conf/__init__.py +++ b/nimble/conf/__init__.py @@ -19,6 +19,9 @@ from nimble.conf import api from nimble.conf import database from nimble.conf import default from nimble.conf import engine +from nimble.conf import ironic +from nimble.conf import keystone +from nimble.conf import neutron CONF = cfg.CONF @@ -26,3 +29,6 @@ api.register_opts(CONF) database.register_opts(CONF) default.register_opts(CONF) engine.register_opts(CONF) +ironic.register_opts(CONF) +keystone.register_opts(CONF) +neutron.register_opts(CONF) diff --git a/nimble/conf/ironic.py b/nimble/conf/ironic.py new file mode 100644 index 00000000..5a01d378 --- /dev/null +++ b/nimble/conf/ironic.py @@ -0,0 +1,103 @@ +# Copyright 2015 Intel Corporation +# 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 oslo_config import cfg + +opt_group = cfg.OptGroup( + 'ironic', + title='Ironic Options', + help=""" +Configuration options for Ironic driver (Bare Metal). +If using the Ironic driver following options must be set: +* admin_url +* admin_tenant_name +* admin_username +* admin_password +* api_endpoint +""") + +opts = [ + cfg.StrOpt( + # TODO(raj_singh): Get this value from keystone service catalog + 'api_endpoint', + sample_default='http://ironic.example.org:6385/', + help='URL for the Ironic API endpoint'), + cfg.StrOpt( + 'admin_username', + help='Ironic keystone admin username'), + cfg.StrOpt( + 'admin_password', + secret=True, + help='Ironic keystone admin password'), + cfg.StrOpt( + 'admin_auth_token', + secret=True, + deprecated_for_removal=True, + help=""" +Ironic keystone auth token. This option is deprecated and +admin_username, admin_password and admin_tenant_name options +are used for authorization. +"""), + cfg.StrOpt( + # TODO(raj_singh): Change this option admin_url->auth_url to make it + # consistent with other clients (Neutron, Cinder). It requires lot + # of work in Ironic client to make this happen. + 'admin_url', + help='Keystone public API endpoint'), + cfg.StrOpt( + 'cafile', + default=None, + help=""" +Path to the PEM encoded Certificate Authority file to be used when verifying +HTTPs connections with the Ironic driver. By default this option is not used. + +Possible values: + +* None - Default +* Path to the CA file +"""), + cfg.StrOpt( + 'admin_tenant_name', + help='Ironic keystone tenant name'), + cfg.IntOpt( + 'api_max_retries', + # TODO(raj_singh): Change this default to some sensible number + default=60, + min=0, + help=""" +The number of times to retry when a request conflicts. +If set to 0, only try once, no retries. + +Related options: + +* api_retry_interval +"""), + cfg.IntOpt( + 'api_retry_interval', + default=2, + min=0, + help=""" +The number of seconds to wait before retrying the request. + +Related options: + +* api_max_retries +"""), +] + + +def register_opts(conf): + conf.register_group(opt_group) + conf.register_opts(opts, group=opt_group) diff --git a/nimble/conf/keystone.py b/nimble/conf/keystone.py new file mode 100644 index 00000000..fb50e88b --- /dev/null +++ b/nimble/conf/keystone.py @@ -0,0 +1,26 @@ +# +# 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 oslo_config import cfg + +from nimble.common.i18n import _ + +opts = [ + cfg.StrOpt('region_name', + help=_('The region used for getting endpoints of OpenStack' + ' services.')), +] + + +def register_opts(conf): + conf.register_opts(opts, group='keystone') diff --git a/nimble/conf/neutron.py b/nimble/conf/neutron.py new file mode 100644 index 00000000..634934e2 --- /dev/null +++ b/nimble/conf/neutron.py @@ -0,0 +1,41 @@ +# +# 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 oslo_config import cfg + +from nimble.common.i18n import _ +from nimble.conf import auth + +opts = [ + cfg.StrOpt('url', + help=_("URL for connecting to neutron.")), + cfg.IntOpt('url_timeout', + default=30, + help=_('Timeout value for connecting to neutron in seconds.')), + cfg.IntOpt('retries', + default=3, + help=_('Client retries in the case of a failed request.')), +] + +opt_group = cfg.OptGroup(name='neutron', + title='Options for the neutron service') + + +def register_opts(conf): + conf.register_group(opt_group) + conf.register_opts(opts, group=opt_group) + auth.register_auth_opts(conf, 'neutron') + + +def list_opts(): + return auth.add_auth_opts(opts) diff --git a/nimble/conf/opts.py b/nimble/conf/opts.py index f4060998..40e044a6 100644 --- a/nimble/conf/opts.py +++ b/nimble/conf/opts.py @@ -16,6 +16,9 @@ import nimble.conf.api import nimble.conf.database import nimble.conf.default import nimble.conf.engine +import nimble.conf.ironic +import nimble.conf.keystone +import nimble.conf.neutron _default_opt_lists = [ nimble.conf.default.api_opts, @@ -29,6 +32,9 @@ _opts = [ ('api', nimble.conf.api.opts), ('database', nimble.conf.database.opts), ('engine', nimble.conf.engine.opts), + ('ironic', nimble.conf.ironic.opts), + ('keystone', nimble.conf.keystone.opts), + ('neutron', nimble.conf.neutron.opts), ] diff --git a/nimble/db/sqlalchemy/alembic/versions/91941bf1ebc9_initial_migration.py b/nimble/db/sqlalchemy/alembic/versions/91941bf1ebc9_initial_migration.py index 8c0f0638..aabe336c 100644 --- a/nimble/db/sqlalchemy/alembic/versions/91941bf1ebc9_initial_migration.py +++ b/nimble/db/sqlalchemy/alembic/versions/91941bf1ebc9_initial_migration.py @@ -68,6 +68,8 @@ def upgrade(): sa.Column('power_state', sa.String(length=255), nullable=True), sa.Column('task_state', sa.String(length=255), nullable=True), sa.Column('instance_type_id', sa.Integer(), nullable=True), + sa.Column('image_uuid', sa.String(length=36), nullable=True), + sa.Column('network_uuid', sa.String(length=36), nullable=True), sa.Column('launched_at', sa.DateTime(), nullable=True), sa.Column('terminated_at', sa.DateTime(), nullable=True), sa.Column('availability_zone', sa.String(length=255), nullable=True), diff --git a/nimble/db/sqlalchemy/models.py b/nimble/db/sqlalchemy/models.py index 924709a3..0290c9df 100644 --- a/nimble/db/sqlalchemy/models.py +++ b/nimble/db/sqlalchemy/models.py @@ -104,5 +104,7 @@ class Instance(Base): task_state = Column(String(255), nullable=True) instance_type_id = Column(Integer, nullable=True) availability_zone = Column(String(255), nullable=True) + image_uuid = Column(String(36), nullable=True) + network_uuid = Column(String(36), nullable=True) node_uuid = Column(String(36), nullable=True) extra = Column(Text, nullable=True) diff --git a/nimble/engine/baremetal/__init__.py b/nimble/engine/baremetal/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/nimble/engine/baremetal/ironic.py b/nimble/engine/baremetal/ironic.py new file mode 100644 index 00000000..f8489744 --- /dev/null +++ b/nimble/engine/baremetal/ironic.py @@ -0,0 +1,83 @@ +# Copyright 2016 Huawei Technologies Co.,LTD. +# 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 nimble.common import ironic +from nimble.engine.baremetal import ironic_states + +_NODE_FIELDS = ('uuid', 'power_state', 'target_power_state', 'provision_state', + 'target_provision_state', 'last_error', 'maintenance', + 'properties', 'instance_uuid') + + +def get_macs_from_node(node_uuid): + """List the MAC addresses from a node.""" + ironicclient = ironic.IronicClientWrapper() + ports = ironicclient.call("node.list_ports", node_uuid) + return [p.address for p in ports] + + +def plug_vifs(node_uuid, port_id): + ironicclient = ironic.IronicClientWrapper() + ports = ironicclient.call("node.list_ports", node_uuid) + patch = [{'op': 'add', + 'path': '/extra/vif_port_id', + 'value': port_id}] + ironicclient.call("port.update", ports[0].uuid, patch) + + +def set_instance_info(instance): + ironicclient = ironic.IronicClientWrapper() + + patch = [] + # Associate the node with an instance + patch.append({'path': '/instance_uuid', 'op': 'add', + 'value': instance.uuid}) + # Add the required fields to deploy a node. + patch.append({'path': '/instance_info/image_source', 'op': 'add', + 'value': instance.image_uuid}) + patch.append({'path': '/instance_info/root_gb', 'op': 'add', + 'value': '10'}) + patch.append({'path': '/instance_info/swap_mb', 'op': 'add', + 'value': '0'}) + patch.append({'path': '/instance_info/display_name', + 'op': 'add', 'value': instance.name}) + patch.append({'path': '/instance_info/vcpus', 'op': 'add', + 'value': '1'}) + patch.append({'path': '/instance_info/memory_mb', 'op': 'add', + 'value': '10240'}) + patch.append({'path': '/instance_info/local_gb', 'op': 'add', + 'value': '10'}) + + ironicclient.call("node.update", instance.node_uuid, patch) + + +def do_node_deploy(node_uuid): + # trigger the node deploy + ironicclient = ironic.IronicClientWrapper() + ironicclient.call("node.set_provision_state", node_uuid, + ironic_states.ACTIVE) + + +def get_node_by_instance(instance_uuid): + ironicclient = ironic.IronicClientWrapper() + return ironicclient.call('node.get_by_instance_uuid', + instance_uuid, fields=_NODE_FIELDS) + + +def destroy_node(node_uuid): + # trigger the node destroy + ironicclient = ironic.IronicClientWrapper() + ironicclient.call("node.set_provision_state", node_uuid, + ironic_states.DELETED) diff --git a/nimble/engine/baremetal/ironic_states.py b/nimble/engine/baremetal/ironic_states.py new file mode 100644 index 00000000..162dbcd7 --- /dev/null +++ b/nimble/engine/baremetal/ironic_states.py @@ -0,0 +1,156 @@ +# Copyright (c) 2012 NTT DOCOMO, INC. +# Copyright 2010 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. + +""" +Mapping of bare metal node states. + +Setting the node `power_state` is handled by the conductor's power +synchronization thread. Based on the power state retrieved from the driver +for the node, the state is set to POWER_ON or POWER_OFF, accordingly. +Should this fail, the `power_state` value is left unchanged, and the node +is placed into maintenance mode. + +The `power_state` can also be set manually via the API. A failure to change +the state leaves the current state unchanged. The node is NOT placed into +maintenance mode in this case. +""" + + +##################### +# Provisioning states +##################### + +NOSTATE = None +""" No state information. + +This state is used with power_state to represent a lack of knowledge of +power state, and in target_*_state fields when there is no target. + +Prior to the Kilo release, Ironic set node.provision_state to NOSTATE +when the node was available for provisioning. During Kilo cycle, this was +changed to the AVAILABLE state. +""" + +MANAGEABLE = 'manageable' +""" Node is in a manageable state. +This state indicates that Ironic has verified, at least once, that it had +sufficient information to manage the hardware. While in this state, the node +is not available for provisioning (it must be in the AVAILABLE state for that). +""" + +AVAILABLE = 'available' +""" Node is available for use and scheduling. + +This state is replacing the NOSTATE state used prior to Kilo. +""" + +ACTIVE = 'active' +""" Node is successfully deployed and associated with an instance. """ + +DEPLOYWAIT = 'wait call-back' +""" Node is waiting to be deployed. + +This will be the node `provision_state` while the node is waiting for +the driver to finish deployment. +""" + +DEPLOYING = 'deploying' +""" Node is ready to receive a deploy request, or is currently being deployed. + +A node will have its `provision_state` set to DEPLOYING briefly before it +receives its initial deploy request. It will also move to this state from +DEPLOYWAIT after the callback is triggered and deployment is continued +(disk partitioning and image copying). +""" + +DEPLOYFAIL = 'deploy failed' +""" Node deployment failed. """ + +DEPLOYDONE = 'deploy complete' +""" Node was successfully deployed. + +This is mainly a target provision state used during deployment. A successfully +deployed node should go to ACTIVE status. +""" + +DELETING = 'deleting' +""" Node is actively being torn down. """ + +DELETED = 'deleted' +""" Node tear down was successful. + +In Juno, target_provision_state was set to this value during node tear down. +In Kilo, this will be a transitory value of provision_state, and never +represented in target_provision_state. +""" + +CLEANING = 'cleaning' +""" Node is being automatically cleaned to prepare it for provisioning. """ + +CLEANWAIT = 'clean wait' +""" Node is waiting for a clean step to be finished. + +This will be the node's `provision_state` while the node is waiting for +the driver to finish a cleaning step. +""" + +CLEANFAIL = 'clean failed' +""" Node failed cleaning. This requires operator intervention to resolve. """ + +ERROR = 'error' +""" An error occurred during node processing. + +The `last_error` attribute of the node details should contain an error message. +""" + +REBUILD = 'rebuild' +""" Node is to be rebuilt. +This is not used as a state, but rather as a "verb" when changing the node's +provision_state via the REST API. +""" + +INSPECTING = 'inspecting' +""" Node is under inspection. +This is the provision state used when inspection is started. A successfully +inspected node shall transition to MANAGEABLE status. +""" + +INSPECTFAIL = 'inspect failed' +""" Node inspection failed. """ + + +############## +# Power states +############## + +POWER_ON = 'power on' +""" Node is powered on. """ + +POWER_OFF = 'power off' +""" Node is powered off. """ + +REBOOT = 'rebooting' +""" Node is rebooting. """ + +################## +# Helper constants +################## + +PROVISION_STATE_LIST = (NOSTATE, MANAGEABLE, AVAILABLE, ACTIVE, DEPLOYWAIT, + DEPLOYING, DEPLOYFAIL, DEPLOYDONE, DELETING, DELETED, + CLEANING, CLEANWAIT, CLEANFAIL, ERROR, REBUILD, + INSPECTING, INSPECTFAIL) +""" A list of all provision states. """ diff --git a/nimble/engine/manager.py b/nimble/engine/manager.py index d3e1f462..6eb6c122 100644 --- a/nimble/engine/manager.py +++ b/nimble/engine/manager.py @@ -15,10 +15,15 @@ from oslo_log import log import oslo_messaging as messaging +from oslo_service import loopingcall from oslo_service import periodic_task +from nimble.common import exception from nimble.common.i18n import _LI +from nimble.common import neutron from nimble.conf import CONF +from nimble.engine.baremetal import ironic +from nimble.engine.baremetal import ironic_states from nimble.engine import base_manager MANAGER_TOPIC = 'nimble.engine_manager' @@ -38,9 +43,74 @@ class EngineManager(base_manager.BaseEngineManager): def _sync_node_resources(self, context): LOG.info(_LI("During sync_node_resources.")) + def _build_networks(self, context, instance): + macs = ironic.get_macs_from_node(instance.node_uuid) + port = neutron.create_ports(context, instance.network_uuid, macs[0]) + ironic.plug_vifs(instance.node_uuid, port['port']['id']) + + def _wait_for_active(self, instance): + """Wait for the node to be marked as ACTIVE in Ironic.""" + + node = ironic.get_node_by_instance(instance.uuid) + LOG.debug('Current ironic node state is %s', node.provision_state) + if node.provision_state == ironic_states.ACTIVE: + # job is done + LOG.debug("Ironic node %(node)s is now ACTIVE", + dict(node=node.uuid)) + instance.status = ironic_states.ACTIVE + instance.save() + raise loopingcall.LoopingCallDone() + + if node.target_provision_state in (ironic_states.DELETED, + ironic_states.AVAILABLE): + # ironic is trying to delete it now + raise exception.InstanceNotFound(instance_id=instance.uuid) + + if node.provision_state in (ironic_states.NOSTATE, + ironic_states.AVAILABLE): + # ironic already deleted it + raise exception.InstanceNotFound(instance_id=instance.uuid) + + if node.provision_state == ironic_states.DEPLOYFAIL: + # ironic failed to deploy + msg = (_("Failed to provision instance %(inst)s: %(reason)s") + % {'inst': instance.uuid, 'reason': node.last_error}) + raise exception.InstanceDeployFailure(msg) + + def _build_instance(self, context, instance): + ironic.set_instance_info(instance) + ironic.do_node_deploy(instance.node_uuid) + + timer = loopingcall.FixedIntervalLoopingCall(self._wait_for_active, + instance) + timer.start(interval=CONF.ironic.api_retry_interval).wait() + LOG.info(_LI('Successfully provisioned Ironic node %s'), + instance.node_uuid) + + def _destroy_instance(self, context, instance): + ironic.destroy_node(instance.node_uuid) + LOG.info(_LI('Successfully destroyed Ironic node %s'), + instance.node_uuid) + def create_instance(self, context, instance): """Signal to engine service to perform a deployment.""" - LOG.debug("During create instance.") - instance.task_state = 'deploying' + LOG.debug("Strating instance...") + instance.status = 'building' + + # Scheduling... + # instance.node_uuid = '8d22309b-b47a-41a7-80e3-e758fae9dedd' instance.save() + + self._build_networks(context, instance) + + self._build_instance(context, instance) + return instance + + def delete_instance(self, context, instance): + """Signal to engine service to delete an instance.""" + LOG.debug("Deleting instance...") + + self._destroy_instance(context, instance) + + instance.destroy() diff --git a/nimble/engine/rpcapi.py b/nimble/engine/rpcapi.py index 9354b659..97b38d5d 100644 --- a/nimble/engine/rpcapi.py +++ b/nimble/engine/rpcapi.py @@ -52,4 +52,9 @@ class EngineAPI(object): def create_instance(self, context, instance): """Signal to engine service to perform a deployment.""" cctxt = self.client.prepare(topic=self.topic, server=CONF.host) - return cctxt.call(context, 'create_instance', instance=instance) + return cctxt.cast(context, 'create_instance', instance=instance) + + def delete_instance(self, context, instance): + """Signal to engine service to delete an instance.""" + cctxt = self.client.prepare(topic=self.topic, server=CONF.host) + return cctxt.call(context, 'delete_instance', instance=instance) diff --git a/nimble/objects/instance.py b/nimble/objects/instance.py index 1ddb8b7a..6b61f01e 100644 --- a/nimble/objects/instance.py +++ b/nimble/objects/instance.py @@ -38,6 +38,8 @@ class Instance(base.NimbleObject, object_base.VersionedObjectDictCompat): 'task_state': object_fields.StringField(nullable=True), 'instance_type_id': object_fields.IntegerField(nullable=True), 'availability_zone': object_fields.StringField(nullable=True), + 'image_uuid': object_fields.UUIDField(nullable=True), + 'network_uuid': object_fields.UUIDField(nullable=True), 'node_uuid': object_fields.UUIDField(nullable=True), 'extra': object_fields.FlexibleDictField(nullable=True), }