API Validation for Trove API

Replaced validation with new and improved json schema validation
    Fixed malformed json schema bug #1177969
    Fixed integration test for create_user; correct databases
    Fixed integration for create_users; invalid character_set param

Implements: json-schema-support

Change-Id: I6ca09803654d9e78362fde69185b5b9e05a5eb6b
This commit is contained in:
justin-hopper 2013-06-18 12:38:38 -07:00
parent eafc62f61b
commit ccdf59f21e
22 changed files with 1170 additions and 266 deletions

View File

@ -18,3 +18,4 @@ python-keystoneclient
python-swiftclient
iso8601
oslo.config>=1.1.0
jsonschema>=1.0.0,!=1.4.0,<2

View File

@ -22,6 +22,7 @@ from trove.common import exception
from trove.common import cfg
from trove.openstack.common import log as logging
from trove.openstack.common.gettextutils import _
import trove.common.apischema as apischema
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
@ -31,6 +32,7 @@ class BackupController(wsgi.Controller):
"""
Controller for accessing backups in the OpenStack API.
"""
schemas = apischema.backup
def index(self, req, tenant_id):
"""
@ -51,7 +53,6 @@ class BackupController(wsgi.Controller):
def create(self, req, body, tenant_id):
LOG.debug("Creating a Backup for tenant '%s'" % tenant_id)
self._validate_create_body(body)
context = req.environ[wsgi.CONTEXT_KEY]
data = body['backup']
instance = data['instance']
@ -65,14 +66,3 @@ class BackupController(wsgi.Controller):
context = req.environ[wsgi.CONTEXT_KEY]
Backup.delete(context, id)
return wsgi.Result(None, 202)
def _validate_create_body(self, body):
try:
body['backup']
body['backup']['name']
body['backup']['instance']
except KeyError as e:
LOG.error(_("Create Backup Required field(s) "
"- %s") % e)
raise exception.TroveError(
"Required element/key - %s was not specified" % e)

312
trove/common/apischema.py Normal file
View File

@ -0,0 +1,312 @@
# Copyright 2013 Hewlett-Packard Development Company, L.P.
# 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.
#
flavorref = {
'oneOf': [
{
"type": "string",
"minLength": 8,
"pattern": 'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]'
'|(?:%[0-9a-fA-F][0-9a-fA-F]))+'
},
{
"type": "string",
"maxLength": 5,
"pattern": "[0-9]+"
},
{
"type": "integer"
}]
}
volume_size = {
"oneOf": [
{
"type": "integer",
"minimum": 0
},
{
"type": "string",
"minLength": 1,
"pattern": "[0-9]+"
}]
}
non_empty_string = {
"type": "string",
"minLength": 1,
"maxLength": 255,
"pattern": "^.*[0-9a-zA-Z]+.*$"
}
uuid = {
"type": "string",
"minLength": 1,
"maxLength": 64,
"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}$"
}
volume = {
"type": "object",
"required": ["size"],
"properties": {
"size": volume_size,
"required": True
}
}
databases_ref_list = {
"type": "array",
"minItems": 0,
"uniqueItems": True,
"items": {
"type": "object",
"required": ["name"],
"additionalProperties": False,
"properties": {
"name": non_empty_string
}
}
}
databases_ref_list_required = {
"type": "array",
"minItems": 1,
"uniqueItems": True,
"items": {
"type": "object",
"required": ["name"],
"additionalProperties": False,
"properties": {
"name": non_empty_string
}
}
}
databases_ref = {
"type": "object",
"required": ["databases"],
"additionalProperties": False,
"properties": {
"databases": databases_ref_list_required
}
}
databases_def = {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["name"],
"additionalProperties": False,
"properties": {
"name": non_empty_string,
"character_set": non_empty_string,
"collate": non_empty_string
}
}
}
users_list = {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["name", "password"],
"additionalProperties": False,
"properties": {
"name": non_empty_string,
"password": non_empty_string,
"host": non_empty_string,
"databases": databases_ref_list
}
}
}
instance = {
"create": {
"type": "object",
"required": ["instance"],
"additionalProperties": False,
"properties": {
"instance": {
"type": "object",
"required": ["name", "flavorRef", "volume"],
"additionalProperties": False,
"properties": {
"name": non_empty_string,
"flavorRef": flavorref,
"volume": volume,
"databases": databases_def,
"users": users_list,
"restorePoint": {
"type": "object",
"required": ["backupRef"],
"additionalProperties": False,
"properties": {
"backupRef": uuid
}
}
}
}
}
},
"action": {
"resize": {
"volume": {
"type": "object",
"required": ["resize"],
"additionalProperties": False,
"properties": {
"resize": {
"type": "object",
"required": ["volume"],
"additionalProperties": False,
"properties": {
"volume": volume
}
}
}
},
'flavorRef': {
"type": "object",
"required": ["resize"],
"additionalProperties": False,
"properties": {
"resize": {
"type": "object",
"required": ["flavorRef"],
"additionalProperties": False,
"properties": {
"flavorRef": flavorref
}
}
}
}
},
"restart": {
"type": "object",
"required": ["restart"],
"additionalProperties": False,
"properties": {
"restart": {
"type": "object"
}
}
}
}
}
mgmt_instance = {
"action": {
'migrate': {
"type": "object",
"required": ["migrate"],
"additionalProperties": False,
"properties": {
"migrate": {
"type": "object"
}
}
},
"reboot": {
"type": "object",
"required": ["reboot"],
"additionalProperties": False,
"properties": {
"reboot": {
"type": "object"
}
}
},
"stop": {
"type": "object",
"required": ["stop"],
"additionalProperties": False,
"properties": {
"stop": {
"type": "object"
}
}
}
}
}
user = {
"create": {
"name": "users:create",
"type": "object",
"required": ["users"],
"properties": {
"users": users_list
}
},
"update": {
"users": {
"type": "object",
"required": ["users"],
"additionalProperties": False,
"properties": {
"users": users_list
}
},
"databases": databases_ref
}
}
dbschema = {
"create": {
"type": "object",
"required": ["databases"],
"additionalProperties": False,
"properties": {
"databases": databases_def
}
}
}
backup = {
"create": {
"name": "backup:create",
"type": "object",
"required": ["backup"],
"properties": {
"backup": {
"type": "object",
"required": ["instance", "name"],
"properties": {
"description": non_empty_string,
"instance": uuid,
"name": non_empty_string
}
}
}
}
}
account = {
'create': {
"type": "object",
"name": "users",
"required": ["users"],
"additionalProperties": False,
"properties": {
"users": users_list
}
}
}

View File

@ -18,6 +18,7 @@
import eventlet.wsgi
import math
import jsonschema
import paste.urlmap
import re
import time
@ -27,7 +28,6 @@ import webob
import webob.dec
import webob.exc
from lxml import etree
from paste import deploy
from xml.dom import minidom
from trove.common import context as rd_context
@ -156,11 +156,11 @@ def serializers(**serializers):
func.wsgi_serializers = {}
func.wsgi_serializers.update(serializers)
return func
return decorator
class TroveMiddleware(Middleware):
# Note: taken from nova
@classmethod
def factory(cls, global_config, **local_config):
@ -185,13 +185,14 @@ class TroveMiddleware(Middleware):
but using the kwarg passing it shouldn't be necessary.
"""
def _factory(app):
return cls(app, **local_config)
return _factory
class VersionedURLMap(object):
def __init__(self, urlmap):
self.urlmap = urlmap
@ -208,7 +209,6 @@ class VersionedURLMap(object):
class Router(openstack_wsgi.Router):
# Original router did not allow for serialization of the 404 error.
# To fix this the _dispatch was modified to use Fault() objects.
@staticmethod
@ -228,7 +228,6 @@ class Router(openstack_wsgi.Router):
class Request(openstack_wsgi.Request):
@property
def params(self):
return utils.stringify_keys(super(Request, self).params)
@ -303,7 +302,6 @@ class Result(object):
class Resource(openstack_wsgi.Resource):
def __init__(self, controller, deserializer, serializer,
exception_map=None):
exception_map = exception_map or {}
@ -318,6 +316,7 @@ class Resource(openstack_wsgi.Resource):
if getattr(self.controller, action, None) is None:
return Fault(webob.exc.HTTPNotFound())
try:
self.controller.validate_request(action, action_args)
result = super(Resource, self).execute_action(
action,
request,
@ -382,8 +381,6 @@ class Resource(openstack_wsgi.Resource):
class Controller(object):
"""Base controller that creates a Resource with default serializers."""
exclude_attr = []
exception_map = {
webob.exc.HTTPUnprocessableEntity: [
exception.UnprocessableEntity,
@ -428,6 +425,38 @@ class Controller(object):
],
}
schemas = {}
@classmethod
def get_schema(cls, action, body):
LOG.debug("Getting schema for %s:%s" %
(cls.__class__.__name__, action))
if cls.schemas:
matching_schema = cls.schemas.get(action, {})
if matching_schema:
LOG.debug("Found Schema: %s" % matching_schema.get("name",
"none"))
return matching_schema
def validate_request(self, action, action_args):
body = action_args.get('body', {})
schema = self.get_schema(action, body)
if schema:
validator = jsonschema.Draft4Validator(schema)
if not validator.is_valid(body):
errors = sorted(validator.iter_errors(body),
key=lambda e: e.path)
messages = []
for error in errors:
messages.append(error.message)
for suberror in sorted(error.context,
key=lambda e: e.schema_path):
messages.append(suberror.message)
error_msg = "; ".join(messages)
LOG.info("Validation failed: %s" % error_msg)
raise exception.BadRequest(
message="Validation error: %s" % error_msg)
def create_resource(self):
serializer = TroveResponseSerializer(
body_serializers={'application/xml': TroveXMLDictSerializer()})
@ -441,12 +470,6 @@ class Controller(object):
return dict([(key, params[key]) for key in params.keys()
if key in ["limit", "marker"]])
def _extract_required_params(self, params, model_name):
params = params or {}
model_params = params.get(model_name, {})
return utils.stringify_keys(utils.exclude(model_params,
*self.exclude_attr))
class TroveRequestDeserializer(RequestDeserializer):
"""Break up a Request object into more useful pieces."""
@ -462,14 +485,12 @@ class TroveRequestDeserializer(RequestDeserializer):
class TroveXMLDeserializer(XMLDeserializer):
def __init__(self, metadata=None):
"""
:param metadata: information needed to deserialize xml into
a dictionary.
"""
if metadata is None:
metadata = {}
metadata = metadata or {}
metadata['plurals'] = CUSTOM_PLURALS_METADATA
super(TroveXMLDeserializer, self).__init__(metadata)
@ -478,11 +499,37 @@ class TroveXMLDeserializer(XMLDeserializer):
# hub-cap: This feels wrong but minidom keeps the newlines
# and spaces as childNodes which is expected behavior.
return {'body': self._from_xml(re.sub(r'((?<=>)\s+)*\n*(\s+(?=<))*',
'', datastring))}
'', datastring))}
def _from_xml_node(self, node, listnames):
"""Convert a minidom node to a simple Python type.
Overridden from openstack deserializer to skip xmlns attributes and
remove certain unicode characters
:param listnames: list of XML node names whose subnodes should
be considered list items.
"""
if len(node.childNodes) == 1 and node.childNodes[0].nodeType == 3:
return node.childNodes[0].nodeValue
elif node.nodeName in listnames:
return [self._from_xml_node(n, listnames) for n in node.childNodes]
else:
result = dict()
for attr in node.attributes.keys():
if attr == 'xmlns':
continue
result[attr] = node.attributes[attr].nodeValue
for child in node.childNodes:
if child.nodeType != node.TEXT_NODE:
result[child.nodeName] = self._from_xml_node(child,
listnames)
return result
class TroveXMLDictSerializer(openstack_wsgi.XMLDictSerializer):
def __init__(self, metadata=None, xmlns=None):
super(TroveXMLDictSerializer, self).__init__(metadata, XMLNS)
@ -526,7 +573,6 @@ class TroveXMLDictSerializer(openstack_wsgi.XMLDictSerializer):
class TroveResponseSerializer(openstack_wsgi.ResponseSerializer):
def serialize_body(self, response, data, content_type, action):
"""Overrides body serialization in openstack_wsgi.ResponseSerializer.
@ -587,7 +633,7 @@ class Fault(webob.exc.HTTPException):
name = exc.__class__.__name__
if name in named_exceptions:
return named_exceptions[name]
# If the exception isn't in our list, at least strip off the
# If the exception isn't in our list, at least strip off the
# HTTP from the name, and then drop the case on the first letter.
name = name.split("HTTP").pop()
name = name[:1].lower() + name[1:]
@ -623,14 +669,13 @@ class Fault(webob.exc.HTTPException):
class ContextMiddleware(openstack_wsgi.Middleware):
def __init__(self, application):
self.admin_roles = CONF.admin_roles
super(ContextMiddleware, self).__init__(application)
def _extract_limits(self, params):
return dict([(key, params[key]) for key in params.keys()
if key in ["limit", "marker"]])
if key in ["limit", "marker"]])
def process_request(self, request):
tenant_id = request.headers.get('X-Tenant-Id', None)
@ -657,6 +702,7 @@ class ContextMiddleware(openstack_wsgi.Middleware):
LOG.debug(_("Created context middleware with config: %s") %
local_config)
return cls(app)
return _factory
@ -764,7 +810,6 @@ class JSONDictSerializer(DictSerializer):
class XMLDictSerializer(DictSerializer):
def __init__(self, metadata=None, xmlns=None):
"""
:param metadata: information needed to deserialize xml into

View File

@ -26,12 +26,14 @@ from trove.extensions.account import models
from trove.extensions.account import views
from trove.instance.models import DBInstance
from trove.openstack.common.gettextutils import _
import trove.common.apischema as apischema
LOG = logging.getLogger(__name__)
class AccountController(wsgi.Controller):
"""Controller for account functionality"""
schemas = apischema.account
@admin_context
def show(self, req, tenant_id, id):

View File

@ -30,6 +30,7 @@ from trove.extensions.mysql import models as mysql_models
from trove.instance.service import InstanceController
from trove.openstack.common import log as logging
from trove.openstack.common.gettextutils import _
import trove.common.apischema as apischema
LOG = logging.getLogger(__name__)
@ -37,6 +38,12 @@ LOG = logging.getLogger(__name__)
class MgmtInstanceController(InstanceController):
"""Controller for instance functionality"""
schemas = apischema.mgmt_instance
@classmethod
def get_action_schema(cls, body, action_schema):
action_type = body.keys()[0]
return action_schema.get(action_type, {})
@admin_context
def index(self, req, tenant_id, detailed=False):

View File

@ -28,8 +28,8 @@ from trove.extensions.mysql import views
from trove.guestagent.db import models as guest_models
from trove.openstack.common import log as logging
from trove.openstack.common.gettextutils import _
import trove.common.apischema as apischema
from urllib import unquote
LOG = logging.getLogger(__name__)
@ -58,19 +58,15 @@ class RootController(wsgi.Controller):
class UserController(wsgi.Controller):
"""Controller for instance functionality"""
schemas = apischema.user
@classmethod
def validate(cls, body):
"""Validate that the request has all the required parameters"""
if not body:
raise exception.BadRequest("The request contains an empty body")
if body.get('users') is None:
raise exception.MissingKey(key='users')
for user in body.get('users'):
if not user.get('name'):
raise exception.MissingKey(key='name')
if not user.get('password'):
raise exception.MissingKey(key='password')
def get_schema(cls, action, body):
action_schema = super(UserController, cls).get_schema(action, body)
if 'update' == action:
update_type = body.keys()[0]
action_schema = action_schema.get(update_type, {})
return action_schema
def index(self, req, tenant_id, instance_id):
"""Return all users."""
@ -89,7 +85,6 @@ class UserController(wsgi.Controller):
LOG.info(_("req : '%s'\n\n") % req)
LOG.info(_("body : '%s'\n\n") % body)
context = req.environ[wsgi.CONTEXT_KEY]
self.validate(body)
users = body['users']
try:
model_users = populate_users(users)
@ -140,7 +135,6 @@ class UserController(wsgi.Controller):
LOG.info(_("Updating user passwords for instance '%s'") % instance_id)
LOG.info(_("req : '%s'\n\n") % req)
context = req.environ[wsgi.CONTEXT_KEY]
self.validate(body)
users = body['users']
model_users = []
for user in users:
@ -165,19 +159,14 @@ class UserController(wsgi.Controller):
class UserAccessController(wsgi.Controller):
"""Controller for adding and removing database access for a user."""
schemas = apischema.user
@classmethod
def validate(cls, body):
"""Validate that the request has all the required parameters"""
if not body:
raise exception.BadRequest("The request contains an empty body")
if not body.get('databases', []):
raise exception.MissingKey(key='databases')
if type(body['databases']) is not list:
raise exception.BadRequest("Databases must be provided as a list.")
for database in body.get('databases'):
if not database.get('name', ''):
raise exception.MissingKey(key='name')
def get_schema(cls, action, body):
schema = {}
if 'update' == action:
schema = cls.schemas.get(action).get('databases')
return schema
def _get_user(self, context, instance_id, user_id):
username, hostname = unquote_user_host(user_id)
@ -206,7 +195,6 @@ class UserAccessController(wsgi.Controller):
LOG.info(_("Granting user access for instance '%s'") % instance_id)
LOG.info(_("req : '%s'\n\n") % req)
context = req.environ[wsgi.CONTEXT_KEY]
self.validate(body)
user = self._get_user(context, instance_id, user_id)
username, hostname = unquote_user_host(user_id)
databases = [db['name'] for db in body['databases']]
@ -230,17 +218,7 @@ class UserAccessController(wsgi.Controller):
class SchemaController(wsgi.Controller):
"""Controller for instance functionality"""
@classmethod
def validate(cls, body):
"""Validate that the request has all the required parameters"""
if not body:
raise exception.BadRequest("The request contains an empty body")
if not body.get('databases', ''):
raise exception.MissingKey(key='databases')
for database in body.get('databases'):
if not database.get('name', ''):
raise exception.MissingKey(key='name')
schemas = apischema.dbschema
def index(self, req, tenant_id, instance_id):
"""Return all schemas."""
@ -259,7 +237,6 @@ class SchemaController(wsgi.Controller):
LOG.info(_("req : '%s'\n\n") % req)
LOG.info(_("body : '%s'\n\n") % body)
context = req.environ[wsgi.CONTEXT_KEY]
self.validate(body)
schemas = body['databases']
model_schemas = populate_validated_databases(schemas)
models.Schema.create(context, instance_id, model_schemas)

View File

@ -115,3 +115,38 @@ class SecurityGroupRuleController(wsgi.Controller):
"- %s") % e)
raise exception.SecurityGroupRuleCreationError(
"Required element/key - %s was not specified" % e)
schemas = {
"type": "object",
"name": "security_group_rule:create",
"required": True,
"properties": {
"security_group_rule": {
"type": "object",
"required": True,
"properties": {
"cidr": {
"type": "string",
"required": True,
"minLength": 9,
"maxLength": 18
},
"group_id": {
"type": "string",
"required": True,
"maxLength": 255
},
"from_port": {
"type": "integer",
"minimum": 0,
"maximum": 65535
},
"to_port": {
"type": "integer",
"minimum": 0,
"maximum": 65535
}
}
}
}
}

View File

@ -29,33 +29,44 @@ from trove.backup.models import Backup as backup_model
from trove.backup import views as backup_views
from trove.openstack.common import log as logging
from trove.openstack.common.gettextutils import _
import trove.common.apischema as apischema
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
class api_validation:
""" api validation wrapper """
def __init__(self, action=None):
self.action = action
def __call__(self, f):
"""
Apply validation of the api body
"""
def wrapper(*args, **kwargs):
body = kwargs['body']
if self.action == 'create':
InstanceController._validate(body)
return f(*args, **kwargs)
return wrapper
class InstanceController(wsgi.Controller):
"""Controller for instance functionality"""
schemas = apischema.instance
@classmethod
def get_action_schema(cls, body, action_schema):
action_type = body.keys()[0]
action_schema = action_schema.get(action_type, {})
if action_type == 'resize':
# volume or flavorRef
resize_action = body[action_type].keys()[0]
action_schema = action_schema.get(resize_action, {})
return action_schema
@classmethod
def get_schema(cls, action, body):
action_schema = super(InstanceController, cls).get_schema(action, body)
if action == 'action':
# resize or restart
action_schema = cls.get_action_schema(body, action_schema)
return action_schema
def action(self, req, body, tenant_id, id):
"""
Handles requests that modify existing instances in some manner. Actions
could include 'resize', 'restart', 'reset_password'
:param req: http request object
:param body: deserialized body of the request as a dict
:param tenant_id: the tenant id for whom owns the instance
:param id: ???
"""
LOG.info("req : '%s'\n\n" % req)
LOG.info("Comitting an ACTION again instance %s for tenant '%s'"
% (id, tenant_id))
@ -71,18 +82,8 @@ class InstanceController(wsgi.Controller):
selected_action = None
for key in body:
if key in _actions:
if selected_action is not None:
msg = _("Only one action can be specified per request.")
raise exception.BadRequest(msg)
selected_action = _actions[key]
else:
msg = _("Invalid instance action: %s") % key
raise exception.BadRequest(msg)
if selected_action:
return selected_action(instance, body)
else:
raise exception.BadRequest(_("Invalid request body."))
return selected_action(instance, body)
def _action_restart(self, instance, body):
instance.restart()
@ -106,20 +107,12 @@ class InstanceController(wsgi.Controller):
args = None
for key in options:
if key in body['resize']:
if selected_option is not None:
msg = _("Not allowed to resize volume and flavor at the "
"same time.")
raise exception.BadRequest(msg)
selected_option = options[key]
args = body['resize'][key]
if selected_option:
return selected_option(instance, args)
else:
raise exception.BadRequest(_("Missing resize arguments."))
break
return selected_option(instance, args)
def _action_resize_volume(self, instance, volume):
InstanceController._validate_resize_volume(volume)
instance.resize_volume(volume['size'])
return wsgi.Result(None, 202)
@ -175,7 +168,6 @@ class InstanceController(wsgi.Controller):
# TODO(cp16net): need to set the return code correctly
return wsgi.Result(None, 202)
@api_validation(action="create")
def create(self, req, body, tenant_id):
# TODO(hub-cap): turn this into middleware
LOG.info(_("Creating a database instance for tenant '%s'") % tenant_id)
@ -197,18 +189,15 @@ class InstanceController(wsgi.Controller):
users = populate_users(body['instance'].get('users', []))
except ValueError as ve:
raise exception.BadRequest(msg=ve)
if 'volume' in body['instance']:
try:
volume_size = int(body['instance']['volume']['size'])
except ValueError as e:
raise exception.BadValue(msg=e)
volume_size = int(body['instance']['volume']['size'])
else:
volume_size = None
if 'restorePoint' in body['instance']:
backupRef = body['instance']['restorePoint']['backupRef']
backup_id = utils.get_id_from_href(backupRef)
else:
backup_id = None
@ -219,62 +208,3 @@ class InstanceController(wsgi.Controller):
view = views.InstanceDetailView(instance, req=req)
return wsgi.Result(view.data(), 200)
@staticmethod
def _validate_body_not_empty(body):
"""Check that the body is not empty"""
if not body:
msg = "The request contains an empty body"
raise exception.TroveError(msg)
@staticmethod
def _validate_resize_volume(volume):
"""
We are going to check that volume resizing data is present.
"""
if 'size' not in volume:
raise exception.BadRequest(
"Missing 'size' property of 'volume' in request body.")
InstanceController._validate_volume_size(volume['size'])
@staticmethod
def _validate_volume_size(size):
"""Validate the various possible errors for volume size"""
try:
volume_size = float(size)
except (ValueError, TypeError) as err:
LOG.error(err)
msg = ("Required element/key - instance volume 'size' was not "
"specified as a number (value was %s)." % size)
raise exception.TroveError(msg)
if int(volume_size) != volume_size or int(volume_size) < 1:
msg = ("Volume 'size' needs to be a positive "
"integer value, %s cannot be accepted."
% volume_size)
raise exception.TroveError(msg)
@staticmethod
def _validate(body):
"""Validate that the request has all the required parameters"""
InstanceController._validate_body_not_empty(body)
try:
body['instance']
body['instance']['flavorRef']
name = body['instance'].get('name', '').strip()
if not name:
raise exception.MissingKey(key='name')
if CONF.trove_volume_support:
if body['instance'].get('volume', None):
if body['instance']['volume'].get('size', None):
volume_size = body['instance']['volume']['size']
InstanceController._validate_volume_size(volume_size)
else:
raise exception.MissingKey(key="size")
else:
raise exception.MissingKey(key="volume")
except KeyError as e:
LOG.error(_("Create Instance Required field(s) - %s") % e)
raise exception.TroveError("Required element/key - %s "
"was not specified" % e)

View File

@ -12,12 +12,14 @@
# 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 uuid
from proboscis.asserts import assert_equal
from proboscis.asserts import assert_not_equal
from proboscis.asserts import assert_raises
from proboscis import test
from proboscis import SkipTest
from proboscis.decorators import time_out
import troveclient
from trove.tests.util import poll_until
from trove.tests.util import test_config
from trove.tests.util import create_dbaas_client
@ -28,6 +30,7 @@ from trove.tests.api.instances import WaitForGuestInstallationToFinish
from trove.tests.api.instances import instance_info
from trove.tests.api.instances import assert_unprocessable
GROUP = "dbaas.api.backups"
BACKUP_NAME = 'backup_test'
BACKUP_DESC = 'test description'
@ -41,11 +44,30 @@ restore_instance_id = None
groups=[GROUP])
class CreateBackups(object):
@test
def test_backup_create_instance_invalid(self):
"""test create backup with unknown instance"""
invalid_inst_id = 'invalid-inst-id'
try:
instance_info.dbaas.backups.create(BACKUP_NAME, invalid_inst_id,
BACKUP_DESC)
except exceptions.BadRequest as e:
resp, body = instance_info.dbaas.client.last_response
assert_equal(resp.status, 400)
if not isinstance(instance_info.dbaas.client,
troveclient.xml.TroveXmlClient):
assert_equal(e.message, "Validation error: u'%s' "
"does not match "
"'^([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}$'" %
invalid_inst_id)
@test
def test_backup_create_instance_not_found(self):
"""test create backup with unknown instance"""
assert_raises(exceptions.NotFound, instance_info.dbaas.backups.create,
BACKUP_NAME, 'nonexistent_instance', BACKUP_DESC)
BACKUP_NAME, str(uuid.uuid4()), BACKUP_DESC)
@test
def test_backup_create_instance(self):

View File

@ -83,7 +83,7 @@ class TestDatabases(object):
@test
def test_cannot_create_taboo_database_names(self):
for name in self.system_dbs:
databases = [{"name": name, "charset": "latin2",
databases = [{"name": name, "character_set": "latin2",
"collate": "latin2_general_ci"}]
assert_raises(exceptions.BadRequest, self.dbaas.databases.create,
instance_info.id, databases)
@ -92,7 +92,7 @@ class TestDatabases(object):
@test
def test_create_database(self):
databases = []
databases.append({"name": self.dbname, "charset": "latin2",
databases.append({"name": self.dbname, "character_set": "latin2",
"collate": "latin2_general_ci"})
databases.append({"name": self.dbname2})
@ -116,7 +116,7 @@ class TestDatabases(object):
@test(depends_on=[test_create_database])
def test_fails_when_creating_a_db_twice(self):
databases = []
databases.append({"name": self.dbname, "charset": "latin2",
databases.append({"name": self.dbname, "character_set": "latin2",
"collate": "latin2_general_ci"})
databases.append({"name": self.dbname2})
@ -138,7 +138,7 @@ class TestDatabases(object):
@test
def test_create_database_on_missing_instance(self):
databases = [{"name": "invalid_db", "charset": "latin2",
databases = [{"name": "invalid_db", "character_set": "latin2",
"collate": "latin2_general_ci"}]
assert_raises(exceptions.NotFound, self.dbaas.databases.create,
-1, databases)
@ -190,7 +190,7 @@ class TestDatabases(object):
@test
def test_pagination(self):
databases = []
databases.append({"name": "Sprockets", "charset": "latin2",
databases.append({"name": "Sprockets", "character_set": "latin2",
"collate": "latin2_general_ci"})
databases.append({"name": "Cogs"})
databases.append({"name": "Widgets"})

View File

@ -61,7 +61,6 @@ from trove.tests.util.usage import create_usage_verifier
from trove.tests.util import iso_time
from trove.tests.util import process
from trove.tests.util.users import Requirements
from trove.tests.util import skip_if_xml
from trove.tests.util import string_in_list
from trove.tests.util import poll_until
from trove.tests.util.check import AttrCheck
@ -641,7 +640,7 @@ class AfterInstanceCreation(unittest.TestCase):
def test_users_create_after_create(self):
users = list()
users.append({"name": "testuser", "password": "password",
"database": "testdb"})
"databases": [{"name": "testdb"}]})
assert_unprocessable(dbaas.users.create, instance_info.id, users)
def test_resize_instance_after_create(self):

View File

@ -120,7 +120,7 @@ class ActionTestBase(object):
"""Create a MySQL user we can use for this test."""
users = [{"name": MYSQL_USERNAME, "password": MYSQL_PASSWORD,
"database": MYSQL_USERNAME}]
"databases": [{"name": MYSQL_USERNAME}]}]
self.dbaas.users.create(instance_info.id, users)
def has_user():

View File

@ -91,22 +91,22 @@ class ErroredInstanceDelete(TestBase):
self.delete_error = self.create_instance('test_ERROR_ON_DELETE')
@test
@time_out(20)
@time_out(30)
def delete_server_error(self):
self.delete_errored_instance(self.server_error)
@test(enabled=VOLUME_SUPPORT)
@time_out(20)
@time_out(30)
def delete_volume_error(self):
self.delete_errored_instance(self.volume_error)
@test(enabled=False)
@time_out(20)
@time_out(30)
def delete_dns_error(self):
self.delete_errored_instance(self.dns_error)
@test
@time_out(20)
@time_out(30)
def delete_error_on_delete_instance(self):
id = self.delete_error
self.wait_for_instance_status(id, 'ACTIVE')

View File

@ -1,24 +1,20 @@
from proboscis import test
from proboscis import test, SkipTest
from proboscis.asserts import *
from proboscis import after_class
from proboscis import before_class
from proboscis.asserts import Check
import troveclient
from trove.tests.config import CONFIG
from trove.tests.api.instances import instance_info
from trove.tests.api.instances import VOLUME_SUPPORT
from troveclient import exceptions
import json
import requests
from trove.tests.util.users import Requirements
from trove.tests.util import create_dbaas_client
import trove.tests.util as tests_utils
from trove.tests.util import poll_until
from nose.plugins.skip import SkipTest
@test(groups=["dbaas.api.mgmt.malformed_json"])
class MalformedJson(object):
@before_class
def setUp(self):
self.reqs = Requirements(is_admin=False)
@ -40,30 +36,46 @@ class MalformedJson(object):
@test
def test_bad_instance_data(self):
raise SkipTest("Please see Launchpad Bug #1177969")
databases = "foo"
users = "bar"
try:
self.dbaas.instances.create("bad_instance", 3, 3,
databases="foo",
users="bar")
databases=databases,
users=users)
except Exception as e:
resp, body = self.dbaas.client.last_response
httpCode = resp.status
assert_true(httpCode == 400,
"Create instance failed with code %s, exception %s"
% (httpCode, e))
assert_equal(httpCode, 400,
"Create instance failed with code %s, exception %s"
% (httpCode, e))
if not isinstance(self.dbaas.client,
troveclient.xml.TroveXmlClient):
databases = "u'foo'"
users = "u'bar'"
assert_equal(e.message,
"Validation error: "
"%s is not of type 'array'; "
"%s is not of type 'array'; "
"3 is not of type 'object'" % (databases, users))
@test
def test_bad_database_data(self):
raise SkipTest("Please see Launchpad Bug #1177969")
tests_utils.skip_if_xml()
_bad_db_data = "{foo}"
try:
self.dbaas.databases.create(self.instance.id, _bad_db_data)
except Exception as e:
resp, body = self.dbaas.client.last_response
httpCode = resp.status
assert_true(httpCode == 400,
"Create database failed with code %s, exception %s"
% (httpCode, e))
assert_equal(httpCode, 400,
"Create database failed with code %s, exception %s"
% (httpCode, e))
if not isinstance(self.dbaas.client,
troveclient.xml.TroveXmlClient):
_bad_db_data = "u'{foo}'"
assert_equal(e.message,
"Validation error: %s is not of type 'array'" %
_bad_db_data)
@test
def test_bad_user_data(self):
@ -76,29 +88,34 @@ class MalformedJson(object):
except Exception as e:
resp, body = self.dbaas.client.last_response
httpCode = resp.status
assert_true(httpCode == 400,
"Create user failed with code %s, exception %s"
% (httpCode, e))
assert_equal(httpCode, 400,
"Create user failed with code %s, exception %s"
% (httpCode, e))
assert_equal(e.message,
"Validation error: Additional properties are not "
"allowed "
"(u'password12', u'name12' were unexpected); "
"'name' is a required property; "
"'password' is a required property")
@test
def test_bad_resize_instance_data(self):
raise SkipTest("Please see Launchpad Bug #1177969")
def _check_instance_status():
inst = self.dbaas.instances.get(self.instance)
if inst.status == "ACTIVE":
return True
else:
return False
poll_until(_check_instance_status)
try:
self.dbaas.instances.resize_instance(self.instance.id, "bad data")
except Exception as e:
resp, body = self.dbaas.client.last_response
httpCode = resp.status
assert_true(httpCode == 400,
"Resize instance failed with code %s, exception %s"
% (httpCode, e))
assert_equal(httpCode, 400,
"Resize instance failed with code %s, exception %s"
% (httpCode, e))
@test
def test_bad_resize_vol_data(self):
@ -108,19 +125,28 @@ class MalformedJson(object):
return True
else:
return False
poll_until(_check_instance_status)
data = "bad data"
try:
self.dbaas.instances.resize_volume(self.instance.id, "bad data")
self.dbaas.instances.resize_volume(self.instance.id, data)
except Exception as e:
resp, body = self.dbaas.client.last_response
httpCode = resp.status
assert_true(httpCode == 400,
"Resize instance failed with code %s, exception %s"
% (httpCode, e))
assert_equal(httpCode, 400,
"Resize instance failed with code %s, exception %s"
% (httpCode, e))
data = "u'bad data'"
assert_equal(e.message,
"Validation error: "
"%s is not valid under any of the given schemas; "
"%s is not of type 'integer'; "
"%s does not match '[0-9]+'" % (data, data, data))
@test
def test_bad_change_user_password(self):
users = [{"name": ""}]
password = ""
users = [{"name": password}]
def _check_instance_status():
inst = self.dbaas.instances.get(self.instance)
@ -128,15 +154,25 @@ class MalformedJson(object):
return True
else:
return False
poll_until(_check_instance_status)
try:
self.dbaas.users.change_passwords(self.instance, users)
except Exception as e:
resp, body = self.dbaas.client.last_response
httpCode = resp.status
assert_true(httpCode == 400,
"Change usr/passwd failed with code %s, exception %s" %
(httpCode, e))
assert_equal(httpCode, 400,
"Change usr/passwd failed with code %s, exception %s"
% (httpCode, e))
if not isinstance(self.dbaas.client,
troveclient.xml.TroveXmlClient):
password = "u''"
assert_equal(e.message, "Validation error: "
"'password' is a required property; "
"%s is too short; "
"%s does not match "
"'^.*[0-9a-zA-Z]+.*$'" %
(password, password))
@test
def test_bad_grant_user_access(self):
@ -148,15 +184,17 @@ class MalformedJson(object):
return True
else:
return False
poll_until(_check_instance_status)
try:
self.dbaas.users.grant(self.instance, self.user, dbs)
except Exception as e:
resp, body = self.dbaas.client.last_response
httpCode = resp.status
assert_true(httpCode == 400,
"Grant user access failed with code %s, exception %s" %
(httpCode, e))
assert_equal(httpCode, 400,
"Grant user access failed with code %s, exception "
"%s" %
(httpCode, e))
@test
def test_bad_revoke_user_access(self):
@ -168,19 +206,22 @@ class MalformedJson(object):
return True
else:
return False
poll_until(_check_instance_status)
try:
self.dbaas.users.revoke(self.instance, self.user, db)
except Exception as e:
resp, body = self.dbaas.client.last_response
httpCode = resp.status
assert_true(httpCode == 404,
"Revoke user access failed w/code %s, exception %s" %
(httpCode, e))
assert_equal(httpCode, 404,
"Revoke user access failed w/code %s, exception %s" %
(httpCode, e))
assert_equal(e.message, "The resource could not be found.")
@test
def test_bad_body_flavorid_create_instance(self):
raise SkipTest("Please see Launchpad Bug #1177969")
tests_utils.skip_if_xml()
flavorId = ["?"]
try:
self.dbaas.instances.create("test_instance",
@ -189,14 +230,25 @@ class MalformedJson(object):
except Exception as e:
resp, body = self.dbaas.client.last_response
httpCode = resp.status
assert_true(httpCode == 400,
"Create instance failed with code %s, exception %s" %
(httpCode, e))
assert_equal(httpCode, 400,
"Create instance failed with code %s, exception %s" %
(httpCode, e))
if not isinstance(self.dbaas.client,
troveclient.xml.TroveXmlClient):
flavorId = [u'?']
assert_equal(e.message,
"Validation error: %s is not valid under any "
"of the given schemas; "
"%s is not of type 'string'; "
"%s is not of type 'string'; "
"%s is not of type 'integer'; "
"2 is not of type 'object'" %
(flavorId, flavorId, flavorId, flavorId))
@test
def test_bad_body_volsize_create_instance(self):
raise SkipTest("Please see Launchpad Bug #1177969")
volsize = ("h3ll0")
volsize = "h3ll0"
try:
self.dbaas.instances.create("test_instance",
"1",
@ -204,6 +256,12 @@ class MalformedJson(object):
except Exception as e:
resp, body = self.dbaas.client.last_response
httpCode = resp.status
assert_true(httpCode == 400,
"Create instance failed with code %s, exception %s" %
(httpCode, e))
assert_equal(httpCode, 400,
"Create instance failed with code %s, exception %s" %
(httpCode, e))
if not isinstance(self.dbaas.client,
troveclient.xml.TroveXmlClient):
volsize = "u'h3ll0'"
assert_equal(e.message,
"Validation error: %s is not of type 'object'" %
volsize)

View File

@ -13,7 +13,6 @@
# under the License.
import time
import re
from troveclient import exceptions
@ -22,23 +21,17 @@ from proboscis import before_class
from proboscis import test
from proboscis.asserts import assert_equal
from proboscis.asserts import assert_false
from proboscis.asserts import assert_not_equal
from proboscis.asserts import assert_raises
from proboscis.asserts import assert_true
from proboscis.asserts import fail
from proboscis.decorators import expect_exception
from proboscis.decorators import time_out
from trove import tests
from trove.tests.api.databases import TestDatabases
from trove.tests.api.instances import GROUP_START
from trove.tests.api.instances import instance_info
from trove.tests import util
from trove.tests.util import skip_if_xml
from trove.tests.util import test_config
from trove.tests.api.databases import TestMysqlAccess
from urllib import quote
GROUP = "dbaas.api.users"
FAKE = test_config.values['fake_mode']
@ -62,17 +55,20 @@ class TestUsers(object):
created_users = [username, username1]
system_users = ['root', 'debian_sys_maint']
@before_class
def setUp(self):
def __init__(self):
self.dbaas = util.create_dbaas_client(instance_info.user)
self.dbaas_admin = util.create_dbaas_client(instance_info.admin_user)
databases = [{"name": self.db1, "charset": "latin2",
@before_class
def setUp(self):
databases = [{"name": self.db1, "character_set": "latin2",
"collate": "latin2_general_ci"},
{"name": self.db2}]
try:
self.dbaas.databases.create(instance_info.id, databases)
except exceptions.BadRequest:
pass # If the db already exists that's OK.
except exceptions.BadRequest as e:
if "Validation error" in e.message:
raise e
if not FAKE:
time.sleep(5)
@ -154,12 +150,10 @@ class TestUsers(object):
#tests for users that should not be listed
users = self.dbaas.users.list(instance_info.id)
assert_equal(200, self.dbaas.last_http_code)
found = False
for user in self.system_users:
found = any(result.name == user for result in users)
msg = "User '%s' SHOULD NOT BE found in result" % user
assert_false(found, msg)
found = False
@test(depends_on=[test_create_users_list],
runs_after=[test_fails_when_creating_user_twice])
@ -251,9 +245,8 @@ class TestUsers(object):
@test
def test_username_too_long(self):
users = []
users.append({"name": "1233asdwer345tyg56", "password": self.password,
"database": self.db1})
users = [{"name": "1233asdwer345tyg56", "password": self.password,
"database": self.db1}]
assert_raises(exceptions.BadRequest, self.dbaas.users.create,
instance_info.id, users)
assert_equal(400, self.dbaas.last_http_code)
@ -287,9 +280,8 @@ class TestUsers(object):
@test
def test_invalid_password(self):
users = []
users.append({"name": "anouser", "password": "sdf,;",
"database": self.db1})
users = [{"name": "anouser", "password": "sdf,;",
"database": self.db1}]
assert_raises(exceptions.BadRequest, self.dbaas.users.create,
instance_info.id, users)
assert_equal(400, self.dbaas.last_http_code)

View File

@ -0,0 +1,43 @@
# Copyright 2013 Hewlett-Packard Development Company, L.P.
# 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 jsonschema
from testtools import TestCase
from testtools.matchers import Equals
from trove.backup.service import BackupController
from trove.common import apischema
class TestBackupController(TestCase):
def test_validate_create_complete(self):
body = {"backup": {"instance": "d6338c9c-3cc8-4313-b98f-13cc0684cf15",
"name": "testback-backup"}}
controller = BackupController()
schema = controller.get_schema('create', body)
validator = jsonschema.Draft4Validator(schema)
self.assertTrue(validator.is_valid(body))
def test_validate_create_invalid_uuid(self):
invalid_uuid = "ead-edsa-e23-sdf-23"
body = {"backup": {"instance": invalid_uuid,
"name": "testback-backup"}}
controller = BackupController()
schema = controller.get_schema('create', body)
validator = jsonschema.Draft4Validator(schema)
self.assertFalse(validator.is_valid(body))
errors = sorted(validator.iter_errors(body), key=lambda e: e.path)
self.assertThat(errors[0].message,
Equals("'%s' does not match '%s'" %
(invalid_uuid, apischema.uuid['pattern'])))

View File

@ -0,0 +1,15 @@
# Copyright 2013 Hewlett-Packard Development Company, L.P.
# 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.
#

View File

@ -0,0 +1,217 @@
# Copyright 2013 Hewlett-Packard Development Company, L.P.
# 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 jsonschema
from testtools import TestCase
from testtools.matchers import Is, Equals
from testtools.testcase import skip
from trove.common import apischema
from trove.instance.service import InstanceController
class TestInstanceController(TestCase):
def setUp(self):
super(TestInstanceController, self).setUp()
self.controller = InstanceController()
self.instance = {
"instance": {
"volume": {"size": "1"},
"users": [
{"name": "user1",
"password": "litepass",
"databases": [{"name": "firstdb"}]}
],
"flavorRef": "https://localhost:8779/v1.0/2500/1",
"name": "TEST-XYS2d2fe2kl;zx;jkl2l;sjdcma239(E)@(D",
"databases": [
{
"name": "firstdb",
"collate": "latin2_general_ci",
"character_set": "latin2"
},
{
"name": "db2"
}
]
}
}
def verify_errors(self, errors, msg=None, properties=None, path=None):
msg = msg or []
properties = properties or []
self.assertThat(len(errors), Is(len(msg)))
i = 0
while i < len(msg):
self.assertThat(errors[i].message, Equals(msg[i]))
if path:
self.assertThat(path, Equals(properties[i]))
else:
self.assertThat(errors[i].path.pop(), Equals(properties[i]))
i += 1
def test_get_schema_create(self):
schema = self.controller.get_schema('create', {'instance': {}})
self.assertIsNotNone(schema)
self.assertTrue('instance' in schema['properties'])
def test_get_schema_action_restart(self):
schema = self.controller.get_schema('action', {'restart': {}})
self.assertIsNotNone(schema)
self.assertTrue('restart' in schema['properties'])
def test_get_schema_action_resize_volume(self):
schema = self.controller.get_schema(
'action', {'resize': {'volume': {}}})
self.assertIsNotNone(schema)
self.assertTrue('resize' in schema['properties'])
self.assertTrue(
'volume' in schema['properties']['resize']['properties'])
def test_get_schema_action_resize_flavorRef(self):
schema = self.controller.get_schema(
'action', {'resize': {'flavorRef': {}}})
self.assertIsNotNone(schema)
self.assertTrue('resize' in schema['properties'])
self.assertTrue(
'flavorRef' in schema['properties']['resize']['properties'])
def test_get_schema_action_other(self):
schema = self.controller.get_schema(
'action', {'supersized': {'flavorRef': {}}})
self.assertIsNotNone(schema)
self.assertThat(len(schema.keys()), Is(0))
def test_validate_create_complete(self):
body = self.instance
schema = self.controller.get_schema('create', body)
validator = jsonschema.Draft4Validator(schema)
self.assertTrue(validator.is_valid(body))
def test_validate_create_complete_with_restore(self):
body = self.instance
body['instance']['restorePoint'] = {
"backupRef": "d761edd8-0771-46ff-9743-688b9e297a3b"
}
schema = self.controller.get_schema('create', body)
validator = jsonschema.Draft4Validator(schema)
self.assertTrue(validator.is_valid(body))
def test_validate_create_complete_with_restore(self):
body = self.instance
backup_id_ref = "invalid-backup-id-ref"
body['instance']['restorePoint'] = {
"backupRef": backup_id_ref
}
schema = self.controller.get_schema('create', body)
validator = jsonschema.Draft4Validator(schema)
self.assertFalse(validator.is_valid(body))
errors = sorted(validator.iter_errors(body), key=lambda e: e.path)
self.assertThat(len(errors), Is(1))
self.assertThat(errors[0].message,
Equals("'%s' does not match '%s'" %
(backup_id_ref, apischema.uuid['pattern'])))
def test_validate_create_blankname(self):
body = self.instance
body['instance']['name'] = " "
schema = self.controller.get_schema('create', body)
validator = jsonschema.Draft4Validator(schema)
self.assertFalse(validator.is_valid(body))
errors = sorted(validator.iter_errors(body), key=lambda e: e.path)
self.assertThat(len(errors), Is(1))
self.assertThat(errors[0].message,
Equals("' ' does not match '^.*[0-9a-zA-Z]+.*$'"))
def test_validate_restart(self):
body = {"restart": {}}
schema = self.controller.get_schema('action', body)
validator = jsonschema.Draft4Validator(schema)
self.assertTrue(validator.is_valid(body))
def test_validate_invalid_action(self):
# TODO(juice) perhaps we should validate the schema not recognized
body = {"restarted": {}}
schema = self.controller.get_schema('action', body)
validator = jsonschema.Draft4Validator(schema)
self.assertTrue(validator.is_valid(body))
def test_validate_resize_volume(self):
body = {"resize": {"volume": {"size": 4}}}
schema = self.controller.get_schema('action', body)
validator = jsonschema.Draft4Validator(schema)
self.assertTrue(validator.is_valid(body))
def test_validate_resize_volume_string(self):
body = {"resize": {"volume": {"size": '-44.0'}}}
schema = self.controller.get_schema('action', body)
validator = jsonschema.Draft4Validator(schema)
self.assertTrue(validator.is_valid(body))
def test_validate_resize_volume_invalid(self):
body = {"resize": {"volume": {"size": 'x'}}}
schema = self.controller.get_schema('action', body)
validator = jsonschema.Draft4Validator(schema)
self.assertFalse(validator.is_valid(body))
errors = sorted(validator.iter_errors(body), key=lambda e: e.path)
self.assertThat(errors[0].context[0].message,
Equals("'x' is not of type 'integer'"))
self.assertThat(errors[0].context[1].message,
Equals("'x' does not match '[0-9]+'"))
self.assertThat(errors[0].path.pop(), Equals('size'))
def test_validate_resize_instance(self):
body = {"resize": {"flavorRef": "https://endpoint/v1.0/123/flavors/2"}}
schema = self.controller.get_schema('action', body)
validator = jsonschema.Draft4Validator(schema)
self.assertTrue(validator.is_valid(body))
def test_validate_resize_instance_int(self):
body = {"resize": {"flavorRef": 2}}
schema = self.controller.get_schema('action', body)
validator = jsonschema.Draft4Validator(schema)
self.assertTrue(validator.is_valid(body))
def test_validate_resize_instance_int_xml(self):
body = {"resize": {"flavorRef": "2"}}
schema = self.controller.get_schema('action', body)
validator = jsonschema.Draft4Validator(schema)
self.assertTrue(validator.is_valid(body))
def test_validate_resize_instance_empty_url(self):
body = {"resize": {"flavorRef": ""}}
schema = self.controller.get_schema('action', body)
validator = jsonschema.Draft4Validator(schema)
self.assertFalse(validator.is_valid(body))
errors = sorted(validator.iter_errors(body), key=lambda e: e.path)
self.verify_errors(errors[0].context,
["'' is too short",
"'' does not match 'http[s]?://(?:[a-zA-Z]"
"|[0-9]|[$-_@.&+]|[!*\\\\(\\\\),]"
"|(?:%[0-9a-fA-F][0-9a-fA-F]))+'",
"'' does not match '[0-9]+'",
"'' is not of type 'integer'"],
["flavorRef", "flavorRef", "flavorRef",
"flavorRef"],
errors[0].path.pop())
@skip("This damn URI validator allows just about any garbage you give it")
def test_validate_resize_instance_invalid_url(self):
body = {"resize": {"flavorRef": "xyz-re1f2-daze329d-f23901"}}
schema = self.controller.get_schema('action', body)
self.assertIsNotNone(schema)
validator = jsonschema.Draft4Validator(schema)
self.assertFalse(validator.is_valid(body))
errors = sorted(validator.iter_errors(body), key=lambda e: e.path)
self.verify_errors(errors, ["'' is too short"], ["flavorRef"])

View File

@ -0,0 +1,15 @@
# Copyright 2013 Hewlett-Packard Development Company, L.P.
# 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.
#

View File

@ -0,0 +1,252 @@
# Copyright 2013 Hewlett-Packard Development Company, L.P.
# 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 jsonschema
from testtools import TestCase
from testtools.matchers import Is, Equals
from trove.extensions.mysql.service import UserController
from trove.extensions.mysql.service import UserAccessController
from trove.extensions.mysql.service import SchemaController
class TestUserController(TestCase):
def setUp(self):
super(TestUserController, self).setUp()
self.controller = UserController()
def test_get_create_schema(self):
body = {'users': [{'name': 'test', 'password': 'test'}]}
schema = self.controller.get_schema('create', body)
self.assertTrue('users' in schema['properties'])
def test_get_update_user_pw(self):
body = {'users': [{'name': 'test', 'password': 'test'}]}
schema = self.controller.get_schema('update', body)
self.assertTrue('users' in schema['properties'])
def test_get_update_user_db(self):
body = {'databases': [{'name': 'test'}, {'name': 'test'}]}
schema = self.controller.get_schema('update', body)
self.assertTrue('databases' in schema['properties'])
def test_validate_create_empty(self):
body = {"users": []}
schema = self.controller.get_schema('create', body)
validator = jsonschema.Draft4Validator(schema)
self.assertFalse(validator.is_valid(body))
errors = sorted(validator.iter_errors(body), key=lambda e: e.path)
self.assertThat(len(errors), Is(1))
self.assertThat(errors[0].message, Equals("[] is too short"))
self.assertThat(errors[0].path.pop(), Equals("users"))
def test_validate_create_short_password(self):
body = {"users": [{"name": "joe", "password": ""}]}
schema = self.controller.get_schema('create', body)
validator = jsonschema.Draft4Validator(schema)
self.assertFalse(validator.is_valid(body))
errors = sorted(validator.iter_errors(body), key=lambda e: e.path)
self.assertThat(len(errors), Is(2))
self.assertThat(errors[0].message, Equals("'' is too short"))
self.assertThat(errors[1].message,
Equals("'' does not match '^.*[0-9a-zA-Z]+.*$'"))
self.assertThat(errors[0].path.pop(), Equals("password"))
def test_validate_create_no_password(self):
body = {"users": [{"name": "joe"}]}
schema = self.controller.get_schema('create', body)
validator = jsonschema.Draft4Validator(schema)
self.assertFalse(validator.is_valid(body))
errors = sorted(validator.iter_errors(body), key=lambda e: e.path)
self.assertThat(len(errors), Is(1))
self.assertThat(errors[0].message,
Equals("'password' is a required property"))
def test_validate_create_short_name(self):
body = {"users": [{"name": ""}]}
schema = self.controller.get_schema('create', body)
validator = jsonschema.Draft4Validator(schema)
self.assertFalse(validator.is_valid(body))
errors = sorted(validator.iter_errors(body), key=lambda e: e.path)
self.assertThat(len(errors), Is(3))
self.assertThat(errors[0].message,
Equals("'password' is a required property"))
self.assertThat(errors[1].message, Equals("'' is too short"))
self.assertThat(errors[2].message,
Equals("'' does not match '^.*[0-9a-zA-Z]+.*$'"))
self.assertThat(errors[1].path.pop(), Equals("name"))
def test_validate_create_complete_db_empty(self):
body = {"users": [{"databases": [], "name": "joe", "password": "123"}]}
schema = self.controller.get_schema('create', body)
validator = jsonschema.Draft4Validator(schema)
self.assertTrue(validator.is_valid(body))
errors = sorted(validator.iter_errors(body), key=lambda e: e.path)
self.assertThat(len(errors), Is(0))
def test_validate_create_complete_db_no_name(self):
body = {"users": [{"databases": [{}], "name": "joe",
"password": "123"}]}
schema = self.controller.get_schema('create', body)
validator = jsonschema.Draft4Validator(schema)
self.assertFalse(validator.is_valid(body))
errors = sorted(validator.iter_errors(body), key=lambda e: e.path)
self.assertThat(len(errors), Is(1))
self.assertThat(errors[0].message,
Equals("'name' is a required property"))
def test_validate_create_complete_db(self):
body = {"users": [{"databases": [{"name": "x"}], "name": "joe",
"password": "123"}]}
schema = self.controller.get_schema('create', body)
validator = jsonschema.Draft4Validator(schema)
self.assertTrue(validator.is_valid(body))
def test_validate_update_empty(self):
body = {"users": []}
schema = self.controller.get_schema('update', body)
validator = jsonschema.Draft4Validator(schema)
self.assertFalse(validator.is_valid(body))
errors = sorted(validator.iter_errors(body), key=lambda e: e.path)
self.assertThat(len(errors), Is(1))
self.assertThat(errors[0].message, Equals("[] is too short"))
self.assertThat(errors[0].path.pop(), Equals("users"))
def test_validate_update_short_password(self):
body = {"users": [{"name": "joe", "password": ""}]}
schema = self.controller.get_schema('update', body)
validator = jsonschema.Draft4Validator(schema)
self.assertFalse(validator.is_valid(body))
errors = sorted(validator.iter_errors(body), key=lambda e: e.path)
self.assertThat(len(errors), Is(2))
self.assertThat(errors[0].message, Equals("'' is too short"))
self.assertThat(errors[1].message,
Equals("'' does not match '^.*[0-9a-zA-Z]+.*$'"))
self.assertThat(errors[0].path.pop(), Equals("password"))
def test_validate_update_user_complete(self):
body = {"users": [{"name": "joe", "password": "",
"databases": [{"name": "testdb"}]}]}
schema = self.controller.get_schema('update', body)
validator = jsonschema.Draft4Validator(schema)
self.assertFalse(validator.is_valid(body))
errors = sorted(validator.iter_errors(body), key=lambda e: e.path)
self.assertThat(len(errors), Is(2))
self.assertThat(errors[0].message, Equals("'' is too short"))
self.assertThat(errors[1].message, Equals(
"'' does not match '^.*[0-9a-zA-Z]+.*$'"))
self.assertThat(errors[0].path.pop(), Equals("password"))
def test_validate_update_user_with_db_short_password(self):
body = {"users": [{"name": "joe", "password": "",
"databases": [{"name": "testdb"}]}]}
schema = self.controller.get_schema('update', body)
validator = jsonschema.Draft4Validator(schema)
self.assertFalse(validator.is_valid(body))
errors = sorted(validator.iter_errors(body), key=lambda e: e.path)
self.assertThat(len(errors), Is(2))
self.assertThat(errors[0].message, Equals("'' is too short"))
self.assertThat(errors[0].path.pop(), Equals("password"))
def test_validate_update_no_password(self):
body = {"users": [{"name": "joe"}]}
schema = self.controller.get_schema('update', body)
validator = jsonschema.Draft4Validator(schema)
self.assertFalse(validator.is_valid(body))
errors = sorted(validator.iter_errors(body), key=lambda e: e.path)
self.assertThat(len(errors), Is(1))
self.assertThat(errors[0].message,
Equals("'password' is a required property"))
def test_validate_update_database_complete(self):
body = {"databases": [{"name": "test1"}, {"name": "test2"}]}
schema = self.controller.get_schema('update', body)
validator = jsonschema.Draft4Validator(schema)
self.assertTrue(validator.is_valid(body))
def test_validate_update_database_empty(self):
body = {"databases": []}
schema = self.controller.get_schema('update', body)
validator = jsonschema.Draft4Validator(schema)
self.assertFalse(validator.is_valid(body))
errors = sorted(validator.iter_errors(body), key=lambda e: e.path)
self.assertThat(len(errors), Is(1))
self.assertThat(errors[0].message, Equals('[] is too short'))
def test_validate_update_short_name(self):
body = {"users": [{"name": ""}]}
schema = self.controller.get_schema('update', body)
validator = jsonschema.Draft4Validator(schema)
self.assertFalse(validator.is_valid(body))
errors = sorted(validator.iter_errors(body), key=lambda e: e.path)
self.assertThat(len(errors), Is(3))
self.assertThat(errors[0].message,
Equals("'password' is a required property"))
self.assertThat(errors[1].message, Equals("'' is too short"))
self.assertThat(errors[2].message,
Equals("'' does not match '^.*[0-9a-zA-Z]+.*$'"))
self.assertThat(errors[1].path.pop(), Equals("name"))
class TestUserAccessController(TestCase):
def test_validate_update_db(self):
body = {"databases": []}
schema = (UserAccessController()).get_schema('update', body)
validator = jsonschema.Draft4Validator(schema)
self.assertFalse(validator.is_valid(body))
errors = sorted(validator.iter_errors(body), key=lambda e: e.path)
self.assertThat(len(errors), Is(1))
self.assertThat(errors[0].message, Equals("[] is too short"))
self.assertThat(errors[0].path.pop(), Equals("databases"))
class TestSchemaController(TestCase):
def setUp(self):
super(TestSchemaController, self).setUp()
self.controller = SchemaController()
self.body = {
"databases": [
{
"name": "first_db",
"collate": "latin2_general_ci",
"character_set": "latin2"
},
{
"name": "second_db"
}
]
}
def test_validate_mixed(self):
schema = self.controller.get_schema('create', self.body)
self.assertIsNotNone(schema)
validator = jsonschema.Draft4Validator(schema)
self.assertTrue(validator.is_valid(self.body))
def test_validate_mixed_with_no_name(self):
body = self.body.copy()
body['databases'].append({"collate": "some_collation"})
schema = self.controller.get_schema('create', body)
self.assertIsNotNone(schema)
validator = jsonschema.Draft4Validator(schema)
self.assertFalse(validator.is_valid(body))
def test_validate_empty(self):
body = {"databases": []}
schema = self.controller.get_schema('create', body)
self.assertIsNotNone(schema)
self.assertTrue('databases' in body)
validator = jsonschema.Draft4Validator(schema)
self.assertFalse(validator.is_valid(body))

View File

@ -26,15 +26,12 @@
.. moduleauthor:: Tim Simpson <tim.simpson@rackspace.com>
"""
import re
import subprocess
import sys
import time
from trove.tests.config import CONFIG as test_config
from urllib import unquote
try:
from eventlet import event
from eventlet import greenthread
@ -46,13 +43,8 @@ from sqlalchemy import create_engine
from troveclient import exceptions
from proboscis import test
from proboscis.asserts import assert_false
from proboscis.asserts import assert_raises
from proboscis.asserts import assert_true
from proboscis.asserts import Check
from proboscis.asserts import fail
from proboscis.asserts import ASSERTION_ERROR
from proboscis import SkipTest
from troveclient import Dbaas
from troveclient.client import TroveHTTPClient