Add Instances API
Change-Id: Idccee80dce3ed60b491bd3a0fa56429770c618d0
This commit is contained in:
@@ -16,13 +16,122 @@
|
|||||||
import pecan
|
import pecan
|
||||||
from pecan import rest
|
from pecan import rest
|
||||||
from six.moves import http_client
|
from six.moves import http_client
|
||||||
|
import wsme
|
||||||
|
from wsme import types as wtypes
|
||||||
|
|
||||||
|
from nimble.api.controllers import base
|
||||||
|
from nimble.api.controllers import link
|
||||||
from nimble.api.controllers.v1 import types
|
from nimble.api.controllers.v1 import types
|
||||||
from nimble.api import expose
|
from nimble.api import expose
|
||||||
|
from nimble import objects
|
||||||
|
|
||||||
|
|
||||||
|
class Instance(base.APIBase):
|
||||||
|
"""API representation of a instance.
|
||||||
|
|
||||||
|
This class enforces type checking and value constraints, and converts
|
||||||
|
between the internal object model and the API representation of
|
||||||
|
a instance.
|
||||||
|
"""
|
||||||
|
|
||||||
|
uuid = types.uuid
|
||||||
|
"""The UUID of the instance"""
|
||||||
|
|
||||||
|
name = wtypes.text
|
||||||
|
"""The name of the instance"""
|
||||||
|
|
||||||
|
description = wtypes.text
|
||||||
|
"""The description of the instance"""
|
||||||
|
|
||||||
|
status = wtypes.text
|
||||||
|
"""The status of the instance"""
|
||||||
|
|
||||||
|
power_state = wtypes.text
|
||||||
|
"""The power state of the instance"""
|
||||||
|
|
||||||
|
task_state = wtypes.text
|
||||||
|
"""The task state of the instance"""
|
||||||
|
|
||||||
|
availability_zone = wtypes.text
|
||||||
|
"""The availability zone of the instance"""
|
||||||
|
|
||||||
|
links = wsme.wsattr([link.Link], readonly=True)
|
||||||
|
"""A list containing a self link"""
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.fields = []
|
||||||
|
for field in objects.Instance.fields:
|
||||||
|
# Skip fields we do not expose.
|
||||||
|
if not hasattr(self, field):
|
||||||
|
continue
|
||||||
|
self.fields.append(field)
|
||||||
|
setattr(self, field, kwargs.get(field, wtypes.Unset))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def convert_with_links(cls, rpc_instance):
|
||||||
|
instance = Instance(**rpc_instance.as_dict())
|
||||||
|
url = pecan.request.public_url
|
||||||
|
instance.links = [link.Link.make_link('self',
|
||||||
|
url,
|
||||||
|
'instance', instance.uuid),
|
||||||
|
link.Link.make_link('bookmark',
|
||||||
|
url,
|
||||||
|
'instance', instance.uuid,
|
||||||
|
bookmark=True)
|
||||||
|
]
|
||||||
|
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
class InstanceCollection(base.APIBase):
|
||||||
|
"""API representation of a collection of instance."""
|
||||||
|
|
||||||
|
instance = [Instance]
|
||||||
|
"""A list containing instance objects"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def convert_with_links(instance, url=None, **kwargs):
|
||||||
|
collection = InstanceCollection()
|
||||||
|
collection.instance = [Instance.convert_with_links(inst)
|
||||||
|
for inst in instance]
|
||||||
|
return collection
|
||||||
|
|
||||||
|
|
||||||
class InstanceController(rest.RestController):
|
class InstanceController(rest.RestController):
|
||||||
"""REST controller for Chassis."""
|
"""REST controller for Instance."""
|
||||||
|
|
||||||
|
@expose.expose(InstanceCollection)
|
||||||
|
def get_all(self):
|
||||||
|
"""Retrieve a list of instance."""
|
||||||
|
|
||||||
|
instance = objects.Instance.list(pecan.request.context)
|
||||||
|
return InstanceCollection.convert_with_links(instance)
|
||||||
|
|
||||||
|
@expose.expose(Instance, types.uuid)
|
||||||
|
def get_one(self, instance_uuid):
|
||||||
|
"""Retrieve information about the given instance.
|
||||||
|
|
||||||
|
:param instance_uuid: UUID of a instance.
|
||||||
|
"""
|
||||||
|
rpc_instance = objects.Instance.get(pecan.request.context,
|
||||||
|
instance_uuid)
|
||||||
|
return Instance.convert_with_links(rpc_instance)
|
||||||
|
|
||||||
|
@expose.expose(Instance, body=Instance, status_code=http_client.CREATED)
|
||||||
|
def post(self, instance):
|
||||||
|
"""Create a new instance.
|
||||||
|
|
||||||
|
:param instance: a instance within the request body.
|
||||||
|
"""
|
||||||
|
instance_obj = objects.Instance(pecan.request.context,
|
||||||
|
**instance.as_dict())
|
||||||
|
instance_obj.create()
|
||||||
|
# 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)
|
||||||
|
|
||||||
@expose.expose(None, types.uuid, status_code=http_client.NO_CONTENT)
|
@expose.expose(None, types.uuid, status_code=http_client.NO_CONTENT)
|
||||||
def delete(self, instance_uuid):
|
def delete(self, instance_uuid):
|
||||||
@@ -30,4 +139,6 @@ class InstanceController(rest.RestController):
|
|||||||
|
|
||||||
:param instance_uuid: UUID of a instance.
|
:param instance_uuid: UUID of a instance.
|
||||||
"""
|
"""
|
||||||
pecan.request.rpcapi.do_node_deploy(pecan.request.context)
|
rpc_instance = objects.Instance.get(pecan.request.context,
|
||||||
|
instance_uuid)
|
||||||
|
rpc_instance.destroy()
|
||||||
|
|||||||
@@ -106,6 +106,12 @@ class Invalid(NimbleException):
|
|||||||
code = http_client.BAD_REQUEST
|
code = http_client.BAD_REQUEST
|
||||||
|
|
||||||
|
|
||||||
|
# Cannot be templated as the error syntax varies.
|
||||||
|
# msg needs to be constructed when raised.
|
||||||
|
class InvalidParameterValue(Invalid):
|
||||||
|
_msg_fmt = _("%(err)s")
|
||||||
|
|
||||||
|
|
||||||
class Conflict(NimbleException):
|
class Conflict(NimbleException):
|
||||||
_msg_fmt = _('Conflict.')
|
_msg_fmt = _('Conflict.')
|
||||||
code = http_client.CONFLICT
|
code = http_client.CONFLICT
|
||||||
@@ -141,7 +147,19 @@ class FlavorNotFound(NotFound):
|
|||||||
msg_fmt = _("Flavor %(flavor)s could not be found.")
|
msg_fmt = _("Flavor %(flavor)s could not be found.")
|
||||||
|
|
||||||
|
|
||||||
|
class InstanceAlreadyExists(NimbleException):
|
||||||
|
_msg_fmt = _("Instance with name %(name)s already exists.")
|
||||||
|
|
||||||
|
|
||||||
|
class InstanceNotFound(NotFound):
|
||||||
|
msg_fmt = _("Instance %(instance)s could not be found.")
|
||||||
|
|
||||||
|
|
||||||
class NoFreeEngineWorker(TemporaryFailure):
|
class NoFreeEngineWorker(TemporaryFailure):
|
||||||
_msg_fmt = _('Requested action cannot be performed due to lack of free '
|
_msg_fmt = _('Requested action cannot be performed due to lack of free '
|
||||||
'engine workers.')
|
'engine workers.')
|
||||||
code = http_client.SERVICE_UNAVAILABLE
|
code = http_client.SERVICE_UNAVAILABLE
|
||||||
|
|
||||||
|
|
||||||
|
class DuplicateName(Conflict):
|
||||||
|
_msg_fmt = _("A instance with name %(name)s already exists.")
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ class Connection(object):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""Constructor."""
|
"""Constructor."""
|
||||||
|
|
||||||
|
# Flavors
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def flavor_create(self, values):
|
def flavor_create(self, values):
|
||||||
"""Create a new instance type."""
|
"""Create a new instance type."""
|
||||||
@@ -55,3 +56,19 @@ class Connection(object):
|
|||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def flavor_destroy(name):
|
def flavor_destroy(name):
|
||||||
"""Delete an instance type."""
|
"""Delete an instance type."""
|
||||||
|
|
||||||
|
# Instances
|
||||||
|
@abc.abstractmethod
|
||||||
|
def instance_create(self, values):
|
||||||
|
"""Create a new instance."""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def instance_get(uuid):
|
||||||
|
"""Get instance by name."""
|
||||||
|
|
||||||
|
def instance_get_all():
|
||||||
|
"""Get all instances."""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def instance_destroy(name):
|
||||||
|
"""Delete an instance."""
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ def upgrade():
|
|||||||
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('power_state', sa.String(length=255), nullable=True),
|
||||||
sa.Column('task_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=False),
|
sa.Column('instance_type_id', sa.Integer(), nullable=True),
|
||||||
sa.Column('launched_at', sa.DateTime(), nullable=True),
|
sa.Column('launched_at', sa.DateTime(), nullable=True),
|
||||||
sa.Column('terminated_at', sa.DateTime(), nullable=True),
|
sa.Column('terminated_at', sa.DateTime(), nullable=True),
|
||||||
sa.Column('availability_zone', sa.String(length=255), nullable=True),
|
sa.Column('availability_zone', sa.String(length=255), nullable=True),
|
||||||
@@ -75,6 +75,7 @@ def upgrade():
|
|||||||
sa.Column('extra', sa.Text(), nullable=True),
|
sa.Column('extra', sa.Text(), nullable=True),
|
||||||
sa.PrimaryKeyConstraint('id'),
|
sa.PrimaryKeyConstraint('id'),
|
||||||
sa.UniqueConstraint('uuid', name='uniq_instances0uuid'),
|
sa.UniqueConstraint('uuid', name='uniq_instances0uuid'),
|
||||||
|
sa.UniqueConstraint('name', name='uniq_instances0name'),
|
||||||
mysql_ENGINE='InnoDB',
|
mysql_ENGINE='InnoDB',
|
||||||
mysql_DEFAULT_CHARSET='UTF8'
|
mysql_DEFAULT_CHARSET='UTF8'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -115,3 +115,60 @@ class Connection(api.Connection):
|
|||||||
count = query.delete()
|
count = query.delete()
|
||||||
if count != 1:
|
if count != 1:
|
||||||
raise exception.FlavorNotFound(flavor=flavor_id)
|
raise exception.FlavorNotFound(flavor=flavor_id)
|
||||||
|
|
||||||
|
def instance_create(self, values):
|
||||||
|
if not values.get('uuid'):
|
||||||
|
values['uuid'] = uuidutils.generate_uuid()
|
||||||
|
|
||||||
|
instance = models.Instance()
|
||||||
|
instance.update(values)
|
||||||
|
|
||||||
|
with _session_for_write() as session:
|
||||||
|
try:
|
||||||
|
session.add(instance)
|
||||||
|
session.flush()
|
||||||
|
except db_exc.DBDuplicateEntry:
|
||||||
|
raise exception.InstanceAlreadyExists(name=values['name'])
|
||||||
|
return instance
|
||||||
|
|
||||||
|
def instance_get(self, instance_id):
|
||||||
|
query = model_query(models.Instance).filter_by(uuid=instance_id)
|
||||||
|
try:
|
||||||
|
return query.one()
|
||||||
|
except NoResultFound:
|
||||||
|
raise exception.InstanceNotFound(instance=instance_id)
|
||||||
|
|
||||||
|
def instance_get_all(self):
|
||||||
|
return model_query(models.Instance)
|
||||||
|
|
||||||
|
def instance_destroy(self, instance_id):
|
||||||
|
with _session_for_write():
|
||||||
|
query = model_query(models.Instance)
|
||||||
|
query = add_identity_filter(query, instance_id)
|
||||||
|
|
||||||
|
count = query.delete()
|
||||||
|
if count != 1:
|
||||||
|
raise exception.InstanceNotFound(instance=instance_id)
|
||||||
|
|
||||||
|
def update_instance(self, instance_id, values):
|
||||||
|
if 'uuid' in values:
|
||||||
|
msg = _("Cannot overwrite UUID for an existing Instance.")
|
||||||
|
raise exception.InvalidParameterValue(err=msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self._do_update_instance(instance_id, values)
|
||||||
|
except db_exc.DBDuplicateEntry as e:
|
||||||
|
if 'name' in e.columns:
|
||||||
|
raise exception.DuplicateName(name=values['name'])
|
||||||
|
|
||||||
|
def _do_update_instance(self, instance_id, values):
|
||||||
|
with _session_for_write():
|
||||||
|
query = model_query(models.Instance)
|
||||||
|
query = add_identity_filter(query, instance_id)
|
||||||
|
try:
|
||||||
|
ref = query.with_lockmode('update').one()
|
||||||
|
except NoResultFound:
|
||||||
|
raise exception.InstanceNotFound(instance=instance_id)
|
||||||
|
|
||||||
|
ref.update(values)
|
||||||
|
return ref
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ from oslo_db import options as db_options
|
|||||||
from oslo_db.sqlalchemy import models
|
from oslo_db.sqlalchemy import models
|
||||||
import six.moves.urllib.parse as urlparse
|
import six.moves.urllib.parse as urlparse
|
||||||
from sqlalchemy import Boolean, Column
|
from sqlalchemy import Boolean, Column
|
||||||
from sqlalchemy import schema, String, Integer
|
from sqlalchemy import schema, String, Integer, Text
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
|
||||||
from nimble.common import paths
|
from nimble.common import paths
|
||||||
@@ -85,3 +85,24 @@ class InstanceTypeProjects(Base):
|
|||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
instance_type_id = Column(Integer, nullable=True)
|
instance_type_id = Column(Integer, nullable=True)
|
||||||
project_id = Column(String(36), nullable=True)
|
project_id = Column(String(36), nullable=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Instance(Base):
|
||||||
|
"""Represents possible types for instances."""
|
||||||
|
|
||||||
|
__tablename__ = 'instances'
|
||||||
|
__table_args__ = (
|
||||||
|
schema.UniqueConstraint('uuid', name='uniq_instances0uuid'),
|
||||||
|
table_args()
|
||||||
|
)
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
uuid = Column(String(36), nullable=True)
|
||||||
|
name = Column(String(255), nullable=False)
|
||||||
|
description = 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_id = Column(Integer, nullable=True)
|
||||||
|
availability_zone = Column(String(255), nullable=True)
|
||||||
|
node_uuid = Column(String(36), nullable=True)
|
||||||
|
extra = Column(Text, nullable=True)
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ class EngineManager(base_manager.BaseEngineManager):
|
|||||||
def _sync_node_resources(self, context):
|
def _sync_node_resources(self, context):
|
||||||
LOG.info(_LI("During sync_node_resources."))
|
LOG.info(_LI("During sync_node_resources."))
|
||||||
|
|
||||||
def do_node_deploy(self, context):
|
def create_instance(self, context, instance):
|
||||||
"""Signal to engine service to perform a deployment."""
|
"""Signal to engine service to perform a deployment."""
|
||||||
LOG.debug("During do node deploy.")
|
LOG.debug("During create instance.")
|
||||||
|
instance.task_state = 'deploying'
|
||||||
|
instance.save()
|
||||||
|
return instance
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ class EngineAPI(object):
|
|||||||
version_cap=self.RPC_API_VERSION,
|
version_cap=self.RPC_API_VERSION,
|
||||||
serializer=serializer)
|
serializer=serializer)
|
||||||
|
|
||||||
def do_node_deploy(self, context):
|
def create_instance(self, context, instance):
|
||||||
"""Signal to engine service to perform a deployment."""
|
"""Signal to engine service to perform a deployment."""
|
||||||
cctxt = self.client.prepare(topic=self.topic, server=CONF.host)
|
cctxt = self.client.prepare(topic=self.topic, server=CONF.host)
|
||||||
return cctxt.cast(context, 'do_node_deploy')
|
return cctxt.call(context, 'create_instance', instance=instance)
|
||||||
|
|||||||
@@ -26,3 +26,4 @@ def register_all():
|
|||||||
# function in order for it to be registered by services that may
|
# function in order for it to be registered by services that may
|
||||||
# need to receive it via RPC.
|
# need to receive it via RPC.
|
||||||
__import__('nimble.objects.flavor')
|
__import__('nimble.objects.flavor')
|
||||||
|
__import__('nimble.objects.instance')
|
||||||
|
|||||||
79
nimble/objects/instance.py
Normal file
79
nimble/objects/instance.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# 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 oslo_versionedobjects import base as object_base
|
||||||
|
|
||||||
|
from nimble.db import api as dbapi
|
||||||
|
from nimble.objects import base
|
||||||
|
from nimble.objects import fields as object_fields
|
||||||
|
|
||||||
|
|
||||||
|
@base.NimbleObjectRegistry.register
|
||||||
|
class Instance(base.NimbleObject, object_base.VersionedObjectDictCompat):
|
||||||
|
# Version 1.0: Initial version
|
||||||
|
VERSION = '1.0'
|
||||||
|
|
||||||
|
dbapi = dbapi.get_instance()
|
||||||
|
|
||||||
|
fields = {
|
||||||
|
'id': object_fields.IntegerField(),
|
||||||
|
'uuid': object_fields.UUIDField(nullable=True),
|
||||||
|
'name': object_fields.StringField(nullable=True),
|
||||||
|
'description': 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_id': object_fields.IntegerField(nullable=True),
|
||||||
|
'availability_zone': object_fields.StringField(nullable=True),
|
||||||
|
'node_uuid': object_fields.UUIDField(nullable=True),
|
||||||
|
'extra': object_fields.FlexibleDictField(nullable=True),
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _from_db_object_list(db_objects, cls, context):
|
||||||
|
"""Converts a list of database entities to a list of formal objects."""
|
||||||
|
return [Instance._from_db_object(cls(context), obj)
|
||||||
|
for obj in db_objects]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def list(cls, context):
|
||||||
|
"""Return a list of Instance objects."""
|
||||||
|
db_instances = cls.dbapi.instance_get_all()
|
||||||
|
return Instance._from_db_object_list(db_instances, cls, context)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get(cls, context, instance_id):
|
||||||
|
"""Find a instance and return a Instance object."""
|
||||||
|
db_instance = cls.dbapi.instance_get(instance_id)
|
||||||
|
instance = Instance._from_db_object(cls(context), db_instance)
|
||||||
|
return instance
|
||||||
|
|
||||||
|
def create(self, context=None):
|
||||||
|
"""Create a Instance record in the DB."""
|
||||||
|
values = self.obj_get_changes()
|
||||||
|
db_instance = self.dbapi.instance_create(values)
|
||||||
|
self._from_db_object(self, db_instance)
|
||||||
|
|
||||||
|
def destroy(self, context=None):
|
||||||
|
"""Delete the Instance from the DB."""
|
||||||
|
self.dbapi.instance_destroy(self.uuid)
|
||||||
|
self.obj_reset_changes()
|
||||||
|
|
||||||
|
def save(self, context=None):
|
||||||
|
"""Save updates to this Instance."""
|
||||||
|
updates = self.obj_get_changes()
|
||||||
|
self.dbapi.update_instance(self.uuid, updates)
|
||||||
|
self.obj_reset_changes()
|
||||||
Reference in New Issue
Block a user