Building instance base codes

Change-Id: Ic34268df71625b39e588179e6586ca02466b5c6e
This commit is contained in:
Zhenguo Niu 2016-08-28 01:27:34 +08:00
parent 41d7272a4b
commit d71cb7b50c
18 changed files with 927 additions and 101 deletions

View File

@ -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 = <None>
# Ironic keystone admin password (string value)
#admin_password = <None>
# 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 = <None>
# Keystone public API endpoint (string value)
#admin_url = <None>
#
# 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 = <None>
# Ironic keystone tenant name (string value)
#admin_tenant_name = <None>
#
# 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 = <None>
[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 = <None>
# 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 = <None>
@ -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 = <None>
# 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 = <None>
# 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]
@ -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]

View File

@ -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)

View File

@ -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.')

View File

@ -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.
#
# 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.
# 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}
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:
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}
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:
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)
# This code should be unreachable actually
raise

View File

@ -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,14 +31,6 @@ 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'
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
@ -55,8 +48,24 @@ def get_client(token=None):
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})
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

View File

@ -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)

103
nimble/conf/ironic.py Normal file
View File

@ -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)

26
nimble/conf/keystone.py Normal file
View File

@ -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')

41
nimble/conf/neutron.py Normal file
View File

@ -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)

View File

@ -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),
]

View File

@ -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),

View File

@ -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)

View File

View File

@ -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)

View File

@ -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. """

View File

@ -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()

View File

@ -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)

View File

@ -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),
}