Modifying CRD return type from dict to object

- Adding warlock library and json-schema dependency
- Adding resources directory which contains cluster object schema
- Modifying unit test cases
- Changing doc content

Change-Id: I54e4ee316a8cac45ce75ea5d528211c96ef83e25
This commit is contained in:
Abitha Palaniappan 2015-03-11 10:18:55 -07:00
parent 8c47aa654b
commit b3024868c3
8 changed files with 253 additions and 15 deletions

View File

@ -0,0 +1,64 @@
{
"$schema": "http://json-schema.org/draft-04/schema",
"name": "Cluster",
"title": "Cluster",
"type" : "object",
"properties": {
"id": {
"type": "string",
"description": "Cluster Identifier",
"pattern": "^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}$"
},
"name": {
"type": "string",
"description": "Cluster name",
"required": true
},
"network_id": {
"type": "string",
"description": "Network Identifier",
"required": true
},
"status": {
"type": "string",
"description": "Cluster status"
},
"created_at": {
"type": "string",
"description": "Date and time of cluster creation",
"format": "date-time"
},
"updated_at": {
"type": "string",
"description": "Date and time of cluster update",
"format": "date-time"
},
"end_points": {
"type": [
"array",
"null"
],
"description": "Cluster endpoints"
},
"flavor": {
"type": "string",
"description": "Cluster flavor",
"required": true
},
"size": {
"type": "integer",
"description": "Cluster size",
"required": true
},
"volume_size": {
"type": [
"integer",
"null"
],
"description": "Cluster volume"
}
},
"additionalProperties": false
}

View File

@ -57,8 +57,7 @@ class TestCreateCluster(base.TestCueBase):
('flavor', self.cluster_flavor),
('size', self.cluster_size)
]
response = {"cluster": {
"id": "222",
response = {"id": "222",
"project_id": "test",
"network_id": self.cluster_network_id,
"name": self.cluster_name,
@ -69,7 +68,7 @@ class TestCreateCluster(base.TestCueBase):
"deleted": "0",
"created_at": "2015-02-04 00:35:02",
"updated_at": "2015-02-04 00:35:02",
"deleted_at": ""}}
"deleted_at": ""}
mocker = mock.Mock(return_value=response)
self.app.client_manager.mq.clusters.create = mocker
@ -162,7 +161,7 @@ class TestShowCluster(base.TestCueBase):
"""test show cluster with correct cluster id"""
cluster_id = 'e531f2b3-3d97-42c0-b3b5-b7b6ab532018'
response = {"cluster": {
response = {
"id": cluster_id,
"project_id": "test",
"network_id": "26477575",
@ -170,7 +169,7 @@ class TestShowCluster(base.TestCueBase):
"status": "BUILDING",
"flavor": "1",
"size": "2",
"volume_size": "1024"}
"volume_size": "1024"
}
arglist = [cluster_id]

View File

@ -12,8 +12,11 @@
# License for the specific language governing permissions and limitations
# under the License.
#
import json
import os
import pkg_resources
def env(*vars, **kwargs):
"""Search for the first defined of possibly many env vars
@ -50,3 +53,32 @@ def get_item_properties(item, fields, mixed_case_fields=[], formatters={}):
else:
row.append(data)
return tuple(row)
def resource_string(*args, **kwargs):
"""Return specified resource as a string"""
if len(args) == 0:
raise ValueError()
package = kwargs.pop('package', None)
if not package:
package = 'cueclient'
resource_path = os.path.join('resources', *args)
if not pkg_resources.resource_exists(package, resource_path):
# TODO(ap): add exceptions
# raise exceptions.ResourceNotFound('Could not find the requested '
# 'resource: %s' % resource_path)
pass
return pkg_resources.resource_string(package, resource_path)
def load_schema(version, name, package=None):
"""Load json schema from resources"""
schema_string = resource_string('schemas', version, '%s.json' % name,
package=package)
return json.loads(schema_string)

View File

@ -59,11 +59,11 @@ class ShowClusterCommand(show.ShowOne):
data = client.clusters.get(parsed_args.id)
return zip(*sorted(six.iteritems(data['cluster'])))
return zip(*sorted(six.iteritems(data)))
class CreateClusterCommand(show.ShowOne):
"""Create Domain"""
"""Create Cluster"""
def get_parser(self, prog_name):
parser = super(CreateClusterCommand, self).get_parser(prog_name)
@ -87,7 +87,7 @@ class CreateClusterCommand(show.ShowOne):
size=parsed_args.size,
volume_size=parsed_args.volume_size)
return zip(*sorted(six.iteritems(data['cluster'])))
return zip(*sorted(six.iteritems(data)))
class SetClusterCommand(command.Command):

View File

@ -14,10 +14,16 @@
# License for the specific language governing permissions and limitations
# under the License.
from cueclient import controller
from cueclient import utils
from cueclient import warlock
Cluster = warlock.model_factory(utils.load_schema('v1', 'cluster'))
class ClusterController(controller.Controller):
"""Cluster Controller to manages operations."""
def create(self, name, nic, flavor, size, volume_size):
"""Create Cluster"""
data = {
"network_id": nic,
"name": name,
@ -25,20 +31,22 @@ class ClusterController(controller.Controller):
"size": size,
"volume_size": volume_size
}
url = self.build_url("/clusters")
return self._post(url, json=data)
return Cluster(self._post(url, json=data)['cluster'])
def list(self, marker=None, limit=None, params=None):
"""List Clusters"""
url = self.build_url("/clusters", marker, limit, params)
return self._get(url, "clusters")
response = self._get(url, "clusters")
return [Cluster(i) for i in response]
def get(self, cluster_id):
"""Show Cluster"""
url = self.build_url("/clusters/%s" % cluster_id)
return self._get(url)
return Cluster(self._get(url)['cluster'])
def update(self, cluster_id, values):
data = {
@ -50,6 +58,7 @@ class ClusterController(controller.Controller):
return self._patch(url, data=data)
def delete(self, cluster_id):
"""Delete Cluster"""
url = self.build_url("/clusters/%s" % cluster_id)
return self._delete(url)

134
cueclient/warlock.py Normal file
View File

@ -0,0 +1,134 @@
# Copyright 2012 Brian Waldon
#
# 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.
#
# Code copied from Warlock, as warlock depends on jsonschema==0.2
# Hopefully we can upstream the changes ASAP.
#
import copy
import logging
import jsonschema
LOG = logging.getLogger(__name__)
class InvalidOperation(RuntimeError):
pass
class ValidationError(ValueError):
pass
def model_factory(schema):
"""Generate a model class based on the provided JSON Schema
:param schema: dict representing valid JSON schema
"""
schema = copy.deepcopy(schema)
def validator(obj):
"""Apply a JSON schema to an object"""
try:
jsonschema.validate(obj, schema, cls=jsonschema.Draft3Validator)
except jsonschema.ValidationError as e:
raise ValidationError(str(e))
class Model(dict):
"""Self-validating model for arbitrary objects"""
def __init__(self, *args, **kwargs):
d = dict(*args, **kwargs)
# we overload setattr so set this manually
self.__dict__['validator'] = validator
try:
self.validator(d)
except ValidationError as e:
raise ValueError('Validation Error: %s' % str(e))
else:
dict.__init__(self, d)
self.__dict__['changes'] = {}
def __getattr__(self, key):
try:
return self.__getitem__(key)
except KeyError:
raise AttributeError(key)
def __setitem__(self, key, value):
mutation = dict(self.items())
mutation[key] = value
try:
self.validator(mutation)
except ValidationError as e:
raise InvalidOperation(str(e))
dict.__setitem__(self, key, value)
self.__dict__['changes'][key] = value
def __setattr__(self, key, value):
self.__setitem__(key, value)
def clear(self):
raise InvalidOperation()
def pop(self, key, default=None):
raise InvalidOperation()
def popitem(self):
raise InvalidOperation()
def __delitem__(self, key):
raise InvalidOperation()
# NOTE(termie): This is kind of the opposite of what copy usually does
def copy(self):
return copy.deepcopy(dict(self))
def update(self, other):
# NOTE(kiall): It seems update() doesn't update the
# self.__dict__['changes'] dict correctly.
mutation = dict(self.items())
mutation.update(other)
try:
self.validator(mutation)
except ValidationError as e:
raise InvalidOperation(str(e))
dict.update(self, other)
def iteritems(self):
return copy.deepcopy(dict(self)).iteritems()
def items(self):
return copy.deepcopy(dict(self)).items()
def itervalues(self):
return copy.deepcopy(dict(self)).itervalues()
def keys(self):
return copy.deepcopy(dict(self)).keys()
def values(self):
return copy.deepcopy(dict(self)).values()
@property
def changes(self):
return copy.deepcopy(self.__dict__['changes'])
Model.__name__ = str(schema['title'])
return Model

View File

@ -32,15 +32,14 @@ the bindings.
# Create an instance of the client
cue_client = client.Client(session=session)
# Cluster List
# Cluster List - returns list of cluster objects
list_response = cue_client.clusters.list()
# Iterate the list, printing some useful information
for cluster in list_response:
print "Cluster ID: %s \t Name: %s \t NetworkId: %s \t Flavor: %s \t Size: %s" % \
(cluster['id'], cluster['name'], cluster['network_id'],
cluster['flavor'], cluster['size'])
(cluster.id, cluster.name, cluster.network_id, cluster.flavor, cluster.size)
And the output this program might produce:

View File

@ -2,6 +2,7 @@
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
cliff>=1.7.0 # Apache-2.0
jsonschema>=2.0.0,<3.0.0
pbr>=0.6,!=0.7,<1.0
python-keystoneclient>=0.11.1
requests>=2.2.0,!=2.4.0