Merge "Refactoring and removing duplicate code of "base.Manager" heirs without changing the API"

This commit is contained in:
Jenkins 2015-12-13 02:00:43 +00:00 committed by Gerrit Code Review
commit 9a563b06d4
7 changed files with 280 additions and 113 deletions

View File

@ -17,11 +17,14 @@
Base utilities to build API operation managers and objects on top of.
"""
import abc
import copy
import six
import six.moves.urllib.parse as urlparse
from ironicclient.common.apiclient import base
from ironicclient import exc
def getid(obj):
@ -36,17 +39,49 @@ def getid(obj):
return obj
@six.add_metaclass(abc.ABCMeta)
class Manager(object):
"""Provides CRUD operations with a particular API."""
resource_class = None
def __init__(self, api):
self.api = api
def _create(self, url, body):
resp, body = self.api.json_request('POST', url, body=body)
if body:
return self.resource_class(self, body)
def _path(self, resource_id=None):
"""Returns a request path for a given resource identifier.
:param resource_id: Identifier of the resource to generate the request
path.
"""
return ('/v1/%s/%s' % (self._resource_name, resource_id)
if resource_id else '/v1/%s' % self._resource_name)
@abc.abstractproperty
def resource_class(self):
"""The resource class
"""
@abc.abstractproperty
def _resource_name(self):
"""The resource name.
"""
def _get(self, resource_id, fields=None):
"""Retrieve a resource.
:param resource_id: Identifier of the resource.
:param fields: List of specific fields to be returned.
"""
if fields is not None:
resource_id = '%s?fields=' % resource_id
resource_id += ','.join(fields)
try:
return self._list(self._path(resource_id))[0]
except IndexError:
return None
def _format_body_data(self, body, response_key):
if response_key:
@ -124,14 +159,57 @@ class Manager(object):
data = self._format_body_data(body, response_key)
return [obj_class(self, res, loaded=True) for res in data if res]
def _update(self, url, body, method='PATCH', response_key=None):
resp, body = self.api.json_request(method, url, body=body)
def _update(self, resource_id, patch, method='PATCH'):
"""Update a resource.
:param resource_id: Resource identifier.
:param patch: New version of a given resource.
:param method: Name of the method for the request.
"""
url = self._path(resource_id)
resp, body = self.api.json_request(method, url, body=patch)
# PATCH/PUT requests may not return a body
if body:
return self.resource_class(self, body)
def _delete(self, url):
self.api.raw_request('DELETE', url)
def _delete(self, resource_id):
"""Delete a resource.
:param resource_id: Resource identifier.
"""
self.api.raw_request('DELETE', self._path(resource_id))
@six.add_metaclass(abc.ABCMeta)
class CreateManager(Manager):
"""Provides creation operations with a particular API."""
@abc.abstractproperty
def _creation_attributes(self):
"""A list of required creation attributes for a resource type.
"""
def create(self, **kwargs):
"""Create a resource based on a kwargs dictionary of attributes.
:param kwargs: A dictionary containing the attributes of the resource
that will be created.
:raises exc.InvalidAttribute: For invalid attributes that are not
needed to create the resource.
"""
new = {}
for (key, value) in kwargs.items():
if key in self._creation_attributes:
new[key] = value
else:
raise exc.InvalidAttribute()
url = self._path()
resp, body = self.api.json_request('POST', url, body=new)
if body:
return self.resource_class(self, body)
class Resource(base.Resource):

View File

@ -0,0 +1,157 @@
# Copyright 2013 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 copy
import testtools
from ironicclient.common import base
from ironicclient import exc
from ironicclient.tests.unit import utils
TESTABLE_RESOURCE = {
'id': 987,
'uuid': '11111111-2222-3333-4444-555555555555',
'attribute1': '1',
'attribute2': '2',
}
CREATE_TESTABLE_RESOURCE = copy.deepcopy(TESTABLE_RESOURCE)
del CREATE_TESTABLE_RESOURCE['id']
del CREATE_TESTABLE_RESOURCE['uuid']
INVALID_ATTRIBUTE_TESTABLE_RESOURCE = {
'non-existent-attribute': 'blablabla',
'attribute1': '1',
'attribute2': '2',
}
UPDATED_TESTABLE_RESOURCE = copy.deepcopy(TESTABLE_RESOURCE)
NEW_ATTRIBUTE_VALUE = 'brand-new-attribute-value'
UPDATED_TESTABLE_RESOURCE['attribute1'] = NEW_ATTRIBUTE_VALUE
fake_responses = {
'/v1/testableresources':
{
'GET': (
{},
{"testableresources": [TESTABLE_RESOURCE]},
),
'POST': (
{},
CREATE_TESTABLE_RESOURCE,
),
},
'/v1/testableresources/%s' % TESTABLE_RESOURCE['uuid']:
{
'GET': (
{},
TESTABLE_RESOURCE,
),
'DELETE': (
{},
None,
),
'PATCH': (
{},
UPDATED_TESTABLE_RESOURCE,
),
},
}
class TestableResource(base.Resource):
def __repr__(self):
return "<TestableResource %s>" % self._info
class TestableManager(base.CreateManager):
resource_class = TestableResource
_creation_attributes = ['attribute1', 'attribute2']
_resource_name = 'testableresources'
def _path(self, id=None):
return ('/v1/testableresources/%s' % id if id
else '/v1/testableresources')
def get(self, testable_resource_id, fields=None):
return self._get(resource_id=testable_resource_id,
fields=fields)
def delete(self, testable_resource_id):
return self._delete(resource_id=testable_resource_id)
def update(self, testable_resource_id, patch):
return self._update(resource_id=testable_resource_id,
patch=patch)
class ManagerTestCase(testtools.TestCase):
def setUp(self):
super(ManagerTestCase, self).setUp()
self.api = utils.FakeAPI(fake_responses)
self.manager = TestableManager(self.api)
def test_create(self):
resource = self.manager.create(**CREATE_TESTABLE_RESOURCE)
expect = [
('POST', '/v1/testableresources', {}, CREATE_TESTABLE_RESOURCE),
]
self.assertEqual(expect, self.api.calls)
self.assertTrue(resource)
self.assertIsInstance(resource, TestableResource)
def test_create_with_invalid_attribute(self):
self.assertRaises(exc.InvalidAttribute, self.manager.create,
**INVALID_ATTRIBUTE_TESTABLE_RESOURCE)
def test_get(self):
resource = self.manager.get(TESTABLE_RESOURCE['uuid'])
expect = [
('GET', '/v1/testableresources/%s' % TESTABLE_RESOURCE['uuid'],
{}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(TESTABLE_RESOURCE['uuid'], resource.uuid)
self.assertEqual(TESTABLE_RESOURCE['attribute1'], resource.attribute1)
def test_update(self):
patch = {'op': 'replace',
'value': NEW_ATTRIBUTE_VALUE,
'path': '/attribute1'}
resource = self.manager.update(
testable_resource_id=TESTABLE_RESOURCE['uuid'],
patch=patch
)
expect = [
('PATCH', '/v1/testableresources/%s' % TESTABLE_RESOURCE['uuid'],
{}, patch),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(NEW_ATTRIBUTE_VALUE, resource.attribute1)
def test_delete(self):
resource = self.manager.delete(
testable_resource_id=TESTABLE_RESOURCE['uuid']
)
expect = [
('DELETE', '/v1/testableresources/%s' % TESTABLE_RESOURCE['uuid'],
{}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertIsNone(resource)

View File

@ -20,20 +20,15 @@ from ironicclient.common import utils
from ironicclient import exc
CREATION_ATTRIBUTES = ['description', 'extra']
class Chassis(base.Resource):
def __repr__(self):
return "<Chassis %s>" % self._info
class ChassisManager(base.Manager):
class ChassisManager(base.CreateManager):
resource_class = Chassis
@staticmethod
def _path(id=None):
return '/v1/chassis/%s' % id if id else '/v1/chassis'
_resource_name = 'chassis'
_creation_attributes = ['description', 'extra']
def list(self, marker=None, limit=None, sort_key=None,
sort_dir=None, detail=False, fields=None):
@ -166,26 +161,10 @@ class ChassisManager(base.Manager):
limit=limit)
def get(self, chassis_id, fields=None):
if fields is not None:
chassis_id = '%s?fields=' % chassis_id
chassis_id += ','.join(fields)
try:
return self._list(self._path(chassis_id))[0]
except IndexError:
return None
def create(self, **kwargs):
new = {}
for (key, value) in kwargs.items():
if key in CREATION_ATTRIBUTES:
new[key] = value
else:
raise exc.InvalidAttribute()
return self._create(self._path(), new)
return self._get(resource_id=chassis_id, fields=fields)
def delete(self, chassis_id):
return self._delete(self._path(chassis_id))
return self._delete(resource_id=chassis_id)
def update(self, chassis_id, patch):
return self._update(self._path(chassis_id), patch)
return self._update(resource_id=chassis_id, patch=patch)

View File

@ -25,37 +25,29 @@ class Driver(base.Resource):
class DriverManager(base.Manager):
resource_class = Driver
_resource_name = 'drivers'
def list(self):
return self._list('/v1/drivers', "drivers")
return self._list('/v1/drivers', self._resource_name)
def get(self, driver_name):
try:
return self._list('/v1/drivers/%s' % driver_name)[0]
except IndexError:
return None
return self._get(resource_id=driver_name)
def update(self, driver_name, patch, http_method='PATCH'):
path = '/v1/drivers/%s' % driver_name
return self._update(path, patch, method=http_method)
return self._update(resource_id=driver_name, patch=patch,
method=http_method)
def delete(self, driver_name):
return self._delete('/v1/drivers/%s' % driver_name)
return self._delete(resource_id=driver_name)
def properties(self, driver_name):
try:
info = self._list('/v1/drivers/%s/properties' % driver_name)[0]
if info:
return info.to_dict()
return {}
except IndexError:
return {}
return self._get(resource_id='%s/properties' % driver_name).to_dict()
def vendor_passthru(self, driver_name, method, args=None,
http_method=None):
"""Issue requests for vendor-specific actions on a given driver.
:param driver_name: Name of the driver.
:param driver_name: The name of the driver.
:param method: Name of the vendor method.
:param args: Optional. The arguments to be passed to the method.
:param http_method: The HTTP method to use on the request.

View File

@ -21,9 +21,6 @@ from ironicclient.common.i18n import _
from ironicclient.common import utils
from ironicclient import exc
CREATION_ATTRIBUTES = ['chassis_uuid', 'driver', 'driver_info', 'extra',
'uuid', 'properties', 'name']
_power_states = {
'on': 'power on',
@ -37,12 +34,11 @@ class Node(base.Resource):
return "<Node %s>" % self._info
class NodeManager(base.Manager):
class NodeManager(base.CreateManager):
resource_class = Node
@staticmethod
def _path(id=None):
return '/v1/nodes/%s' % id if id else '/v1/nodes'
_creation_attributes = ['chassis_uuid', 'driver', 'driver_info',
'extra', 'uuid', 'properties', 'name']
_resource_name = 'nodes'
def list(self, associated=None, maintenance=None, marker=None, limit=None,
detail=False, sort_key=None, sort_dir=None, fields=None,
@ -171,14 +167,7 @@ class NodeManager(base.Manager):
limit=limit)
def get(self, node_id, fields=None):
if fields is not None:
node_id = '%s?fields=' % node_id
node_id += ','.join(fields)
try:
return self._list(self._path(node_id))[0]
except IndexError:
return None
return self._get(resource_id=node_id, fields=fields)
def get_by_instance_uuid(self, instance_uuid, fields=None):
path = '?instance_uuid=%s' % instance_uuid
@ -196,22 +185,15 @@ class NodeManager(base.Manager):
else:
raise exc.NotFound()
def create(self, **kwargs):
new = {}
for (key, value) in kwargs.items():
if key in CREATION_ATTRIBUTES:
new[key] = value
else:
raise exc.InvalidAttribute()
return self._create(self._path(), new)
def delete(self, node_id):
return self._delete(self._path(node_id))
return self._delete(resource_id=node_id)
def update(self, node_id, patch, http_method='PATCH'):
return self._update(self._path(node_id), patch, method=http_method)
return self._update(resource_id=node_id, patch=patch,
method=http_method)
def vendor_passthru(self, node_id, method, args=None, http_method=None):
def vendor_passthru(self, node_id, method, args=None,
http_method=None):
"""Issue requests for vendor-specific actions on a given node.
:param node_id: The UUID of the node.
@ -219,7 +201,6 @@ class NodeManager(base.Manager):
:param args: Optional. The arguments to be passed to the method.
:param http_method: The HTTP method to use on the request.
Defaults to POST.
"""
if args is None:
args = {}
@ -265,14 +246,14 @@ class NodeManager(base.Manager):
path = "%s/maintenance" % node_id
if maintenance_mode:
reason = {'reason': maint_reason}
return self._update(self._path(path), reason, method='PUT')
return self.update(path, reason, http_method='PUT')
else:
return self._delete(self._path(path))
return self.delete(path)
def set_power_state(self, node_id, state):
path = "%s/states/power" % node_id
target = {'target': _power_states.get(state, state)}
return self._update(self._path(path), target, method='PUT')
return self.update(path, target, http_method='PUT')
def validate(self, node_uuid):
path = "%s/validate" % node_uuid
@ -289,7 +270,7 @@ class NodeManager(base.Manager):
configdrive = utils.make_configdrive(configdrive)
body['configdrive'] = configdrive
return self._update(self._path(path), body, method='PUT')
return self.update(path, body, http_method='PUT')
def states(self, node_uuid):
path = "%s/states" % node_uuid
@ -312,12 +293,12 @@ class NodeManager(base.Manager):
"""
path = "%s/states/console" % node_uuid
target = {'enabled': enabled}
return self._update(self._path(path), target, method='PUT')
return self.update(path, target, http_method='PUT')
def set_boot_device(self, node_uuid, boot_device, persistent=False):
path = "%s/management/boot_device" % node_uuid
target = {'boot_device': boot_device, 'persistent': persistent}
return self._update(self._path(path), target, method='PUT')
return self.update(path, target, http_method='PUT')
def get_boot_device(self, node_uuid):
path = "%s/management/boot_device" % node_uuid

View File

@ -19,20 +19,16 @@ from ironicclient.common.i18n import _
from ironicclient.common import utils
from ironicclient import exc
CREATION_ATTRIBUTES = ['address', 'extra', 'node_uuid']
class Port(base.Resource):
def __repr__(self):
return "<Port %s>" % self._info
class PortManager(base.Manager):
class PortManager(base.CreateManager):
resource_class = Port
@staticmethod
def _path(id=None):
return '/v1/ports/%s' % id if id else '/v1/ports'
_creation_attributes = ['address', 'extra', 'node_uuid']
_resource_name = 'ports'
def list(self, address=None, limit=None, marker=None, sort_key=None,
sort_dir=None, detail=False, fields=None):
@ -92,14 +88,7 @@ class PortManager(base.Manager):
limit=limit)
def get(self, port_id, fields=None):
if fields is not None:
port_id = '%s?fields=' % port_id
port_id += ','.join(fields)
try:
return self._list(self._path(port_id))[0]
except IndexError:
return None
return self._get(resource_id=port_id, fields=fields)
def get_by_address(self, address, fields=None):
path = '?address=%s' % address
@ -116,17 +105,8 @@ class PortManager(base.Manager):
else:
raise exc.NotFound()
def create(self, **kwargs):
new = {}
for (key, value) in kwargs.items():
if key in CREATION_ATTRIBUTES:
new[key] = value
else:
raise exc.InvalidAttribute()
return self._create(self._path(), new)
def delete(self, port_id):
return self._delete(self._path(port_id))
return self._delete(resource_id=port_id)
def update(self, port_id, patch):
return self._update(self._path(port_id), patch)
return self._update(resource_id=port_id, patch=patch)