Add instance list and detail api

Add list instance, list instance detail and instance detail api.
It's working in progress. Need some extra work to add query
parameters like pagination and sorting.

GET /instances/uuid?fields=uuid
GET /instances/?fields=uuid,name
GET /instances/detail

Change-Id: I0c1e048badd20c6af2c1762350e6ab104f9a7b5c
This commit is contained in:
LeiZhang 2016-09-26 23:04:34 -04:00
parent 6ed34997e1
commit 640f2089bc
7 changed files with 159 additions and 30 deletions

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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