Browse Source
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: If8761b06a364127683099afb4dc51ea551be6f89changes/72/275072/3
21 changed files with 29 additions and 1345 deletions
@ -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() |
@ -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') |
@ -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] |
@ -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) |
@ -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()) |
||||