From 29f9f1ffd464c08b53995a3891cdcb5b0867a727 Mon Sep 17 00:00:00 2001 From: Fabio Verboso Date: Thu, 20 Sep 2018 13:09:16 +0200 Subject: [PATCH] Fleets CRUD actions for Fleets for Sebba :) Change-Id: Ia6ee145f330a844e532473784e21bc375190a707 --- iotronic/api/controllers/v1/__init__.py | 13 + iotronic/api/controllers/v1/fleet.py | 347 ++++++++++++++++++ iotronic/api/controllers/v1/utils.py | 33 +- iotronic/common/exception.py | 12 + iotronic/common/policy.py | 18 +- iotronic/conductor/endpoints.py | 21 +- iotronic/conductor/rpcapi.py | 42 +++ iotronic/db/api.py | 51 +++ .../versions/b578199e4e64_add_fleets.py | 37 ++ iotronic/db/sqlalchemy/api.py | 97 +++++ iotronic/db/sqlalchemy/models.py | 16 + iotronic/objects/__init__.py | 5 +- iotronic/objects/fleet.py | 189 ++++++++++ 13 files changed, 875 insertions(+), 6 deletions(-) create mode 100644 iotronic/api/controllers/v1/fleet.py create mode 100644 iotronic/db/sqlalchemy/alembic/versions/b578199e4e64_add_fleets.py create mode 100644 iotronic/objects/fleet.py diff --git a/iotronic/api/controllers/v1/__init__.py b/iotronic/api/controllers/v1/__init__.py index 573ea25..c911eb3 100644 --- a/iotronic/api/controllers/v1/__init__.py +++ b/iotronic/api/controllers/v1/__init__.py @@ -25,6 +25,7 @@ from wsme import types as wtypes from iotronic.api.controllers import base from iotronic.api.controllers import link +from iotronic.api.controllers.v1 import fleet from iotronic.api.controllers.v1 import plugin from iotronic.api.controllers.v1 import port from iotronic.api.controllers.v1 import service @@ -70,6 +71,9 @@ class V1(base.APIBase): ports = [link.Link] """Links to the boards resource""" + fleet = [link.Link] + """Links to the boards resource""" + @staticmethod def convert(): v1 = V1() @@ -113,6 +117,14 @@ class V1(base.APIBase): pecan.request.public_url, 'ports', '', bookmark=True)] + v1.fleets = [link.Link.make_link('self', pecan.request.public_url, + 'fleets', ''), + link.Link.make_link('bookmark', + pecan.request.public_url, + 'fleets', '', + bookmark=True) + ] + return v1 @@ -123,6 +135,7 @@ class Controller(rest.RestController): plugins = plugin.PluginsController() services = service.ServicesController() ports = port.PortsController() + fleets = fleet.FleetsController() @expose.expose(V1) def get(self): diff --git a/iotronic/api/controllers/v1/fleet.py b/iotronic/api/controllers/v1/fleet.py new file mode 100644 index 0000000..a36b291 --- /dev/null +++ b/iotronic/api/controllers/v1/fleet.py @@ -0,0 +1,347 @@ +# 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 iotronic.api.controllers import base +from iotronic.api.controllers import link +from iotronic.api.controllers.v1 import collection +from iotronic.api.controllers.v1 import types +from iotronic.api.controllers.v1 import utils as api_utils +from iotronic.api import expose +from iotronic.common import exception +from iotronic.common import policy +from iotronic import objects + +import pecan +from pecan import rest +import wsme +from wsme import types as wtypes + +_DEFAULT_RETURN_FIELDS = ( + 'name', 'uuid', 'project', 'description', 'extra') + + +class Fleet(base.APIBase): + """API representation of a fleet. + + """ + uuid = types.uuid + name = wsme.wsattr(wtypes.text) + project = types.uuid + description = wsme.wsattr(wtypes.text) + extra = types.jsontype + + links = wsme.wsattr([link.Link], readonly=True) + + def __init__(self, **kwargs): + self.fields = [] + fields = list(objects.Fleet.fields) + for k in fields: + # Skip fields we do not expose. + if not hasattr(self, k): + continue + self.fields.append(k) + setattr(self, k, kwargs.get(k, wtypes.Unset)) + + @staticmethod + def _convert_with_links(fleet, url, fields=None): + fleet_uuid = fleet.uuid + if fields is not None: + fleet.unset_fields_except(fields) + + fleet.links = [link.Link.make_link('self', url, 'fleets', + fleet_uuid), + link.Link.make_link('bookmark', url, 'fleets', + fleet_uuid, bookmark=True) + ] + return fleet + + @classmethod + def convert_with_links(cls, rpc_fleet, fields=None): + fleet = Fleet(**rpc_fleet.as_dict()) + + if fields is not None: + api_utils.check_for_invalid_fields(fields, fleet.as_dict()) + + return cls._convert_with_links(fleet, pecan.request.public_url, + fields=fields) + + +class FleetCollection(collection.Collection): + """API representation of a collection of fleets.""" + + fleets = [Fleet] + """A list containing fleets objects""" + + def __init__(self, **kwargs): + self._type = 'fleets' + + @staticmethod + def convert_with_links(fleets, limit, url=None, fields=None, **kwargs): + collection = FleetCollection() + collection.fleets = [Fleet.convert_with_links(n, fields=fields) + for n in fleets] + collection.next = collection.get_next(limit, url=url, **kwargs) + return collection + + +class PublicFleetsController(rest.RestController): + """REST controller for Public Fleets.""" + + invalid_sort_key_list = ['extra'] + + def _get_fleets_collection(self, marker, limit, + sort_key, sort_dir, + fields=None): + + limit = api_utils.validate_limit(limit) + sort_dir = api_utils.validate_sort_dir(sort_dir) + + marker_obj = None + if marker: + marker_obj = objects.Fleet.get_by_uuid(pecan.request.context, + marker) + + if sort_key in self.invalid_sort_key_list: + raise exception.InvalidParameterValue( + ("The sort_key value %(key)s is an invalid field for " + "sorting") % {'key': sort_key}) + + filters = {} + filters['public'] = True + + fleets = objects.Fleet.list(pecan.request.context, limit, + marker_obj, + sort_key=sort_key, sort_dir=sort_dir, + filters=filters) + + parameters = {'sort_key': sort_key, 'sort_dir': sort_dir} + + return FleetCollection.convert_with_links(fleets, limit, + fields=fields, + **parameters) + + @expose.expose(FleetCollection, types.uuid, int, wtypes.text, + wtypes.text, types.listtype, types.boolean, types.boolean) + def get_all(self, marker=None, + limit=None, sort_key='id', sort_dir='asc', + fields=None): + """Retrieve a list of fleets. + + :param marker: pagination marker for large data sets. + :param limit: maximum number of resources to return in a single result. + This value cannot be larger than the value of max_limit + in the [api] section of the ironic configuration, or only + max_limit resources will be returned. + :param sort_key: column to sort results by. Default: id. + :param sort_dir: direction to sort. "asc" or "desc". Default: asc. + :param fields: Optional, a list with a specified set of fields + of the resource to be returned. + """ + cdict = pecan.request.context.to_policy_values() + policy.authorize('iot:fleet:get', cdict, cdict) + + if fields is None: + fields = _DEFAULT_RETURN_FIELDS + return self._get_fleets_collection(marker, + limit, sort_key, sort_dir, + fields=fields) + + +class FleetsController(rest.RestController): + """REST controller for Fleets.""" + + public = PublicFleetsController() + + invalid_sort_key_list = ['extra', ] + + _custom_actions = { + 'detail': ['GET'], + } + + def _get_fleets_collection(self, marker, limit, + sort_key, sort_dir, + fields=None): + + limit = api_utils.validate_limit(limit) + sort_dir = api_utils.validate_sort_dir(sort_dir) + + marker_obj = None + if marker: + marker_obj = objects.Fleet.get_by_uuid(pecan.request.context, + marker) + + if sort_key in self.invalid_sort_key_list: + raise exception.InvalidParameterValue( + ("The sort_key value %(key)s is an invalid field for " + "sorting") % {'key': sort_key}) + + filters = {} + fleets = objects.Fleet.list(pecan.request.context, limit, + marker_obj, + sort_key=sort_key, sort_dir=sort_dir, + filters=filters) + + parameters = {'sort_key': sort_key, 'sort_dir': sort_dir} + + return FleetCollection.convert_with_links(fleets, limit, + fields=fields, + **parameters) + + @expose.expose(Fleet, types.uuid_or_name, types.listtype) + def get_one(self, fleet_ident, fields=None): + """Retrieve information about the given fleet. + + :param fleet_ident: UUID or logical name of a fleet. + :param fields: Optional, a list with a specified set of fields + of the resource to be returned. + """ + + rpc_fleet = api_utils.get_rpc_fleet(fleet_ident) + cdict = pecan.request.context.to_policy_values() + cdict['project'] = rpc_fleet.project + policy.authorize('iot:fleet:get_one', cdict, cdict) + + return Fleet.convert_with_links(rpc_fleet, fields=fields) + + @expose.expose(FleetCollection, types.uuid, int, wtypes.text, + wtypes.text, types.listtype, types.boolean, types.boolean) + def get_all(self, marker=None, + limit=None, sort_key='id', sort_dir='asc', + fields=None): + """Retrieve a list of fleets. + + :param marker: pagination marker for large data sets. + :param limit: maximum number of resources to return in a single result. + This value cannot be larger than the value of max_limit + in the [api] section of the ironic configuration, or only + max_limit resources will be returned. + :param sort_key: column to sort results by. Default: id. + :param sort_dir: direction to sort. "asc" or "desc". Default: asc. + :param with_public: Optional boolean to get also public pluings. + :param all_fleets: Optional boolean to get all the pluings. + Only for the admin + :param fields: Optional, a list with a specified set of fields + of the resource to be returned. + """ + cdict = pecan.request.context.to_policy_values() + policy.authorize('iot:fleet:get', cdict, cdict) + + if fields is None: + fields = _DEFAULT_RETURN_FIELDS + return self._get_fleets_collection(marker, + limit, sort_key, sort_dir, + fields=fields) + + @expose.expose(Fleet, body=Fleet, status_code=201) + def post(self, Fleet): + """Create a new Fleet. + + :param Fleet: a Fleet within the request body. + """ + context = pecan.request.context + cdict = context.to_policy_values() + policy.authorize('iot:fleet:create', cdict, cdict) + + if not Fleet.name: + raise exception.MissingParameterValue( + ("Name is not specified.")) + + if Fleet.name: + if not api_utils.is_valid_name(Fleet.name): + msg = ("Cannot create fleet with invalid name %(name)s") + raise wsme.exc.ClientSideError(msg % {'name': Fleet.name}, + status_code=400) + + new_Fleet = objects.Fleet(pecan.request.context, + **Fleet.as_dict()) + + new_Fleet.project = cdict['project_id'] + new_Fleet = pecan.request.rpcapi.create_fleet( + pecan.request.context, + new_Fleet) + + return Fleet.convert_with_links(new_Fleet) + + @expose.expose(None, types.uuid_or_name, status_code=204) + def delete(self, fleet_ident): + """Delete a fleet. + + :param fleet_ident: UUID or logical name of a fleet. + """ + context = pecan.request.context + cdict = context.to_policy_values() + policy.authorize('iot:fleet:delete', cdict, cdict) + + rpc_fleet = api_utils.get_rpc_fleet(fleet_ident) + pecan.request.rpcapi.destroy_fleet(pecan.request.context, + rpc_fleet.uuid) + + @expose.expose(Fleet, types.uuid_or_name, body=Fleet, status_code=200) + def patch(self, fleet_ident, val_Fleet): + """Update a fleet. + + :param fleet_ident: UUID or logical name of a fleet. + :param Fleet: values to be changed + :return updated_fleet: updated_fleet + """ + + rpc_fleet = api_utils.get_rpc_fleet(fleet_ident) + cdict = pecan.request.context.to_policy_values() + cdict['project'] = rpc_fleet.project + policy.authorize('iot:fleet:update', cdict, cdict) + + val_Fleet = val_Fleet.as_dict() + for key in val_Fleet: + try: + rpc_fleet[key] = val_Fleet[key] + except Exception: + pass + + updated_fleet = pecan.request.rpcapi.update_fleet( + pecan.request.context, rpc_fleet) + return Fleet.convert_with_links(updated_fleet) + + @expose.expose(FleetCollection, types.uuid, int, wtypes.text, + wtypes.text, types.listtype, types.boolean, types.boolean) + def detail(self, marker=None, + limit=None, sort_key='id', sort_dir='asc', + fields=None, with_public=False, all_fleets=False): + """Retrieve a list of fleets. + + :param marker: pagination marker for large data sets. + :param limit: maximum number of resources to return in a single result. + This value cannot be larger than the value of max_limit + in the [api] section of the ironic configuration, or only + max_limit resources will be returned. + :param sort_key: column to sort results by. Default: id. + :param sort_dir: direction to sort. "asc" or "desc". Default: asc. + :param with_public: Optional boolean to get also public fleet. + :param all_fleets: Optional boolean to get all the fleets. + Only for the admin + :param fields: Optional, a list with a specified set of fields + of the resource to be returned. + """ + + cdict = pecan.request.context.to_policy_values() + policy.authorize('iot:fleet:get', cdict, cdict) + + # /detail should only work against collections + parent = pecan.request.path.split('/')[:-1][-1] + if parent != "fleets": + raise exception.HTTPNotFound() + + return self._get_fleets_collection(marker, + limit, sort_key, sort_dir, + with_public=with_public, + all_fleets=all_fleets, + fields=fields) diff --git a/iotronic/api/controllers/v1/utils.py b/iotronic/api/controllers/v1/utils.py index d330fd6..ecd75a6 100644 --- a/iotronic/api/controllers/v1/utils.py +++ b/iotronic/api/controllers/v1/utils.py @@ -156,13 +156,13 @@ def get_rpc_port(port_ident): :raises: InvalidUuidOrName if the name or uuid provided is not valid. :raises: portNotFound if the port is not found. """ -# Check to see if the port_ident is a valid UUID. If it is, treat it -# as a UUID. + # Check to see if the port_ident is a valid UUID. If it is, treat it + # as a UUID. if uuidutils.is_uuid_like(port_ident): return objects.Port.get_by_uuid(pecan.request.context, port_ident) -# We can refer to ports by their name, if the client supports it + # We can refer to ports by their name, if the client supports it else: return objects.Port.get_by_name(pecan.request.context, port_ident) @@ -172,6 +172,33 @@ def get_rpc_port(port_ident): raise exception.PortNottFound(uuid=port_ident) +def get_rpc_fleet(fleet_ident): + """Get the RPC fleet from the fleet uuid or logical name. + + :param fleet_ident: the UUID or logical name of a fleet. + + :returns: The RPC Fleet. + :raises: InvalidUuidOrName if the name or uuid provided is not valid. + :raises: FleetNotFound if the fleet is not found. + """ + # Check to see if the fleet_ident is a valid UUID. If it is, treat it + # as a UUID. + if uuidutils.is_uuid_like(fleet_ident): + return objects.Fleet.get_by_uuid(pecan.request.context, + fleet_ident) + + # We can refer to fleets by their name, if the client supports it + # if allow_fleet_logical_names(): + # if utils.is_hostname_safe(fleet_ident): + else: + return objects.Fleet.get_by_name(pecan.request.context, + fleet_ident) + + raise exception.InvalidUuidOrName(name=fleet_ident) + + raise exception.FleetNotFound(fleet=fleet_ident) + + def is_valid_board_name(name): """Determine if the provided name is a valid board name. diff --git a/iotronic/common/exception.py b/iotronic/common/exception.py index 361fd67..95dd78e 100644 --- a/iotronic/common/exception.py +++ b/iotronic/common/exception.py @@ -629,3 +629,15 @@ class NetworkError(IotronicException): class DatabaseVersionTooOld(IotronicException): _msg_fmt = _("Database version is too old") + + +class FleetNotFound(NotFound): + message = _("Fleet %(Fleet)s could not be found.") + + +class FleetAlreadyExists(Conflict): + message = _("A Fleet with UUID %(uuid)s already exists.") + + +class FleetAlreadyExposed(Conflict): + message = _("A Fleet with UUID %(uuid)s already exposed.") diff --git a/iotronic/common/policy.py b/iotronic/common/policy.py index ae17eae..122e30d 100644 --- a/iotronic/common/policy.py +++ b/iotronic/common/policy.py @@ -104,7 +104,6 @@ plugin_policies = [ ] - injection_plugin_policies = [ policy.RuleDefault('iot:plugin_on_board:get', 'rule:admin_or_owner', @@ -166,6 +165,22 @@ exposed_service_policies = [ ] +fleet_policies = [ + policy.RuleDefault('iot:fleet:get', + 'rule:is_admin or rule:is_iot_member', + description='Retrieve Fleet records'), + policy.RuleDefault('iot:fleet:create', + 'rule:is_iot_member', + description='Create Fleet records'), + policy.RuleDefault('iot:fleet:get_one', 'rule:admin_or_owner', + description='Retrieve a Fleet record'), + policy.RuleDefault('iot:fleet:delete', 'rule:admin_or_owner', + description='Delete Fleet records'), + policy.RuleDefault('iot:fleet:update', 'rule:admin_or_owner', + description='Update Fleet records'), + +] + def list_policies(): policies = (default_policies @@ -175,6 +190,7 @@ def list_policies(): + service_policies + exposed_service_policies + port_on_board_policies + + fleet_policies ) return policies diff --git a/iotronic/conductor/endpoints.py b/iotronic/conductor/endpoints.py index fa2a865..302f0ad 100644 --- a/iotronic/conductor/endpoints.py +++ b/iotronic/conductor/endpoints.py @@ -26,7 +26,6 @@ from oslo_log import log as logging import oslo_messaging import random - LOG = logging.getLogger(__name__) serializer = objects_base.IotronicObjectSerializer() @@ -489,3 +488,23 @@ class ConductorEndpoint(object): except Exception as e: LOG.error(str(e)) + + def create_fleet(self, ctx, fleet_obj): + new_fleet = serializer.deserialize_entity(ctx, fleet_obj) + LOG.debug('Creating fleet %s', + new_fleet.name) + new_fleet.create() + return serializer.serialize_entity(ctx, new_fleet) + + def destroy_fleet(self, ctx, fleet_id): + LOG.info('Destroying fleet with id %s', + fleet_id) + fleet = objects.Fleet.get_by_uuid(ctx, fleet_id) + fleet.destroy() + return + + def update_fleet(self, ctx, fleet_obj): + fleet = serializer.deserialize_entity(ctx, fleet_obj) + LOG.debug('Updating fleet %s', fleet.name) + fleet.save() + return serializer.serialize_entity(ctx, fleet) diff --git a/iotronic/conductor/rpcapi.py b/iotronic/conductor/rpcapi.py index 7939038..c8f75b9 100644 --- a/iotronic/conductor/rpcapi.py +++ b/iotronic/conductor/rpcapi.py @@ -309,3 +309,45 @@ class ConductorAPI(object): return cctxt.call(context, 'remove_VIF_from_board', board_uuid=board_uuid, port_uuid=port_uuid) + + def create_fleet(self, context, fleet_obj, topic=None): + """Add a fleet on the cloud + + :param context: request context. + :param fleet_obj: a changed (but not saved) fleet object. + :param topic: RPC topic. Defaults to self.topic. + :returns: created fleet object + + """ + cctxt = self.client.prepare(topic=topic or self.topic, version='1.0') + return cctxt.call(context, 'create_fleet', + fleet_obj=fleet_obj) + + def destroy_fleet(self, context, fleet_id, topic=None): + """Delete a fleet. + + :param context: request context. + :param fleet_id: fleet id or uuid. + :raises: FleetLocked if fleet is locked by another conductor. + :raises: FleetAssociated if the fleet contains an instance + associated with it. + :raises: InvalidState if the fleet is in the wrong provision + state to perform deletion. + """ + cctxt = self.client.prepare(topic=topic or self.topic, version='1.0') + return cctxt.call(context, 'destroy_fleet', fleet_id=fleet_id) + + def update_fleet(self, context, fleet_obj, topic=None): + """Synchronously, have a conductor update the fleet's information. + + Update the fleet's information in the database and + return a fleet object. + + :param context: request context. + :param fleet_obj: a changed (but not saved) fleet object. + :param topic: RPC topic. Defaults to self.topic. + :returns: updated fleet object, including all fields. + + """ + cctxt = self.client.prepare(topic=topic or self.topic, version='1.0') + return cctxt.call(context, 'update_fleet', fleet_obj=fleet_obj) diff --git a/iotronic/db/api.py b/iotronic/db/api.py index 7c5770e..dc6ec74 100644 --- a/iotronic/db/api.py +++ b/iotronic/db/api.py @@ -575,3 +575,54 @@ class Connection(object): :param port_uuid: The uuid of a port. """ + + @abc.abstractmethod + def get_fleet_by_id(self, fleet_id): + """Return a fleet. + + :param fleet_id: The id of a fleet. + :returns: A fleet. + """ + + @abc.abstractmethod + def get_fleet_by_uuid(self, fleet_uuid): + """Return a fleet. + + :param fleet_uuid: The uuid of a fleet. + :returns: A fleet. + """ + + @abc.abstractmethod + def get_fleet_by_name(self, fleet_name): + """Return a fleet. + + :param fleet_name: The logical name of a fleet. + :returns: A fleet. + """ + + @abc.abstractmethod + def create_fleet(self, values): + """Create a new fleet. + + :param values: A dict containing several items used to identify + and track the fleet + :returns: A fleet. + """ + + @abc.abstractmethod + def destroy_fleet(self, fleet_id): + """Destroy a fleet and all associated interfaces. + + :param fleet_id: The id or uuid of a fleet. + """ + + @abc.abstractmethod + def update_fleet(self, fleet_id, values): + """Update properties of a fleet. + + :param fleet_id: The id or uuid of a fleet. + :param values: Dict of values to update. + :returns: A fleet. + :raises: FleetAssociated + :raises: FleetNotFound + """ diff --git a/iotronic/db/sqlalchemy/alembic/versions/b578199e4e64_add_fleets.py b/iotronic/db/sqlalchemy/alembic/versions/b578199e4e64_add_fleets.py new file mode 100644 index 0000000..e56c734 --- /dev/null +++ b/iotronic/db/sqlalchemy/alembic/versions/b578199e4e64_add_fleets.py @@ -0,0 +1,37 @@ +# 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. + +# revision identifiers, used by Alembic. +revision = 'b578199e4e64' +down_revision = 'df35e9cbeaff' + +from alembic import op +import iotronic.db.sqlalchemy.models +import sqlalchemy as sa + + +def upgrade(): + op.create_table('fleets', + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('uuid', sa.String(length=36), nullable=True), + sa.Column('name', sa.String(length=36), nullable=True), + sa.Column('project', sa.String(length=36), nullable=True), + sa.Column('description', sa.String(length=300), + nullable=True), + sa.Column('extra', + iotronic.db.sqlalchemy.models.JSONEncodedDict(), + nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('uuid', name='uniq_fleets0uuid') + ) diff --git a/iotronic/db/sqlalchemy/api.py b/iotronic/db/sqlalchemy/api.py index 4055385..a9a96ca 100644 --- a/iotronic/db/sqlalchemy/api.py +++ b/iotronic/db/sqlalchemy/api.py @@ -161,6 +161,14 @@ class Connection(api.Connection): query = query.filter(models.Plugin.owner == filters['owner']) return query + def _add_fleets_filters(self, query, filters): + if filters is None: + filters = [] + + if 'project' in filters: + query = query.filter(models.Fleet.project == filters['project']) + return query + def _add_wampagents_filters(self, query, filters): if filters is None: filters = [] @@ -927,3 +935,92 @@ class Connection(api.Connection): count = query.delete() if count == 0: raise exception.PortNotFound(uuid=uuid) + +# FLEET api + + def get_fleet_by_id(self, fleet_id): + query = model_query(models.Fleet).filter_by(id=fleet_id) + try: + return query.one() + except NoResultFound: + raise exception.FleetNotFound(fleet=fleet_id) + + def get_fleet_by_uuid(self, fleet_uuid): + query = model_query(models.Fleet).filter_by(uuid=fleet_uuid) + try: + return query.one() + except NoResultFound: + raise exception.FleetNotFound(fleet=fleet_uuid) + + def get_fleet_by_name(self, fleet_name): + query = model_query(models.Fleet).filter_by(name=fleet_name) + try: + return query.one() + except NoResultFound: + raise exception.FleetNotFound(fleet=fleet_name) + + def destroy_fleet(self, fleet_id): + + session = get_session() + with session.begin(): + query = model_query(models.Fleet, session=session) + query = add_identity_filter(query, fleet_id) + try: + fleet_ref = query.one() + except NoResultFound: + raise exception.FleetNotFound(fleet=fleet_id) + + # Get fleet ID, if an UUID was supplied. The ID is + # required for deleting all ports, attached to the fleet. + if uuidutils.is_uuid_like(fleet_id): + fleet_id = fleet_ref['id'] + + query.delete() + + def update_fleet(self, fleet_id, values): + # NOTE(dtantsur): this can lead to very strange errors + if 'uuid' in values: + msg = _("Cannot overwrite UUID for an existing Fleet.") + raise exception.InvalidParameterValue(err=msg) + + try: + return self._do_update_fleet(fleet_id, values) + except db_exc.DBDuplicateEntry as e: + if 'name' in e.columns: + raise exception.DuplicateName(name=values['name']) + elif 'uuid' in e.columns: + raise exception.FleetAlreadyExists(uuid=values['uuid']) + else: + raise e + + def create_fleet(self, values): + # ensure defaults are present for new fleets + if 'uuid' not in values: + values['uuid'] = uuidutils.generate_uuid() + fleet = models.Fleet() + fleet.update(values) + try: + fleet.save() + except db_exc.DBDuplicateEntry: + raise exception.FleetAlreadyExists(uuid=values['uuid']) + return fleet + + def get_fleet_list(self, filters=None, limit=None, marker=None, + sort_key=None, sort_dir=None): + query = model_query(models.Fleet) + query = self._add_fleets_filters(query, filters) + return _paginate_query(models.Fleet, limit, marker, + sort_key, sort_dir, query) + + def _do_update_fleet(self, fleet_id, values): + session = get_session() + with session.begin(): + query = model_query(models.Fleet, session=session) + query = add_identity_filter(query, fleet_id) + try: + ref = query.with_lockmode('update').one() + except NoResultFound: + raise exception.FleetNotFound(fleet=fleet_id) + + ref.update(values) + return ref diff --git a/iotronic/db/sqlalchemy/models.py b/iotronic/db/sqlalchemy/models.py index 25555ca..351d411 100644 --- a/iotronic/db/sqlalchemy/models.py +++ b/iotronic/db/sqlalchemy/models.py @@ -264,4 +264,20 @@ class Port(Base): ip = Column(String(36)) # status = Column(String(36)) network = Column(String(36)) + + # security_groups = Column(String(40)) + +class Fleet(Base): + """Represents a fleet.""" + + __tablename__ = 'fleets' + __table_args__ = ( + schema.UniqueConstraint('uuid', name='uniq_fleets0uuid'), + table_args()) + id = Column(Integer, primary_key=True) + uuid = Column(String(36)) + name = Column(String(36)) + project = Column(String(36)) + description = Column(String(300)) + extra = Column(JSONEncodedDict) diff --git a/iotronic/objects/__init__.py b/iotronic/objects/__init__.py index d0c03e1..499a4f6 100644 --- a/iotronic/objects/__init__.py +++ b/iotronic/objects/__init__.py @@ -15,6 +15,7 @@ from iotronic.objects import board from iotronic.objects import conductor from iotronic.objects import exposedservice +from iotronic.objects import fleet from iotronic.objects import injectionplugin from iotronic.objects import location from iotronic.objects import plugin @@ -33,6 +34,7 @@ SessionWP = sessionwp.SessionWP WampAgent = wampagent.WampAgent Service = service.Service Port = port.Port +Fleet = fleet.Fleet __all__ = ( Conductor, @@ -44,5 +46,6 @@ __all__ = ( Plugin, InjectionPlugin, ExposedService, - Port + Port, + Fleet ) diff --git a/iotronic/objects/fleet.py b/iotronic/objects/fleet.py new file mode 100644 index 0000000..67b2723 --- /dev/null +++ b/iotronic/objects/fleet.py @@ -0,0 +1,189 @@ +# coding=utf-8 +# +# +# 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_utils import strutils +from oslo_utils import uuidutils + +from iotronic.common import exception +from iotronic.db import api as db_api +from iotronic.objects import base +from iotronic.objects import utils as obj_utils + + +class Fleet(base.IotronicObject): + # Version 1.0: Initial version + VERSION = '1.0' + + dbapi = db_api.get_instance() + + fields = { + 'id': int, + 'uuid': obj_utils.str_or_none, + 'name': obj_utils.str_or_none, + 'project': obj_utils.str_or_none, + 'description': obj_utils.str_or_none, + 'extra': obj_utils.dict_or_none, + } + + @staticmethod + def _from_db_object(fleet, db_fleet): + """Converts a database entity to a formal object.""" + for field in fleet.fields: + fleet[field] = db_fleet[field] + fleet.obj_reset_changes() + return fleet + + @base.remotable_classmethod + def get(cls, context, fleet_id): + """Find a fleet based on its id or uuid and return a Board object. + + :param fleet_id: the id *or* uuid of a fleet. + :returns: a :class:`Board` object. + """ + if strutils.is_int_like(fleet_id): + return cls.get_by_id(context, fleet_id) + elif uuidutils.is_uuid_like(fleet_id): + return cls.get_by_uuid(context, fleet_id) + else: + raise exception.InvalidIdentity(identity=fleet_id) + + @base.remotable_classmethod + def get_by_id(cls, context, fleet_id): + """Find a fleet based on its integer id and return a Board object. + + :param fleet_id: the id of a fleet. + :returns: a :class:`Board` object. + """ + db_fleet = cls.dbapi.get_fleet_by_id(fleet_id) + fleet = Fleet._from_db_object(cls(context), db_fleet) + return fleet + + @base.remotable_classmethod + def get_by_uuid(cls, context, uuid): + """Find a fleet based on uuid and return a Board object. + + :param uuid: the uuid of a fleet. + :returns: a :class:`Board` object. + """ + db_fleet = cls.dbapi.get_fleet_by_uuid(uuid) + fleet = Fleet._from_db_object(cls(context), db_fleet) + return fleet + + @base.remotable_classmethod + def get_by_name(cls, context, name): + """Find a fleet based on name and return a Board object. + + :param name: the logical name of a fleet. + :returns: a :class:`Board` object. + """ + db_fleet = cls.dbapi.get_fleet_by_name(name) + fleet = Fleet._from_db_object(cls(context), db_fleet) + return fleet + + @base.remotable_classmethod + def list(cls, context, limit=None, marker=None, sort_key=None, + sort_dir=None, filters=None): + """Return a list of Fleet objects. + + :param context: Security context. + :param limit: maximum number of resources to return in a single result. + :param marker: pagination marker for large data sets. + :param sort_key: column to sort results by. + :param sort_dir: direction to sort. "asc" or "desc". + :param filters: Filters to apply. + :returns: a list of :class:`Fleet` object. + + """ + db_fleets = cls.dbapi.get_fleet_list(filters=filters, + limit=limit, + marker=marker, + sort_key=sort_key, + sort_dir=sort_dir) + return [Fleet._from_db_object(cls(context), obj) + for obj in db_fleets] + + @base.remotable + def create(self, context=None): + """Create a Fleet record in the DB. + + Column-wise updates will be made based on the result of + self.what_changed(). If target_power_state is provided, + it will be checked against the in-database copy of the + fleet before updates are made. + + :param context: Security context. NOTE: This should only + be used internally by the indirection_api. + Unfortunately, RPC requires context as the first + argument, even though we don't use it. + A context should be set when instantiating the + object, e.g.: Fleet(context) + + """ + + values = self.obj_get_changes() + db_fleet = self.dbapi.create_fleet(values) + self._from_db_object(self, db_fleet) + + @base.remotable + def destroy(self, context=None): + """Delete the Fleet from the DB. + + :param context: Security context. NOTE: This should only + be used internally by the indirection_api. + Unfortunately, RPC requires context as the first + argument, even though we don't use it. + A context should be set when instantiating the + object, e.g.: Fleet(context) + """ + self.dbapi.destroy_fleet(self.uuid) + self.obj_reset_changes() + + @base.remotable + def save(self, context=None): + """Save updates to this Fleet. + + Column-wise updates will be made based on the result of + self.what_changed(). If target_power_state is provided, + it will be checked against the in-database copy of the + fleet before updates are made. + + :param context: Security context. NOTE: This should only + be used internally by the indirection_api. + Unfortunately, RPC requires context as the first + argument, even though we don't use it. + A context should be set when instantiating the + object, e.g.: Fleet(context) + """ + updates = self.obj_get_changes() + self.dbapi.update_fleet(self.uuid, updates) + self.obj_reset_changes() + + @base.remotable + def refresh(self, context=None): + """Refresh the object by re-fetching from the DB. + + :param context: Security context. NOTE: This should only + be used internally by the indirection_api. + Unfortunately, RPC requires context as the first + argument, even though we don't use it. + A context should be set when instantiating the + object, e.g.: Fleet(context) + """ + current = self.__class__.get_by_uuid(self._context, self.uuid) + for field in self.fields: + if (hasattr( + self, base.get_attrname(field)) + and self[field] != current[field]): + self[field] = current[field]