Revert "Refactoring and removing duplicate code of "base.Manager" heirs"

This reverts commit 46b279bb36.

The patch being reverted has broken the API compatibility. Until a fix
for this issue has been created, the patch should be reverted.

Change-Id: I1295c3e0d5ca228253874719befd7ed842d505fa
Closes-Bug: #1521429
This commit is contained in:
John L. Villalovos 2015-11-30 18:59:50 -08:00
parent b1bd67cc41
commit 861b1f4b8e
11 changed files with 208 additions and 288 deletions

View File

@ -17,15 +17,11 @@
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.common.i18n import _
from ironicclient import exc
def getid(obj):
@ -40,74 +36,17 @@ 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 _path(self, id=None):
"""Returns a request path for a given resource identifier.
:param id: Identifier of the resource to generate the request path.
"""
return ('/v1/%s/%s' % (self._resource_name, id) if 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 vendor_passthru(self, resource_identifier, method, args=None,
http_method='POST'):
"""Issue requests for vendor-specific actions on a given driver.
:param resource_identifier: Identifier of the resource.
: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.
Defaults to POST.
"""
if args is None:
args = {}
http_method = http_method.upper()
path = "%s/vendor_passthru/%s" % (resource_identifier, method)
if http_method in ('POST', 'PUT', 'PATCH'):
return self.update(path, args, http_method=http_method)
elif http_method == 'DELETE':
return self.delete(path)
elif http_method == 'GET':
return self.get(path)
else:
raise exc.InvalidAttribute(
_('Unknown HTTP method: %s') % http_method)
def _create(self, url, body):
resp, body = self.api.json_request('POST', url, body=body)
if body:
return self.resource_class(self, body)
def _format_body_data(self, body, response_key):
if response_key:
@ -185,57 +124,14 @@ 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, 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)
def _update(self, url, body, method='PATCH', response_key=None):
resp, body = self.api.json_request(method, url, body=body)
# PATCH/PUT requests may not return a body
if body:
return self.resource_class(self, body)
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)
def _delete(self, url):
self.api.raw_request('DELETE', url)
class Resource(base.Resource):

View File

@ -1,142 +0,0 @@
# 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')
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(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(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

@ -327,7 +327,7 @@ class ChassisManagerTest(testtools.TestCase):
self.assertTrue(chassis)
def test_delete(self):
chassis = self.mgr.delete(resource_id=CHASSIS['uuid'])
chassis = self.mgr.delete(chassis_id=CHASSIS['uuid'])
expect = [
('DELETE', '/v1/chassis/%s' % CHASSIS['uuid'], {}, None),
]
@ -338,7 +338,7 @@ class ChassisManagerTest(testtools.TestCase):
patch = {'op': 'replace',
'value': NEW_DESCR,
'path': '/description'}
chassis = self.mgr.update(resource_id=CHASSIS['uuid'], patch=patch)
chassis = self.mgr.update(chassis_id=CHASSIS['uuid'], patch=patch)
expect = [
('PATCH', '/v1/chassis/%s' % CHASSIS['uuid'], {}, patch),
]

View File

@ -106,7 +106,7 @@ class DriverManagerTest(testtools.TestCase):
# anything to verify.
vendor_passthru_args = {'arg1': 'val1'}
kwargs = {
'resource_identifier': 'driver_name',
'driver_name': 'driver_name',
'method': 'method',
'args': vendor_passthru_args
}
@ -123,7 +123,7 @@ class DriverManagerTest(testtools.TestCase):
@mock.patch.object(driver.DriverManager, 'get')
def test_vendor_passthru_get(self, get_mock):
kwargs = {
'resource_identifier': 'driver_name',
'driver_name': 'driver_name',
'method': 'method',
'http_method': 'GET',
}
@ -135,7 +135,7 @@ class DriverManagerTest(testtools.TestCase):
@mock.patch.object(driver.DriverManager, 'delete')
def test_vendor_passthru_delete(self, delete_mock):
kwargs = {
'resource_identifier': 'driver_name',
'driver_name': 'driver_name',
'method': 'method',
'http_method': 'DELETE',
}
@ -147,7 +147,7 @@ class DriverManagerTest(testtools.TestCase):
@mock.patch.object(driver.DriverManager, 'delete')
def test_vendor_passthru_unknown_http_method(self, delete_mock):
kwargs = {
'resource_identifier': 'driver_name',
'driver_name': 'driver_name',
'method': 'method',
'http_method': 'UNKNOWN',
}

View File

@ -597,7 +597,7 @@ class NodeManagerTest(testtools.TestCase):
self.assertTrue(node)
def test_delete(self):
node = self.mgr.delete(resource_id=NODE1['uuid'])
node = self.mgr.delete(node_id=NODE1['uuid'])
expect = [
('DELETE', '/v1/nodes/%s' % NODE1['uuid'], {}, None),
]
@ -608,7 +608,7 @@ class NodeManagerTest(testtools.TestCase):
patch = {'op': 'replace',
'value': NEW_DRIVER,
'path': '/driver'}
node = self.mgr.update(resource_id=NODE1['uuid'], patch=patch)
node = self.mgr.update(node_id=NODE1['uuid'], patch=patch)
expect = [
('PATCH', '/v1/nodes/%s' % NODE1['uuid'], {}, patch),
]
@ -863,7 +863,7 @@ class NodeManagerTest(testtools.TestCase):
# anything to verify.
vendor_passthru_args = {'arg1': 'val1'}
kwargs = {
'resource_identifier': 'node_uuid',
'node_id': 'node_uuid',
'method': 'method',
'args': vendor_passthru_args
}
@ -880,7 +880,7 @@ class NodeManagerTest(testtools.TestCase):
@mock.patch.object(node.NodeManager, 'get')
def test_vendor_passthru_get(self, get_mock):
kwargs = {
'resource_identifier': 'node_uuid',
'node_id': 'node_uuid',
'method': 'method',
'http_method': 'GET',
}
@ -892,7 +892,7 @@ class NodeManagerTest(testtools.TestCase):
@mock.patch.object(node.NodeManager, 'delete')
def test_vendor_passthru_delete(self, delete_mock):
kwargs = {
'resource_identifier': 'node_uuid',
'node_id': 'node_uuid',
'method': 'method',
'http_method': 'DELETE',
}
@ -904,7 +904,7 @@ class NodeManagerTest(testtools.TestCase):
@mock.patch.object(node.NodeManager, 'delete')
def test_vendor_passthru_unknown_http_method(self, delete_mock):
kwargs = {
'resource_identifier': 'node_uuid',
'node_id': 'node_uuid',
'method': 'method',
'http_method': 'UNKNOWN',
}

View File

@ -291,7 +291,7 @@ class PortManagerTest(testtools.TestCase):
self.assertTrue(port)
def test_delete(self):
port = self.mgr.delete(resource_id=PORT['uuid'])
port = self.mgr.delete(port_id=PORT['uuid'])
expect = [
('DELETE', '/v1/ports/%s' % PORT['uuid'], {}, None),
]
@ -302,7 +302,7 @@ class PortManagerTest(testtools.TestCase):
patch = {'op': 'replace',
'value': NEW_ADDR,
'path': '/address'}
port = self.mgr.update(resource_id=PORT['uuid'], patch=patch)
port = self.mgr.update(port_id=PORT['uuid'], patch=patch)
expect = [
('PATCH', '/v1/ports/%s' % PORT['uuid'], {}, patch),
]

View File

@ -20,15 +20,20 @@ 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.CreateManager):
class ChassisManager(base.Manager):
resource_class = Chassis
_resource_name = 'chassis'
_creation_attributes = ['description', 'extra']
@staticmethod
def _path(id=None):
return '/v1/chassis/%s' % id if id else '/v1/chassis'
def list(self, marker=None, limit=None, sort_key=None,
sort_dir=None, detail=False, fields=None):
@ -159,3 +164,28 @@ class ChassisManager(base.CreateManager):
else:
return self._list_pagination(self._path(path), "nodes",
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)
def delete(self, chassis_id):
return self._delete(self._path(chassis_id))
def update(self, chassis_id, patch):
return self._update(self._path(chassis_id), patch)

View File

@ -14,6 +14,8 @@
# under the License.
from ironicclient.common import base
from ironicclient.common.i18n import _
from ironicclient import exc
class Driver(base.Resource):
@ -23,13 +25,60 @@ class Driver(base.Resource):
class DriverManager(base.Manager):
resource_class = Driver
_resource_name = 'drivers'
def list(self):
return self._list('/v1/drivers', self._resource_name)
return self._list('/v1/drivers', "drivers")
def get(self, driver_name):
try:
return self._list('/v1/drivers/%s' % driver_name)[0]
except IndexError:
return None
def update(self, driver_name, patch, http_method='PATCH'):
path = '/v1/drivers/%s' % driver_name
return self._update(path, patch, method=http_method)
def delete(self, driver_name):
return self._delete('/v1/drivers/%s' % driver_name)
def properties(self, driver_name):
return self.get('%s/properties' % driver_name).to_dict()
try:
info = self._list('/v1/drivers/%s/properties' % driver_name)[0]
if info:
return info.to_dict()
return {}
except IndexError:
return {}
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 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.
Defaults to POST.
"""
if args is None:
args = {}
if http_method is None:
http_method = 'POST'
http_method = http_method.upper()
path = "%s/vendor_passthru/%s" % (driver_name, method)
if http_method in ('POST', 'PUT', 'PATCH'):
return self.update(path, args, http_method=http_method)
elif http_method == 'DELETE':
return self.delete(path)
elif http_method == 'GET':
return self.get(path)
else:
raise exc.InvalidAttribute(
_('Unknown HTTP method: %s') % http_method)
def get_vendor_passthru_methods(self, driver_name):
path = "%s/vendor_passthru/methods" % driver_name

View File

@ -21,6 +21,9 @@ 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',
@ -34,11 +37,12 @@ class Node(base.Resource):
return "<Node %s>" % self._info
class NodeManager(base.CreateManager):
class NodeManager(base.Manager):
resource_class = Node
_creation_attributes = ['chassis_uuid', 'driver', 'driver_info',
'extra', 'uuid', 'properties', 'name']
_resource_name = 'nodes'
@staticmethod
def _path(id=None):
return '/v1/nodes/%s' % id if id else '/v1/nodes'
def list(self, associated=None, maintenance=None, marker=None, limit=None,
detail=False, sort_key=None, sort_dir=None, fields=None,
@ -166,6 +170,16 @@ class NodeManager(base.CreateManager):
return self._list_pagination(self._path(path), "ports",
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
def get_by_instance_uuid(self, instance_uuid, fields=None):
path = '?instance_uuid=%s' % instance_uuid
if fields is not None:
@ -182,6 +196,50 @@ class NodeManager(base.CreateManager):
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))
def update(self, node_id, patch, http_method='PATCH'):
return self._update(self._path(node_id), patch, method=http_method)
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.
: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.
Defaults to POST.
"""
if args is None:
args = {}
if http_method is None:
http_method = 'POST'
http_method = http_method.upper()
path = "%s/vendor_passthru/%s" % (node_id, method)
if http_method in ('POST', 'PUT', 'PATCH'):
return self.update(path, args, http_method=http_method)
elif http_method == 'DELETE':
return self.delete(path)
elif http_method == 'GET':
return self.get(path)
else:
raise exc.InvalidAttribute(
_('Unknown HTTP method: %s') % http_method)
def set_maintenance(self, node_id, state, maint_reason=None):
"""Set the maintenance mode for the node.
@ -207,14 +265,14 @@ class NodeManager(base.CreateManager):
path = "%s/maintenance" % node_id
if maintenance_mode:
reason = {'reason': maint_reason}
return self.update(path, reason, method='PUT')
return self._update(self._path(path), reason, method='PUT')
else:
return self.delete(path)
return self._delete(self._path(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(path, target, method='PUT')
return self._update(self._path(path), target, method='PUT')
def validate(self, node_uuid):
path = "%s/validate" % node_uuid
@ -231,7 +289,7 @@ class NodeManager(base.CreateManager):
configdrive = utils.make_configdrive(configdrive)
body['configdrive'] = configdrive
return self.update(path, body, method='PUT')
return self._update(self._path(path), body, method='PUT')
def states(self, node_uuid):
path = "%s/states" % node_uuid
@ -254,12 +312,12 @@ class NodeManager(base.CreateManager):
"""
path = "%s/states/console" % node_uuid
target = {'enabled': enabled}
return self.update(path, target, method='PUT')
return self._update(self._path(path), target, 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(path, target, method='PUT')
return self._update(self._path(path), target, method='PUT')
def get_boot_device(self, node_uuid):
path = "%s/management/boot_device" % node_uuid

View File

@ -19,16 +19,20 @@ 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.CreateManager):
class PortManager(base.Manager):
resource_class = Port
_creation_attributes = ['address', 'extra', 'node_uuid']
_resource_name = 'ports'
@staticmethod
def _path(id=None):
return '/v1/ports/%s' % id if id else '/v1/ports'
def list(self, address=None, limit=None, marker=None, sort_key=None,
sort_dir=None, detail=False, fields=None):
@ -87,6 +91,16 @@ class PortManager(base.CreateManager):
return self._list_pagination(self._path(path), "ports",
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
def get_by_address(self, address, fields=None):
path = '?address=%s' % address
if fields is not None:
@ -101,3 +115,18 @@ class PortManager(base.CreateManager):
return ports[0]
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))
def update(self, port_id, patch):
return self._update(self._path(port_id), patch)