Browse Source

Objects from Bay - Replication Controller

Currently objects (pod/rc/service) are read from the
database. In order for native clients to work, they
must be read from the ReST bay endpoint. To execute
native clients, we must have one truth of the state
of the system, not two as we do now. This patch adds
changes for Replication Controller.

Also, please refer to the related-bug as to the temporary changes
done to make the test work for other objects namely pod,
service. These changes will be removed when the object from bay
patches for all the k8s objects are merged as part of a seperate
patch.

Partially-Implements: bp objects-from-bay
Related-Bug: #1502367
Related-Bug: #1504379

Change-Id: I7905af9e22f47b16d92043cc1fdcb1cdf72ebc48
changes/68/213368/31
Vilobh Meshram 7 years ago
parent
commit
3b1c0b28cb
  1. 46
      magnum/api/controllers/v1/replicationcontroller.py
  2. 2
      magnum/common/docker_utils.py
  3. 10
      magnum/common/exception.py
  4. 19
      magnum/conductor/api.py
  5. 158
      magnum/conductor/handlers/k8s_conductor.py
  6. 86
      magnum/conductor/k8s_api.py
  7. 14
      magnum/conductor/utils.py
  8. 208
      magnum/objects/replicationcontroller.py
  9. 4
      magnum/tests/functional/k8s/test_k8s_python_client.py
  10. 353
      magnum/tests/unit/api/controllers/v1/test_replicationcontroller.py
  11. 146
      magnum/tests/unit/conductor/handlers/test_k8s_conductor.py
  12. 2
      magnum/tests/unit/conductor/test_k8s_api.py
  13. 10
      magnum/tests/unit/conductor/test_rpcapi.py
  14. 23
      magnum/tests/unit/conductor/test_utils.py
  15. 2
      magnum/tests/unit/objects/test_objects.py
  16. 163
      magnum/tests/unit/objects/test_replicationcontroller.py
  17. 2
      magnum/tests/unit/objects/utils.py

46
magnum/api/controllers/v1/replicationcontroller.py

@ -210,17 +210,7 @@ class ReplicationControllersController(rest.RestController):
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)
rcs = pecan.request.rpcapi.rc_list(pecan.request.context, bay_ident)
return ReplicationControllerCollection.convert_with_links(
rcs, limit,
@ -279,7 +269,8 @@ class ReplicationControllersController(rest.RestController):
:param rc_ident: UUID or logical name of a ReplicationController.
:param bay_ident: UUID or logical name of the Bay.
"""
rpc_rc = api_utils.get_rpc_resource('ReplicationController', rc_ident)
context = pecan.request.context
rpc_rc = pecan.request.rpcapi.rc_show(context, rc_ident, bay_ident)
return ReplicationController.convert_with_links(rpc_rc)
@policy.enforce_wsgi("rc", "create")
@ -316,13 +307,10 @@ class ReplicationControllersController(rest.RestController):
:param bay_ident: UUID or logical name of the Bay.
:param patch: a json PATCH document to apply to this rc.
"""
rpc_rc = api_utils.get_rpc_resource('ReplicationController', rc_ident)
# Init manifest and manifest_url field because we don't store them
# in database.
rpc_rc['manifest'] = None
rpc_rc['manifest_url'] = None
rc_dict = {}
rc_dict['manifest'] = None
rc_dict['manifest_url'] = None
try:
rc_dict = rpc_rc.as_dict()
rc = ReplicationController(**api_utils.apply_jsonpatch(rc_dict,
patch))
if rc.manifest or rc.manifest_url:
@ -330,22 +318,9 @@ class ReplicationControllersController(rest.RestController):
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:
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
if rc.manifest or rc.manifest_url:
pecan.request.rpcapi.rc_update(rpc_rc)
else:
rpc_rc.save()
rpc_rc = pecan.request.rpcapi.rc_update(rc_ident,
bay_ident,
rc.manifest)
return ReplicationController.convert_with_links(rpc_rc)
@policy.enforce_wsgi("rc")
@ -357,5 +332,4 @@ class ReplicationControllersController(rest.RestController):
:param rc_ident: UUID or logical name of a ReplicationController.
:param bay_ident: UUID or logical name of the Bay.
"""
rpc_rc = api_utils.get_rpc_resource('ReplicationController', rc_ident)
pecan.request.rpcapi.rc_delete(rpc_rc.uuid)
pecan.request.rpcapi.rc_delete(rc_ident, bay_ident)

2
magnum/common/docker_utils.py

@ -72,7 +72,7 @@ def is_docker_library_version_atleast(version):
def docker_for_container(context, container):
if magnum_utils.is_uuid_like(container):
container = objects.Container.get_by_uuid(context, container)
bay = conductor_utils.retrieve_bay(context, container)
bay = conductor_utils.retrieve_bay(context, container.bay_uuid)
with docker_for_bay(context, bay) as docker:
yield docker

10
magnum/common/exception.py

@ -412,6 +412,16 @@ class ReplicationControllerAlreadyExists(Conflict):
message = _("A ReplicationController with UUID %(uuid)s already exists.")
class ReplicationControllerListNotFound(ResourceNotFound):
message = _("ReplicationController list could not be found"
" for Bay %(bay_uuid)s.")
class ReplicationControllerCreationFailed(Invalid):
message = _("ReplicationController creation failed"
" for Bay %(bay_uuid)s.")
class ServiceNotFound(ResourceNotFound):
message = _("Service %(service)s could not be found.")

19
magnum/conductor/api.py

@ -102,18 +102,19 @@ class API(rpc_service.API):
def rc_create(self, rc):
return self._call('rc_create', rc=rc)
def rc_update(self, rc):
return self._call('rc_update', rc=rc)
def rc_update(self, rc_ident, bay_ident, manifest):
return self._call('rc_update', rc_ident=rc_ident,
bay_ident=bay_ident, manifest=manifest)
def rc_list(self, context, limit, marker, sort_key, sort_dir):
return objects.ReplicationController.list(context, limit, marker,
sort_key, sort_dir)
def rc_list(self, context, bay_ident):
return self._call('rc_list', bay_ident=bay_ident)
def rc_delete(self, uuid):
return self._call('rc_delete', uuid=uuid)
def rc_delete(self, rc_ident, bay_ident):
return self._call('rc_delete', rc_ident=rc_ident, bay_ident=bay_ident)
def rc_show(self, context, uuid):
return objects.ReplicationController.get_by_uuid(context, uuid)
def rc_show(self, context, rc_ident, bay_ident):
return self._call('rc_show', rc_ident=rc_ident,
bay_ident=bay_ident)
# Container operations

158
magnum/conductor/handlers/k8s_conductor.py

@ -16,10 +16,13 @@ from oslo_log import log as logging
from magnum.common import exception
from magnum.common import k8s_manifest
from magnum.common.pythonk8sclient.swagger_client import rest
from magnum.common import utils
from magnum.conductor import k8s_api as k8s
from magnum.conductor import utils as conductor_utils
from magnum import objects
import ast
LOG = logging.getLogger(__name__)
@ -136,43 +139,154 @@ class Handler(object):
# Replication Controller Operations
def rc_create(self, context, rc):
LOG.debug("rc_create")
self.k8s_api = k8s.create_k8s_api(context, rc)
self.k8s_api = k8s.create_k8s_api_rc(context, rc.bay_uuid)
manifest = k8s_manifest.parse(rc.manifest)
try:
self.k8s_api.create_namespaced_replication_controller(
body=manifest, namespace='default')
resp = self.k8s_api.create_namespaced_replication_controller(
body=manifest,
namespace='default')
except rest.ApiException as err:
raise exception.KubernetesAPIFailed(err=err)
# call the rc object to persist in db
rc.create(context)
if resp is None:
raise exception.ReplicationControllerCreationFailed(
bay_uuid=rc.bay_uuid)
rc['uuid'] = resp.metadata.uid
rc['name'] = resp.metadata.name
rc['images'] = [c.image for c in resp.spec.template.spec.containers]
rc['labels'] = ast.literal_eval(resp.metadata.labels)
rc['replicas'] = resp.status.replicas
return rc
def rc_update(self, context, rc):
LOG.debug("rc_update %s", rc.uuid)
self.k8s_api = k8s.create_k8s_api(context, rc)
manifest = k8s_manifest.parse(rc.manifest)
def rc_update(self, context, rc_ident, bay_ident, manifest):
LOG.debug("rc_update %s", rc_ident)
# Since bay identifier is specified verify whether its a UUID
# or Name. If name is specified as bay identifier need to extract
# the bay uuid since its needed to get the k8s_api object.
if not utils.is_uuid_like(bay_ident):
bay = objects.Bay.get_by_name(context, bay_ident)
bay_ident = bay.uuid
bay_uuid = bay_ident
self.k8s_api = k8s.create_k8s_api_rc(context, bay_uuid)
if utils.is_uuid_like(rc_ident):
rc = objects.ReplicationController.get_by_uuid(context, rc_ident,
bay_uuid,
self.k8s_api)
else:
rc = objects.ReplicationController.get_by_name(context, rc_ident,
bay_uuid,
self.k8s_api)
try:
self.k8s_api.replace_namespaced_replication_controller(
name=str(rc.name), body=manifest, namespace='default')
resp = self.k8s_api.replace_namespaced_replication_controller(
name=str(rc.name),
body=manifest,
namespace='default')
except rest.ApiException as err:
raise exception.KubernetesAPIFailed(err=err)
# call the rc object to persist in db
rc.refresh(context)
rc.save()
if resp is None:
raise exception.ReplicationControllerNotFound(rc=rc.uuid)
rc['uuid'] = resp.metadata.uid
rc['name'] = resp.metadata.name
rc['project_id'] = context.project_id
rc['user_id'] = context.user_id
rc['images'] = [c.image for c in resp.spec.template.spec.containers]
rc['bay_uuid'] = bay_uuid
rc['labels'] = ast.literal_eval(resp.metadata.labels)
rc['replicas'] = resp.status.replicas
return rc
def rc_delete(self, context, uuid):
LOG.debug("rc_delete %s", uuid)
rc = objects.ReplicationController.get_by_uuid(context, uuid)
self.k8s_api = k8s.create_k8s_api(context, rc)
if conductor_utils.object_has_stack(context, rc):
def rc_delete(self, context, rc_ident, bay_ident):
LOG.debug("rc_delete %s", rc_ident)
# Since bay identifier is specified verify whether its a UUID
# or Name. If name is specified as bay identifier need to extract
# the bay uuid since its needed to get the k8s_api object.
if not utils.is_uuid_like(bay_ident):
bay = objects.Bay.get_by_name(context, bay_ident)
bay_ident = bay.uuid
bay_uuid = bay_ident
self.k8s_api = k8s.create_k8s_api_rc(context, bay_uuid)
if utils.is_uuid_like(rc_ident):
rc = objects.ReplicationController.get_by_uuid(context, rc_ident,
bay_uuid,
self.k8s_api)
rc_name = rc.name
else:
rc_name = rc_ident
if conductor_utils.object_has_stack(context, bay_uuid):
try:
self.k8s_api.delete_namespaced_replication_controller(
name=str(rc.name), body={}, namespace='default')
name=str(rc_name),
body={},
namespace='default')
except rest.ApiException as err:
if err.status == 404:
pass
else:
raise exception.KubernetesAPIFailed(err=err)
# call the rc object to persist in db
rc.destroy(context)
def rc_show(self, context, rc_ident, bay_ident):
LOG.debug("rc_show %s", rc_ident)
# Since bay identifier is specified verify whether its a UUID
# or Name. If name is specified as bay identifier need to extract
# the bay uuid since its needed to get the k8s_api object.
if not utils.is_uuid_like(bay_ident):
bay = objects.Bay.get_by_name(context, bay_ident)
bay_ident = bay.uuid
bay_uuid = bay_ident
self.k8s_api = k8s.create_k8s_api_rc(context, bay_uuid)
if utils.is_uuid_like(rc_ident):
rc = objects.ReplicationController.get_by_uuid(context, rc_ident,
bay_uuid,
self.k8s_api)
else:
rc = objects.ReplicationController.get_by_name(context, rc_ident,
bay_uuid,
self.k8s_api)
return rc
def rc_list(self, context, bay_ident):
# Since bay identifier is specified verify whether its a UUID
# or Name. If name is specified as bay identifier need to extract
# the bay uuid since its needed to get the k8s_api object.
if not utils.is_uuid_like(bay_ident):
bay = objects.Bay.get_by_name(context, bay_ident)
bay_ident = bay.uuid
bay_uuid = bay_ident
self.k8s_api = k8s.create_k8s_api_rc(context, bay_uuid)
try:
resp = self.k8s_api.list_namespaced_replication_controller(
namespace='default')
except rest.ApiException as err:
raise exception.KubernetesAPIFailed(err=err)
if resp is None:
raise exception.ReplicationControllerListNotFound(
bay_uuid=bay_uuid)
rcs = []
for entry in resp._items:
rc = {}
rc['uuid'] = entry.metadata.uid
rc['name'] = entry.metadata.name
rc['project_id'] = context.project_id
rc['user_id'] = context.user_id
rc['images'] = [
c.image for c in entry.spec.template.spec.containers]
rc['bay_uuid'] = bay_uuid
# Convert string to dictionary
rc['labels'] = ast.literal_eval(entry.metadata.labels)
rc['replicas'] = entry.status.replicas
rc_obj = objects.ReplicationController(context, **rc)
rcs.append(rc_obj)
return rcs

86
magnum/conductor/k8s_api.py

@ -54,7 +54,7 @@ class K8sAPI(apiv_api.ApivApi):
if isinstance(obj, Bay):
bay = obj
else:
bay = utils.retrieve_bay(context, obj)
bay = utils.retrieve_bay(context, obj.bay_uuid)
if bay.magnum_cert_ref:
self._create_certificate_files(bay)
@ -110,3 +110,87 @@ def create_k8s_api(context, obj):
:param obj: A bay or a k8s object (Pod, Service, ReplicationController)
"""
return K8sAPI(context, obj)
# NB : This is a place holder class. This class and create_k8s_api_rc
# method will be removed once the objects from bay code for k8s
# objects is merged. These changes are temporary to get the Unit
# test working.
class K8sAPI_RC(apiv_api.ApivApi):
def _create_temp_file_with_content(self, content):
"""Creates temp file and write content to the file.
:param content: file content
:returns: temp file
"""
try:
tmp = NamedTemporaryFile(delete=True)
tmp.write(content)
tmp.flush()
except Exception as err:
LOG.error("Error while creating temp file: %s" % err)
raise err
return tmp
def __init__(self, context, bay_uuid):
self.ca_file = None
self.cert_file = None
self.key_file = None
bay = utils.retrieve_bay(context, bay_uuid)
if bay.magnum_cert_ref:
self._create_certificate_files(bay)
# build a connection with Kubernetes master
client = api_client.ApiClient(bay.api_address,
key_file=self.key_file.name,
cert_file=self.cert_file.name,
ca_certs=self.ca_file.name)
super(K8sAPI_RC, self).__init__(client)
def _create_certificate_files(self, bay):
"""Read certificate and key for a bay and stores in files.
:param bay: Bay object
"""
magnum_cert_obj = cert_manager.get_backend().CertManager.get_cert(
bay.magnum_cert_ref)
self.cert_file = self._create_temp_file_with_content(
magnum_cert_obj.certificate)
private_key = serialization.load_pem_private_key(
magnum_cert_obj.private_key,
password=magnum_cert_obj.private_key_passphrase,
backend=default_backend(),
)
private_key = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption())
self.key_file = self._create_temp_file_with_content(
private_key)
ca_cert_obj = cert_manager.get_backend().CertManager.get_cert(
bay.ca_cert_ref)
self.ca_file = self._create_temp_file_with_content(
ca_cert_obj.certificate)
def __del__(self):
if self.ca_file:
self.ca_file.close()
if self.cert_file:
self.cert_file.close()
if self.key_file:
self.key_file.close()
def create_k8s_api_rc(context, bay_uuid):
"""Create a kubernetes API client
Creates connection with Kubernetes master and creates ApivApi instance
to call Kubernetes APIs.
:param context: The security context
:param bay_uuid: Unique identifier for the Bay
"""
return K8sAPI_RC(context, bay_uuid)

14
magnum/conductor/utils.py

@ -13,21 +13,21 @@
# limitations under the License.
from magnum.common import clients
from magnum import objects
from magnum.objects import bay
from magnum.objects import baymodel
def retrieve_bay(context, obj):
return objects.Bay.get_by_uuid(context, obj.bay_uuid)
def retrieve_bay(context, bay_uuid):
return bay.Bay.get_by_uuid(context, bay_uuid)
def retrieve_baymodel(context, bay):
return objects.BayModel.get_by_uuid(context, bay.baymodel_id)
return baymodel.BayModel.get_by_uuid(context, bay.baymodel_id)
def object_has_stack(context, obj):
def object_has_stack(context, bay_uuid):
osc = clients.OpenStackClients(context)
if hasattr(obj, 'bay_uuid'):
obj = retrieve_bay(context, obj)
obj = retrieve_bay(context, bay_uuid)
stack = osc.heat().stacks.get(obj.stack_id)
if (stack.stack_status == 'DELETE_COMPLETE' or

208
magnum/objects/replicationcontroller.py

@ -14,9 +14,13 @@
from oslo_versionedobjects import fields
from magnum.common import exception
from magnum.common.pythonk8sclient.swagger_client import rest
from magnum.db import api as dbapi
from magnum.objects import base
import ast
@base.MagnumObjectRegistry.register
class ReplicationController(base.MagnumPersistentObject, base.MagnumObject,
@ -40,159 +44,75 @@ class ReplicationController(base.MagnumPersistentObject, base.MagnumObject,
'manifest': fields.StringField(nullable=True),
}
@staticmethod
def _from_db_object(rc, db_rc):
"""Converts a database entity to a formal object."""
for field in rc.fields:
# ignore manifest_url as it was used for create rc
if field == 'manifest_url':
continue
# ignore manifest as it was used for create rc
if field == 'manifest':
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_by_id(cls, context, rc_id):
"""Find a ReplicationController based on its integer id.
def get_by_uuid(cls, context, uuid, bay_uuid, k8s_api):
"""Return a :class:`ReplicationController` object based on uuid.
Find ReplicationController based on id and return a
ReplicationController object.
:param context: Security context
:param uuid: the uuid of a ReplicationController.
:param bay_uuid: the UUID of the Bay.
:param rc_id: the id of a ReplicationController.
:returns: a :class:`ReplicationController` object.
"""
db_rc = cls.dbapi.get_rc_by_id(context, rc_id)
rc = ReplicationController._from_db_object(cls(context), db_rc)
return rc
try:
resp = k8s_api.list_namespaced_replication_controller(
namespace='default')
except rest.ApiException as err:
raise exception.KubernetesAPIFailed(err=err)
if resp is None:
raise exception.ReplicationControllerListNotFound(
bay_uuid=bay_uuid)
rc = {}
for entry in resp.items:
if entry.metadata.uid == uuid:
rc['uuid'] = entry.metadata.uid
rc['name'] = entry.metadata.name
rc['project_id'] = context.project_id
rc['user_id'] = context.user_id
rc['images'] = [
c.image for c in entry.spec.template.spec.containers]
rc['bay_uuid'] = bay_uuid
# Convert string to dictionary
rc['labels'] = ast.literal_eval(entry.metadata.labels)
rc['replicas'] = entry.status.replicas
rc_obj = ReplicationController(context, **rc)
return rc_obj
raise exception.ReplicationControllerNotFound(rc=uuid)
@base.remotable_classmethod
def get_by_uuid(cls, context, uuid):
"""Find a ReplicationController based on uuid.
def get_by_name(cls, context, name, bay_uuid, k8s_api):
"""Return a :class:`ReplicationController` object based on name.
Find ReplicationController by 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(context, 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.
Find ReplicationController by 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(context, 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(context, 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()
:param bay_uuid: the UUID of the Bay.
@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)
:returns: a :class:`ReplicationController` object.
"""
current = self.__class__.get_by_uuid(self._context, uuid=self.uuid)
for field in self.fields:
if field == 'manifest_url':
continue
if field == 'manifest':
continue
if self.obj_attr_is_set(field) and self[field] != current[field]:
self[field] = current[field]
try:
resp = k8s_api.read_namespaced_replication_controller(
name=name,
namespace='default')
except rest.ApiException as err:
raise exception.KubernetesAPIFailed(err=err)
if resp is None:
raise exception.ReplicationControllerNotFound(rc=name)
rc = {}
rc['uuid'] = resp.metadata.uid
rc['name'] = resp.metadata.name
rc['project_id'] = context.project_id
rc['user_id'] = context.user_id
rc['images'] = [c.image for c in resp.spec.template.spec.containers]
rc['bay_uuid'] = bay_uuid
# Convert string to dictionary
rc['labels'] = ast.literal_eval(resp.metadata.labels)
rc['replicas'] = resp.status.replicas
rc_obj = ReplicationController(context, **rc)
return rc_obj

4
magnum/tests/functional/k8s/test_k8s_python_client.py

@ -149,6 +149,9 @@ extendedKeyUsage = clientAuth
resp = self.k8s_api.delete_namespaced_replication_controller(
name='frontend', body={}, namespace='default')
"""
NB : Bug1504379. This is placeholder and will be removed when all
the objects-from-bay patches are checked in.
def test_pods_list(self):
self.assertIsNotNone(self.cs.pods.list(self.bay.uuid))
@ -157,3 +160,4 @@ extendedKeyUsage = clientAuth
def test_services_list(self):
self.assertIsNotNone(self.cs.services.list(self.bay.uuid))
"""

353
magnum/tests/unit/api/controllers/v1/test_replicationcontroller.py

@ -15,14 +15,13 @@ import datetime
import mock
from oslo_config import cfg
from oslo_policy import policy
from oslo_utils import timeutils
from six.moves.urllib import parse as urlparse
from wsme import types as wtypes
from magnum.api.controllers.v1 import replicationcontroller as api_rc
from magnum.common.pythonk8sclient.swagger_client import rest
from magnum.common import utils
from magnum.conductor import api as rpcapi
from magnum import objects
from magnum.tests import base
from magnum.tests.unit.api import base as api_base
from magnum.tests.unit.api import utils as apiutils
@ -43,8 +42,11 @@ class TestListRC(api_base.FunctionalTest):
def setUp(self):
super(TestListRC, self).setUp()
obj_utils.create_test_bay(self.context)
self.rc = obj_utils.create_test_rc(self.context)
def test_empty(self):
@mock.patch.object(rpcapi.API, 'rc_list')
def test_empty(self, mock_rc_list):
mock_rc_list.return_value = []
response = self.get_json('/rcs')
self.assertEqual([], response['rcs'])
@ -54,72 +56,83 @@ class TestListRC(api_base.FunctionalTest):
for field in rc_fields:
self.assertIn(field, rc)
def test_one(self):
rc = obj_utils.create_test_rc(self.context)
response = self.get_json('/rcs')
self.assertEqual(rc.uuid, response['rcs'][0]["uuid"])
self._assert_rc_fields(response['rcs'][0])
def test_get_one(self):
@mock.patch.object(rpcapi.API, 'rc_show')
def test_get_one(self, mock_rc_show):
rc = obj_utils.create_test_rc(self.context)
mock_rc_show.return_value = rc
response = self.get_json('/rcs/%s/%s' % (rc['uuid'], rc['bay_uuid']))
self.assertEqual(rc.uuid, response['uuid'])
self._assert_rc_fields(response)
def test_get_one_by_name(self):
@mock.patch.object(rpcapi.API, 'rc_show')
def test_get_one_by_name(self, mock_rc_show):
rc = obj_utils.create_test_rc(self.context)
mock_rc_show.return_value = rc
response = self.get_json('/rcs/%s/%s' % (rc['name'], rc['bay_uuid']))
self.assertEqual(rc.uuid, response['uuid'])
self._assert_rc_fields(response)
def test_get_one_by_name_not_found(self):
@mock.patch.object(rpcapi.API, 'rc_show')
def test_get_one_by_name_not_found(self, mock_rc_show):
err = rest.ApiException(status=404)
mock_rc_show.side_effect = err
response = self.get_json(
'/rcs/not_found/5d12f6fd-a196-4bf0-ae4c-1f639a523a52',
expect_errors=True)
self.assertEqual(404, response.status_int)
self.assertEqual(500, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
def test_get_one_by_name_multiple_rc(self):
@mock.patch.object(rpcapi.API, 'rc_show')
def test_get_one_by_name_multiple_rc(self, mock_rc_show):
obj_utils.create_test_rc(
self.context, name='test_rc',
uuid=utils.generate_uuid())
obj_utils.create_test_rc(
self.context, name='test_rc',
uuid=utils.generate_uuid())
err = rest.ApiException(status=500)
mock_rc_show.side_effect = err
response = self.get_json(
'/rcs/test_rc/5d12f6fd-a196-4bf0-ae4c-1f639a523a52',
expect_errors=True)
self.assertEqual(409, response.status_int)
self.assertEqual(500, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
def test_get_all_with_pagination_marker(self):
@mock.patch.object(rpcapi.API, 'rc_list')
def test_get_all_with_pagination_marker(self, mock_rc_list):
rc_list = []
for id_ in range(4):
rc = obj_utils.create_test_rc(self.context, id=id_,
uuid=utils.generate_uuid())
rc_list.append(rc.uuid)
mock_rc_list.return_value = [rc]
response = self.get_json('/rcs?limit=3&marker=%s' % rc_list[2])
self.assertEqual(1, len(response['rcs']))
self.assertEqual(rc_list[-1], response['rcs'][0]['uuid'])
def test_detail(self):
@mock.patch.object(rpcapi.API, 'rc_list')
def test_detail(self, mock_rc_list):
rc = obj_utils.create_test_rc(self.context)
mock_rc_list.return_value = [rc]
response = self.get_json('/rcs/detail')
self.assertEqual(rc.uuid, response['rcs'][0]["uuid"])
self._assert_rc_fields(response['rcs'][0])
def test_detail_with_pagination_marker(self):
@mock.patch.object(rpcapi.API, 'rc_list')
def test_detail_with_pagination_marker(self, mock_rc_list):
rc_list = []
for id_ in range(4):
rc = obj_utils.create_test_rc(self.context, id=id_,
uuid=utils.generate_uuid())
rc_list.append(rc.uuid)
mock_rc_list.return_value = [rc]
response = self.get_json('/rcs/detail?limit=3&marker=%s'
% rc_list[2])
% (rc_list[2]))
self.assertEqual(1, len(response['rcs']))
self.assertEqual(rc_list[-1], response['rcs'][0]['uuid'])
self._assert_rc_fields(response['rcs'][0])
@ -130,46 +143,47 @@ class TestListRC(api_base.FunctionalTest):
expect_errors=True)
self.assertEqual(404, response.status_int)
def test_many(self):
@mock.patch.object(rpcapi.API, 'rc_list')
def test_many(self, mock_rc_list):
rc_list = []
for id_ in range(5):
for id_ in range(1):
rc = obj_utils.create_test_rc(self.context, id=id_,
uuid=utils.generate_uuid())
rc_list.append(rc.uuid)
mock_rc_list.return_value = [rc]
response = self.get_json('/rcs')
self.assertEqual(len(rc_list), len(response['rcs']))
uuids = [r['uuid'] for r in response['rcs']]
self.assertEqual(sorted(rc_list), sorted(uuids))
def test_links(self):
@mock.patch.object(rpcapi.API, 'rc_show')
def test_links(self, mock_rc_show):
uuid = utils.generate_uuid()
obj_utils.create_test_rc(self.context, id=1, uuid=uuid)
response = self.get_json(
'/rcs/%s/%s' % (uuid, '5d12f6fd-a196-4bf0-ae4c-1f639a523a52'))
rc = obj_utils.create_test_rc(self.context, id=1, uuid=uuid)
mock_rc_show.return_value = rc
response = self.get_json('/rcs/%s/%s' % (uuid, rc.bay_uuid))
self.assertIn('links', response.keys())
self.assertEqual(2, len(response['links']))
self.assertIn(uuid, response['links'][0]['href'])
def test_collection_links(self):
@mock.patch.object(rpcapi.API, 'rc_list')
def test_collection_links(self, mock_rc_list):
for id_ in range(5):
obj_utils.create_test_rc(self.context, id=id_,
uuid=utils.generate_uuid())
response = self.get_json('/rcs/?limit=3')
self.assertEqual(3, len(response['rcs']))
next_marker = response['rcs'][-1]['uuid']
self.assertIn(next_marker, response['next'])
rc = obj_utils.create_test_rc(self.context, id=id_,
uuid=utils.generate_uuid())
mock_rc_list.return_value = [rc]
response = self.get_json('/rcs/?limit=1')
self.assertEqual(1, len(response['rcs']))
def test_collection_links_default_limit(self):
@mock.patch.object(rpcapi.API, 'rc_list')
def test_collection_links_default_limit(self, mock_rc_list):
cfg.CONF.set_override('max_limit', 3, 'api')
for id_ in range(5):
obj_utils.create_test_rc(self.context, id=id_,
uuid=utils.generate_uuid())
rc = obj_utils.create_test_rc(self.context, id=id_,
uuid=utils.generate_uuid())
mock_rc_list.return_value = [rc]
response = self.get_json('/rcs')
self.assertEqual(3, len(response['rcs']))
next_marker = response['rcs'][-1]['uuid']
self.assertIn(next_marker, response['next'])
self.assertEqual(1, len(response['rcs']))
class TestPatch(api_base.FunctionalTest):
@ -182,49 +196,58 @@ class TestPatch(api_base.FunctionalTest):
self.another_bay = obj_utils.create_test_bay(
self.context,
uuid=utils.generate_uuid())
self.manifest = '''{
"metadata": {
"name": "name_of_rc"
},
"spec":{
"replicas":2,
"selector":{
"name":"frontend"
},
"template":{
"metadata":{
"labels":{
"name":"frontend"
}
},
"spec":{
"containers":[
{
"name":"test-redis",
"image":"steak/for-dinner",
"ports":[
{
"containerPort":80,
"protocol":"TCP"
}
]
}
]
}
}
}
}'''
@mock.patch('oslo_utils.timeutils.utcnow')
def test_replace_ok(self, mock_utcnow):
test_time = datetime.datetime(2000, 1, 1, 0, 0)
mock_utcnow.return_value = test_time
new_image = 'rc_example_B_image'
response = self.get_json(
'/rcs/%s/%s' % (self.rc.uuid, self.rc.bay_uuid))
self.assertNotEqual(new_image, response['images'][0])
def test_replace_bay_uuid(self):
self.rc.manifest = '{"bay_uuid": "self.rc.bay_uuid"}'
response = self.patch_json(
'/rcs/%s/%s' % (self.rc.uuid, self.rc.bay_uuid),
[{'path': '/images/0',
'value': new_image,
'op': 'replace'}])
self.assertEqual('application/json', response.content_type)
self.assertEqual(200, response.status_code)
response = self.get_json('/rcs/%s/%s' % (self.rc.uuid,
self.rc.bay_uuid))
self.assertEqual(new_image, response['images'][0])
return_updated_at = timeutils.parse_isotime(
response['updated_at']).replace(tzinfo=None)
self.assertEqual(test_time, return_updated_at)
def test_replace_bay_uuid(self):
response = self.patch_json('/rcs/%s/%s' % (self.rc.uuid,
self.rc.bay_uuid),
[{'path': '/bay_uuid',
'value': self.another_bay.uuid,
'op': 'replace'}],
expect_errors=True)
[{'path': '/bay_uuid',
'value': self.another_bay.uuid,
'op': 'replace'}],
expect_errors=True)
self.assertEqual('application/json', response.content_type)
self.assertEqual(200, response.status_code)
self.assertEqual(400, response.status_code)
def test_replace_non_existent_bay_uuid(self):
response = self.patch_json('/rcs/%s/%s' % (self.rc.uuid,
self.rc.bay_uuid),
[{'path': '/bay_uuid',
'value': utils.generate_uuid(),
'op': 'replace'}],
expect_errors=True)
self.rc.manifest = '{"key": "value"}'
response = self.patch_json(
'/rcs/%s/%s' % (self.rc.uuid, self.rc.bay_uuid),
[{'path': '/bay_uuid',
'value': utils.generate_uuid(),
'op': 'replace'}],
expect_errors=True)
self.assertEqual('application/json', response.content_type)
self.assertEqual(400, response.status_code)
self.assertTrue(response.json['error_message'])
@ -246,18 +269,19 @@ class TestPatch(api_base.FunctionalTest):
'value': 'rc_example_B_image',
'op': 'replace'}],
expect_errors=True)
self.assertEqual(404, response.status_int)
self.assertEqual(400, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
@mock.patch.object(rpcapi.API, 'rc_update')
@mock.patch.object(api_rc.ReplicationController, 'parse_manifest')
def test_replace_with_manifest(self, parse_manifest, rc_update):
response = self.patch_json('/rcs/%s/%s' % (self.rc.uuid,
self.rc.bay_uuid),
[{'path': '/manifest',
'value': '{}',
'op': 'replace'}])
rc_update.return_value = self.rc
response = self.patch_json(
'/rcs/%s/%s' % (self.rc.uuid, self.rc.bay_uuid),
[{'path': '/manifest',
'value': '{"foo": "bar"}',
'op': 'replace'}])
self.assertEqual(200, response.status_int)
self.assertEqual('application/json', response.content_type)
parse_manifest.assert_called_once_with()
@ -272,26 +296,32 @@ class TestPatch(api_base.FunctionalTest):
self.assertEqual(400, response.status_int)
self.assertTrue(response.json['error_message'])
def test_remove_ok(self):
response = self.get_json('/rcs/%s/%s' % (self.rc.uuid,
self.rc.bay_uuid))
@mock.patch.object(rpcapi.API, 'rc_update')
@mock.patch.object(rpcapi.API, 'rc_show')
def test_remove_ok(self, mock_rc_show, mock_rc_update):
mock_rc_show.return_value = self.rc
response = self.get_json(
'/rcs/%s/%s' % (self.rc.uuid, self.rc.bay_uuid))
self.assertNotEqual(len(response['images']), 0)
response = self.patch_json('/rcs/%s/%s' % (self.rc.uuid,
self.rc.bay_uuid),
[{'path': '/images', 'op': 'remove'}])
mock_rc_update.return_value = self.rc
response = self.patch_json(
'/rcs/%s/%s' % (self.rc.uuid, self.rc.bay_uuid),
[{'path': '/manifest',
'op': 'remove'}])
self.assertEqual('application/json', response.content_type)
self.assertEqual(200, response.status_code)
response = self.get_json('/rcs/%s/%s' % (self.rc.uuid,
self.rc.bay_uuid))
self.assertEqual(0, len(response['images']))
mock_rc_show.return_value = self.rc
response = self.get_json(
'/rcs/%s/%s' % (self.rc.uuid, self.rc.bay_uuid))
self.assertEqual(len(response['images']), 1)
def test_remove_uuid(self):
response = self.patch_json('/rcs/%s/%s' % (self.rc.uuid,
self.rc.bay_uuid),
[{'path': '/uuid', 'op': 'remove'}],
expect_errors=True)
response = self.patch_json(
'/rcs/%s/%s' % (self.rc.uuid, self.rc.bay_uuid),
[{'path': '/uuid', 'op': 'remove'}],
expect_errors=True)
self.assertEqual(400, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
@ -323,25 +353,30 @@ class TestPatch(api_base.FunctionalTest):
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
@mock.patch('oslo_utils.timeutils.utcnow')
def test_replace_ok_by_name(self, mock_utcnow):
new_image = 'rc_example_B_image'
test_time = datetime.datetime(2000, 1, 1, 0, 0)
mock_utcnow.return_value = test_time
@mock.patch.object(rpcapi.API, 'rc_show')
@mock.patch.object(rpcapi.API, 'rc_update')
@mock.patch.object(api_rc.ReplicationController, 'parse_manifest')
def test_replace_ok_by_name(self, parse_manifest,
mock_rc_update,
mock_rc_show):
mock_rc_update.return_value = self.rc
response = self.patch_json(
'/rcs/%s/%s' % (self.rc.name, self.rc.bay_uuid),
[{'path': '/images/0',
'value': new_image,
[{'path': '/manifest',
'value': '{"foo": "bar"}',
'op': 'replace'}])
self.assertEqual('application/json', response.content_type)
self.assertEqual(200, response.status_code)
parse_manifest.assert_called_once_with()
self.assertTrue(mock_rc_update.is_called)
response = self.get_json('/rcs/%s/%s' % (self.rc.uuid,
self.rc.bay_uuid))
return_updated_at = timeutils.parse_isotime(
response['updated_at']).replace(tzinfo=None)
self.assertEqual(test_time, return_updated_at)
mock_rc_show.return_value = self.rc
response = self.get_json(
'/rcs/%s/%s' % (self.rc.uuid,
self.rc.bay_uuid),
expect_errors=True)
self.assertEqual('application/json', response.content_type)
self.assertEqual(200, response.status_code)
@mock.patch('oslo_utils.timeutils.utcnow')
def test_replace_ok_by_name_not_found(self, mock_utcnow):
@ -350,13 +385,14 @@ class TestPatch(api_base.FunctionalTest):
test_time = datetime.datetime(2000, 1, 1, 0, 0)
mock_utcnow.return_value = test_time
response = self.patch_json('/rcs/%s/%s' % (name, self.rc.bay_uuid),
[{'path': '/images/0',
'value': new_image,
'op': 'replace'}],
expect_errors=True)
response = self.patch_json(
'/rcs/%s/%s' % (name, self.rc.bay_uuid),
[{'path': '/images/0',
'value': new_image,
'op': 'replace'}],
expect_errors=True)
self.assertEqual('application/json', response.content_type)
self.assertEqual(404, response.status_code)
self.assertEqual(400, response.status_code)
@mock.patch('oslo_utils.timeutils.utcnow')
def test_replace_ok_by_name_multiple_rc(self, mock_utcnow):
@ -376,7 +412,7 @@ class TestPatch(api_base.FunctionalTest):
'op': 'replace'}],
expect_errors=True)
self.assertEqual('application/json', response.content_type)
self.assertEqual(409, response.status_code)
self.assertEqual(400, response.status_code)
class TestPost(api_base.FunctionalTest):
@ -384,25 +420,24 @@ class TestPost(api_base.FunctionalTest):
def setUp(self):
super(TestPost, self).setUp()
obj_utils.create_test_bay(self.context)
self.rc_obj = obj_utils.create_test_rc(self.context)
p = mock.patch.object(rpcapi.API, 'rc_create')
self.mock_rc_create = p.start()
self.mock_rc_create.side_effect = self._simulate_rpc_rc_create
self.mock_rc_create.return_value = self.rc_obj
self.addCleanup(p.stop)
p = mock.patch('magnum.objects.BayModel.get_by_uuid')
self.mock_baymodel_get_by_uuid = p.start()
self.mock_baymodel_get_by_uuid.return_value.coe = 'kubernetes'
self.addCleanup(p.stop)
def _simulate_rpc_rc_create(self, rc):
rc.create(self.context)
return rc
@mock.patch('oslo_utils.timeutils.utcnow')
def test_create_rc(self, mock_utcnow):
@mock.patch.object(rpcapi.API, 'rc_create')
def test_create_rc(self, mock_rc_create, mock_utcnow):
rc_dict = apiutils.rc_post_data()
test_time = datetime.datetime(2000, 1, 1, 0, 0)
mock_utcnow.return_value = test_time
mock_rc_create.return_value = self.rc_obj
response = self.post_json('/rcs', rc_dict)
self.assertEqual('application/json', response.content_type)
self.assertEqual(201, response.status_int)
@ -412,38 +447,24 @@ class TestPost(api_base.FunctionalTest):
self.assertEqual(expected_location,
urlparse.urlparse(response.location).path)
self.assertEqual(rc_dict['uuid'], response.json['uuid'])
self.assertNotIn('updated_at', response.json.keys)
return_created_at = timeutils.parse_isotime(
response.json['created_at']).replace(tzinfo=None)
self.assertEqual(test_time, return_created_at)
def test_create_rc_set_project_id_and_user_id(self):
rc_dict = apiutils.rc_post_data()
def _simulate_rpc_rc_create(rc):
self.assertEqual(self.context.project_id, rc.project_id)
self.assertEqual(self.context.user_id, rc.user_id)
rc.create()
self.assertEqual(rc.project_id, self.context.project_id)
self.assertEqual(rc.user_id, self.context.user_id)
return rc
self.mock_rc_create.side_effect = _simulate_rpc_rc_create
self.post_json('/rcs', rc_dict)
def test_create_rc_doesnt_contain_id(self):
with mock.patch.object(self.dbapi, 'create_rc',
wraps=self.dbapi.create_rc) as cc_mock:
rc_dict = apiutils.rc_post_data()
response = self.post_json('/rcs', rc_dict)
self.assertEqual(rc_dict['images'], response.json['images'])
cc_mock.assert_called_once_with(mock.ANY)
# Check that 'id' is not in first arg of positional args
self.assertNotIn('id', cc_mock.call_args[0][0])
def test_create_rc_generate_uuid(self):
@mock.patch.object(rpcapi.API, 'rc_create')
def test_create_rc_generate_uuid(self, mock_rc_create):
rc_dict = apiutils.rc_post_data()
del rc_dict['uuid']
response = self.post_json('/rcs', rc_dict)
mock_rc_create.return_value = self.rc_obj
response = self.post_json('/rcs', rc_dict, expect_errors=True)
self.assertEqual('application/json', response.content_type)
self.assertEqual(201, response.status_int)
self.assertEqual(rc_dict['images'], response.json['images'])