Browse Source

Merge "ng-5: APIs for listing and showing nodegroups"

changes/59/514959/17
Zuul 2 years ago
committed by Gerrit Code Review
parent
commit
1fc856812b
8 changed files with 500 additions and 2 deletions
  1. +11
    -0
      magnum/api/controllers/v1/__init__.py
  2. +3
    -0
      magnum/api/controllers/v1/cluster.py
  3. +219
    -0
      magnum/api/controllers/v1/nodegroup.py
  4. +3
    -1
      magnum/common/policies/__init__.py
  5. +73
    -0
      magnum/common/policies/nodegroup.py
  6. +7
    -1
      magnum/tests/unit/api/controllers/test_root.py
  7. +179
    -0
      magnum/tests/unit/api/controllers/v1/test_nodegroup.py
  8. +5
    -0
      magnum/tests/unit/api/utils.py

+ 11
- 0
magnum/api/controllers/v1/__init__.py View File

@ -103,6 +103,9 @@ class V1(controllers_base.APIBase):
# Links to the federations resources
federations = [link.Link]
nodegroups = [link.Link]
"""Links to the nodegroups resource"""
@staticmethod
def convert():
v1 = V1()
@ -171,6 +174,14 @@ class V1(controllers_base.APIBase):
pecan.request.host_url,
'federations', '',
bookmark=True)]
v1.nodegroups = [link.Link.make_link('self', pecan.request.host_url,
'clusters/{cluster_id}',
'nodegroups'),
link.Link.make_link('bookmark',
pecan.request.host_url,
'clusters/{cluster_id}',
'nodegroups',
bookmark=True)]
return v1


+ 3
- 0
magnum/api/controllers/v1/cluster.py View File

@ -27,6 +27,7 @@ from magnum.api.controllers import base
from magnum.api.controllers import link
from magnum.api.controllers.v1 import cluster_actions
from magnum.api.controllers.v1 import collection
from magnum.api.controllers.v1 import nodegroup
from magnum.api.controllers.v1 import types
from magnum.api import expose
from magnum.api import utils as api_utils
@ -334,6 +335,8 @@ class ClustersController(base.Controller):
sort_key=sort_key,
sort_dir=sort_dir)
nodegroups = nodegroup.NodeGroupController()
@expose.expose(ClusterCollection, types.uuid, int, wtypes.text,
wtypes.text)
def get_all(self, marker=None, limit=None, sort_key='id',


+ 219
- 0
magnum/api/controllers/v1/nodegroup.py View File

@ -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)

+ 3
- 1
magnum/common/policies/__init__.py View File

@ -22,6 +22,7 @@ from magnum.common.policies import cluster
from magnum.common.policies import cluster_template
from magnum.common.policies import federation
from magnum.common.policies import magnum_service
from magnum.common.policies import nodegroup
from magnum.common.policies import quota
from magnum.common.policies import stats
@ -37,5 +38,6 @@ def list_rules():
federation.list_rules(),
magnum_service.list_rules(),
quota.list_rules(),
stats.list_rules()
stats.list_rules(),
nodegroup.list_rules()
)

+ 73
- 0
magnum/common/policies/nodegroup.py View File

@ -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

+ 7
- 1
magnum/tests/unit/api/controllers/test_root.py View File

@ -90,7 +90,13 @@ class TestRootController(api_base.FunctionalTest):
u'federations': [{u'href': u'http://localhost/v1/federations/',
u'rel': u'self'},
{u'href': u'http://localhost/federations/',
u'rel': u'bookmark'}]}
u'rel': u'bookmark'}],
u'nodegroups': [{u'href': u'http://localhost/v1/clusters/'
'{cluster_id}/nodegroups',
u'rel': u'self'},
{u'href': u'http://localhost/clusters/'
'{cluster_id}/nodegroups',
u'rel': u'bookmark'}]}
def make_app(self, paste_file):
file_name = self.get_path(paste_file)


+ 179
- 0
magnum/tests/unit/api/controllers/v1/test_nodegroup.py View File

@ -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)

+ 5
- 0
magnum/tests/unit/api/utils.py View File

@ -95,3 +95,8 @@ def federation_post_data(**kw):
federation = utils.get_test_federation(**kw)
internal = federation_controller.FederationPatchType.internal_attrs()
return remove_internal(federation, internal)
def nodegroup_post_data(**kw):
nodegroup = utils.get_test_nodegroup(**kw)
return nodegroup

Loading…
Cancel
Save