Merge "Add instance list and detail api"
This commit is contained in:
commit
9639edd5cd
@ -14,6 +14,7 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import jsonschema
|
import jsonschema
|
||||||
|
from oslo_log import log
|
||||||
import pecan
|
import pecan
|
||||||
from pecan import rest
|
from pecan import rest
|
||||||
from six.moves import http_client
|
from six.moves import http_client
|
||||||
@ -26,9 +27,17 @@ from nimble.api.controllers.v1 import types
|
|||||||
from nimble.api import expose
|
from nimble.api import expose
|
||||||
from nimble.common import exception
|
from nimble.common import exception
|
||||||
from nimble.common.i18n import _
|
from nimble.common.i18n import _
|
||||||
|
from nimble.common.i18n import _LW
|
||||||
|
from nimble.engine.baremetal.ironic import get_node_by_instance
|
||||||
|
from nimble.engine.baremetal.ironic import get_node_list
|
||||||
from nimble.engine.baremetal import ironic_states as ir_states
|
from nimble.engine.baremetal import ironic_states as ir_states
|
||||||
from nimble import objects
|
from nimble import objects
|
||||||
|
|
||||||
|
_DEFAULT_INSTANCE_RETURN_FIELDS = ('uuid', 'name', 'description',
|
||||||
|
'status')
|
||||||
|
_NODE_FIELDS = ['power_state', 'instance_uuid']
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
_CREATE_INSTANCE_SCHEMA = {
|
_CREATE_INSTANCE_SCHEMA = {
|
||||||
"$schema": "http://json-schema.org/schema#",
|
"$schema": "http://json-schema.org/schema#",
|
||||||
@ -162,9 +171,6 @@ class Instance(base.APIBase):
|
|||||||
power_state = wtypes.text
|
power_state = wtypes.text
|
||||||
"""The power state of the instance"""
|
"""The power state of the instance"""
|
||||||
|
|
||||||
task_state = wtypes.text
|
|
||||||
"""The task state of the instance"""
|
|
||||||
|
|
||||||
availability_zone = wtypes.text
|
availability_zone = wtypes.text
|
||||||
"""The availability zone of the instance"""
|
"""The availability zone of the instance"""
|
||||||
|
|
||||||
@ -181,6 +187,7 @@ class Instance(base.APIBase):
|
|||||||
"""A list containing a self link"""
|
"""A list containing a self link"""
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
|
super(Instance, self).__init__(**kwargs)
|
||||||
self.fields = []
|
self.fields = []
|
||||||
for field in objects.Instance.fields:
|
for field in objects.Instance.fields:
|
||||||
# Skip fields we do not expose.
|
# Skip fields we do not expose.
|
||||||
@ -190,18 +197,20 @@ class Instance(base.APIBase):
|
|||||||
setattr(self, field, kwargs.get(field, wtypes.Unset))
|
setattr(self, field, kwargs.get(field, wtypes.Unset))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def convert_with_links(cls, rpc_instance):
|
def convert_with_links(cls, instance_data, fields=None):
|
||||||
instance = Instance(**rpc_instance.as_dict())
|
instance = Instance(**instance_data)
|
||||||
|
instance_uuid = instance.uuid
|
||||||
|
if fields is not None:
|
||||||
|
instance.unset_fields_except(fields)
|
||||||
url = pecan.request.public_url
|
url = pecan.request.public_url
|
||||||
instance.links = [link.Link.make_link('self',
|
instance.links = [link.Link.make_link('self',
|
||||||
url,
|
url,
|
||||||
'instances', instance.uuid),
|
'instances', instance_uuid),
|
||||||
link.Link.make_link('bookmark',
|
link.Link.make_link('bookmark',
|
||||||
url,
|
url,
|
||||||
'instances', instance.uuid,
|
'instances', instance_uuid,
|
||||||
bookmark=True)
|
bookmark=True)
|
||||||
]
|
]
|
||||||
|
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
|
||||||
@ -212,10 +221,10 @@ class InstanceCollection(base.APIBase):
|
|||||||
"""A list containing instance objects"""
|
"""A list containing instance objects"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def convert_with_links(instances, url=None, **kwargs):
|
def convert_with_links(instances_data, fields=None):
|
||||||
collection = InstanceCollection()
|
collection = InstanceCollection()
|
||||||
collection.instances = [Instance.convert_with_links(inst)
|
collection.instances = [Instance.convert_with_links(inst, fields)
|
||||||
for inst in instances]
|
for inst in instances_data]
|
||||||
return collection
|
return collection
|
||||||
|
|
||||||
|
|
||||||
@ -224,22 +233,84 @@ class InstanceController(rest.RestController):
|
|||||||
|
|
||||||
states = InstanceStatesController()
|
states = InstanceStatesController()
|
||||||
|
|
||||||
@expose.expose(InstanceCollection)
|
_custom_actions = {
|
||||||
def get_all(self):
|
'detail': ['GET']
|
||||||
"""Retrieve a list of instance."""
|
}
|
||||||
|
|
||||||
|
def _get_instance_collection(self, fields=None):
|
||||||
instances = objects.Instance.list(pecan.request.context)
|
instances = objects.Instance.list(pecan.request.context)
|
||||||
return InstanceCollection.convert_with_links(instances)
|
instances_data = [instance.as_dict() for instance in instances]
|
||||||
|
|
||||||
@expose.expose(Instance, types.uuid)
|
if fields is None or 'power_state' in fields:
|
||||||
def get_one(self, instance_uuid):
|
try:
|
||||||
|
node_list = get_node_list(
|
||||||
|
associated=True, limit=0,
|
||||||
|
fields=_NODE_FIELDS)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
LOG.warning(
|
||||||
|
_LW("Failed to retrieve node list from"
|
||||||
|
"ironic api: %(msg)s") % {"msg": e})
|
||||||
|
node_list = []
|
||||||
|
|
||||||
|
if node_list:
|
||||||
|
node_dict = {node.instance_uuid: node.to_dict()
|
||||||
|
for node in node_list}
|
||||||
|
# Merge nimble instance info with ironic node power state
|
||||||
|
for instance_data in instances_data:
|
||||||
|
uuid = instance_data['uuid']
|
||||||
|
if uuid in node_dict:
|
||||||
|
instance_data['power_state'] = \
|
||||||
|
node_dict[uuid]['power_state']
|
||||||
|
|
||||||
|
return InstanceCollection.convert_with_links(instances_data,
|
||||||
|
fields=fields)
|
||||||
|
|
||||||
|
@expose.expose(InstanceCollection, types.listtype)
|
||||||
|
def get_all(self, fields=None):
|
||||||
|
"""Retrieve a list of instance.
|
||||||
|
|
||||||
|
:param fields: Optional, a list with a specified set of fields
|
||||||
|
of the resource to be returned.
|
||||||
|
"""
|
||||||
|
if fields is None:
|
||||||
|
fields = _DEFAULT_INSTANCE_RETURN_FIELDS
|
||||||
|
return self._get_instance_collection(fields=fields)
|
||||||
|
|
||||||
|
@expose.expose(Instance, types.uuid, types.listtype)
|
||||||
|
def get_one(self, instance_uuid, fields=None):
|
||||||
"""Retrieve information about the given instance.
|
"""Retrieve information about the given instance.
|
||||||
|
|
||||||
:param instance_uuid: UUID of a instance.
|
:param instance_uuid: UUID of a instance.
|
||||||
|
:param fields: Optional, a list with a specified set of fields
|
||||||
|
of the resource to be returned.
|
||||||
"""
|
"""
|
||||||
rpc_instance = objects.Instance.get(pecan.request.context,
|
rpc_instance = objects.Instance.get(pecan.request.context,
|
||||||
instance_uuid)
|
instance_uuid)
|
||||||
return Instance.convert_with_links(rpc_instance)
|
instance_data = rpc_instance.as_dict()
|
||||||
|
if fields is None or 'power_state' in fields:
|
||||||
|
# Only fetch node info if fields parameter is not specified
|
||||||
|
# or node fields is not requested.
|
||||||
|
try:
|
||||||
|
node = get_node_by_instance(instance_uuid, _NODE_FIELDS)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.warning(
|
||||||
|
_LW("Failed to retrieve node by instance_uuid"
|
||||||
|
" %(instance_uuid)s from ironic api: %(msg)s") % {
|
||||||
|
"instance_uuid": instance_uuid,
|
||||||
|
"msg": e})
|
||||||
|
|
||||||
|
instance_data['power_state'] = node.power_state
|
||||||
|
return Instance.convert_with_links(instance_data, fields=fields)
|
||||||
|
|
||||||
|
@expose.expose(InstanceCollection)
|
||||||
|
def detail(self):
|
||||||
|
"""Retrieve detail of a list of instances."""
|
||||||
|
# /detail should only work against collections
|
||||||
|
parent = pecan.request.path.split('/')[:-1][-1]
|
||||||
|
if parent != "instances":
|
||||||
|
raise exception.NotFound()
|
||||||
|
return self._get_instance_collection()
|
||||||
|
|
||||||
@expose.expose(Instance, body=types.jsontype,
|
@expose.expose(Instance, body=types.jsontype,
|
||||||
status_code=http_client.CREATED)
|
status_code=http_client.CREATED)
|
||||||
|
@ -16,10 +16,9 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import six
|
|
||||||
|
|
||||||
from oslo_utils import strutils
|
from oslo_utils import strutils
|
||||||
from oslo_utils import uuidutils
|
from oslo_utils import uuidutils
|
||||||
|
import six
|
||||||
from wsme import types as wtypes
|
from wsme import types as wtypes
|
||||||
|
|
||||||
from nimble.common import exception
|
from nimble.common import exception
|
||||||
@ -91,7 +90,33 @@ class JsonType(wtypes.UserType):
|
|||||||
return JsonType.validate(value)
|
return JsonType.validate(value)
|
||||||
|
|
||||||
|
|
||||||
|
class ListType(wtypes.UserType):
|
||||||
|
"""A simple list type."""
|
||||||
|
|
||||||
|
basetype = wtypes.text
|
||||||
|
name = 'list'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate(value):
|
||||||
|
"""Validate and convert the input to a ListType.
|
||||||
|
|
||||||
|
:param value: A comma separated string of values
|
||||||
|
:returns: A list of unique values, whose order is not guaranteed.
|
||||||
|
"""
|
||||||
|
items = [v.strip().lower() for v in six.text_type(value).split(',')]
|
||||||
|
# filter() to remove empty items
|
||||||
|
# set() to remove duplicated items
|
||||||
|
return list(set(filter(None, items)))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def frombasetype(value):
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
return ListType.validate(value)
|
||||||
|
|
||||||
|
|
||||||
boolean = BooleanType()
|
boolean = BooleanType()
|
||||||
uuid = UuidType()
|
uuid = UuidType()
|
||||||
# Can't call it 'json' because that's the name of the stdlib module
|
# Can't call it 'json' because that's the name of the stdlib module
|
||||||
jsontype = JsonType()
|
jsontype = JsonType()
|
||||||
|
listtype = ListType()
|
||||||
|
37
nimble/api/controllers/v1/utils.py
Normal file
37
nimble/api/controllers/v1/utils.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# Copyright 2016 Intel, Inc.
|
||||||
|
# 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.i18n import _
|
||||||
|
from oslo_config import cfg
|
||||||
|
import wsme
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
|
def validate_limit(limit):
|
||||||
|
if limit is None:
|
||||||
|
return CONF.api.max_limit
|
||||||
|
|
||||||
|
if limit <= 0:
|
||||||
|
raise wsme.exc.ClientSideError(_("Limit must be positive"))
|
||||||
|
|
||||||
|
return min(CONF.api.max_limit, limit)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_sort_dir(sort_dir):
|
||||||
|
if sort_dir not in ['asc', 'desc']:
|
||||||
|
raise wsme.exc.ClientSideError(_("Invalid sort direction: %s. "
|
||||||
|
"Acceptable values are "
|
||||||
|
"'asc' or 'desc'") % sort_dir)
|
||||||
|
return sort_dir
|
@ -78,8 +78,6 @@ def upgrade():
|
|||||||
sa.Column('name', sa.String(length=255), nullable=True),
|
sa.Column('name', sa.String(length=255), nullable=True),
|
||||||
sa.Column('description', sa.String(length=255), nullable=True),
|
sa.Column('description', sa.String(length=255), nullable=True),
|
||||||
sa.Column('status', sa.String(length=255), nullable=True),
|
sa.Column('status', sa.String(length=255), nullable=True),
|
||||||
sa.Column('power_state', sa.String(length=255), nullable=True),
|
|
||||||
sa.Column('task_state', sa.String(length=255), nullable=True),
|
|
||||||
sa.Column('instance_type_uuid', sa.String(length=36), nullable=True),
|
sa.Column('instance_type_uuid', sa.String(length=36), nullable=True),
|
||||||
sa.Column('image_uuid', sa.String(length=36), nullable=True),
|
sa.Column('image_uuid', sa.String(length=36), nullable=True),
|
||||||
sa.Column('network_info', sa.Text(), nullable=True),
|
sa.Column('network_info', sa.Text(), nullable=True),
|
||||||
|
@ -127,8 +127,6 @@ class Instance(Base):
|
|||||||
project_id = Column(String(36), nullable=True)
|
project_id = Column(String(36), nullable=True)
|
||||||
user_id = Column(String(36), nullable=True)
|
user_id = Column(String(36), nullable=True)
|
||||||
status = Column(String(255), nullable=True)
|
status = Column(String(255), nullable=True)
|
||||||
power_state = Column(String(255), nullable=True)
|
|
||||||
task_state = Column(String(255), nullable=True)
|
|
||||||
instance_type_uuid = Column(String(36), nullable=True)
|
instance_type_uuid = Column(String(36), nullable=True)
|
||||||
availability_zone = Column(String(255), nullable=True)
|
availability_zone = Column(String(255), nullable=True)
|
||||||
image_uuid = Column(String(36), nullable=True)
|
image_uuid = Column(String(36), nullable=True)
|
||||||
|
@ -85,10 +85,12 @@ def do_node_deploy(node_uuid):
|
|||||||
ironic_states.ACTIVE)
|
ironic_states.ACTIVE)
|
||||||
|
|
||||||
|
|
||||||
def get_node_by_instance(instance_uuid):
|
def get_node_by_instance(instance_uuid, fields=None):
|
||||||
|
if fields is None:
|
||||||
|
fields = _NODE_FIELDS
|
||||||
ironicclient = ironic.IronicClientWrapper()
|
ironicclient = ironic.IronicClientWrapper()
|
||||||
return ironicclient.call('node.get_by_instance_uuid',
|
return ironicclient.call('node.get_by_instance_uuid',
|
||||||
instance_uuid, fields=_NODE_FIELDS)
|
instance_uuid, fields=fields)
|
||||||
|
|
||||||
|
|
||||||
def destroy_node(node_uuid):
|
def destroy_node(node_uuid):
|
||||||
|
@ -36,8 +36,6 @@ class Instance(base.NimbleObject, object_base.VersionedObjectDictCompat):
|
|||||||
'project_id': object_fields.UUIDField(nullable=True),
|
'project_id': object_fields.UUIDField(nullable=True),
|
||||||
'user_id': object_fields.UUIDField(nullable=True),
|
'user_id': object_fields.UUIDField(nullable=True),
|
||||||
'status': object_fields.StringField(nullable=True),
|
'status': object_fields.StringField(nullable=True),
|
||||||
'power_state': object_fields.StringField(nullable=True),
|
|
||||||
'task_state': object_fields.StringField(nullable=True),
|
|
||||||
'instance_type_uuid': object_fields.UUIDField(nullable=True),
|
'instance_type_uuid': object_fields.UUIDField(nullable=True),
|
||||||
'availability_zone': object_fields.StringField(nullable=True),
|
'availability_zone': object_fields.StringField(nullable=True),
|
||||||
'image_uuid': object_fields.UUIDField(nullable=True),
|
'image_uuid': object_fields.UUIDField(nullable=True),
|
||||||
@ -59,9 +57,9 @@ class Instance(base.NimbleObject, object_base.VersionedObjectDictCompat):
|
|||||||
return Instance._from_db_object_list(db_instances, cls, context)
|
return Instance._from_db_object_list(db_instances, cls, context)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get(cls, context, instance_id):
|
def get(cls, context, uuid):
|
||||||
"""Find a instance and return a Instance object."""
|
"""Find a instance and return a Instance object."""
|
||||||
db_instance = cls.dbapi.instance_get(instance_id)
|
db_instance = cls.dbapi.instance_get(uuid)
|
||||||
instance = Instance._from_db_object(cls(context), db_instance)
|
instance = Instance._from_db_object(cls(context), db_instance)
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user