Merge "Remove node object from Magnum"
This commit is contained in:
commit
e15a6421f1
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
|
@ -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.")
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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')
|
|
@ -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 = {}
|
||||
|
|
|
@ -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."""
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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]
|
|
@ -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": "",
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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)
|
|
@ -18,7 +18,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
|
||||
|
@ -139,12 +138,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()
|
||||
|
|
|
@ -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())
|
||||
|
||||
res = self.dbapi.get_node_list(self.context, filters={'type': 'virt'})
|
||||
self.assertEqual([node1.id], [r.id for r in res])
|
||||
|
||||
res = self.dbapi.get_node_list(self.context,
|
||||
filters={'type': 'bad-type'})
|
||||
self.assertEqual([], [r.id for r in res])
|
||||
|
||||
res = self.dbapi.get_node_list(self.context,
|
||||
filters={'associated': True})
|
||||
self.assertEqual([node1.id], [r.id for r in res])
|
||||
|
||||
res = self.dbapi.get_node_list(self.context,
|
||||
filters={'associated': False})
|
||||
self.assertEqual([node2.id], [r.id for r in res])
|
||||
|
||||
def test_destroy_node(self):
|
||||
node = utils.create_test_node()
|
||||
self.dbapi.destroy_node(node.id)
|
||||
self.assertRaises(exception.NodeNotFound,
|
||||
self.dbapi.get_node_by_id, self.context, node.id)
|
||||
|
||||
def test_destroy_node_by_uuid(self):
|
||||
node = utils.create_test_node()
|
||||
self.dbapi.destroy_node(node.uuid)
|
||||
self.assertRaises(exception.NodeNotFound,
|
||||
self.dbapi.get_node_by_uuid,
|
||||
self.context, node.uuid)
|
||||
|
||||
def test_destroy_node_that_does_not_exist(self):
|
||||
self.assertRaises(exception.NodeNotFound,
|
||||
self.dbapi.destroy_node,
|
||||
magnum_utils.generate_uuid())
|
||||
|
||||
def test_update_node(self):
|
||||
node = utils.create_test_node()
|
||||
old_image = node.image_id
|
||||
new_image = 'new-image'
|
||||
self.assertNotEqual(old_image, new_image)
|
||||
|
||||
res = self.dbapi.update_node(node.id, {'image_id': new_image})
|
||||
self.assertEqual(new_image, res.image_id)
|
||||
|
||||
def test_update_node_not_found(self):
|
||||
node_uuid = magnum_utils.generate_uuid()
|
||||
new_image = 'new-image'
|
||||
self.assertRaises(exception.NodeNotFound, self.dbapi.update_node,
|
||||
node_uuid, {'image_id': new_image})
|
||||
|
||||
def test_update_node_uuid(self):
|
||||
node = utils.create_test_node()
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
self.dbapi.update_node, node.id,
|
||||
{'uuid': ''})
|
||||
|
||||
def test_update_node_associate_and_disassociate(self):
|
||||
node = utils.create_test_node()
|
||||
new_i_uuid = magnum_utils.generate_uuid()
|
||||
res = self.dbapi.update_node(node.id, {'ironic_node_id': new_i_uuid})
|
||||
self.assertEqual(new_i_uuid, res.ironic_node_id)
|
||||
res = self.dbapi.update_node(node.id, {'ironic_node_id': None})
|
||||
self.assertIsNone(res.ironic_node_id)
|
||||
|
||||
def test_update_node_already_associated(self):
|
||||
node = utils.create_test_node()
|
||||
new_i_uuid_one = magnum_utils.generate_uuid()
|
||||
self.dbapi.update_node(node.id, {'ironic_node_id': new_i_uuid_one})
|
||||
new_i_uuid_two = magnum_utils.generate_uuid()
|
||||
self.assertRaises(exception.NodeAssociated,
|
||||
self.dbapi.update_node, node.id,
|
||||
{'ironic_node_id': new_i_uuid_two})
|
||||
|
||||
def test_update_node_instance_already_associated(self):
|
||||
node1 = utils.create_test_node(uuid=magnum_utils.generate_uuid())
|
||||
new_i_uuid = magnum_utils.generate_uuid()
|
||||
self.dbapi.update_node(node1.id, {'ironic_node_id': new_i_uuid})
|
||||
node2 = utils.create_test_node(uuid=magnum_utils.generate_uuid())
|
||||
self.assertRaises(exception.InstanceAssociated,
|
||||
self.dbapi.update_node, node2.id,
|
||||
{'ironic_node_id': new_i_uuid})
|
|
@ -183,35 +183,6 @@ def create_test_service(**kw):
|
|||
return dbapi.create_service(service)
|
||||
|
||||
|
||||
def get_test_node(**kw):
|
||||
return {
|
||||
'id': kw.get('id', 42),
|
||||
'uuid': kw.get('uuid', 'ea8e2a25-2901-438d-8157-de7ffd68d051'),
|
||||
'type': kw.get('type', 'virt'),
|
||||
'project_id': kw.get('project_id', 'fake_project'),
|
||||
'user_id': kw.get('user_id', 'fake_user'),
|
||||
'image_id': kw.get('image_id', 'ubuntu'),
|
||||
'ironic_node_id': kw.get('ironic_node_id'),
|
||||
'created_at': kw.get('created_at'),
|
||||
'updated_at': kw.get('updated_at'),
|
||||
}
|
||||
|
||||
|
||||
def create_test_node(**kw):
|
||||
"""Create test node entry in DB and return Node DB object.
|
||||
|
||||
Function to be used to create test Node objects in the database.
|
||||
:param kw: kwargs with overriding values for node's attributes.
|
||||
:returns: Test Node DB object.
|
||||
"""
|
||||
node = get_test_node(**kw)
|
||||
# Let DB generate ID if it isn't specified explicitly
|
||||
if 'id' not in kw:
|
||||
del node['id']
|
||||
dbapi = db_api.get_instance()
|
||||
return dbapi.create_node(node)
|
||||
|
||||
|
||||
def get_test_container(**kw):
|
||||
return {
|
||||
'id': kw.get('id', 42),
|
||||
|
|
|
@ -1,112 +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.
|
||||
|
||||
import mock
|
||||
from testtools.matchers import HasLength
|
||||
|
||||
from magnum.common import utils as magnum_utils
|
||||
from magnum import objects
|
||||
from magnum.tests.unit.db import base
|
||||
from magnum.tests.unit.db import utils
|
||||
|
||||
|
||||
class TestNodeObject(base.DbTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestNodeObject, self).setUp()
|
||||
self.fake_node = utils.get_test_node()
|
||||
|
||||
def test_get_by_id(self):
|
||||
node_id = self.fake_node['id']
|
||||
with mock.patch.object(self.dbapi, 'get_node_by_id',
|
||||
autospec=True) as mock_get_node:
|
||||
mock_get_node.return_value = self.fake_node
|
||||
node = objects.Node.get_by_id(self.context, node_id)
|
||||
mock_get_node.assert_called_once_with(self.context, node_id)
|
||||
self.assertEqual(self.context, node._context)
|
||||
|
||||
def test_get_by_uuid(self):
|
||||
uuid = self.fake_node['uuid']
|
||||
with mock.patch.object(self.dbapi, 'get_node_by_uuid',
|
||||
autospec=True) as mock_get_node:
|
||||
mock_get_node.return_value = self.fake_node
|
||||
node = objects.Node.get_by_uuid(self.context, uuid)
|
||||
mock_get_node.assert_called_once_with(self.context, uuid)
|
||||
self.assertEqual(self.context, node._context)
|
||||
|
||||
def test_list(self):
|
||||
with mock.patch.object(self.dbapi, 'get_node_list',
|
||||
autospec=True) as mock_get_list:
|
||||
mock_get_list.return_value = [self.fake_node]
|
||||
nodes = objects.Node.list(self.context)
|
||||
self.assertEqual(1, mock_get_list.call_count)
|
||||
self.assertThat(nodes, HasLength(1))
|
||||
self.assertIsInstance(nodes[0], objects.Node)
|
||||
self.assertEqual(self.context, nodes[0]._context)
|
||||
|
||||
def test_create(self):
|
||||
with mock.patch.object(self.dbapi, 'create_node',
|
||||
autospec=True) as mock_create_node:
|
||||
mock_create_node.return_value = self.fake_node
|
||||
node = objects.Node(self.context, **self.fake_node)
|
||||
node.create()
|
||||
mock_create_node.assert_called_once_with(self.fake_node)
|
||||
self.assertEqual(self.context, node._context)
|
||||
|
||||
def test_destroy(self):
|
||||
uuid = self.fake_node['uuid']
|
||||
with mock.patch.object(self.dbapi, 'get_node_by_uuid',
|
||||
autospec=True) as mock_get_node:
|
||||
mock_get_node.return_value = self.fake_node
|
||||
with mock.patch.object(self.dbapi, 'destroy_node',
|
||||
autospec=True) as mock_destroy_node:
|
||||
node = objects.Node.get_by_uuid(self.context, uuid)
|
||||
node.destroy()
|
||||
mock_get_node.assert_called_once_with(self.context, uuid)
|
||||
mock_destroy_node.assert_called_once_with(uuid)
|
||||
self.assertEqual(self.context, node._context)
|
||||
|
||||
def test_save(self):
|
||||
uuid = self.fake_node['uuid']
|
||||
with mock.patch.object(self.dbapi, 'get_node_by_uuid',
|
||||
autospec=True) as mock_get_node:
|
||||
mock_get_node.return_value = self.fake_node
|
||||
with mock.patch.object(self.dbapi, 'update_node',
|
||||
autospec=True) as mock_update_node:
|
||||
node = objects.Node.get_by_uuid(self.context, uuid)
|
||||
node.type = 'bare'
|
||||
node.save()
|
||||
|
||||
mock_get_node.assert_called_once_with(self.context, uuid)
|
||||
mock_update_node.assert_called_once_with(
|
||||
uuid, {'type': 'bare'})
|
||||
self.assertEqual(self.context, node._context)
|
||||
|
||||
def test_refresh(self):
|
||||
uuid = self.fake_node['uuid']
|
||||
new_uuid = magnum_utils.generate_uuid()
|
||||
returns = [dict(self.fake_node, uuid=uuid),
|
||||
dict(self.fake_node, uuid=new_uuid)]
|
||||
expected = [mock.call(self.context, uuid),
|
||||
mock.call(self.context, uuid)]
|
||||
with mock.patch.object(self.dbapi, 'get_node_by_uuid',
|
||||
side_effect=returns,
|
||||
autospec=True) as mock_get_node:
|
||||
node = objects.Node.get_by_uuid(self.context, uuid)
|
||||
self.assertEqual(uuid, node.uuid)
|
||||
node.refresh()
|
||||
self.assertEqual(new_uuid, node.uuid)
|
||||
self.assertEqual(expected, mock_get_node.call_args_list)
|
||||
self.assertEqual(self.context, node._context)
|
|
@ -428,7 +428,6 @@ object_data = {
|
|||
'Certificate': '1.0-2aff667971b85c1edf8d15684fd7d5e2',
|
||||
'Container': '1.3-e2d9d2e8a8844d421148cd9fde6c6bd6',
|
||||
'MyObj': '1.0-b43567e512438205e32f4e95ca616697',
|
||||
'Node': '1.0-30943e6e3387a2fae7490b57c4239a17',
|
||||
'Pod': '1.1-39f221ad1dad0eb7f7bee3569d42fa7e',
|
||||
'ReplicationController': '1.0-a471c2429c212ed91833cfcf0f934eab',
|
||||
'Service': '1.0-f4a1c5a4618708824a553568c1ada0ea',
|
||||
|
|
|
@ -160,33 +160,6 @@ def create_test_rc(context, **kw):
|
|||
return rc
|
||||
|
||||
|
||||
def get_test_node(context, **kw):
|
||||
"""Return a Node object with appropriate attributes.
|
||||
|
||||
NOTE: The object leaves the attributes marked as changed, such
|
||||
that a create() could be used to commit it to the DB.
|
||||
"""
|
||||
db_node = db_utils.get_test_node(**kw)
|
||||
# Let DB generate ID if it isn't specified explicitly
|
||||
if 'id' not in kw:
|
||||
del db_node['id']
|
||||
node = objects.Node(context)
|
||||
for key in db_node:
|
||||
setattr(node, key, db_node[key])
|
||||
return node
|
||||
|
||||
|
||||
def create_test_node(context, **kw):
|
||||
"""Create and return a test Node object.
|
||||
|
||||
Create a node in the DB and return a Node object with appropriate
|
||||
attributes.
|
||||
"""
|
||||
node = get_test_node(context, **kw)
|
||||
node.create()
|
||||
return node
|
||||
|
||||
|
||||
def get_test_x509keypair(context, **kw):
|
||||
"""Return a X509KeyPair object with appropriate attributes.
|
||||
|
||||
|
|
Loading…
Reference in New Issue