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:
parent
6ed34997e1
commit
640f2089bc
@ -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