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:
parent
eafc62f61b
commit
ccdf59f21e
@ -18,3 +18,4 @@ python-keystoneclient
|
||||
python-swiftclient
|
||||
iso8601
|
||||
oslo.config>=1.1.0
|
||||
jsonschema>=1.0.0,!=1.4.0,<2
|
||||
|
@ -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
312
trove/common/apischema.py
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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"})
|
||||
|
@ -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):
|
||||
|
@ -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():
|
||||
|
@ -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')
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
43
trove/tests/unittests/backup/test_backup_controller.py
Normal file
43
trove/tests/unittests/backup/test_backup_controller.py
Normal 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'])))
|
15
trove/tests/unittests/instance/__init__.py
Normal file
15
trove/tests/unittests/instance/__init__.py
Normal 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.
|
||||
#
|
217
trove/tests/unittests/instance/test_instance_controller.py
Normal file
217
trove/tests/unittests/instance/test_instance_controller.py
Normal 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"])
|
15
trove/tests/unittests/mysql/__init__.py
Normal file
15
trove/tests/unittests/mysql/__init__.py
Normal 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.
|
||||
#
|
252
trove/tests/unittests/mysql/test_user_controller.py
Normal file
252
trove/tests/unittests/mysql/test_user_controller.py
Normal 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))
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user