Browse Source
This adds all needed changes to support listing and showing nodegroups in an existing cluster. Change-Id: I5607c27eb0e84677acda29af006335374b60dd27changes/24/604824/12
8 changed files with 500 additions and 2 deletions
@ -0,0 +1,219 @@
|
||||
# Copyright (c) 2018 European Organization for Nuclear Research. |
||||
# 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 pecan |
||||
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 policy |
||||
from magnum import objects |
||||
|
||||
|
||||
class NodeGroup(base.APIBase): |
||||
"""API representation of a Node group. |
||||
|
||||
This class enforces type checking and value constraints, and converts |
||||
between the internal object model and the API representation of NodeGroup. |
||||
""" |
||||
id = wsme.wsattr(wtypes.IntegerType(minimum=1)) |
||||
"""unique id""" |
||||
|
||||
uuid = types.uuid |
||||
"""Unique UUID for this nodegroup""" |
||||
|
||||
name = wsme.wsattr(wtypes.StringType(min_length=1, max_length=255), |
||||
default=None) |
||||
"""Name of this nodegroup""" |
||||
|
||||
cluster_id = types.uuid |
||||
"""Unique UUID for the cluster where the nodegroup belongs to""" |
||||
|
||||
project_id = wsme.wsattr(wtypes.text, readonly=True) |
||||
"""Project UUID for this nodegroup""" |
||||
|
||||
docker_volume_size = wtypes.IntegerType(minimum=1) |
||||
"""The size in GB of the docker volume""" |
||||
|
||||
labels = wtypes.DictType(str, str) |
||||
"""One or more key/value pairs""" |
||||
|
||||
links = wsme.wsattr([link.Link], readonly=True) |
||||
"""A list containing a self link and associated nodegroup links""" |
||||
|
||||
flavor_id = wtypes.StringType(min_length=1, max_length=255) |
||||
"""The flavor of this nodegroup""" |
||||
|
||||
image_id = wtypes.StringType(min_length=1, max_length=255) |
||||
"""The image used for this nodegroup""" |
||||
|
||||
node_addresses = wsme.wsattr([wtypes.text], readonly=True) |
||||
"""IP addresses of nodegroup nodes""" |
||||
|
||||
node_count = wsme.wsattr(wtypes.IntegerType(minimum=1), default=1) |
||||
"""The node count for this nodegroup. Default to 1 if not set""" |
||||
|
||||
role = wtypes.StringType(min_length=1, max_length=255) |
||||
"""The role of the nodes included in this nodegroup""" |
||||
|
||||
min_node_count = wsme.wsattr(wtypes.IntegerType(minimum=1), default=1) |
||||
"""The minimum allowed nodes for this nodegroup. Default to 1 if not set""" |
||||
|
||||
max_node_count = wsme.wsattr(wtypes.IntegerType(minimum=1), default=None) |
||||
"""The maximum allowed nodes for this nodegroup. Default to 1 if not set""" |
||||
|
||||
is_default = types.BooleanType() |
||||
"""Specifies is a nodegroup was created by default or not""" |
||||
|
||||
def __init__(self, **kwargs): |
||||
super(NodeGroup, self).__init__() |
||||
self.fields = [] |
||||
for field in objects.NodeGroup.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)) |
||||
|
||||
@classmethod |
||||
def convert(cls, nodegroup, expand=True): |
||||
url = pecan.request.host_url |
||||
cluster_path = 'clusters/%s' % nodegroup.cluster_id |
||||
nodegroup_path = 'nodegroups/%s' % nodegroup.uuid |
||||
|
||||
ng = NodeGroup(**nodegroup.as_dict()) |
||||
if not expand: |
||||
ng.unset_fields_except(["uuid", "name", "flavor_id", "node_count", |
||||
"role", "is_default", "image_id"]) |
||||
else: |
||||
ng.links = [link.Link.make_link('self', url, cluster_path, |
||||
nodegroup_path), |
||||
link.Link.make_link('bookmark', url, |
||||
cluster_path, nodegroup_path, |
||||
bookmark=True)] |
||||
return ng |
||||
|
||||
|
||||
class NodeGroupCollection(collection.Collection): |
||||
"""API representation of a collection of Node Groups.""" |
||||
|
||||
nodegroups = [NodeGroup] |
||||
"""A list containing quota objects""" |
||||
|
||||
def __init__(self, **kwargs): |
||||
self._type = 'nodegroups' |
||||
|
||||
@staticmethod |
||||
def convert(nodegroups, limit, expand=True, **kwargs): |
||||
collection = NodeGroupCollection() |
||||
collection.nodegroups = [NodeGroup.convert(ng, expand) |
||||
for ng in nodegroups] |
||||
collection.next = collection.get_next(limit, |
||||
marker_attribute='id', |
||||
**kwargs) |
||||
return collection |
||||
|
||||
|
||||
class NodeGroupController(base.Controller): |
||||
"""REST controller for Node Groups.""" |
||||
|
||||
def __init__(self): |
||||
super(NodeGroupController, self).__init__() |
||||
|
||||
def _get_nodegroup_collection(self, cluster_id, marker, limit, sort_key, |
||||
sort_dir, filters, expand=True): |
||||
|
||||
limit = api_utils.validate_limit(limit) |
||||
sort_dir = api_utils.validate_sort_dir(sort_dir) |
||||
|
||||
marker_obj = None |
||||
if marker: |
||||
marker_obj = objects.NodeGroup.list(pecan.request.context, |
||||
cluster_id, |
||||
marker) |
||||
|
||||
nodegroups = objects.NodeGroup.list(pecan.request.context, |
||||
cluster_id, |
||||
limit, |
||||
marker_obj, |
||||
sort_key=sort_key, |
||||
sort_dir=sort_dir, |
||||
filters=filters) |
||||
|
||||
return NodeGroupCollection.convert(nodegroups, |
||||
limit, |
||||
expand=expand, |
||||
sort_key=sort_key, |
||||
sort_dir=sort_dir) |
||||
|
||||
@expose.expose(NodeGroupCollection, types.uuid_or_name, int, int, |
||||
wtypes.text, wtypes.text, wtypes.text) |
||||
def get_all(self, cluster_id, marker=None, limit=None, sort_key='id', |
||||
sort_dir='asc', role=None): |
||||
"""Retrieve a list of nodegroups. |
||||
|
||||
:param cluster_id: the cluster id or name |
||||
: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. |
||||
:param role: list all nodegroups with the specified role. |
||||
""" |
||||
context = pecan.request.context |
||||
policy.enforce(context, 'nodegroup:get_all', |
||||
action='nodegroup:get_all') |
||||
|
||||
if context.is_admin: |
||||
policy.enforce(context, 'nodegroup:get_all_all_projects', |
||||
action='nodegroup:get_all_all_projects') |
||||
context.all_tenants = True |
||||
|
||||
cluster = api_utils.get_resource('Cluster', cluster_id) |
||||
|
||||
filters = {} |
||||
if not context.is_admin: |
||||
filters = {"project_id": context.project_id} |
||||
if role: |
||||
filters.update({'role': role}) |
||||
|
||||
return self._get_nodegroup_collection(cluster.uuid, |
||||
marker, |
||||
limit, |
||||
sort_key, |
||||
sort_dir, |
||||
filters, |
||||
expand=False) |
||||
|
||||
@expose.expose(NodeGroup, types.uuid_or_name, types.uuid_or_name) |
||||
def get_one(self, cluster_id, nodegroup_id): |
||||
"""Retrieve information for the given nodegroup in a cluster. |
||||
|
||||
:param id: cluster id. |
||||
:param resource: nodegroup id. |
||||
""" |
||||
context = pecan.request.context |
||||
policy.enforce(context, 'nodegroup:get', action='nodegroup:get') |
||||
if context.is_admin: |
||||
policy.enforce(context, "nodegroup:get_one_all_projects", |
||||
action="nodegroup:get_one_all_projects") |
||||
context.all_tenants = True |
||||
cluster = api_utils.get_resource('Cluster', cluster_id) |
||||
nodegroup = objects.NodeGroup.get(context, cluster.uuid, nodegroup_id) |
||||
return NodeGroup.convert(nodegroup) |
@ -0,0 +1,73 @@
|
||||
# Copyright (c) 2018 European Organization for Nuclear Research. |
||||
# 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_policy import policy |
||||
|
||||
from magnum.common.policies import base |
||||
|
||||
|
||||
NODEGROUP = 'nodegroup:%s' |
||||
|
||||
|
||||
rules = [ |
||||
policy.DocumentedRuleDefault( |
||||
name=NODEGROUP % 'get', |
||||
check_str=base.RULE_ADMIN_OR_OWNER, |
||||
description='Retrieve information about the given nodegroup.', |
||||
operations=[ |
||||
{ |
||||
'path': '/v1/clusters/{cluster_id}/nodegroup/{nodegroup}', |
||||
'method': 'GET' |
||||
} |
||||
] |
||||
), |
||||
policy.DocumentedRuleDefault( |
||||
name=NODEGROUP % 'get_all', |
||||
check_str=base.RULE_ADMIN_OR_OWNER, |
||||
description='Retrieve a list of nodegroups that belong to a cluster.', |
||||
operations=[ |
||||
{ |
||||
'path': '/v1/clusters/{cluster_id}/nodegroups/', |
||||
'method': 'GET' |
||||
} |
||||
] |
||||
), |
||||
policy.DocumentedRuleDefault( |
||||
name=NODEGROUP % 'get_all_all_projects', |
||||
check_str=base.RULE_ADMIN_API, |
||||
description='Retrieve a list of nodegroups across projects.', |
||||
operations=[ |
||||
{ |
||||
'path': '/v1/clusters/{cluster_id}/nodegroups/', |
||||
'method': 'GET' |
||||
} |
||||
] |
||||
), |
||||
policy.DocumentedRuleDefault( |
||||
name=NODEGROUP % 'get_one_all_projects', |
||||
check_str=base.RULE_ADMIN_API, |
||||
description='Retrieve infornation for a given nodegroup.', |
||||
operations=[ |
||||
{ |
||||
'path': '/v1/clusters/{cluster_id}/nodegroups/{nodegroup}', |
||||
'method': 'GET' |
||||
} |
||||
] |
||||
), |
||||
] |
||||
|
||||
|
||||
def list_rules(): |
||||
return rules |
@ -0,0 +1,179 @@
|
||||
# Copyright (c) 2018 European Organization for Nuclear Research. |
||||
# 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 oslo_utils import uuidutils |
||||
|
||||
from magnum.api.controllers.v1 import nodegroup as api_nodegroup |
||||
import magnum.conf |
||||
from magnum import objects |
||||
from magnum.tests import base |
||||
from magnum.tests.unit.api import base as api_base |
||||
from magnum.tests.unit.api import utils as apiutils |
||||
from magnum.tests.unit.db import utils as db_utils |
||||
from magnum.tests.unit.objects import utils as obj_utils |
||||
|
||||
CONF = magnum.conf.CONF |
||||
|
||||
|
||||
class TestNodegroupObject(base.TestCase): |
||||
def test_nodegroup_init(self): |
||||
nodegroup_dict = apiutils.nodegroup_post_data() |
||||
del nodegroup_dict['node_count'] |
||||
del nodegroup_dict['min_node_count'] |
||||
del nodegroup_dict['max_node_count'] |
||||
nodegroup = api_nodegroup.NodeGroup(**nodegroup_dict) |
||||
self.assertEqual(1, nodegroup.node_count) |
||||
self.assertEqual(1, nodegroup.min_node_count) |
||||
self.assertIsNone(nodegroup.max_node_count) |
||||
|
||||
|
||||
class TestListNodegroups(api_base.FunctionalTest): |
||||
_expanded_attrs = ["id", "project_id", "docker_volume_size", "labels", |
||||
"node_addresses", "links"] |
||||
|
||||
_nodegroup_attrs = ["uuid", "name", "flavor_id", "node_count", "role", |
||||
"is_default", "image_id", "min_node_count", |
||||
"max_node_count"] |
||||
|
||||
def setUp(self): |
||||
super(TestListNodegroups, self).setUp() |
||||
obj_utils.create_test_cluster_template(self.context) |
||||
self.cluster_uuid = uuidutils.generate_uuid() |
||||
obj_utils.create_test_cluster( |
||||
self.context, uuid=self.cluster_uuid) |
||||
self.cluster = objects.Cluster.get_by_uuid(self.context, |
||||
self.cluster_uuid) |
||||
|
||||
def _test_list_nodegroups(self, cluster_id, filters=None, expected=None): |
||||
url = '/clusters/%s/nodegroups' % cluster_id |
||||
if filters is not None: |
||||
filter_list = ['%s=%s' % (k, v) for k, v in filters.items()] |
||||
url += '?' + '&'.join(f for f in filter_list) |
||||
response = self.get_json(url) |
||||
if expected is None: |
||||
expected = [] |
||||
ng_uuids = [ng['uuid'] for ng in response['nodegroups']] |
||||
self.assertEqual(expected, ng_uuids) |
||||
for ng in response['nodegroups']: |
||||
self._verify_attrs(self._nodegroup_attrs, ng) |
||||
self._verify_attrs(self._expanded_attrs, ng, positive=False) |
||||
|
||||
def test_get_all(self): |
||||
expected = [ng.uuid for ng in self.cluster.nodegroups] |
||||
self._test_list_nodegroups(self.cluster_uuid, expected=expected) |
||||
|
||||
def test_get_all_by_name(self): |
||||
expected = [ng.uuid for ng in self.cluster.nodegroups] |
||||
self._test_list_nodegroups(self.cluster.name, expected=expected) |
||||
|
||||
def test_get_all_by_name_non_default_ngs(self): |
||||
db_utils.create_test_nodegroup(cluster_id=self.cluster_uuid, |
||||
name='non_default_ng') |
||||
expected = [ng.uuid for ng in self.cluster.nodegroups] |
||||
self._test_list_nodegroups(self.cluster.name, expected=expected) |
||||
|
||||
def test_get_all_by_role(self): |
||||
filters = {'role': 'master'} |
||||
expected = [self.cluster.default_ng_master.uuid] |
||||
self._test_list_nodegroups(self.cluster.name, filters=filters, |
||||
expected=expected) |
||||
filters = {'role': 'worker'} |
||||
expected = [self.cluster.default_ng_worker.uuid] |
||||
self._test_list_nodegroups(self.cluster.name, filters=filters, |
||||
expected=expected) |
||||
|
||||
def test_get_all_by_non_existent_role(self): |
||||
filters = {'role': 'non-existent'} |
||||
self._test_list_nodegroups(self.cluster.name, filters=filters) |
||||
|
||||
@mock.patch("magnum.common.policy.enforce") |
||||
@mock.patch("magnum.common.context.make_context") |
||||
def test_get_all_as_admin(self, mock_context, mock_policy): |
||||
temp_uuid = uuidutils.generate_uuid() |
||||
obj_utils.create_test_cluster(self.context, uuid=temp_uuid, |
||||
project_id=temp_uuid) |
||||
self.context.is_admin = True |
||||
self.context.all_tenants = True |
||||
cluster = objects.Cluster.get_by_uuid(self.context, temp_uuid) |
||||
expected = [ng.uuid for ng in cluster.nodegroups] |
||||
self._test_list_nodegroups(cluster.uuid, expected=expected) |
||||
|
||||
def test_get_all_non_existent_cluster(self): |
||||
response = self.get_json('/clusters/not-here/nodegroups', |
||||
expect_errors=True) |
||||
self.assertEqual(404, response.status_code) |
||||
|
||||
def test_get_one(self): |
||||
worker = self.cluster.default_ng_worker |
||||
url = '/clusters/%s/nodegroups/%s' % (self.cluster.uuid, worker.uuid) |
||||
response = self.get_json(url) |
||||
self.assertEqual(worker.name, response['name']) |
||||
self._verify_attrs(self._nodegroup_attrs, response) |
||||
self._verify_attrs(self._expanded_attrs, response) |
||||
|
||||
def test_get_one_non_existent_ng(self): |
||||
url = '/clusters/%s/nodegroups/not-here' % self.cluster.uuid |
||||
response = self.get_json(url, expect_errors=True) |
||||
self.assertEqual(404, response.status_code) |
||||
|
||||
@mock.patch("magnum.common.policy.enforce") |
||||
@mock.patch("magnum.common.context.make_context") |
||||
def test_get_one_as_admin(self, mock_context, mock_policy): |
||||
temp_uuid = uuidutils.generate_uuid() |
||||
obj_utils.create_test_cluster(self.context, uuid=temp_uuid, |
||||
project_id=temp_uuid) |
||||
self.context.is_admin = True |
||||
self.context.all_tenants = True |
||||
cluster = objects.Cluster.get_by_uuid(self.context, temp_uuid) |
||||
worker = cluster.default_ng_worker |
||||
url = '/clusters/%s/nodegroups/%s' % (cluster.uuid, worker.uuid) |
||||
response = self.get_json(url) |
||||
self.assertEqual(worker.name, response['name']) |
||||
self._verify_attrs(self._nodegroup_attrs, response) |
||||
self._verify_attrs(self._expanded_attrs, response) |
||||
|
||||
|
||||
class TestNodeGroupPolicyEnforcement(api_base.FunctionalTest): |
||||
def setUp(self): |
||||
super(TestNodeGroupPolicyEnforcement, self).setUp() |
||||
obj_utils.create_test_cluster_template(self.context) |
||||
self.cluster_uuid = uuidutils.generate_uuid() |
||||
obj_utils.create_test_cluster( |
||||
self.context, uuid=self.cluster_uuid) |
||||
self.cluster = objects.Cluster.get_by_uuid(self.context, |
||||
self.cluster_uuid) |
||||
|
||||
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, |
||||
response.json['errors'][0]['detail']) |
||||
|
||||
def test_policy_disallow_get_all(self): |
||||
self._common_policy_check( |
||||
"nodegroup:get_all", self.get_json, |
||||
'/clusters/%s/nodegroups' % self.cluster_uuid, expect_errors=True) |
||||
|
||||
def test_policy_disallow_get_one(self): |
||||
worker = self.cluster.default_ng_worker |
||||
self._common_policy_check( |
||||
"nodegroup:get", self.get_json, |
||||
'/clusters/%s/nodegroups/%s' % (self.cluster.uuid, worker.uuid), |
||||
expect_errors=True) |
Loading…
Reference in new issue