Browse Source

Remove node object from Magnum

The node object represents either a bare metal or virtual machine
node that is provisioned with an OS to run the containers, or
alternatively, run kubernetes. Magnum use Heat to deploy the nodes,
so it is unnecessary to maintain node object in Magnum. Heat can do
the work for us. The code about node object is useless now, so let's
remove it from Magnum.

Closes-Bug: #1540790
Change-Id: If8761b06a364127683099afb4dc51ea551be6f89
changes/72/275072/3
Hua Wang 6 years ago
parent
commit
342e83f033
  1. 1
      doc/source/index.rst
  2. 1
      doc/source/objects.rst
  3. 7
      etc/magnum/policy.json
  4. 2
      magnum/api/controllers/v1/__init__.py
  5. 255
      magnum/api/controllers/v1/node.py
  6. 17
      magnum/common/exception.py
  7. 72
      magnum/db/api.py
  8. 29
      magnum/db/sqlalchemy/alembic/versions/bb42b7cad130_remove_node_object.py
  9. 105
      magnum/db/sqlalchemy/api.py
  10. 19
      magnum/db/sqlalchemy/models.py
  11. 3
      magnum/objects/__init__.py
  12. 161
      magnum/objects/node.py
  13. 7
      magnum/tests/fake_policy.py
  14. 3
      magnum/tests/functional/k8s/test_magnum_python_client.py
  15. 341
      magnum/tests/unit/api/controllers/v1/test_node.py
  16. 7
      magnum/tests/unit/api/utils.py
  17. 175
      magnum/tests/unit/db/test_node.py
  18. 29
      magnum/tests/unit/db/utils.py
  19. 112
      magnum/tests/unit/objects/test_node.py
  20. 1
      magnum/tests/unit/objects/test_objects.py
  21. 27
      magnum/tests/unit/objects/utils.py

1
doc/source/index.rst

@ -35,7 +35,6 @@ There are several different types of objects in the magnum system:
* **Bay:** A collection of node objects where work is scheduled
* **BayModel:** An object stores template information about the bay which is
used to create new bays consistently
* **Node:** A baremetal or virtual machine where work executes
* **Pod:** A collection of containers running on one physical or virtual
machine
* **Service:** An abstraction which defines a logical set of pods and a policy

1
doc/source/objects.rst

@ -86,7 +86,6 @@ magnum/tests/unit/objects/test_objects.py::
'BayModel': '1.0-06863f04ab4b98307e3d1b736d3137bf',
'Container': '1.1-22b40e8eed0414561ca921906b189820',
'MyObj': '1.0-b43567e512438205e32f4e95ca616697',
'Node': '1.0-30943e6e3387a2fae7490b57c4239a17',
'Pod': '1.0-69b579203c6d726be7878c606626e438',
'ReplicationController': '1.0-782b7deb9307b2807101541b7e58b8a2',
'Service': '1.0-d4b8c0f3a234aec35d273196e18f7ed1',

7
etc/magnum/policy.json

@ -19,13 +19,6 @@
"baymodel:update": "rule:default",
"baymodel:publish": "rule:admin_or_owner",
"node:create": "rule:default",
"node:delete": "rule:default",
"node:detail": "rule:default",
"node:get": "rule:default",
"node:get_all": "rule:default",
"node:update": "rule:default",
"pod:create": "rule:default",
"pod:delete": "rule:default",
"pod:detail": "rule:default",

2
magnum/api/controllers/v1/__init__.py

@ -31,7 +31,6 @@ from magnum.api.controllers.v1 import baymodel
from magnum.api.controllers.v1 import certificate
from magnum.api.controllers.v1 import container
from magnum.api.controllers.v1 import magnum_services
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
@ -192,7 +191,6 @@ class Controller(rest.RestController):
bays = bay.BaysController()
baymodels = baymodel.BayModelsController()
containers = container.ContainersController()
nodes = node.NodesController()
pods = pod.PodsController()
rcs = rc.ReplicationControllersController()
services = service.ServicesController()

255
magnum/api/controllers/v1/node.py

@ -1,255 +0,0 @@
# Copyright 2013 UnitedStack Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from oslo_utils import timeutils
import pecan
from pecan import rest
import wsme
from wsme import types as wtypes
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 import expose
from magnum.api import utils as api_utils
from magnum.common import exception
from magnum.common import policy
from magnum import objects
class NodePatchType(types.JsonPatchType):
pass
class Node(base.APIBase):
"""API representation of a node.
This class enforces type checking and value constraints, and converts
between the internal object model and the API representation of a node.
"""
uuid = types.uuid
"""Unique UUID for this node"""
type = wtypes.text
"""Type of this node"""
image_id = wtypes.text
"""The image name or UUID to use as a base image for this node"""
ironic_node_id = wtypes.text
"""The Ironic node ID associated with this node"""
links = wsme.wsattr([link.Link], readonly=True)
"""A list containing a self link and associated node links"""
def __init__(self, **kwargs):
self.fields = []
for field in objects.Node.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))
@staticmethod
def _convert_with_links(node, url, expand=True):
if not expand:
node.unset_fields_except(['uuid', 'name', 'type', 'image_id',
'ironic_node_id'])
node.links = [link.Link.make_link('self', url,
'nodes', node.uuid),
link.Link.make_link('bookmark', url,
'nodes', node.uuid,
bookmark=True)]
return node
@classmethod
def convert_with_links(cls, rpc_node, expand=True):
node = Node(**rpc_node.as_dict())
return cls._convert_with_links(node, pecan.request.host_url, expand)
@classmethod
def sample(cls, expand=True):
sample = cls(uuid='27e3153e-d5bf-4b7e-b517-fb518e17f34c',
type='virt',
image_id='Fedora-k8s',
ironic_node_id='4b6ec4a9-d412-494a-be77-a2fd16361402',
created_at=timeutils.utcnow(),
updated_at=timeutils.utcnow())
return cls._convert_with_links(sample, 'http://localhost:9511', expand)
class NodeCollection(collection.Collection):
"""API representation of a collection of nodes."""
nodes = [Node]
"""A list containing nodes objects"""
def __init__(self, **kwargs):
self._type = 'nodes'
@staticmethod
def convert_with_links(rpc_nodes, limit, url=None, expand=False, **kwargs):
collection = NodeCollection()
collection.nodes = [Node.convert_with_links(p, expand)
for p in rpc_nodes]
collection.next = collection.get_next(limit, url=url, **kwargs)
return collection
@classmethod
def sample(cls):
sample = cls()
sample.nodes = [Node.sample(expand=False)]
return sample
class NodesController(rest.RestController):
"""REST controller for Nodes."""
_custom_actions = {
'detail': ['GET'],
}
def _get_nodes_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.Node.get_by_uuid(pecan.request.context,
marker)
nodes = objects.Node.list(pecan.request.context, limit,
marker_obj, sort_key=sort_key,
sort_dir=sort_dir)
return NodeCollection.convert_with_links(nodes, limit,
url=resource_url,
expand=expand,
sort_key=sort_key,
sort_dir=sort_dir)
@expose.expose(NodeCollection, types.uuid, int, wtypes.text,
wtypes.text)
@policy.enforce_wsgi("node")
def get_all(self, marker=None, limit=None, sort_key='id',
sort_dir='asc'):
"""Retrieve a list of nodes.
: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_nodes_collection(marker, limit, sort_key,
sort_dir)
@expose.expose(NodeCollection, types.uuid, int, wtypes.text,
wtypes.text)
@policy.enforce_wsgi("node")
def detail(self, marker=None, limit=None, sort_key='id',
sort_dir='asc'):
"""Retrieve a list of nodes with detail.
: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(lucasagomes): /detail should only work against collections
parent = pecan.request.path.split('/')[:-1][-1]
if parent != "nodes":
raise exception.HTTPNotFound
expand = True
resource_url = '/'.join(['nodes', 'detail'])
return self._get_nodes_collection(marker, limit,
sort_key, sort_dir, expand,
resource_url)
@expose.expose(Node, types.uuid)
@policy.enforce_wsgi("node", "get")
def get_one(self, node_uuid):
"""Retrieve information about the given node.
:param node_uuid: UUID of a node.
"""
rpc_node = objects.Node.get_by_uuid(pecan.request.context, node_uuid)
return Node.convert_with_links(rpc_node)
@expose.expose(Node, body=Node, status_code=201)
@policy.enforce_wsgi("node", "create")
def post(self, node):
"""Create a new node.
:param node: a node within the request body.
"""
node_dict = node.as_dict()
context = pecan.request.context
node_dict['project_id'] = context.project_id
node_dict['user_id'] = context.user_id
new_node = objects.Node(context, **node_dict)
new_node.create()
# Set the HTTP Location Header
pecan.response.location = link.build_url('nodes', new_node.uuid)
return Node.convert_with_links(new_node)
@wsme.validate(types.uuid, [NodePatchType])
@expose.expose(Node, types.uuid, body=[NodePatchType])
@policy.enforce_wsgi("node", "update")
def patch(self, node_uuid, patch):
"""Update an existing node.
:param node_uuid: UUID of a node.
:param patch: a json PATCH document to apply to this node.
"""
rpc_node = objects.Node.get_by_uuid(pecan.request.context, node_uuid)
try:
node_dict = rpc_node.as_dict()
node = Node(**api_utils.apply_jsonpatch(node_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.Node.fields:
try:
patch_val = getattr(node, field)
except AttributeError:
# Ignore fields that aren't exposed in the API
continue
if patch_val == wtypes.Unset:
patch_val = None
if rpc_node[field] != patch_val:
rpc_node[field] = patch_val
rpc_node.save()
return Node.convert_with_links(rpc_node)
@expose.expose(None, types.uuid, status_code=204)
@policy.enforce_wsgi("node", "delete")
def delete(self, node_uuid):
"""Delete a node.
:param node_uuid: UUID of a node.
"""
rpc_node = objects.Node.get_by_uuid(pecan.request.context,
node_uuid)
rpc_node.destroy()

17
magnum/common/exception.py

@ -314,11 +314,6 @@ class InvalidParameterValue(Invalid):
message = _("%(err)s")
class InstanceAssociated(Conflict):
message = _("Instance %(instance_uuid)s is already associated with a node,"
" it cannot be associated with this other node %(node)s")
class InstanceNotFound(ResourceNotFound):
message = _("Instance %(instance)s could not be found.")
@ -351,18 +346,6 @@ class ConfigInvalid(MagnumException):
message = _("Invalid configuration file. %(error_msg)s")
class NodeAlreadyExists(Conflict):
message = _("A node with UUID %(uuid)s already exists.")
class NodeNotFound(ResourceNotFound):
message = _("Node %(node)s could not be found.")
class NodeAssociated(InvalidState):
message = _("Node %(node)s is associated with instance %(instance)s.")
class SSHConnectFailed(MagnumException):
message = _("Failed to establish SSH connection to host %(host)s.")

72
magnum/db/api.py

@ -289,78 +289,6 @@ class Connection(object):
"""
@abc.abstractmethod
def get_node_list(self, context, filters=None, limit=None,
marker=None, sort_key=None, sort_dir=None):
"""Get for matching nodes.
Return a list of the specified columns for all nodes that match the
specified filters.
:param context: The security context
:param filters: Filters to apply. Defaults to None.
:param limit: Maximum number of nodes 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_node(self, values):
"""Create a new node.
:param values: A dict containing several items used to identify
and track the node, and several dicts which are passed
into the Drivers when managing this node. For example:
::
{
'uuid': utils.generate_uuid(),
'name': 'example',
'type': 'virt'
}
:returns: A node.
"""
@abc.abstractmethod
def get_node_by_id(self, context, node_id):
"""Return a node.
:param context: The security context
:param node_id: The id of a node.
:returns: A node.
"""
@abc.abstractmethod
def get_node_by_uuid(self, context, node_uuid):
"""Return a node.
:param context: The security context
:param node_uuid: The uuid of a node.
:returns: A node.
"""
@abc.abstractmethod
def destroy_node(self, node_id):
"""Destroy a node and all associated interfaces.
:param node_id: The id or uuid of a node.
"""
@abc.abstractmethod
def update_node(self, node_id, values):
"""Update properties of a node.
:param node_id: The id or uuid of a node.
:returns: A node.
:raises: NodeAssociated
:raises: NodeNotFound
"""
@abc.abstractmethod
def get_pod_list(self, context, filters=None, limit=None,
marker=None, sort_key=None, sort_dir=None):
"""Get matching pods.

29
magnum/db/sqlalchemy/alembic/versions/bb42b7cad130_remove_node_object.py

@ -0,0 +1,29 @@
# 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.
"""remove node object
Revision ID: bb42b7cad130
Revises: 05d3e97de9ee
Create Date: 2016-02-02 16:04:36.501547
"""
# revision identifiers, used by Alembic.
revision = 'bb42b7cad130'
down_revision = '05d3e97de9ee'
from alembic import op
def upgrade():
op.drop_table('node')

105
magnum/db/sqlalchemy/api.py

@ -21,7 +21,6 @@ from oslo_db.sqlalchemy import utils as db_utils
from oslo_utils import timeutils
from sqlalchemy.orm.exc import MultipleResultsFound
from sqlalchemy.orm.exc import NoResultFound
from sqlalchemy import sql
from magnum.common import exception
from magnum.common import utils
@ -463,110 +462,6 @@ class Connection(api.Connection):
ref.update(values)
return ref
def _add_nodes_filters(self, query, filters):
if filters is None:
filters = {}
if 'associated' in filters:
if filters['associated']:
query = query.filter(models.Node.ironic_node_id != sql.null())
else:
query = query.filter(models.Node.ironic_node_id == sql.null())
if 'type' in filters:
query = query.filter_by(type=filters['type'])
if 'image_id' in filters:
query = query.filter_by(image_id=filters['image_id'])
if 'project_id' in filters:
query = query.filter_by(project_id=filters['project_id'])
if 'user_id' in filters:
query = query.filter_by(user_id=filters['user_id'])
return query
def get_node_list(self, context, filters=None, limit=None, marker=None,
sort_key=None, sort_dir=None):
query = model_query(models.Node)
query = self._add_tenant_filters(context, query)
query = self._add_nodes_filters(query, filters)
return _paginate_query(models.Node, limit, marker,
sort_key, sort_dir, query)
def create_node(self, values):
# ensure defaults are present for new nodes
if not values.get('uuid'):
values['uuid'] = utils.generate_uuid()
node = models.Node()
node.update(values)
try:
node.save()
except db_exc.DBDuplicateEntry as exc:
if 'ironic_node_id' in exc.columns:
raise exception.InstanceAssociated(
instance_uuid=values['ironic_node_id'],
node=values['uuid'])
raise exception.NodeAlreadyExists(uuid=values['uuid'])
return node
def get_node_by_id(self, context, node_id):
query = model_query(models.Node)
query = self._add_tenant_filters(context, query)
query = query.filter_by(id=node_id)
try:
return query.one()
except NoResultFound:
raise exception.NodeNotFound(node=node_id)
def get_node_by_uuid(self, context, node_uuid):
query = model_query(models.Node)
query = self._add_tenant_filters(context, query)
query = query.filter_by(uuid=node_uuid)
try:
return query.one()
except NoResultFound:
raise exception.NodeNotFound(node=node_uuid)
def destroy_node(self, node_id):
session = get_session()
with session.begin():
query = model_query(models.Node, session=session)
query = add_identity_filter(query, node_id)
count = query.delete()
if count != 1:
raise exception.NodeNotFound(node_id)
def update_node(self, node_id, values):
# NOTE(dtantsur): this can lead to very strange errors
if 'uuid' in values:
msg = _("Cannot overwrite UUID for an existing Node.")
raise exception.InvalidParameterValue(err=msg)
try:
return self._do_update_node(node_id, values)
except db_exc.DBDuplicateEntry:
raise exception.InstanceAssociated(
instance_uuid=values['ironic_node_id'],
node=node_id)
def _do_update_node(self, node_id, values):
session = get_session()
with session.begin():
query = model_query(models.Node, session=session)
query = add_identity_filter(query, node_id)
try:
ref = query.with_lockmode('update').one()
except NoResultFound:
raise exception.NodeNotFound(node=node_id)
# Prevent ironic_node_id overwriting
if values.get("ironic_node_id") and ref.ironic_node_id:
raise exception.NodeAssociated(
node=node_id,
instance=ref.ironic_node_id)
ref.update(values)
return ref
def _add_pods_filters(self, query, filters):
if filters is None:
filters = {}

19
magnum/db/sqlalchemy/models.py

@ -190,25 +190,6 @@ class Container(Base):
environment = Column(JSONEncodedDict)
class Node(Base):
"""Represents a node."""
__tablename__ = 'node'
__table_args__ = (
schema.UniqueConstraint('uuid', name='uniq_node0uuid'),
schema.UniqueConstraint('ironic_node_id',
name='uniq_node0ironic_node_id'),
table_args()
)
id = Column(Integer, primary_key=True)
uuid = Column(String(36))
type = Column(String(20))
project_id = Column(String(255))
user_id = Column(String(255))
image_id = Column(String(255))
ironic_node_id = Column(String(36))
class Pod(Base):
"""Represents a pod."""

3
magnum/objects/__init__.py

@ -17,7 +17,6 @@ from magnum.objects import baymodel
from magnum.objects import certificate
from magnum.objects import container
from magnum.objects import magnum_service
from magnum.objects import node
from magnum.objects import pod
from magnum.objects import replicationcontroller as rc
from magnum.objects import service
@ -28,7 +27,6 @@ Container = container.Container
Bay = bay.Bay
BayModel = baymodel.BayModel
MagnumService = magnum_service.MagnumService
Node = node.Node
Pod = pod.Pod
ReplicationController = rc.ReplicationController
Service = service.Service
@ -38,7 +36,6 @@ __all__ = (Bay,
BayModel,
Container,
MagnumService,
Node,
Pod,
ReplicationController,
Service,

161
magnum/objects/node.py

@ -1,161 +0,0 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from oslo_versionedobjects import fields
from magnum.db import api as dbapi
from magnum.objects import base
@base.MagnumObjectRegistry.register
class Node(base.MagnumPersistentObject, base.MagnumObject,
base.MagnumObjectDictCompat):
# Version 1.0: Initial version
VERSION = '1.0'
dbapi = dbapi.get_instance()
fields = {
'id': fields.IntegerField(),
'uuid': fields.StringField(nullable=True),
'project_id': fields.StringField(nullable=True),
'user_id': fields.StringField(nullable=True),
'type': fields.StringField(nullable=True),
'image_id': fields.StringField(nullable=True),
'ironic_node_id': fields.StringField(nullable=True)
}
@staticmethod
def _from_db_object(node, db_node):
"""Converts a database entity to a formal object."""
for field in node.fields:
node[field] = db_node[field]
node.obj_reset_changes()
return node
@staticmethod
def _from_db_object_list(db_objects, cls, context):
"""Converts a list of database entities to a list of formal objects."""
return [Node._from_db_object(cls(context), obj) for obj in db_objects]
@base.remotable_classmethod
def get_by_id(cls, context, node_id):
"""Find a node based on its integer id and return a Node object.
:param node_id: the id of a node.
:param context: Security context
:returns: a :class:`Node` object.
"""
db_node = cls.dbapi.get_node_by_id(context, node_id)
node = Node._from_db_object(cls(context), db_node)
return node
@base.remotable_classmethod
def get_by_uuid(cls, context, uuid):
"""Find a node based on uuid and return a :class:`Node` object.
:param uuid: the uuid of a node.
:param context: Security context
:returns: a :class:`Node` object.
"""
db_node = cls.dbapi.get_node_by_uuid(context, uuid)
node = Node._from_db_object(cls(context), db_node)
return node
@base.remotable_classmethod
def list(cls, context, limit=None, marker=None,
sort_key=None, sort_dir=None):
"""Return a list of Node 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:`Node` object.
"""
db_nodes = cls.dbapi.get_node_list(context, limit=limit,
marker=marker,
sort_key=sort_key,
sort_dir=sort_dir)
return Node._from_db_object_list(db_nodes, cls, context)
@base.remotable
def create(self, context=None):
"""Create a Node 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.: Node(context)
"""
values = self.obj_get_changes()
db_node = self.dbapi.create_node(values)
self._from_db_object(self, db_node)
@base.remotable
def destroy(self, context=None):
"""Delete the Node 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.: Node(context)
"""
self.dbapi.destroy_node(self.uuid)
self.obj_reset_changes()
@base.remotable
def save(self, context=None):
"""Save updates to this Node.
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.: Node(context)
"""
updates = self.obj_get_changes()
self.dbapi.update_node(self.uuid, updates)
self.obj_reset_changes()
@base.remotable
def refresh(self, context=None):
"""Loads updates for this Node.
Loads a node with the same uuid from the database and
checks for updated attributes. Updates are applied from
the loaded node 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.: Node(context)
"""
current = self.__class__.get_by_uuid(self._context, uuid=self.uuid)
for field in self.fields:
if self.obj_attr_is_set(field) and self[field] != current[field]:
self[field] = current[field]

7
magnum/tests/fake_policy.py

@ -34,13 +34,6 @@ policy_data = """
"baymodel:get_all": "",
"baymodel:update": "",
"node:create": "",
"node:delete": "",
"node:detail": "",
"node:get": "",
"node:get_all": "",
"node:update": "",
"pod:create": "",
"pod:delete": "",
"pod:detail": "",

3
magnum/tests/functional/k8s/test_magnum_python_client.py

@ -23,6 +23,3 @@ class TestListResources(BaseMagnumClient):
def test_containers_list(self):
self.assertIsNotNone(self.cs.containers.list())
def test_nodes_list(self):
self.assertIsNotNone(self.cs.nodes.list())

341
magnum/tests/unit/api/controllers/v1/test_node.py

@ -1,341 +0,0 @@
# 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 json
import mock
from oslo_config import cfg
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 node as api_node
from magnum.common import utils
from magnum.tests import base
from magnum.tests.unit.api import base as api_base
from magnum.tests.unit.api import utils as apiutils
from magnum.tests.unit.objects import utils as obj_utils
class TestNodeObject(base.TestCase):
def test_node_init(self):
node_dict = apiutils.node_post_data()
del node_dict['image_id']
node = api_node.Node(**node_dict)
self.assertEqual(wtypes.Unset, node.image_id)
class TestListNode(api_base.FunctionalTest):
def test_empty(self):
response = self.get_json('/nodes')
self.assertEqual([], response['nodes'])
def _assert_node_fields(self, node):
node_fields = ['type', 'image_id', 'ironic_node_id']
for field in node_fields:
self.assertIn(field, node)
def test_one(self):
node = obj_utils.create_test_node(self.context)
response = self.get_json('/nodes')
self.assertEqual(node.uuid, response['nodes'][0]["uuid"])
self._assert_node_fields(response['nodes'][0])
def test_get_one(self):
node = obj_utils.create_test_node(self.context)
response = self.get_json('/nodes/%s' % node['uuid'])
self.assertEqual(node.uuid, response['uuid'])
self._assert_node_fields(response)
def test_get_all_with_pagination_marker(self):
node_list = []
for id_ in range(4):
node = obj_utils.create_test_node(self.context, id=id_,
uuid=utils.generate_uuid())
node_list.append(node.uuid)
response = self.get_json('/nodes?limit=3&marker=%s' % node_list[2])
self.assertEqual(1, len(response['nodes']))
self.assertEqual(node_list[-1], response['nodes'][0]['uuid'])
def test_detail(self):
node = obj_utils.create_test_node(self.context)
response = self.get_json('/nodes/detail')
self.assertEqual(node.uuid, response['nodes'][0]["uuid"])
self._assert_node_fields(response['nodes'][0])
def test_detail_with_pagination_marker(self):
node_list = []
for id_ in range(4):
node = obj_utils.create_test_node(self.context, id=id_,
uuid=utils.generate_uuid())
node_list.append(node.uuid)
response = self.get_json('/nodes/detail?limit=3&marker=%s'
% node_list[2])
self.assertEqual(1, len(response['nodes']))
self.assertEqual(node_list[-1], response['nodes'][0]['uuid'])
self._assert_node_fields(response['nodes'][0])
def test_detail_against_single(self):
node = obj_utils.create_test_node(self.context)
response = self.get_json('/nodes/%s/detail' % node['uuid'],
expect_errors=True)
self.assertEqual(404, response.status_int)
def test_many(self):
node_list = []
for id_ in range(5):
node = obj_utils.create_test_node(self.context, id=id_,
uuid=utils.generate_uuid())
node_list.append(node.uuid)
response = self.get_json('/nodes')
self.assertEqual(len(node_list), len(response['nodes']))
uuids = [s['uuid'] for s in response['nodes']]
self.assertEqual(sorted(node_list), sorted(uuids))
def test_links(self):
uuid = utils.generate_uuid()
obj_utils.create_test_node(self.context, id=1, uuid=uuid)
response = self.get_json('/nodes/%s' % uuid)
self.assertIn('links', response.keys())
self.assertEqual(2, len(response['links']))
self.assertIn(uuid, response['links'][0]['href'])
for l in response['links']:
bookmark = l['rel'] == 'bookmark'
self.assertTrue(self.validate_link(l['href'], bookmark=bookmark))
def test_collection_links(self):
for id_ in range(5):
obj_utils.create_test_node(self.context, id=id_,
uuid=utils.generate_uuid())
response = self.get_json('/nodes/?limit=3')
self.assertEqual(3, len(response['nodes']))
next_marker = response['nodes'][-1]['uuid']
self.assertIn(next_marker, response['next'])
def test_collection_links_default_limit(self):
cfg.CONF.set_override('max_limit', 3, 'api')
for id_ in range(5):
obj_utils.create_test_node(self.context, id=id_,
uuid=utils.generate_uuid())
response = self.get_json('/nodes')
self.assertEqual(3, len(response['nodes']))
next_marker = response['nodes'][-1]['uuid']
self.assertIn(next_marker, response['next'])
class TestPatch(api_base.FunctionalTest):
def setUp(self):
super(TestPatch, self).setUp()
self.node = obj_utils.create_test_node(self.context, image_id='Fedora')
@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 = 'Ubuntu'
response = self.get_json('/nodes/%s' % self.node.uuid)
self.assertNotEqual(new_image, response['image_id'])
response = self.patch_json('/nodes/%s' % self.node.uuid,
[{'path': '/image_id', 'value': new_image,
'op': 'replace'}])
self.assertEqual('application/json', response.content_type)
self.assertEqual(200, response.status_code)
response = self.get_json('/nodes/%s' % self.node.uuid)
self.assertEqual(new_image, response['image_id'])
return_updated_at = timeutils.parse_isotime(
response['updated_at']).replace(tzinfo=None)
self.assertEqual(test_time, return_updated_at)
def test_replace_non_existent_node(self):
response = self.patch_json('/nodes/%s' % utils.generate_uuid(),
[{'path': '/image_id', 'value': 'Ubuntu',
'op': 'replace'}],
expect_errors=True)
self.assertEqual(404, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
def test_add_non_existent_property(self):
response = self.patch_json(
'/nodes/%s' % self.node.uuid,
[{'path': '/foo', 'value': 'bar', 'op': 'add'}],
expect_errors=True)
self.assertEqual('application/json', response.content_type)
self.assertEqual(400, response.status_int)
self.assertTrue(response.json['error_message'])
def test_remove_ok(self):
response = self.get_json('/nodes/%s' % self.node.uuid)
self.assertIsNotNone(response['image_id'])
response = self.patch_json('/nodes/%s' % self.node.uuid,
[{'path': '/image_id', 'op': 'remove'}])
self.assertEqual('application/json', response.content_type)
self.assertEqual(200, response.status_code)
response = self.get_json('/nodes/%s' % self.node.uuid)
self.assertIsNone(response['image_id'])
def test_remove_uuid(self):
response = self.patch_json('/nodes/%s' % self.node.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'])
def test_remove_non_existent_property(self):
response = self.patch_json(
'/nodes/%s' % self.node.uuid,
[{'path': '/non-existent', 'op': 'remove'}],
expect_errors=True)
self.assertEqual(400, response.status_code)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
class TestPost(api_base.FunctionalTest):
@mock.patch('oslo_utils.timeutils.utcnow')
def test_create_node(self, mock_utcnow):
node_dict = apiutils.node_post_data()
test_time = datetime.datetime(2000, 1, 1, 0, 0)
mock_utcnow.return_value = test_time
response = self.post_json('/nodes', node_dict)
self.assertEqual('application/json', response.content_type)
self.assertEqual(201, response.status_int)
# Check location header
self.assertIsNotNone(response.location)
expected_location = '/v1/nodes/%s' % node_dict['uuid']
self.assertEqual(expected_location,
urlparse.urlparse(response.location).path)
self.assertEqual(node_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_node_set_project_id_and_user_id(self):
with mock.patch.object(self.dbapi, 'create_node',
wraps=self.dbapi.create_node) as cc_mock:
node_dict = apiutils.node_post_data()
self.post_json('/nodes', node_dict)
cc_mock.assert_called_once_with(mock.ANY)
self.assertEqual(self.context.project_id,
cc_mock.call_args[0][0]['project_id'])
self.assertEqual(self.context.user_id,
cc_mock.call_args[0][0]['user_id'])
def test_create_node_doesnt_contain_id(self):
with mock.patch.object(self.dbapi, 'create_node',
wraps=self.dbapi.create_node) as cn_mock:
node_dict = apiutils.node_post_data(image_id='Ubuntu')
response = self.post_json('/nodes', node_dict)
self.assertEqual(node_dict['image_id'], response.json['image_id'])
cn_mock.assert_called_once_with(mock.ANY)
# Check that 'id' is not in first arg of positional args
self.assertNotIn('id', cn_mock.call_args[0][0])
def test_create_node_generate_uuid(self):
node_dict = apiutils.node_post_data()
del node_dict['uuid']
response = self.post_json('/nodes', node_dict)
self.assertEqual('application/json', response.content_type)
self.assertEqual(201, response.status_int)
self.assertEqual(node_dict['image_id'],
response.json['image_id'])
self.assertTrue(utils.is_uuid_like(response.json['uuid']))
class TestDelete(api_base.FunctionalTest):
def setUp(self):
super(TestDelete, self).setUp()
self.node = obj_utils.create_test_node(self.context, image_id='Fedora')
def test_delete_node(self):
self.delete('/nodes/%s' % self.node.uuid)
response = self.get_json('/nodes/%s' % self.node.uuid,
expect_errors=True)
self.assertEqual(404, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
def test_delete_node_not_found(self):
uuid = utils.generate_uuid()
response = self.delete('/nodes/%s' % uuid, expect_errors=True)
self.assertEqual(404, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
class TestNodePolicyEnforcement(api_base.FunctionalTest):
def _common_policy_check(self, rule, func, *arg, **kwarg):
self.policy.set_rules({rule: "project:non_fake"})
response = func(*arg, **kwarg)
self.assertEqual(403, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(
"Policy doesn't allow %s to be performed." % rule,
json.loads(response.json['error_message'])['faultstring'])
def test_policy_disallow_get_all(self):
self._common_policy_check(
"node:get_all", self.get_json, '/nodes', expect_errors=True)
def test_policy_disallow_get_one(self):
self._common_policy_check(
"node:get", self.get_json,
'/nodes/%s' % utils.generate_uuid(),
expect_errors=True)
def test_policy_disallow_detail(self):
self._common_policy_check(
"node:detail", self.get_json,
'/nodes/%s/detail' % utils.generate_uuid(),
expect_errors=True)
def test_policy_disallow_update(self):
node = obj_utils.create_test_node(self.context,
type='type_A',
uuid=utils.generate_uuid())
self._common_policy_check(
"node:update", self.patch_json,
'/nodes/%s' % node.uuid,
[{'path': '/type', 'value': "new_type", 'op': 'replace'}],
expect_errors=True)
def test_policy_disallow_create(self):
bdict = apiutils.node_post_data(name='node_example_A')
self._common_policy_check(
"node:create", self.post_json, '/nodes', bdict, expect_errors=True)
def test_policy_disallow_delete(self):
node = obj_utils.create_test_node(self.context,
uuid=utils.generate_uuid())
self._common_policy_check(
"node:delete", self.delete,
'/nodes/%s' % node.uuid, expect_errors=True)

7
magnum/tests/unit/api/utils.py

@ -17,7 +17,6 @@ import pytz
from magnum.api.controllers.v1 import bay as bay_controller
from magnum.api.controllers.v1 import baymodel as baymodel_controller
from magnum.api.controllers.v1 import node as node_controller
from magnum.api.controllers.v1 import pod as pod_controller
from magnum.api.controllers.v1 import replicationcontroller as rc_controller
from magnum.api.controllers.v1 import service as service_controller
@ -138,12 +137,6 @@ def rc_post_data(**kw):
return remove_internal(rc, internal)
def node_post_data(**kw):
node = utils.get_test_node(**kw)
internal = node_controller.NodePatchType.internal_attrs()
return remove_internal(node, internal)
def x509keypair_post_data(**kw):
x509keypair = utils.get_test_x509keypair(**kw)
internal = x509keypair_controller.X509KeyPairPatchType.internal_attrs()

175
magnum/tests/unit/db/test_node.py

@ -1,175 +0,0 @@
# Copyright 2015 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""Tests for manipulating Nodes via the DB API"""
import six
from magnum.common import exception
from magnum.common import utils as magnum_utils
from magnum.tests.unit.db import base
from magnum.tests.unit.db import utils
class DbNodeTestCase(base.DbTestCase):
def test_create_node(self):
utils.create_test_node()
def test_create_node_already_exists(self):
utils.create_test_node()
self.assertRaises(exception.NodeAlreadyExists,
utils.create_test_node)
def test_create_node_instance_already_associated(self):
instance_uuid = magnum_utils.generate_uuid()
utils.create_test_node(uuid=magnum_utils.generate_uuid(),
ironic_node_id=instance_uuid)
self.assertRaises(exception.InstanceAssociated,
utils.create_test_node,
uuid=magnum_utils.generate_uuid(),
ironic_node_id=instance_uuid)
def test_get_node_by_id(self):
node = utils.create_test_node()
res = self.dbapi.get_node_by_id(self.context, node.id)
self.assertEqual(node.id, res.id)
self.assertEqual(node.uuid, res.uuid)
def test_get_node_by_uuid(self):
node = utils.create_test_node()
res = self.dbapi.get_node_by_uuid(self.context, node.uuid)
self.assertEqual(node.id, res.id)
self.assertEqual(node.uuid, res.uuid)
def test_get_node_that_does_not_exist(self):
self.assertRaises(exception.NodeNotFound,
self.dbapi.get_node_by_id, self.context, 99)
self.assertRaises(exception.NodeNotFound,
self.dbapi.get_node_by_uuid,
self.context,
magnum_utils.generate_uuid())
def test_get_node_list(self):
uuids = []
for i in range(1, 6):
node = utils.create_test_node(uuid=magnum_utils.generate_uuid())
uuids.append(six.text_type(node['uuid']))
res = self.dbapi.get_node_list(self.context)
res_uuids = [r.uuid for r in res]
self.assertEqual(sorted(uuids), sorted(res_uuids))
def test_get_node_list_sorted(self):
uuids = []
for _ in range(5):
node = utils.create_test_node(uuid=magnum_utils.generate_uuid())
uuids.append(six.text_type(node.uuid))
res = self.dbapi.get_node_list(self.context, sort_key='uuid')
res_uuids = [r.uuid for r in res]
self.assertEqual(sorted(uuids), res_uuids)
self.assertRaises(exception.InvalidParameterValue,
self.dbapi.get_node_list,
self.context,
sort_key='foo')
def test_get_node_list_with_filters(self):
node1 = utils.create_test_node(
type='virt',
ironic_node_id=magnum_utils.generate_uuid(),
uuid=magnum_utils.generate_uuid())
node2 = utils.create_test_node(
type='bare',
uuid=magnum_utils.generate_uuid())