Add k8s replication support for magnum
Implements bp add-k8s-replication-controller Change-Id: I5aee1af171b0a3a1476fd9fee1fdf26755ab0805
This commit is contained in:
parent
d46ac431e5
commit
b37036c34d
@ -32,6 +32,7 @@ from magnum.api.controllers.v1 import baymodel
|
||||
from magnum.api.controllers.v1 import container
|
||||
from magnum.api.controllers.v1 import node
|
||||
from magnum.api.controllers.v1 import pod
|
||||
from magnum.api.controllers.v1 import replicationcontroller as rc
|
||||
from magnum.api.controllers.v1 import service
|
||||
|
||||
|
||||
@ -90,6 +91,9 @@ class V1(APIBase):
|
||||
pods = [link.Link]
|
||||
"""Links to the pods resource"""
|
||||
|
||||
rcs = [link.Link]
|
||||
"""Links to the rcs resource"""
|
||||
|
||||
baymodels = [link.Link]
|
||||
"""Links to the baymodels resource"""
|
||||
|
||||
@ -123,6 +127,13 @@ class V1(APIBase):
|
||||
'pods', '',
|
||||
bookmark=True)
|
||||
]
|
||||
v1.rcs = [link.Link.make_link('self', pecan.request.host_url,
|
||||
'rcs', ''),
|
||||
link.Link.make_link('bookmark',
|
||||
pecan.request.host_url,
|
||||
'rcs', '',
|
||||
bookmark=True)
|
||||
]
|
||||
v1.baymodels = [link.Link.make_link('self', pecan.request.host_url,
|
||||
'baymodels', ''),
|
||||
link.Link.make_link('bookmark',
|
||||
@ -162,6 +173,7 @@ class Controller(rest.RestController):
|
||||
containers = container.ContainersController()
|
||||
nodes = node.NodesController()
|
||||
pods = pod.PodsController()
|
||||
rcs = rc.ReplicationControllersController()
|
||||
services = service.ServicesController()
|
||||
|
||||
@wsme_pecan.wsexpose(V1)
|
||||
|
335
magnum/api/controllers/v1/replicationcontroller.py
Normal file
335
magnum/api/controllers/v1/replicationcontroller.py
Normal file
@ -0,0 +1,335 @@
|
||||
# Copyright 2015 IBM Corp.
|
||||
#
|
||||
# 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.
|
||||
|
||||
import datetime
|
||||
|
||||
import pecan
|
||||
from pecan import rest
|
||||
import wsme
|
||||
from wsme import types as wtypes
|
||||
import wsmeext.pecan as wsme_pecan
|
||||
|
||||
from magnum.api.controllers import base
|
||||
from magnum.api.controllers import link
|
||||
from magnum.api.controllers.v1 import collection
|
||||
from magnum.api.controllers.v1 import types
|
||||
from magnum.api.controllers.v1 import utils as api_utils
|
||||
from magnum.common import exception
|
||||
from magnum import objects
|
||||
|
||||
|
||||
class ReplicationControllerPatchType(types.JsonPatchType):
|
||||
|
||||
@staticmethod
|
||||
def mandatory_attrs():
|
||||
return ['/rc_uuid']
|
||||
|
||||
|
||||
class ReplicationController(base.APIBase):
|
||||
"""API representation of a ReplicationController.
|
||||
|
||||
This class enforces type checking and value constraints, and converts
|
||||
between the internal object model and the API representation of a
|
||||
ReplicationController.
|
||||
"""
|
||||
|
||||
_rc_uuid = None
|
||||
|
||||
def _get_rc_uuid(self):
|
||||
return self._rc_uuid
|
||||
|
||||
def _set_rc_uuid(self, value):
|
||||
if value and self._rc_uuid != value:
|
||||
try:
|
||||
rc = objects.ReplicationController.get(pecan.request.context,
|
||||
value)
|
||||
self._rc_uuid = rc.uuid
|
||||
# NOTE(jay-lau-513): Create the rc_id attribute on-the-fly
|
||||
# to satisfy the api -> rpc object
|
||||
# conversion.
|
||||
self.rc_id = rc.id
|
||||
except exception.ReplicationControllerNotFound as e:
|
||||
# Change error code because 404 (NotFound) is inappropriate
|
||||
# response for a POST request to create a rc
|
||||
e.code = 400 # BadRequest
|
||||
raise e
|
||||
elif value == wtypes.Unset:
|
||||
self._rc_uuid = wtypes.Unset
|
||||
|
||||
uuid = types.uuid
|
||||
"""Unique UUID for this ReplicationController"""
|
||||
|
||||
name = wtypes.text
|
||||
"""Name of this ReplicationController"""
|
||||
|
||||
images = [wtypes.text]
|
||||
"""A list of images used by containers in this ReplicationController."""
|
||||
|
||||
selector = {wtypes.text: wtypes.text}
|
||||
"""Selector of this ReplicationController"""
|
||||
|
||||
replicas = wtypes.IntegerType()
|
||||
"""Replicas of this ReplicationController """
|
||||
|
||||
rc_definition_url = wtypes.text
|
||||
"""URL for ReplicationController file to create the RC"""
|
||||
|
||||
links = wsme.wsattr([link.Link], readonly=True)
|
||||
"""A list containing a self link and associated rc links"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(ReplicationController, self).__init__()
|
||||
|
||||
self.fields = []
|
||||
fields = list(objects.ReplicationController.fields)
|
||||
# NOTE(jay-lau-513): rc_uuid is not part of
|
||||
# objects.ReplicationController.fields
|
||||
# because it's an API-only attribute
|
||||
fields.append('rc_uuid')
|
||||
for field in 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))
|
||||
|
||||
# NOTE(jay-lau-513): rc_id is an attribute created on-the-fly
|
||||
# by _set_rc_uuid(), it needs to be present in the fields so
|
||||
# that as_dict() will contain rc_id field when converting it
|
||||
# before saving it in the database.
|
||||
self.fields.append('rc_id')
|
||||
setattr(self, 'rc_uuid', kwargs.get('rc_id', wtypes.Unset))
|
||||
|
||||
@staticmethod
|
||||
def _convert_with_links(rc, url, expand=True):
|
||||
if not expand:
|
||||
rc.unset_fields_except(['uuid', 'name', 'images', 'selector',
|
||||
'replicas'])
|
||||
|
||||
# never expose the rc_id attribute
|
||||
rc.rc_id = wtypes.Unset
|
||||
|
||||
rc.links = [link.Link.make_link('self', url,
|
||||
'rcs', rc.uuid),
|
||||
link.Link.make_link('bookmark', url,
|
||||
'rcs', rc.uuid,
|
||||
bookmark=True)
|
||||
]
|
||||
return rc
|
||||
|
||||
@classmethod
|
||||
def convert_with_links(cls, rpc_rc, expand=True):
|
||||
rc = ReplicationController(**rpc_rc.as_dict())
|
||||
return cls._convert_with_links(rc, pecan.request.host_url, expand)
|
||||
|
||||
@classmethod
|
||||
def sample(cls, expand=True):
|
||||
sample = cls(uuid='f978db47-9a37-4e9f-8572-804a10abc0aa',
|
||||
name='MyReplicationController',
|
||||
images=['MyImage'],
|
||||
selector={'name': 'foo'},
|
||||
replicas=2,
|
||||
created_at=datetime.datetime.utcnow(),
|
||||
updated_at=datetime.datetime.utcnow())
|
||||
# NOTE(jay-lau-513): rc_uuid getter() method look at the
|
||||
# _rc_uuid variable
|
||||
sample._rc_uuid = '87504bd9-ca50-40fd-b14e-bcb23ed42b27'
|
||||
return cls._convert_with_links(sample, 'http://localhost:9511', expand)
|
||||
|
||||
|
||||
class ReplicationControllerCollection(collection.Collection):
|
||||
"""API representation of a collection of ReplicationControllers."""
|
||||
|
||||
rcs = [ReplicationController]
|
||||
"""A list containing ReplicationController objects"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._type = 'rcs'
|
||||
|
||||
@staticmethod
|
||||
def convert_with_links(rpc_rcs, limit, url=None, expand=False, **kwargs):
|
||||
collection = ReplicationControllerCollection()
|
||||
collection.rcs = [ReplicationController.convert_with_links(p, expand)
|
||||
for p in rpc_rcs]
|
||||
collection.next = collection.get_next(limit, url=url, **kwargs)
|
||||
return collection
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
sample = cls()
|
||||
sample.rcs = [ReplicationController.sample(expand=False)]
|
||||
return sample
|
||||
|
||||
|
||||
class ReplicationControllersController(rest.RestController):
|
||||
"""REST controller for ReplicationControllers."""
|
||||
|
||||
def __init__(self):
|
||||
super(ReplicationControllersController, self).__init__()
|
||||
|
||||
from_rcs = False
|
||||
"""A flag to indicate if the requests to this controller are coming
|
||||
from the top-level resource ReplicationControllers."""
|
||||
|
||||
_custom_actions = {
|
||||
'detail': ['GET'],
|
||||
}
|
||||
|
||||
def _get_rcs_collection(self, marker, limit,
|
||||
sort_key, sort_dir, expand=False,
|
||||
resource_url=None):
|
||||
|
||||
limit = api_utils.validate_limit(limit)
|
||||
sort_dir = api_utils.validate_sort_dir(sort_dir)
|
||||
|
||||
marker_obj = None
|
||||
if marker:
|
||||
marker_obj = objects.ReplicationController.get_by_uuid(
|
||||
pecan.request.context,
|
||||
marker)
|
||||
|
||||
rcs = pecan.request.rpcapi.rc_list(pecan.request.context, limit,
|
||||
marker_obj, sort_key=sort_key,
|
||||
sort_dir=sort_dir)
|
||||
|
||||
return ReplicationControllerCollection.convert_with_links(rcs, limit,
|
||||
url=resource_url,
|
||||
expand=expand,
|
||||
sort_key=sort_key,
|
||||
sort_dir=sort_dir)
|
||||
|
||||
@wsme_pecan.wsexpose(ReplicationControllerCollection, types.uuid,
|
||||
types.uuid, int, wtypes.text, wtypes.text)
|
||||
def get_all(self, rc_uuid=None, marker=None, limit=None,
|
||||
sort_key='id', sort_dir='asc'):
|
||||
"""Retrieve a list of ReplicationControllers.
|
||||
|
||||
:param marker: pagination marker for large data sets.
|
||||
:param limit: maximum number of resources to return in a single result.
|
||||
:param sort_key: column to sort results by. Default: id.
|
||||
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
|
||||
"""
|
||||
return self._get_rcs_collection(marker, limit, sort_key,
|
||||
sort_dir)
|
||||
|
||||
@wsme_pecan.wsexpose(ReplicationControllerCollection, types.uuid,
|
||||
types.uuid, int, wtypes.text, wtypes.text)
|
||||
def detail(self, rc_uuid=None, marker=None, limit=None,
|
||||
sort_key='id', sort_dir='asc'):
|
||||
"""Retrieve a list of ReplicationControllers with detail.
|
||||
|
||||
:param rc_uuid: UUID of a ReplicationController, to get only
|
||||
ReplicationControllers for the ReplicationController.
|
||||
:param marker: pagination marker for large data sets.
|
||||
:param limit: maximum number of resources to return in a single result.
|
||||
:param sort_key: column to sort results by. Default: id.
|
||||
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
|
||||
"""
|
||||
# NOTE(jay-lau-513): /detail should only work agaist collections
|
||||
parent = pecan.request.path.split('/')[:-1][-1]
|
||||
if parent != "rcs":
|
||||
raise exception.HTTPNotFound
|
||||
|
||||
expand = True
|
||||
resource_url = '/'.join(['rcs', 'detail'])
|
||||
return self._get_rcs_collection(marker, limit,
|
||||
sort_key, sort_dir, expand,
|
||||
resource_url)
|
||||
|
||||
@wsme_pecan.wsexpose(ReplicationController, types.uuid)
|
||||
def get_one(self, rc_uuid):
|
||||
"""Retrieve information about the given ReplicationController.
|
||||
|
||||
:param rc_uuid: UUID of a ReplicationController.
|
||||
"""
|
||||
if self.from_rcs:
|
||||
raise exception.OperationNotPermitted
|
||||
|
||||
rpc_rc = objects.ReplicationController.get_by_uuid(
|
||||
pecan.request.context, rc_uuid)
|
||||
return ReplicationController.convert_with_links(rpc_rc)
|
||||
|
||||
@wsme_pecan.wsexpose(ReplicationController, body=ReplicationController,
|
||||
status_code=201)
|
||||
def post(self, rc):
|
||||
"""Create a new ReplicationController.
|
||||
|
||||
:param rc: a ReplicationController within the request body.
|
||||
"""
|
||||
if self.from_rcs:
|
||||
raise exception.OperationNotPermitted
|
||||
|
||||
rc_obj = objects.ReplicationController(pecan.request.context,
|
||||
**rc.as_dict())
|
||||
new_rc = pecan.request.rpcapi.rc_create(rc_obj)
|
||||
# Set the HTTP Location Header
|
||||
pecan.response.location = link.build_url('rcs', new_rc.uuid)
|
||||
return ReplicationController.convert_with_links(new_rc)
|
||||
|
||||
@wsme.validate(types.uuid, [ReplicationControllerPatchType])
|
||||
@wsme_pecan.wsexpose(ReplicationController, types.uuid,
|
||||
body=[ReplicationControllerPatchType])
|
||||
def patch(self, rc_uuid, patch):
|
||||
"""Update an existing rc.
|
||||
|
||||
:param rc_uuid: UUID of a ReplicationController.
|
||||
:param patch: a json PATCH document to apply to this rc.
|
||||
"""
|
||||
if self.from_rcs:
|
||||
raise exception.OperationNotPermitted
|
||||
|
||||
rpc_rc = objects.ReplicationController.get_by_uuid(
|
||||
pecan.request.context, rc_uuid)
|
||||
try:
|
||||
rc_dict = rpc_rc.as_dict()
|
||||
# NOTE(jay-lau-513):
|
||||
# 1) Remove rc_id because it's an internal value and
|
||||
# not present in the API object
|
||||
# 2) Add rc_uuid
|
||||
rc_dict['rc_uuid'] = rc_dict.pop('rc_id', None)
|
||||
rc = ReplicationController(**api_utils.apply_jsonpatch(rc_dict,
|
||||
patch))
|
||||
except api_utils.JSONPATCH_EXCEPTIONS as e:
|
||||
raise exception.PatchError(patch=patch, reason=e)
|
||||
|
||||
# Update only the fields that have changed
|
||||
for field in objects.ReplicationController.fields:
|
||||
# ignore rc_definition_url as it was used for create rc
|
||||
if field == 'rc_definition_url':
|
||||
continue
|
||||
try:
|
||||
patch_val = getattr(rc, field)
|
||||
except AttributeError:
|
||||
# Ignore fields that aren't exposed in the API
|
||||
continue
|
||||
if patch_val == wtypes.Unset:
|
||||
patch_val = None
|
||||
if rpc_rc[field] != patch_val:
|
||||
rpc_rc[field] = patch_val
|
||||
|
||||
rpc_rc.save()
|
||||
return ReplicationController.convert_with_links(rpc_rc)
|
||||
|
||||
@wsme_pecan.wsexpose(None, types.uuid, status_code=204)
|
||||
def delete(self, rc_uuid):
|
||||
"""Delete a ReplicationController.
|
||||
|
||||
:param rc_uuid: UUID of a ReplicationController.
|
||||
"""
|
||||
if self.from_rcs:
|
||||
raise exception.OperationNotPermitted
|
||||
|
||||
rpc_rc = objects.ReplicationController.get_by_uuid(
|
||||
pecan.request.context, rc_uuid)
|
||||
pecan.request.rpcapi.rc_delete(rpc_rc)
|
@ -407,6 +407,14 @@ class PodNotLocked(Invalid):
|
||||
message = _("Pod %(pod)s found not to be locked on release")
|
||||
|
||||
|
||||
class ReplicationControllerNotFound(ResourceNotFound):
|
||||
message = _("ReplicationController %(rc)s could not be found.")
|
||||
|
||||
|
||||
class ReplicationControllerAlreadyExists(Conflict):
|
||||
message = _("A ReplicationController with UUID %(uuid)s already exists.")
|
||||
|
||||
|
||||
class ServiceNotFound(ResourceNotFound):
|
||||
message = _("Service %(service)s could not be found.")
|
||||
|
||||
@ -439,4 +447,4 @@ class KeystoneFailure(MagnumException):
|
||||
|
||||
class CatalogNotFound(MagnumException):
|
||||
message = _("Service type %(service_type)s with endpoint type "
|
||||
"%(endpoint_type)s not found in keystone service catalog.")
|
||||
"%(endpoint_type)s not found in keystone service catalog.")
|
||||
|
@ -87,6 +87,21 @@ class API(rpc_service.API):
|
||||
def pod_show(self, uuid):
|
||||
return self._call('pod_show', uuid=uuid)
|
||||
|
||||
# ReplicationController Operations
|
||||
|
||||
def rc_create(self, rc):
|
||||
return self._call('rc_create', rc=rc)
|
||||
|
||||
def rc_list(self, context, limit, marker, sort_key, sort_dir):
|
||||
return objects.ReplicationController.list(context, limit, marker,
|
||||
sort_key, sort_dir)
|
||||
|
||||
def rc_delete(self, rc):
|
||||
return self._call('rc_delete', rc)
|
||||
|
||||
def rc_show(self, context, uuid):
|
||||
return objects.ReplicationController.get_by_uuid(context, uuid)
|
||||
|
||||
# Container operations
|
||||
|
||||
def container_create(self, name, container_uuid, container):
|
||||
|
@ -199,3 +199,54 @@ class KubeClient(object):
|
||||
except Exception as e:
|
||||
LOG.error("Couldn't show pod %s due to error %s" % (uuid, e))
|
||||
return None
|
||||
|
||||
# Replication Controller Operations
|
||||
@staticmethod
|
||||
def rc_create(contents):
|
||||
LOG.debug("rc_create contents %s" % contents)
|
||||
try:
|
||||
if contents.rc_definition_url:
|
||||
out, err = utils.trycmd('kubectl', 'create', '-f',
|
||||
contents.rc_definition_url)
|
||||
else:
|
||||
# TODO(jay-lau-513) Translate the contents to a json stdin
|
||||
out, err = utils.trycmd('echo contents | kubectl', 'create',
|
||||
'-f', '-')
|
||||
if err:
|
||||
return False
|
||||
except Exception as e:
|
||||
LOG.error("Couldn't create rc with contents %s due to error %s"
|
||||
% (contents, e))
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def rc_update(contents):
|
||||
LOG.debug("rc_update contents %s" % contents)
|
||||
try:
|
||||
if contents.rc_definition_url:
|
||||
out, err = utils.trycmd('kubectl', 'update', '-f',
|
||||
contents.rc_definition_url)
|
||||
else:
|
||||
# TODO(jay-lau-513) Translate the contents to a json stdin
|
||||
out, err = utils.trycmd('echo contents | kubectl', 'update',
|
||||
'-f', '-')
|
||||
if err:
|
||||
return False
|
||||
except Exception as e:
|
||||
LOG.error("Couldn't update rc with contents %s due to error %s"
|
||||
% (contents, e))
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def rc_delete(uuid):
|
||||
LOG.debug("rc_delete %s" % uuid)
|
||||
try:
|
||||
out, err = utils.trycmd('kubectl', 'delete', 'rc', uuid)
|
||||
if err:
|
||||
return False
|
||||
except Exception as e:
|
||||
LOG.error("Couldn't delete rc %s due to error %s" % (uuid, e))
|
||||
return False
|
||||
return True
|
||||
|
@ -113,3 +113,33 @@ class Handler(object):
|
||||
def pod_show(self, context, uuid):
|
||||
LOG.debug("pod_show")
|
||||
return self.kube_cli.pod_show(uuid)
|
||||
|
||||
# Replication Controller Operations
|
||||
def rc_create(self, context, rc):
|
||||
LOG.debug("rc_create")
|
||||
# trigger a kubectl command
|
||||
status = self.kube_cli.rc_create(rc)
|
||||
if not status:
|
||||
return None
|
||||
# call the rc object to persist in db
|
||||
rc.create(context)
|
||||
return rc
|
||||
|
||||
def rc_update(self, context, rc):
|
||||
LOG.debug("rc_update")
|
||||
# trigger a kubectl command
|
||||
status = self.kube_cli.rc_update(rc)
|
||||
if not status:
|
||||
return None
|
||||
# call the rc object to persist in db
|
||||
rc.refresh(context)
|
||||
return rc
|
||||
|
||||
def rc_delete(self, context, rc):
|
||||
LOG.debug("rc_delete ")
|
||||
# trigger a kubectl command
|
||||
status = self.kube_cli.pod_delete(rc.uuid)
|
||||
if not status:
|
||||
return None
|
||||
# call the rc object to persist in db
|
||||
rc.destroy(context)
|
||||
|
@ -653,3 +653,81 @@ class Connection(object):
|
||||
:raises: BayAssociated
|
||||
:raises: BayNotFound
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_rc_list(self, columns=None, filters=None, limit=None,
|
||||
marker=None, sort_key=None, sort_dir=None):
|
||||
"""Get specific columns for matching ReplicationController.
|
||||
|
||||
Return a list of the specified columns for all rcs that match the
|
||||
specified filters.
|
||||
|
||||
:param columns: List of column names to return.
|
||||
Defaults to 'id' column when columns == None.
|
||||
:param filters: Filters to apply. Defaults to None.
|
||||
|
||||
:param limit: Maximum number of pods to return.
|
||||
:param marker: the last item of the previous page; we return the next
|
||||
result set.
|
||||
:param sort_key: Attribute by which results should be sorted.
|
||||
:param sort_dir: direction in which results should be sorted.
|
||||
(asc, desc)
|
||||
:returns: A list of tuples of the specified columns.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def create_rc(self, values):
|
||||
"""Create a new ReplicationController.
|
||||
|
||||
:param values: A dict containing several items used to identify
|
||||
and track the rc, and several dicts which are passed
|
||||
into the Drivers when managing this pod. For example:
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
'uuid': utils.generate_uuid(),
|
||||
'name': 'example',
|
||||
'images': '["myimage"]'
|
||||
}
|
||||
:returns: A ReplicationController.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_rc_by_id(self, rc_id):
|
||||
"""Return a ReplicationController.
|
||||
|
||||
:param rc_id: The id of a rc.
|
||||
:returns: A ReplicationController.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_rc_by_uuid(self, rc_uuid):
|
||||
"""Return a ReplicationController.
|
||||
|
||||
:param rc_uuid: The uuid of a ReplicationController.
|
||||
:returns: A ReplicationController.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_rc_by_name(self, rc_name):
|
||||
"""Return a ReplicationController.
|
||||
|
||||
:param rc_name: The name of a ReplicationController.
|
||||
:returns: A ReplicationController.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def destroy_rc(self, rc_id):
|
||||
"""Destroy a ReplicationController and all associated interfaces.
|
||||
|
||||
:param rc_id: The id or uuid of a ReplicationController.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def update_rc(self, rc_id, values):
|
||||
"""Update properties of a ReplicationController.
|
||||
|
||||
:param rc_id: The id or uuid of a ReplicationController.
|
||||
:returns: A ReplicationController.
|
||||
"""
|
||||
|
@ -1107,3 +1107,97 @@ class Connection(api.Connection):
|
||||
|
||||
ref.update(values)
|
||||
return ref
|
||||
|
||||
def _add_rcs_filters(self, query, filters):
|
||||
if filters is None:
|
||||
filters = []
|
||||
|
||||
if 'associated' in filters:
|
||||
if filters['associated']:
|
||||
query = query.filter(
|
||||
models.ReplicationController.instance_uuid is not None)
|
||||
else:
|
||||
query = query.filter(
|
||||
models.ReplicationController.instance_uuid is None)
|
||||
|
||||
return query
|
||||
|
||||
def get_rc_list(self, filters=None, limit=None, marker=None,
|
||||
sort_key=None, sort_dir=None):
|
||||
query = model_query(models.ReplicationController)
|
||||
query = self._add_rcs_filters(query, filters)
|
||||
return _paginate_query(models.ReplicationController, limit, marker,
|
||||
sort_key, sort_dir, query)
|
||||
|
||||
def create_rc(self, values):
|
||||
# ensure defaults are present for new ReplicationController
|
||||
if not values.get('uuid'):
|
||||
values['uuid'] = utils.generate_uuid()
|
||||
|
||||
rc = models.ReplicationController()
|
||||
rc.update(values)
|
||||
try:
|
||||
rc.save()
|
||||
except db_exc.DBDuplicateEntry as exc:
|
||||
if 'instance_uuid' in exc.columns:
|
||||
raise exception.InstanceAssociated(
|
||||
instance_uuid=values['instance_uuid'],
|
||||
pod=values['uuid'])
|
||||
raise exception.ReplicationControllerAlreadyExists(
|
||||
uuid=values['uuid'])
|
||||
return rc
|
||||
|
||||
def get_rc_by_id(self, rc_id):
|
||||
query = model_query(models.ReplicationController).filter_by(id=rc_id)
|
||||
try:
|
||||
return query.one()
|
||||
except NoResultFound:
|
||||
raise exception.ReplicationControllerNotFound(rc=rc_id)
|
||||
|
||||
def get_rc_by_uuid(self, rc_uuid):
|
||||
query = model_query(models.ReplicationController).filter_by(
|
||||
uuid=rc_uuid)
|
||||
try:
|
||||
return query.one()
|
||||
except NoResultFound:
|
||||
raise exception.ReplicationControllerNotFound(rc=rc_uuid)
|
||||
|
||||
def get_rc_by_name(self, rc_name):
|
||||
query = model_query(models.ReplicationController).filter_by(
|
||||
name=rc_name)
|
||||
try:
|
||||
return query.one()
|
||||
except NoResultFound:
|
||||
raise exception.ReplicationControllerNotFound(rc=rc_name)
|
||||
|
||||
def destroy_rc(self, rc_id):
|
||||
session = get_session()
|
||||
with session.begin():
|
||||
query = model_query(models.ReplicationController, session=session)
|
||||
query = add_identity_filter(query, rc_id)
|
||||
query.delete()
|
||||
|
||||
def update_rc(self, rc_id, values):
|
||||
if 'uuid' in values:
|
||||
msg = _("Cannot overwrite UUID for an existing rc.")
|
||||
raise exception.InvalidParameterValue(err=msg)
|
||||
|
||||
try:
|
||||
return self._do_update_rc(rc_id, values)
|
||||
except db_exc.DBDuplicateEntry:
|
||||
raise exception.InstanceAssociated(
|
||||
instance_uuid=values['instance_uuid'],
|
||||
rc=rc_id)
|
||||
|
||||
def _do_update_rc(self, rc_id, values):
|
||||
session = get_session()
|
||||
with session.begin():
|
||||
query = model_query(models.ReplicationController, session=session)
|
||||
query = add_identity_filter(query, rc_id)
|
||||
try:
|
||||
ref = query.with_lockmode('update').one()
|
||||
except NoResultFound:
|
||||
raise exception.ReplicationControllerNotFound(rc=rc_id)
|
||||
|
||||
ref.update(values)
|
||||
return ref
|
||||
|
@ -17,6 +17,7 @@ from magnum.objects import baymodel
|
||||
from magnum.objects import container
|
||||
from magnum.objects import node
|
||||
from magnum.objects import pod
|
||||
from magnum.objects import replicationcontroller as rc
|
||||
from magnum.objects import service
|
||||
|
||||
|
||||
@ -25,6 +26,7 @@ Bay = bay.Bay
|
||||
BayModel = baymodel.BayModel
|
||||
Node = node.Node
|
||||
Pod = pod.Pod
|
||||
ReplicationController = rc.ReplicationController
|
||||
Service = service.Service
|
||||
|
||||
__all__ = (Bay,
|
||||
@ -32,4 +34,5 @@ __all__ = (Bay,
|
||||
Container,
|
||||
Node,
|
||||
Pod,
|
||||
ReplicationController,
|
||||
Service)
|
||||
|
196
magnum/objects/replicationcontroller.py
Normal file
196
magnum/objects/replicationcontroller.py
Normal file
@ -0,0 +1,196 @@
|
||||
# Copyright 2015 IBM Corp.
|
||||
#
|
||||
# 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 magnum.common import exception
|
||||
from magnum.common import utils
|
||||
from magnum.db import api as dbapi
|
||||
from magnum.objects import base
|
||||
from magnum.objects import utils as obj_utils
|
||||
|
||||
|
||||
class ReplicationController(base.MagnumObject):
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
|
||||
dbapi = dbapi.get_instance()
|
||||
|
||||
fields = {
|
||||
'id': int,
|
||||
'uuid': obj_utils.str_or_none,
|
||||
'name': obj_utils.str_or_none,
|
||||
'images': obj_utils.list_or_none,
|
||||
'selector': obj_utils.dict_or_none,
|
||||
'replicas': int,
|
||||
'rc_definition_url': obj_utils.str_or_none,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _from_db_object(rc, db_rc):
|
||||
"""Converts a database entity to a formal object."""
|
||||
for field in rc.fields:
|
||||
# ignore rc_definition_url as it was used for create rc
|
||||
if field == 'rc_definition_url':
|
||||
continue
|
||||
rc[field] = db_rc[field]
|
||||
|
||||
rc.obj_reset_changes()
|
||||
return rc
|
||||
|
||||
@staticmethod
|
||||
def _from_db_object_list(db_objects, cls, context):
|
||||
"""Converts a list of database entities to a list of formal objects."""
|
||||
return [ReplicationController._from_db_object(cls(context),
|
||||
obj) for obj in db_objects]
|
||||
|
||||
@base.remotable_classmethod
|
||||
def get(cls, context, rc_id):
|
||||
"""Find a ReplicationController based on its id or uuid and return a
|
||||
replication controller object.
|
||||
|
||||
:param rc_id: the id *or* uuid of a ReplicationController.
|
||||
:returns: a :class:`ReplicationController` object.
|
||||
"""
|
||||
if utils.is_int_like(rc_id):
|
||||
return cls.get_by_id(context, rc_id)
|
||||
elif utils.is_uuid_like(rc_id):
|
||||
return cls.get_by_uuid(context, rc_id)
|
||||
else:
|
||||
raise exception.InvalidIdentity(identity=rc_id)
|
||||
|
||||
@base.remotable_classmethod
|
||||
def get_by_id(cls, context, rc_id):
|
||||
"""Find a ReplicationController based on its integer id and return a
|
||||
ReplicationController object.
|
||||
|
||||
:param rc_id: the id of a ReplicationController.
|
||||
:returns: a :class:`ReplicationController` object.
|
||||
"""
|
||||
db_rc = cls.dbapi.get_rc_by_id(rc_id)
|
||||
rc = ReplicationController._from_db_object(cls(context), db_rc)
|
||||
return rc
|
||||
|
||||
@base.remotable_classmethod
|
||||
def get_by_uuid(cls, context, uuid):
|
||||
"""Find a ReplicationController based on uuid and return
|
||||
a :class:`ReplicationController` object.
|
||||
|
||||
:param uuid: the uuid of a ReplicationController.
|
||||
:param context: Security context
|
||||
:returns: a :class:`ReplicationController` object.
|
||||
"""
|
||||
db_rc = cls.dbapi.get_rc_by_uuid(uuid)
|
||||
rc = ReplicationController._from_db_object(cls(context), db_rc)
|
||||
return rc
|
||||
|
||||
@base.remotable_classmethod
|
||||
def get_by_name(cls, context, name):
|
||||
"""Find a ReplicationController based on name and return
|
||||
a :class:`ReplicationController` object.
|
||||
|
||||
:param name: the name of a ReplicationController.
|
||||
:param context: Security context
|
||||
:returns: a :class:`ReplicationController` object.
|
||||
"""
|
||||
db_rc = cls.dbapi.get_rc_by_name(name)
|
||||
rc = ReplicationController._from_db_object(cls(context), db_rc)
|
||||
return rc
|
||||
|
||||
@base.remotable_classmethod
|
||||
def list(cls, context, limit=None, marker=None,
|
||||
sort_key=None, sort_dir=None):
|
||||
"""Return a list of ReplicationController 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".
|
||||
:returns: a list of :class:`ReplicationController` object.
|
||||
|
||||
"""
|
||||
db_rcs = cls.dbapi.get_rc_list(limit=limit,
|
||||
marker=marker,
|
||||
sort_key=sort_key,
|
||||
sort_dir=sort_dir)
|
||||
return ReplicationController._from_db_object_list(db_rcs, cls, context)
|
||||
|
||||
@base.remotable
|
||||
def create(self, context=None):
|
||||
"""Create a ReplicationController record in 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.: ReplicationController(context)
|
||||
|
||||
"""
|
||||
values = self.obj_get_changes()
|
||||
db_rc = self.dbapi.create_rc(values)
|
||||
self._from_db_object(self, db_rc)
|
||||
|
||||
@base.remotable
|
||||
def destroy(self, context=None):
|
||||
"""Delete the ReplicationController 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.: ReplicationController(context)
|
||||
"""
|
||||
self.dbapi.destroy_rc(self.uuid)
|
||||
self.obj_reset_changes()
|
||||
|
||||
@base.remotable
|
||||
def save(self, context=None):
|
||||
"""Save updates to this ReplicationController.
|
||||
|
||||
Updates will be made column by column based on the result
|
||||
of self.what_changed().
|
||||
|
||||
: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.: ReplicationController(context)
|
||||
"""
|
||||
updates = self.obj_get_changes()
|
||||
self.dbapi.update_rc(self.uuid, updates)
|
||||
|
||||
self.obj_reset_changes()
|
||||
|
||||
@base.remotable
|
||||
def refresh(self, context=None):
|
||||
"""Loads updates for this ReplicationController.
|
||||
|
||||
Loads a rc with the same uuid from the database and
|
||||
checks for updated attributes. Updates are applied from
|
||||
the loaded rc column by column, if there are any updates.
|
||||
|
||||
: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.: ReplicationController(context)
|
||||
"""
|
||||
current = self.__class__.get_by_uuid(self._context, uuid=self.uuid)
|
||||
for field in self.fields:
|
||||
if (hasattr(self, base.get_attrname(field)) and
|
||||
self[field] != current[field]):
|
||||
self[field] = current[field]
|
@ -50,11 +50,15 @@ class TestRootController(tests.FunctionalTest):
|
||||
u'rel': u'self'},
|
||||
{u'href': u'http://localhost/bays/',
|
||||
u'rel': u'bookmark'}],
|
||||
u'pods': [{u'href': u'http://localhost/v1/pods/',
|
||||
u'rel': u'self'},
|
||||
u'pods': [{u'href': u'http://localhost/v1/pods/',
|
||||
u'rel': u'self'},
|
||||
{u'href': u'http://localhost/pods/',
|
||||
u'rel': u'bookmark'}],
|
||||
u'id': u'v1',
|
||||
u'rcs': [{u'href': u'http://localhost/v1/rcs/',
|
||||
u'rel': u'self'},
|
||||
{u'href': u'http://localhost/rcs/',
|
||||
u'rel': u'bookmark'}],
|
||||
u'id': u'v1',
|
||||
u'containers': [{u'href': u'http://localhost/v1/containers/',
|
||||
u'rel': u'self'},
|
||||
{u'href': u'http://localhost/containers/',
|
||||
|
@ -0,0 +1,61 @@
|
||||
# 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 magnum.conductor import api
|
||||
from magnum.tests.db import base as db_base
|
||||
|
||||
from mock import patch
|
||||
|
||||
|
||||
class TestRCController(db_base.DbTestCase):
|
||||
def mock_rc_create(self, rc):
|
||||
rc.create()
|
||||
return rc
|
||||
|
||||
def mock_rc_destroy(self, rc):
|
||||
rc.destroy()
|
||||
|
||||
def test_rc_api(self):
|
||||
with patch.object(api.API, 'rc_create') as mock_method:
|
||||
mock_method.side_effect = self.mock_rc_create
|
||||
# Create a replication controller
|
||||
params = '{"name": "rc_example_A", "images": ["ubuntu"],' \
|
||||
'"selector": {"foo": "foo1"}, "replicas": 2,' \
|
||||
'"rc_definition_url": "http://172.17.1.2/rc.json"}'
|
||||
response = self.app.post('/v1/rcs',
|
||||
params=params,
|
||||
content_type='application/json')
|
||||
self.assertEqual(response.status_int, 201)
|
||||
|
||||
# Get all rcs
|
||||
response = self.app.get('/v1/rcs')
|
||||
self.assertEqual(response.status_int, 200)
|
||||
self.assertEqual(1, len(response.json))
|
||||
c = response.json['rcs'][0]
|
||||
self.assertIsNotNone(c.get('uuid'))
|
||||
self.assertEqual('rc_example_A', c.get('name'))
|
||||
self.assertEqual(['ubuntu'], c.get('images'))
|
||||
self.assertEqual('foo1', c.get('selector')['foo'])
|
||||
|
||||
# Get just the one we created
|
||||
response = self.app.get('/v1/rcs/%s' % c.get('uuid'))
|
||||
self.assertEqual(response.status_int, 200)
|
||||
|
||||
with patch.object(api.API, 'rc_delete') as mock_method:
|
||||
mock_method.side_effect = self.mock_rc_destroy
|
||||
# Delete the rc we created
|
||||
response = self.app.delete('/v1/rcs/%s' % c.get('uuid'))
|
||||
self.assertEqual(response.status_int, 204)
|
||||
|
||||
response = self.app.get('/v1/rcs')
|
||||
self.assertEqual(response.status_int, 200)
|
||||
c = response.json['rcs']
|
||||
self.assertEqual(0, len(c))
|
@ -195,6 +195,14 @@ class TestException(base.BaseTestCase):
|
||||
self.assertRaises(exception.PodNotLocked,
|
||||
lambda: self.raise_(exception.PodNotLocked()))
|
||||
|
||||
def test_ReplicationControllerNotFound(self):
|
||||
self.assertRaises(exception.ReplicationControllerNotFound,
|
||||
lambda: self.raise_(exception.ReplicationControllerNotFound()))
|
||||
|
||||
def test_ReplicationControllerAlreadyExists(self):
|
||||
self.assertRaises(exception.ReplicationControllerAlreadyExists,
|
||||
lambda: self.raise_(exception.ReplicationControllerAlreadyExists()))
|
||||
|
||||
def test_ServiceNotFound(self):
|
||||
self.assertRaises(exception.ServiceNotFound,
|
||||
lambda: self.raise_(exception.ServiceNotFound()))
|
||||
|
Loading…
Reference in New Issue
Block a user