Controller and API changes for backups.
-API controller for backups -adding swift url to the config -Fixing failing tests -Renaming 'instance' param. Checking for NotFound models so that the returned error is friendly -adding feature to list all backups from a specific instance -Adding checks for creating/deleting/restoring backups when it is not completed -Adding unit tests for backup controller -adding check to see if file is in swift -Adding skeleton code for delete backup in the task manager -Fixed backups API to pass in the backup_id during create_backup. -adding int tests for backup controller -Adding backup list and delete int tests -Adding list backups for instance test -Adding quota for create backup BP: https://blueprints.launchpad.net/reddwarf/+spec/consistent-snapshots Change-Id: I35c2fefcce4b3009e76ba7232c52dabf502a3ac0
This commit is contained in:
parent
770c0fd83b
commit
b3c32e3f87
|
@ -30,6 +30,7 @@ db_api_implementation = reddwarf.db.sqlalchemy.api
|
|||
reddwarf_auth_url = http://0.0.0.0:5000/v2.0
|
||||
nova_compute_url = http://localhost:8774/v2
|
||||
nova_volume_url = http://localhost:8776/v1
|
||||
swift_url = http://localhost:8080/v1/AUTH_
|
||||
|
||||
# Config options for enabling volume service
|
||||
reddwarf_volume_support = True
|
||||
|
|
|
@ -40,6 +40,7 @@ api_extensions_path = reddwarf/extensions
|
|||
reddwarf_auth_url = http://0.0.0.0:5000/v2.0
|
||||
nova_compute_url = http://localhost:8774/v2
|
||||
nova_volume_url = http://localhost:8776/v1
|
||||
swift_url = http://localhost:8080/v1/AUTH_
|
||||
|
||||
# Config option for showing the IP address that nova doles out
|
||||
add_addresses = True
|
||||
|
@ -52,6 +53,7 @@ mount_point = /var/lib/mysql
|
|||
max_accepted_volume_size = 10
|
||||
max_instances_per_user = 5
|
||||
max_volumes_per_user = 100
|
||||
max_backups_per_user = 5
|
||||
volume_time_out=30
|
||||
|
||||
# Config options for rate limits
|
||||
|
|
|
@ -67,6 +67,7 @@ mount_point = /var/lib/mysql
|
|||
max_accepted_volume_size = 25
|
||||
max_instances_per_user = 55
|
||||
max_volumes_per_user = 100
|
||||
max_backups_per_user = 5
|
||||
volume_time_out=30
|
||||
|
||||
# Config options for rate limits
|
||||
|
|
|
@ -18,6 +18,11 @@ from reddwarf.common import cfg
|
|||
from reddwarf.common import exception
|
||||
from reddwarf.db.models import DatabaseModelBase
|
||||
from reddwarf.openstack.common import log as logging
|
||||
from swiftclient.client import ClientException
|
||||
from reddwarf.taskmanager import api
|
||||
from reddwarf.common.remote import create_swift_client
|
||||
from reddwarf.common import utils
|
||||
from reddwarf.quota.quota import run_with_quotas
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
@ -37,27 +42,45 @@ class BackupState(object):
|
|||
class Backup(object):
|
||||
|
||||
@classmethod
|
||||
def create(cls, context, instance_id, name, description=None):
|
||||
def create(cls, context, instance, name, description=None):
|
||||
"""
|
||||
create db record for Backup
|
||||
:param cls:
|
||||
:param context: tenant_id included
|
||||
:param instance_id:
|
||||
:param instance:
|
||||
:param name:
|
||||
:param note:
|
||||
:param description:
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
db_info = DBBackup.create(name=name,
|
||||
description=description,
|
||||
tenant_id=context.tenant,
|
||||
state=BackupState.NEW,
|
||||
instance_id=instance_id,
|
||||
deleted=False)
|
||||
|
||||
def _create_resources():
|
||||
# parse the ID from the Ref
|
||||
instance_id = utils.get_id_from_href(instance)
|
||||
|
||||
# verify that the instance exist and can perform actions
|
||||
from reddwarf.instance.models import Instance
|
||||
instance_model = Instance.load(context, instance_id)
|
||||
instance_model.validate_can_perform_action()
|
||||
|
||||
cls.verify_swift_auth_token(context)
|
||||
|
||||
try:
|
||||
db_info = DBBackup.create(name=name,
|
||||
description=description,
|
||||
tenant_id=context.tenant,
|
||||
state=BackupState.NEW,
|
||||
instance_id=instance_id,
|
||||
deleted=False)
|
||||
except exception.InvalidModelError as ex:
|
||||
LOG.exception("Unable to create Backup record:")
|
||||
raise exception.BackupCreationError(str(ex))
|
||||
|
||||
api.API(context).create_backup(db_info.id, instance_id)
|
||||
return db_info
|
||||
except exception.InvalidModelError as ex:
|
||||
LOG.exception("Unable to create Backup record:")
|
||||
raise exception.BackupCreationError(str(ex))
|
||||
|
||||
return run_with_quotas(context.tenant,
|
||||
{'backups': 1},
|
||||
_create_resources)
|
||||
|
||||
@classmethod
|
||||
def running(cls, instance_id, exclude=None):
|
||||
|
@ -115,17 +138,50 @@ class Backup(object):
|
|||
return db_info
|
||||
|
||||
@classmethod
|
||||
def delete(cls, backup_id):
|
||||
def delete(cls, context, backup_id):
|
||||
"""
|
||||
update Backup table on deleted flag for given Backup
|
||||
:param cls:
|
||||
:param context: context containing the tenant id and token
|
||||
:param backup_id: Backup uuid
|
||||
:return:
|
||||
"""
|
||||
#TODO: api (service.py) might take care of actual deletion
|
||||
# on remote swift
|
||||
db_info = cls.get_by_id(backup_id)
|
||||
db_info.delete()
|
||||
|
||||
def _delete_resources():
|
||||
backup = cls.get_by_id(backup_id)
|
||||
if backup.is_running:
|
||||
msg = ("Backup %s cannot be delete because it is running." %
|
||||
backup_id)
|
||||
raise exception.UnprocessableEntity(msg)
|
||||
cls.verify_swift_auth_token(context)
|
||||
api.API(context).delete_backup(backup_id)
|
||||
|
||||
return run_with_quotas(context.tenant,
|
||||
{'backups': -1},
|
||||
_delete_resources)
|
||||
|
||||
@classmethod
|
||||
def verify_swift_auth_token(cls, context):
|
||||
try:
|
||||
client = create_swift_client(context)
|
||||
client.get_account()
|
||||
except ClientException:
|
||||
raise exception.SwiftAuthError(tenant_id=context.tenant)
|
||||
|
||||
@classmethod
|
||||
def check_object_exist(cls, context, location):
|
||||
try:
|
||||
parts = location.split('/')
|
||||
obj = parts[-1]
|
||||
container = parts[-2]
|
||||
client = create_swift_client(context)
|
||||
client.head_object(container, obj)
|
||||
return True
|
||||
except ClientException as e:
|
||||
if e.http_status == 404:
|
||||
return False
|
||||
else:
|
||||
raise exception.SwiftAuthError(tenant_id=context.tenant)
|
||||
|
||||
|
||||
def persisted_models():
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2013 OpenStack LLC
|
||||
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# 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.
|
||||
|
||||
from reddwarf.common import wsgi
|
||||
from reddwarf.backup import views
|
||||
from reddwarf.backup.models import Backup
|
||||
from reddwarf.common import exception
|
||||
from reddwarf.common import cfg
|
||||
from reddwarf.openstack.common import log as logging
|
||||
from reddwarf.openstack.common.gettextutils import _
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BackupController(wsgi.Controller):
|
||||
"""
|
||||
Controller for accessing backups in the OpenStack API.
|
||||
"""
|
||||
|
||||
def index(self, req, tenant_id):
|
||||
"""
|
||||
Return all backups information for a tenant ID.
|
||||
"""
|
||||
LOG.debug("Listing Backups for tenant '%s'" % tenant_id)
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
backups = Backup.list(context)
|
||||
return wsgi.Result(views.BackupViews(backups).data(), 200)
|
||||
|
||||
def show(self, req, tenant_id, id):
|
||||
"""Return a single backup."""
|
||||
LOG.info(_("Showing a backup for tenant '%s'") % tenant_id)
|
||||
LOG.info(_("id : '%s'\n\n") % id)
|
||||
backup = Backup.get_by_id(id)
|
||||
return wsgi.Result(views.BackupView(backup).data(), 200)
|
||||
|
||||
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']
|
||||
name = data['name']
|
||||
desc = data.get('description')
|
||||
backup = Backup.create(context, instance, name, desc)
|
||||
return wsgi.Result(views.BackupView(backup).data(), 202)
|
||||
|
||||
def delete(self, req, tenant_id, id):
|
||||
LOG.debug("Delete Backup for tenant: %s, ID: %s" % (tenant_id, id))
|
||||
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.ReddwarfError(
|
||||
"Required element/key - %s was not specified" % e)
|
|
@ -0,0 +1,48 @@
|
|||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2013 OpenStack LLC.
|
||||
# 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.
|
||||
|
||||
|
||||
class BackupView(object):
|
||||
|
||||
def __init__(self, backup):
|
||||
self.backup = backup
|
||||
|
||||
def data(self):
|
||||
return {"backup": {
|
||||
"id": self.backup.id,
|
||||
"name": self.backup.name,
|
||||
"description": self.backup.description,
|
||||
"locationRef": self.backup.location,
|
||||
"instance_id": self.backup.instance_id,
|
||||
"created": self.backup.created,
|
||||
"updated": self.backup.updated,
|
||||
"status": self.backup.state
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class BackupViews(object):
|
||||
|
||||
def __init__(self, backups):
|
||||
self.backups = backups
|
||||
|
||||
def data(self):
|
||||
backups = []
|
||||
|
||||
for b in self.backups:
|
||||
backups.append(BackupView(b).data()["backup"])
|
||||
return {"backups": backups}
|
|
@ -14,14 +14,11 @@
|
|||
|
||||
import routes
|
||||
|
||||
from reddwarf.common import exception
|
||||
from reddwarf.common import wsgi
|
||||
from reddwarf.extensions.mgmt.host.instance import service as hostservice
|
||||
from reddwarf.flavor.service import FlavorController
|
||||
from reddwarf.instance.service import InstanceController
|
||||
from reddwarf.limits.service import LimitsController
|
||||
from reddwarf.openstack.common import log as logging
|
||||
from reddwarf.openstack.common import rpc
|
||||
from reddwarf.backup.service import BackupController
|
||||
from reddwarf.versions import VersionsController
|
||||
|
||||
|
||||
|
@ -34,6 +31,7 @@ class API(wsgi.Router):
|
|||
self._flavor_router(mapper)
|
||||
self._versions_router(mapper)
|
||||
self._limits_router(mapper)
|
||||
self._backups_router(mapper)
|
||||
|
||||
def _versions_router(self, mapper):
|
||||
versions_resource = VersionsController().create_resource()
|
||||
|
@ -43,7 +41,7 @@ class API(wsgi.Router):
|
|||
instance_resource = InstanceController().create_resource()
|
||||
path = "/{tenant_id}/instances"
|
||||
mapper.resource("instance", path, controller=instance_resource,
|
||||
member={'action': 'POST'})
|
||||
member={'action': 'POST', 'backups': 'GET'})
|
||||
|
||||
def _flavor_router(self, mapper):
|
||||
flavor_resource = FlavorController().create_resource()
|
||||
|
@ -55,6 +53,12 @@ class API(wsgi.Router):
|
|||
path = "/{tenant_id}/limits"
|
||||
mapper.resource("limits", path, controller=limits_resource)
|
||||
|
||||
def _backups_router(self, mapper):
|
||||
backups_resource = BackupController().create_resource()
|
||||
path = "/{tenant_id}/backups"
|
||||
mapper.resource("backups", path, controller=backups_resource,
|
||||
member={'action': 'POST'})
|
||||
|
||||
|
||||
def app_factory(global_conf, **local_conf):
|
||||
return API()
|
||||
|
|
|
@ -42,6 +42,7 @@ common_opts = [
|
|||
help='Remote implementation for using fake integration code'),
|
||||
cfg.StrOpt('nova_compute_url', default='http://localhost:8774/v2'),
|
||||
cfg.StrOpt('nova_volume_url', default='http://localhost:8776/v2'),
|
||||
cfg.StrOpt('swift_url', default='http://localhost:8080/v1/AUTH_'),
|
||||
cfg.StrOpt('reddwarf_auth_url', default='http://0.0.0.0:5000/v2.0'),
|
||||
cfg.StrOpt('backup_swift_container', default='DBaaS-backup'),
|
||||
cfg.StrOpt('host', default='0.0.0.0'),
|
||||
|
@ -83,6 +84,8 @@ common_opts = [
|
|||
help='default maximum volume size for an instance'),
|
||||
cfg.IntOpt('max_volumes_per_user', default=20,
|
||||
help='default maximum for total volume used by a tenant'),
|
||||
cfg.IntOpt('max_backups_per_user', default=5,
|
||||
help='default maximum number of backups created by a tenant'),
|
||||
cfg.StrOpt('quota_driver',
|
||||
default='reddwarf.quota.quota.DbQuotaDriver',
|
||||
help='default driver to use for quota checks'),
|
||||
|
|
|
@ -249,3 +249,19 @@ class SecurityGroupRuleCreationError(ReddwarfError):
|
|||
class SecurityGroupRuleDeletionError(ReddwarfError):
|
||||
|
||||
message = _("Failed to delete Security Group Rule.")
|
||||
|
||||
|
||||
class BackupNotCompleteError(ReddwarfError):
|
||||
|
||||
message = _("Unable to create instance because backup %(backup_id)s is "
|
||||
"not completed")
|
||||
|
||||
|
||||
class BackupFileNotFound(NotFound):
|
||||
message = _("Backup file in %(location)s was not found in the object "
|
||||
"storage.")
|
||||
|
||||
|
||||
class SwiftAuthError(ReddwarfError):
|
||||
|
||||
message = _("Swift account not accessible for tenant %(tenant_id)s.")
|
||||
|
|
|
@ -17,13 +17,14 @@
|
|||
|
||||
from reddwarf.common import cfg
|
||||
from novaclient.v1_1.client import Client
|
||||
from swiftclient.client import Connection
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
COMPUTE_URL = CONF.nova_compute_url
|
||||
PROXY_AUTH_URL = CONF.reddwarf_auth_url
|
||||
VOLUME_URL = CONF.nova_volume_url
|
||||
PROXY_AUTH_URL = CONF.reddwarf_auth_url
|
||||
OBJECT_STORE_URL = CONF.swift_url
|
||||
|
||||
|
||||
def create_dns_client(context):
|
||||
|
@ -56,12 +57,18 @@ def create_nova_volume_client(context):
|
|||
return client
|
||||
|
||||
|
||||
if CONF.remote_implementation == "fake":
|
||||
# Override the functions above with fakes.
|
||||
def create_swift_client(context):
|
||||
client = Connection(preauthurl=OBJECT_STORE_URL + context.tenant,
|
||||
preauthtoken=context.auth_token,
|
||||
tenant_name=context.tenant)
|
||||
return client
|
||||
|
||||
# Override the functions above with fakes.
|
||||
if CONF.remote_implementation == "fake":
|
||||
from reddwarf.tests.fakes.nova import fake_create_nova_client
|
||||
from reddwarf.tests.fakes.nova import fake_create_nova_volume_client
|
||||
from reddwarf.tests.fakes.guestagent import fake_create_guest_client
|
||||
from reddwarf.tests.fakes.swift import FakeSwiftClient
|
||||
|
||||
def create_guest_client(context, id):
|
||||
return fake_create_guest_client(context, id)
|
||||
|
@ -71,3 +78,6 @@ if CONF.remote_implementation == "fake":
|
|||
|
||||
def create_nova_volume_client(context):
|
||||
return fake_create_nova_volume_client(context)
|
||||
|
||||
def create_swift_client(context):
|
||||
return FakeSwiftClient.Connection(context)
|
||||
|
|
|
@ -102,7 +102,7 @@ CUSTOM_SERIALIZER_METADATA = {
|
|||
# mgmt/account
|
||||
'account': {'id': '', 'num_instances': ''},
|
||||
# mgmt/quotas
|
||||
'quotas': {'instances': '', 'volumes': ''},
|
||||
'quotas': {'instances': '', 'volumes': '', 'backups': ''},
|
||||
#mgmt/instance
|
||||
'guest_status': {'state_description': ''},
|
||||
#mgmt/instance/diagnostics
|
||||
|
@ -367,6 +367,7 @@ class Controller(object):
|
|||
],
|
||||
webob.exc.HTTPUnauthorized: [
|
||||
exception.Forbidden,
|
||||
exception.SwiftAuthError,
|
||||
],
|
||||
webob.exc.HTTPBadRequest: [
|
||||
exception.InvalidModelError,
|
||||
|
@ -383,8 +384,11 @@ class Controller(object):
|
|||
exception.UserNotFound,
|
||||
exception.DatabaseNotFound,
|
||||
exception.QuotaResourceUnknown,
|
||||
exception.BackupFileNotFound
|
||||
],
|
||||
webob.exc.HTTPConflict: [
|
||||
exception.BackupNotCompleteError,
|
||||
],
|
||||
webob.exc.HTTPConflict: [],
|
||||
webob.exc.HTTPRequestEntityTooLarge: [
|
||||
exception.OverLimit,
|
||||
exception.QuotaExceeded,
|
||||
|
|
|
@ -17,31 +17,25 @@
|
|||
|
||||
"""Model classes that form the core of instances functionality."""
|
||||
|
||||
import eventlet
|
||||
import netaddr
|
||||
|
||||
from datetime import datetime
|
||||
from novaclient import exceptions as nova_exceptions
|
||||
from reddwarf.common import cfg
|
||||
from reddwarf.common import exception
|
||||
from reddwarf.common import utils
|
||||
from reddwarf.common.remote import create_dns_client
|
||||
from reddwarf.common.remote import create_guest_client
|
||||
from reddwarf.common.remote import create_nova_client
|
||||
from reddwarf.common.remote import create_nova_volume_client
|
||||
from reddwarf.extensions.security_group.models import SecurityGroup
|
||||
from reddwarf.db import models as dbmodels
|
||||
from reddwarf.backup.models import Backup
|
||||
from reddwarf.quota.quota import run_with_quotas
|
||||
from reddwarf.instance.tasks import InstanceTask
|
||||
from reddwarf.instance.tasks import InstanceTasks
|
||||
from reddwarf.guestagent import models as agent_models
|
||||
from reddwarf.taskmanager import api as task_api
|
||||
from reddwarf.openstack.common import log as logging
|
||||
from reddwarf.openstack.common.gettextutils import _
|
||||
|
||||
|
||||
from eventlet import greenthread
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
@ -68,6 +62,7 @@ class InstanceStatus(object):
|
|||
FAILED = "FAILED"
|
||||
REBOOT = "REBOOT"
|
||||
RESIZE = "RESIZE"
|
||||
BACKUP = "BACKUP"
|
||||
SHUTDOWN = "SHUTDOWN"
|
||||
ERROR = "ERROR"
|
||||
|
||||
|
@ -81,22 +76,6 @@ def validate_volume_size(size):
|
|||
raise exception.VolumeQuotaExceeded(msg)
|
||||
|
||||
|
||||
def run_with_quotas(tenant_id, deltas, f):
|
||||
""" Quota wrapper """
|
||||
|
||||
from reddwarf.quota.quota import QUOTAS as quota_engine
|
||||
reservations = quota_engine.reserve(tenant_id, **deltas)
|
||||
result = None
|
||||
try:
|
||||
result = f()
|
||||
except:
|
||||
quota_engine.rollback(reservations)
|
||||
raise
|
||||
else:
|
||||
quota_engine.commit(reservations)
|
||||
return result
|
||||
|
||||
|
||||
def load_simple_instance_server_status(context, db_info):
|
||||
"""Loads a server or raises an exception."""
|
||||
if 'BUILDING' == db_info.task_status.action:
|
||||
|
@ -202,6 +181,10 @@ class SimpleInstance(object):
|
|||
"RESIZE"]:
|
||||
return self.db_info.server_status
|
||||
|
||||
### Check if there is a backup running for this instance
|
||||
if Backup.running(self.id):
|
||||
return InstanceStatus.BACKUP
|
||||
|
||||
### Report as Shutdown while deleting, unless there's an error.
|
||||
if 'DELETING' == ACTION:
|
||||
if self.db_info.server_status in ["ACTIVE", "SHUTDOWN", "DELETED"]:
|
||||
|
@ -430,7 +413,8 @@ class Instance(BuiltInstance):
|
|||
|
||||
@classmethod
|
||||
def create(cls, context, name, flavor_id, image_id,
|
||||
databases, users, service_type, volume_size):
|
||||
databases, users, service_type, volume_size, backup_id):
|
||||
|
||||
def _create_resources():
|
||||
client = create_nova_client(context)
|
||||
security_groups = None
|
||||
|
@ -439,6 +423,16 @@ class Instance(BuiltInstance):
|
|||
except nova_exceptions.NotFound:
|
||||
raise exception.FlavorNotFound(uuid=flavor_id)
|
||||
|
||||
if backup_id is not None:
|
||||
backup_info = Backup.get_by_id(backup_id)
|
||||
if backup_info.is_running:
|
||||
raise exception.BackupNotCompleteError(backup_id=backup_id)
|
||||
|
||||
location = backup_info.location
|
||||
LOG.info(_("Checking if backup exist in '%s'") % location)
|
||||
if not Backup.check_object_exist(context, location):
|
||||
raise exception.BackupFileNotFound(location=location)
|
||||
|
||||
db_info = DBInstance.create(name=name, flavor_id=flavor_id,
|
||||
tenant_id=context.tenant,
|
||||
volume_size=volume_size,
|
||||
|
@ -466,7 +460,7 @@ class Instance(BuiltInstance):
|
|||
flavor.ram, image_id,
|
||||
databases, users,
|
||||
service_type, volume_size,
|
||||
security_groups)
|
||||
security_groups, backup_id)
|
||||
|
||||
return SimpleInstance(context, db_info, service_status)
|
||||
|
||||
|
@ -476,7 +470,7 @@ class Instance(BuiltInstance):
|
|||
_create_resources)
|
||||
|
||||
def resize_flavor(self, new_flavor_id):
|
||||
self._validate_can_perform_action()
|
||||
self.validate_can_perform_action()
|
||||
LOG.debug("resizing instance %s flavor to %s"
|
||||
% (self.id, new_flavor_id))
|
||||
# Validate that the flavor can be found and that it isn't the same size
|
||||
|
@ -501,7 +495,7 @@ class Instance(BuiltInstance):
|
|||
|
||||
def resize_volume(self, new_size):
|
||||
def _resize_resources():
|
||||
self._validate_can_perform_action()
|
||||
self.validate_can_perform_action()
|
||||
LOG.info("Resizing volume of instance %s..." % self.id)
|
||||
if not self.volume_size:
|
||||
raise exception.BadRequest("Instance %s has no volume."
|
||||
|
@ -522,13 +516,13 @@ class Instance(BuiltInstance):
|
|||
_resize_resources)
|
||||
|
||||
def reboot(self):
|
||||
self._validate_can_perform_action()
|
||||
self.validate_can_perform_action()
|
||||
LOG.info("Rebooting instance %s..." % self.id)
|
||||
self.update_db(task_status=InstanceTasks.REBOOTING)
|
||||
task_api.API(self.context).reboot(self.id)
|
||||
|
||||
def restart(self):
|
||||
self._validate_can_perform_action()
|
||||
self.validate_can_perform_action()
|
||||
LOG.info("Restarting MySQL on instance %s..." % self.id)
|
||||
# Set our local status since Nova might not change it quick enough.
|
||||
#TODO(tim.simpson): Possible bad stuff can happen if this service
|
||||
|
@ -540,7 +534,7 @@ class Instance(BuiltInstance):
|
|||
task_api.API(self.context).restart(self.id)
|
||||
|
||||
def migrate(self):
|
||||
self._validate_can_perform_action()
|
||||
self.validate_can_perform_action()
|
||||
LOG.info("Migrating instance %s..." % self.id)
|
||||
self.update_db(task_status=InstanceTasks.MIGRATING)
|
||||
task_api.API(self.context).migrate(self.id)
|
||||
|
@ -549,7 +543,7 @@ class Instance(BuiltInstance):
|
|||
LOG.info("Settting task status to NONE on instance %s..." % self.id)
|
||||
self.update_db(task_status=InstanceTasks.NONE)
|
||||
|
||||
def _validate_can_perform_action(self):
|
||||
def validate_can_perform_action(self):
|
||||
"""
|
||||
Raises exception if an instance action cannot currently be performed.
|
||||
"""
|
||||
|
@ -560,6 +554,8 @@ class Instance(BuiltInstance):
|
|||
status = self.db_info.task_status
|
||||
elif not self.service_status.status.action_is_allowed:
|
||||
status = self.status
|
||||
elif Backup.running(self.id):
|
||||
status = InstanceStatus.BACKUP
|
||||
else:
|
||||
return
|
||||
msg = ("Instance is not currently available for an action to be "
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import routes
|
||||
import webob.exc
|
||||
|
||||
from reddwarf.common import cfg
|
||||
|
@ -26,6 +25,8 @@ from reddwarf.common import wsgi
|
|||
from reddwarf.extensions.mysql.common import populate_databases
|
||||
from reddwarf.extensions.mysql.common import populate_users
|
||||
from reddwarf.instance import models, views
|
||||
from reddwarf.backup.models import Backup as backup_model
|
||||
from reddwarf.backup import views as backup_views
|
||||
from reddwarf.openstack.common import log as logging
|
||||
from reddwarf.openstack.common.gettextutils import _
|
||||
|
||||
|
@ -141,6 +142,15 @@ class InstanceController(wsgi.Controller):
|
|||
marker)
|
||||
return wsgi.Result(paged.data(), 200)
|
||||
|
||||
def backups(self, req, tenant_id, id):
|
||||
"""Return all backups for the specified instance."""
|
||||
LOG.info(_("req : '%s'\n\n") % req)
|
||||
LOG.info(_("Indexing backups for instance '%s'") %
|
||||
id)
|
||||
|
||||
backups = backup_model.list_for_instance(id)
|
||||
return wsgi.Result(backup_views.BackupViews(backups).data(), 200)
|
||||
|
||||
def show(self, req, tenant_id, id):
|
||||
"""Return a single instance."""
|
||||
LOG.info(_("req : '%s'\n\n") % req)
|
||||
|
@ -194,9 +204,17 @@ class InstanceController(wsgi.Controller):
|
|||
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
|
||||
|
||||
instance = models.Instance.create(context, name, flavor_id,
|
||||
image_id, databases, users,
|
||||
service_type, volume_size)
|
||||
service_type, volume_size,
|
||||
backup_id)
|
||||
|
||||
view = views.InstanceDetailView(instance, req=req)
|
||||
return wsgi.Result(view.data(), 200)
|
||||
|
|
|
@ -76,6 +76,7 @@ class Resource(object):
|
|||
|
||||
INSTANCES = 'instances'
|
||||
VOLUMES = 'volumes'
|
||||
BACKUPS = 'backups'
|
||||
|
||||
def __init__(self, name, flag=None):
|
||||
"""
|
||||
|
|
|
@ -310,6 +310,22 @@ QUOTAS = QuotaEngine()
|
|||
|
||||
''' Define all kind of resources here '''
|
||||
resources = [Resource(Resource.INSTANCES, 'max_instances_per_user'),
|
||||
Resource(Resource.VOLUMES, 'max_volumes_per_user')]
|
||||
Resource(Resource.VOLUMES, 'max_volumes_per_user'),
|
||||
Resource(Resource.BACKUPS, 'max_backups_per_user')]
|
||||
|
||||
QUOTAS.register_resources(resources)
|
||||
|
||||
|
||||
def run_with_quotas(tenant_id, deltas, f):
|
||||
""" Quota wrapper """
|
||||
|
||||
reservations = QUOTAS.reserve(tenant_id, **deltas)
|
||||
result = None
|
||||
try:
|
||||
result = f()
|
||||
except:
|
||||
QUOTAS.rollback(reservations)
|
||||
raise
|
||||
else:
|
||||
QUOTAS.commit(reservations)
|
||||
return result
|
||||
|
|
|
@ -88,12 +88,23 @@ class API(ManagerAPI):
|
|||
LOG.debug("Making async call to delete instance: %s" % instance_id)
|
||||
self._cast("delete_instance", instance_id=instance_id)
|
||||
|
||||
def create_backup(self, backup_id, instance_id):
|
||||
LOG.debug("Making async call to create a backup for instance: %s" %
|
||||
instance_id)
|
||||
self._cast("create_backup",
|
||||
backup_id=backup_id,
|
||||
instance_id=instance_id)
|
||||
|
||||
def delete_backup(self, backup_id):
|
||||
LOG.debug("Making async call to delete backup: %s" % backup_id)
|
||||
self._cast("delete_backup", backup_id=backup_id)
|
||||
|
||||
def create_instance(self, instance_id, name, flavor_id, flavor_ram,
|
||||
image_id, databases, users, service_type, volume_size,
|
||||
security_groups):
|
||||
image_id, databases, users, service_type,
|
||||
volume_size, security_groups, backup_id=None):
|
||||
LOG.debug("Making async call to create instance %s " % instance_id)
|
||||
self._cast("create_instance", instance_id=instance_id, name=name,
|
||||
flavor_id=flavor_id, flavor_ram=flavor_ram,
|
||||
image_id=image_id, databases=databases, users=users,
|
||||
service_type=service_type, volume_size=volume_size,
|
||||
security_groups=security_groups)
|
||||
security_groups=security_groups, backup_id=backup_id)
|
||||
|
|
|
@ -15,22 +15,13 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import traceback
|
||||
|
||||
from eventlet import greenthread
|
||||
|
||||
from reddwarf.common import exception
|
||||
from reddwarf.openstack.common import log as logging
|
||||
from reddwarf.openstack.common import periodic_task
|
||||
from reddwarf.openstack.common.rpc.common import UnsupportedRpcVersion
|
||||
from reddwarf.openstack.common.gettextutils import _
|
||||
from reddwarf.taskmanager import models
|
||||
from reddwarf.taskmanager.models import BuiltInstanceTasks
|
||||
from reddwarf.taskmanager.models import FreshInstanceTasks
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
RPC_API_VERSION = "1.0"
|
||||
|
||||
|
||||
|
@ -68,9 +59,16 @@ class Manager(periodic_task.PeriodicTasks):
|
|||
instance_id)
|
||||
instance_tasks.delete_async()
|
||||
|
||||
def delete_backup(self, context, backup_id):
|
||||
models.BackupTasks.delete_backup(backup_id)
|
||||
|
||||
def create_backup(self, context, backup_id, instance_id):
|
||||
instance_tasks = models.BuiltInstanceTasks.load(context, instance_id)
|
||||
instance_tasks.create_backup(backup_id)
|
||||
|
||||
def create_instance(self, context, instance_id, name, flavor_id,
|
||||
flavor_ram, image_id, databases, users, service_type,
|
||||
volume_size, security_groups):
|
||||
volume_size, security_groups, backup_id):
|
||||
instance_tasks = FreshInstanceTasks.load(context, instance_id)
|
||||
instance_tasks.create_instance(flavor_id, flavor_ram, image_id,
|
||||
databases, users, service_type,
|
||||
|
|
|
@ -360,6 +360,18 @@ class BuiltInstanceTasks(BuiltInstance):
|
|||
action = MigrateAction(self)
|
||||
action.execute()
|
||||
|
||||
def create_backup(self, backup_id):
|
||||
# TODO
|
||||
# create a temp volume
|
||||
# nova list
|
||||
# nova show
|
||||
# check in progress - make sure no other snapshot creation in progress
|
||||
# volume create
|
||||
# volume attach
|
||||
# call GA.create_backup()
|
||||
self.guest.create_backup(backup_id)
|
||||
LOG.debug("Called create_backup %s " % self.id)
|
||||
|
||||
def reboot(self):
|
||||
try:
|
||||
LOG.debug("Instance %s calling stop_db..." % self.id)
|
||||
|
|
|
@ -0,0 +1,161 @@
|
|||
# Copyright 2011 OpenStack LLC.
|
||||
# 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.
|
||||
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.decorators import time_out
|
||||
from reddwarf.tests.util import poll_until
|
||||
from reddwarfclient import exceptions
|
||||
from reddwarf.tests.api.instances import WaitForGuestInstallationToFinish
|
||||
from reddwarf.tests.api.instances import instance_info, assert_unprocessable
|
||||
|
||||
|
||||
GROUP = "dbaas.api.backups"
|
||||
BACKUP_NAME = 'backup_test'
|
||||
BACKUP_DESC = 'test description'
|
||||
|
||||
|
||||
backup_info = None
|
||||
|
||||
|
||||
@test(depends_on_classes=[WaitForGuestInstallationToFinish],
|
||||
groups=[GROUP])
|
||||
class CreateBackups(object):
|
||||
|
||||
@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)
|
||||
|
||||
@test
|
||||
def test_backup_create_instance(self):
|
||||
"""test create backup for a given instance"""
|
||||
result = instance_info.dbaas.backups.create(BACKUP_NAME,
|
||||
instance_info.id,
|
||||
BACKUP_DESC)
|
||||
assert_equal(BACKUP_NAME, result.name)
|
||||
assert_equal(BACKUP_DESC, result.description)
|
||||
assert_equal(instance_info.id, result.instance_id)
|
||||
assert_equal('NEW', result.status)
|
||||
instance = instance_info.dbaas.instances.list()[0]
|
||||
assert_equal('BACKUP', instance.status)
|
||||
global backup_info
|
||||
backup_info = result
|
||||
|
||||
|
||||
@test(runs_after=[CreateBackups],
|
||||
groups=[GROUP])
|
||||
class AfterBackupCreation(object):
|
||||
|
||||
@test
|
||||
def test_instance_action_right_after_backup_create(self):
|
||||
"""test any instance action while backup is running"""
|
||||
assert_unprocessable(instance_info.dbaas.instances.resize_volume,
|
||||
instance_info.id, 1)
|
||||
|
||||
@test
|
||||
def test_backup_create_another_backup_running(self):
|
||||
"""test create backup when another backup is running"""
|
||||
assert_unprocessable(instance_info.dbaas.backups.create,
|
||||
'backup_test2', instance_info.id,
|
||||
'test description2')
|
||||
|
||||
@test
|
||||
def test_backup_delete_still_running(self):
|
||||
"""test delete backup when it is running"""
|
||||
result = instance_info.dbaas.backups.list()
|
||||
backup = result[0]
|
||||
assert_unprocessable(instance_info.dbaas.backups.delete, backup.id)
|
||||
|
||||
@test
|
||||
def test_backup_create_quota_exceeded(self):
|
||||
"""test quota exceeded when creating a backup"""
|
||||
instance_info.dbaas_admin.quota.update(instance_info.user.tenant_id,
|
||||
{'backups': 1})
|
||||
assert_raises(exceptions.OverLimit,
|
||||
instance_info.dbaas.backups.create,
|
||||
'Too_many_backups', instance_info.id, BACKUP_DESC)
|
||||
|
||||
|
||||
@test(runs_after=[AfterBackupCreation],
|
||||
groups=[GROUP])
|
||||
class WaitForBackupCreateToFinish(object):
|
||||
"""
|
||||
Wait until the backup create is finished.
|
||||
"""
|
||||
|
||||
@test
|
||||
@time_out(60 * 30)
|
||||
def test_backup_created(self):
|
||||
# This version just checks the REST API status.
|
||||
def result_is_active():
|
||||
backup = instance_info.dbaas.backups.get(backup_info.id)
|
||||
if backup.status == "COMPLETED":
|
||||
return True
|
||||
else:
|
||||
assert_not_equal("FAILED", backup.status)
|
||||
return False
|
||||
|
||||
poll_until(result_is_active)
|
||||
|
||||
|
||||
@test(depends_on=[WaitForBackupCreateToFinish],
|
||||
groups=[GROUP])
|
||||
class ListBackups(object):
|
||||
|
||||
@test
|
||||
def test_backup_list(self):
|
||||
"""test list backups"""
|
||||
result = instance_info.dbaas.backups.list()
|
||||
assert_equal(1, len(result))
|
||||
backup = result[0]
|
||||
assert_equal(BACKUP_NAME, backup.name)
|
||||
assert_equal(BACKUP_DESC, backup.description)
|
||||
assert_equal(instance_info.id, backup.instance_id)
|
||||
assert_equal('COMPLETED', backup.status)
|
||||
|
||||
@test
|
||||
def test_backup_list_for_instance(self):
|
||||
"""test list backups"""
|
||||
result = instance_info.dbaas.instances.backups(instance_info.id)
|
||||
assert_equal(1, len(result))
|
||||
backup = result[0]
|
||||
assert_equal(BACKUP_NAME, backup.name)
|
||||
assert_equal(BACKUP_DESC, backup.description)
|
||||
assert_equal(instance_info.id, backup.instance_id)
|
||||
assert_equal('COMPLETED', backup.status)
|
||||
|
||||
@test
|
||||
def test_backup_get(self):
|
||||
"""test get backup"""
|
||||
backup = instance_info.dbaas.backups.get(backup_info.id)
|
||||
assert_equal(backup_info.id, backup.id)
|
||||
assert_equal(backup_info.name, backup.name)
|
||||
assert_equal(backup_info.description, backup.description)
|
||||
assert_equal(instance_info.id, backup.instance_id)
|
||||
assert_equal('COMPLETED', backup.status)
|
||||
|
||||
|
||||
@test(runs_after=[ListBackups],
|
||||
groups=[GROUP])
|
||||
class DeleteBackups(object):
|
||||
|
||||
@test
|
||||
def test_backup_delete_not_found(self):
|
||||
"""test delete unknown backup"""
|
||||
assert_raises(exceptions.NotFound, instance_info.dbaas.backups.delete,
|
||||
'nonexistent_backup')
|
|
@ -267,6 +267,15 @@ class FakeGuest(object):
|
|||
} for db in current_grants]
|
||||
return dbs
|
||||
|
||||
def create_backup(self, backup_id):
|
||||
from reddwarf.backup.models import Backup, BackupState
|
||||
backup = Backup.get_by_id(backup_id)
|
||||
|
||||
def finish_create_backup():
|
||||
backup.state = BackupState.COMPLETED
|
||||
backup.save()
|
||||
self.event_spawn(1.0, finish_create_backup)
|
||||
|
||||
|
||||
def get_or_create(id):
|
||||
if id not in DB:
|
||||
|
|
|
@ -0,0 +1,398 @@
|
|||
import uuid
|
||||
import logging
|
||||
from mockito import when, any
|
||||
import swiftclient.client as swift_client
|
||||
import swiftclient
|
||||
|
||||
|
||||
# Copyright (C) 2012 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 httplib
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
|
||||
from swiftclient import client as swift
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FakeSwiftClient(object):
|
||||
"""Logs calls instead of executing."""
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def Connection(self, *args, **kargs):
|
||||
LOG.debug("fake FakeSwiftClient Connection")
|
||||
return FakeSwiftConnection()
|
||||
|
||||
|
||||
class FakeSwiftConnection(object):
|
||||
"""Logging calls instead of executing"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def get_auth(self):
|
||||
return (
|
||||
u"http://127.0.0.1:8080/v1/AUTH_c7b038976df24d96bf1980f5da17bd89",
|
||||
u'MIINrwYJKoZIhvcNAQcCoIINoDCCDZwCAQExCTAHBgUrDgMCGjCCDIgGCSqGSIb3'
|
||||
u'DQEHAaCCDHkEggx1eyJhY2Nlc3MiOiB7InRva2VuIjogeyJpc3N1ZWRfYXQiOiAi'
|
||||
u'MjAxMy0wMy0xOFQxODoxMzoyMC41OTMyNzYiLCAiZXhwaXJlcyI6ICIyMDEzLTAz'
|
||||
u'LTE5VDE4OjEzOjIwWiIsICJpZCI6ICJwbGFjZWhvbGRlciIsICJ0ZW5hbnQiOiB7'
|
||||
u'ImVuYWJsZWQiOiB0cnVlLCAiZGVzY3JpcHRpb24iOiBudWxsLCAibmFtZSI6ICJy'
|
||||
u'ZWRkd2FyZiIsICJpZCI6ICJjN2IwMzg5NzZkZjI0ZDk2YmYxOTgwZjVkYTE3YmQ4'
|
||||
u'OSJ9fSwgInNlcnZpY2VDYXRhbG9nIjogW3siZW5kcG9pbnRzIjogW3siYWRtaW5')
|
||||
|
||||
def get_account(self):
|
||||
return ({'content-length': '2', 'accept-ranges': 'bytes',
|
||||
'x-timestamp': '1363049003.92304',
|
||||
'x-trans-id': 'tx9e5da02c49ed496395008309c8032a53',
|
||||
'date': 'Tue, 10 Mar 2013 00:43:23 GMT',
|
||||
'x-account-bytes-used': '0',
|
||||
'x-account-container-count': '0',
|
||||
'content-type': 'application/json; charset=utf-8',
|
||||
'x-account-object-count': '0'}, [])
|
||||
|
||||
def head_container(self, container):
|
||||
LOG.debug("fake head_container(%s)" % container)
|
||||
if container == 'missing_container':
|
||||
raise swift.ClientException('fake exception',
|
||||
http_status=httplib.NOT_FOUND)
|
||||
elif container == 'unauthorized_container':
|
||||
raise swift.ClientException('fake exception',
|
||||
http_status=httplib.UNAUTHORIZED)
|
||||
elif container == 'socket_error_on_head':
|
||||
raise socket.error(111, 'ECONNREFUSED')
|
||||
pass
|
||||
|
||||
def put_container(self, container):
|
||||
LOG.debug("fake put_container(%s)" % container)
|
||||
pass
|
||||
|
||||
def get_container(self, container, **kwargs):
|
||||
LOG.debug("fake get_container(%s)" % container)
|
||||
fake_header = None
|
||||
fake_body = [{'name': 'backup_001'},
|
||||
{'name': 'backup_002'},
|
||||
{'name': 'backup_003'}]
|
||||
return fake_header, fake_body
|
||||
|
||||
def head_object(self, container, name):
|
||||
LOG.debug("fake put_container(%s, %s)" % (container, name))
|
||||
return {'etag': 'fake-md5-sum'}
|
||||
|
||||
def get_object(self, container, name):
|
||||
LOG.debug("fake get_object(%s, %s)" % (container, name))
|
||||
if container == 'socket_error_on_get':
|
||||
raise socket.error(111, 'ECONNREFUSED')
|
||||
if 'metadata' in name:
|
||||
fake_object_header = None
|
||||
metadata = {}
|
||||
if container == 'unsupported_version':
|
||||
metadata['version'] = '9.9.9'
|
||||
else:
|
||||
metadata['version'] = '1.0.0'
|
||||
metadata['backup_id'] = 123
|
||||
metadata['volume_id'] = 123
|
||||
metadata['backup_name'] = 'fake backup'
|
||||
metadata['backup_description'] = 'fake backup description'
|
||||
metadata['created_at'] = '2013-02-19 11:20:54,805'
|
||||
metadata['objects'] = [{
|
||||
'backup_001': {'compression': 'zlib', 'length': 10},
|
||||
'backup_002': {'compression': 'zlib', 'length': 10},
|
||||
'backup_003': {'compression': 'zlib', 'length': 10}
|
||||
}]
|
||||
metadata_json = json.dumps(metadata, sort_keys=True, indent=2)
|
||||
fake_object_body = metadata_json
|
||||
return (fake_object_header, fake_object_body)
|
||||
|
||||
fake_header = None
|
||||
fake_object_body = os.urandom(1024 * 1024)
|
||||
return (fake_header, fake_object_body)
|
||||
|
||||
def put_object(self, container, name, reader):
|
||||
LOG.debug("fake put_object(%s, %s)" % (container, name))
|
||||
if container == 'socket_error_on_put':
|
||||
raise socket.error(111, 'ECONNREFUSED')
|
||||
return 'fake-md5-sum'
|
||||
|
||||
def delete_object(self, container, name):
|
||||
LOG.debug("fake delete_object(%s, %s)" % (container, name))
|
||||
if container == 'socket_error_on_delete':
|
||||
raise socket.error(111, 'ECONNREFUSED')
|
||||
pass
|
||||
|
||||
|
||||
class SwiftClientStub(object):
|
||||
"""
|
||||
Component for controlling behavior of Swift Client Stub. Instantiated
|
||||
before tests are invoked in "fake" mode. Invoke methods to control
|
||||
behavior so that systems under test can interact with this as it is a
|
||||
real swift client with a real backend
|
||||
|
||||
example:
|
||||
|
||||
if FAKE:
|
||||
swift_stub = SwiftClientStub()
|
||||
swift_stub.with_account('xyz')
|
||||
|
||||
# returns swift account info and auth token
|
||||
component_using_swift.get_swift_account()
|
||||
|
||||
if FAKE:
|
||||
swift_stub.with_container('test-container-name')
|
||||
|
||||
# returns swift container information - mostly faked
|
||||
component_using.swift.create_container('test-container-name')
|
||||
component_using_swift.get_container_info('test-container-name')
|
||||
|
||||
if FAKE:
|
||||
swift_stub.with_object('test-container-name', 'test-object-name',
|
||||
'test-object-contents')
|
||||
|
||||
# returns swift object info and contents
|
||||
component_using_swift.create_object('test-container-name',
|
||||
'test-object-name', 'test-contents')
|
||||
component_using_swift.get_object('test-container-name', 'test-object-name')
|
||||
|
||||
if FAKE:
|
||||
swift_stub.without_object('test-container-name', 'test-object-name')
|
||||
|
||||
# allows object to be removed ONCE
|
||||
component_using_swift.remove_object('test-container-name',
|
||||
'test-object-name')
|
||||
# throws ClientException - 404
|
||||
component_using_swift.get_object('test-container-name', 'test-object-name')
|
||||
component_using_swift.remove_object('test-container-name',
|
||||
'test-object-name')
|
||||
|
||||
if FAKE:
|
||||
swift_stub.without_object('test-container-name', 'test-object-name')
|
||||
|
||||
# allows container to be removed ONCE
|
||||
component_using_swift.remove_container('test-container-name')
|
||||
# throws ClientException - 404
|
||||
component_using_swift.get_container('test-container-name')
|
||||
component_using_swift.remove_container('test-container-name')
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._connection = swift_client.Connection()
|
||||
# simulate getting an unknown container
|
||||
when(swift_client.Connection).get_container(any()).thenRaise(
|
||||
swiftclient.ClientException('Resource Not Found', http_status=404))
|
||||
|
||||
self._containers = {}
|
||||
self._containers_list = []
|
||||
self._objects = {}
|
||||
|
||||
def _remove_object(self, name, some_list):
|
||||
idx = [i for i, obj in enumerate(some_list) if obj['name'] == name]
|
||||
if len(idx) == 1:
|
||||
del some_list[idx[0]]
|
||||
|
||||
def _ensure_object_exists(self, container, name):
|
||||
self._connection.get_object(container, name)
|
||||
|
||||
def with_account(self, account_id):
|
||||
"""
|
||||
setups up account headers
|
||||
|
||||
example:
|
||||
|
||||
if FAKE:
|
||||
swift_stub = SwiftClientStub()
|
||||
swift_stub.with_account('xyz')
|
||||
|
||||
# returns swift account info and auth token
|
||||
component_using_swift.get_swift_account()
|
||||
|
||||
:param account_id: account id
|
||||
"""
|
||||
|
||||
def account_resp():
|
||||
return ({'content-length': '2', 'accept-ranges': 'bytes',
|
||||
'x-timestamp': '1363049003.92304',
|
||||
'x-trans-id': 'tx9e5da02c49ed496395008309c8032a53',
|
||||
'date': 'Tue, 10 Mar 2013 00:43:23 GMT',
|
||||
'x-account-bytes-used': '0',
|
||||
'x-account-container-count': '0',
|
||||
'content-type': 'application/json; charset=utf-8',
|
||||
'x-account-object-count': '0'}, self._containers_list)
|
||||
|
||||
when(swift_client.Connection).get_auth().thenReturn((
|
||||
u"http://127.0.0.1:8080/v1/AUTH_c7b038976df24d96bf1980f5da17bd89",
|
||||
u'MIINrwYJKoZIhvcNAQcCoIINoDCCDZwCAQExCTAHBgUrDgMCGjCCDIgGCSqGSIb3'
|
||||
u'DQEHAaCCDHkEggx1eyJhY2Nlc3MiOiB7InRva2VuIjogeyJpc3N1ZWRfYXQiOiAi'
|
||||
u'MjAxMy0wMy0xOFQxODoxMzoyMC41OTMyNzYiLCAiZXhwaXJlcyI6ICIyMDEzLTAz'
|
||||
u'LTE5VDE4OjEzOjIwWiIsICJpZCI6ICJwbGFjZWhvbGRlciIsICJ0ZW5hbnQiOiB7'
|
||||
u'ImVuYWJsZWQiOiB0cnVlLCAiZGVzY3JpcHRpb24iOiBudWxsLCAibmFtZSI6ICJy'
|
||||
u'ZWRkd2FyZiIsICJpZCI6ICJjN2IwMzg5NzZkZjI0ZDk2YmYxOTgwZjVkYTE3YmQ4'
|
||||
u'OSJ9fSwgInNlcnZpY2VDYXRhbG9nIjogW3siZW5kcG9pbnRzIjogW3siYWRtaW5')
|
||||
)
|
||||
when(swift_client.Connection).get_account().thenReturn(account_resp())
|
||||
return self
|
||||
|
||||
def _create_container(self, container_name):
|
||||
container = {'count': 0, 'bytes': 0, 'name': container_name}
|
||||
self._containers[container_name] = container
|
||||
self._containers_list.append(container)
|
||||
self._objects[container_name] = []
|
||||
|
||||
def _ensure_container_exists(self, container):
|
||||
self._connection.get_container(container)
|
||||
|
||||
def _delete_container(self, container):
|
||||
self._remove_object(container, self._containers_list)
|
||||
del self._containers[container]
|
||||
del self._objects[container]
|
||||
|
||||
def with_container(self, container_name):
|
||||
"""
|
||||
sets expectations for creating a container and subsequently getting its
|
||||
information
|
||||
|
||||
example:
|
||||
|
||||
if FAKE:
|
||||
swift_stub.with_container('test-container-name')
|
||||
|
||||
# returns swift container information - mostly faked
|
||||
component_using.swift.create_container('test-container-name')
|
||||
component_using_swift.get_container_info('test-container-name')
|
||||
|
||||
:param container_name: container name that is expected to be created
|
||||
"""
|
||||
|
||||
def container_resp(container):
|
||||
return ({'content-length': '2', 'x-container-object-count': '0',
|
||||
'accept-ranges': 'bytes', 'x-container-bytes-used': '0',
|
||||
'x-timestamp': '1363370869.72356',
|
||||
'x-trans-id': 'tx7731801ac6ec4e5f8f7da61cde46bed7',
|
||||
'date': 'Fri, 10 Mar 2013 18:07:58 GMT',
|
||||
'content-type': 'application/json; charset=utf-8'},
|
||||
self._objects[container])
|
||||
|
||||
# if this is called multiple times then nothing happens
|
||||
when(swift_client.Connection).put_container(container_name).thenReturn(
|
||||
None)
|
||||
self._create_container(container_name)
|
||||
# return container headers
|
||||
when(swift_client.Connection).get_container(container_name).thenReturn(
|
||||
container_resp(container_name))
|
||||
|
||||
return self
|
||||
|
||||
def without_container(self, container):
|
||||
"""
|
||||
sets expectations for removing a container and subsequently throwing an
|
||||
exception for further interactions
|
||||
|
||||
example:
|
||||
|
||||
if FAKE:
|
||||
swift_stub.without_container('test-container-name')
|
||||
|
||||
# returns swift container information - mostly faked
|
||||
component_using.swift.remove_container('test-container-name')
|
||||
# throws exception "Resource Not Found - 404"
|
||||
component_using_swift.get_container_info('test-container-name')
|
||||
|
||||
:param container: container name that is expected to be removed
|
||||
"""
|
||||
# first ensure container
|
||||
self._ensure_container_exists(container)
|
||||
# allow one call to get container and then throw exceptions (may need
|
||||
# to be revised
|
||||
when(swift_client.Connection).delete_container(container).thenRaise(
|
||||
swiftclient.ClientException("Resource Not Found", http_status=404))
|
||||
when(swift_client.Connection).get_container(container).thenRaise(
|
||||
swiftclient.ClientException("Resource Not Found", http_status=404))
|
||||
self._delete_container(container)
|
||||
return self
|
||||
|
||||
def with_object(self, container, name, contents):
|
||||
"""
|
||||
sets expectations for creating an object and subsequently getting its
|
||||
contents
|
||||
|
||||
example:
|
||||
|
||||
if FAKE:
|
||||
swift_stub.with_object('test-container-name', 'test-object-name',
|
||||
'test-object-contents')
|
||||
|
||||
# returns swift object info and contents
|
||||
component_using_swift.create_object('test-container-name',
|
||||
'test-object-name', 'test-contents')
|
||||
component_using_swift.get_object('test-container-name',
|
||||
'test-object-name')
|
||||
|
||||
:param container: container name that is the object belongs
|
||||
:param name: the name of the object expected to be created
|
||||
:param contents: the contents of the object
|
||||
"""
|
||||
|
||||
self._connection.get_container(container)
|
||||
when(swift_client.Connection).put_object(container, name,
|
||||
contents).thenReturn(
|
||||
uuid.uuid1())
|
||||
when(swift_client.Connection).get_object(container, name).thenReturn(
|
||||
({'content-length': len(contents), 'accept-ranges': 'bytes',
|
||||
'last-modified': 'Mon, 10 Mar 2013 01:06:34 GMT',
|
||||
'etag': 'eb15a6874ce265e2c3eb1b4891567bab',
|
||||
'x-timestamp': '1363568794.67584',
|
||||
'x-trans-id': 'txef3aaf26c897420c8e77c9750ce6a501',
|
||||
'date': 'Mon, 10 Mar 2013 05:35:14 GMT',
|
||||
'content-type': 'application/octet-stream'}, contents)
|
||||
)
|
||||
self._remove_object(name, self._objects[container])
|
||||
self._objects[container].append(
|
||||
{'bytes': 13, 'last_modified': '2013-03-15T22:10:49.361950',
|
||||
'hash': 'ccc55aefbf92aa66f42b638802c5e7f6', 'name': name,
|
||||
'content_type': 'application/octet-stream'})
|
||||
return self
|
||||
|
||||
def without_object(self, container, name):
|
||||
"""
|
||||
sets expectations for deleting an object
|
||||
|
||||
example:
|
||||
|
||||
if FAKE:
|
||||
swift_stub.without_object('test-container-name', 'test-object-name')
|
||||
|
||||
# allows container to be removed ONCE
|
||||
component_using_swift.remove_container('test-container-name')
|
||||
# throws ClientException - 404
|
||||
component_using_swift.get_container('test-container-name')
|
||||
component_using_swift.remove_container('test-container-name')
|
||||
|
||||
:param container: container name that is the object belongs
|
||||
:param name: the name of the object expected to be removed
|
||||
"""
|
||||
self._ensure_container_exists(container)
|
||||
self._ensure_object_exists(container, name)
|
||||
# throw exception if someone calls get object
|
||||
when(swift_client.Connection).get_object(container, name).thenRaise(
|
||||
swiftclient.ClientException('Resource Not found', http_status=404))
|
||||
when(swift_client.Connection).delete_object(
|
||||
container, name).thenReturn(None).thenRaise(
|
||||
swiftclient.ClientException('Resource Not Found',
|
||||
http_status=404))
|
||||
self._remove_object(name, self._objects[container])
|
||||
return self
|
|
@ -15,8 +15,11 @@
|
|||
import testtools
|
||||
from reddwarf.backup import models
|
||||
from reddwarf.tests.unittests.util import util
|
||||
from reddwarf.common import utils
|
||||
from reddwarf.common import utils, exception
|
||||
from reddwarf.common.context import ReddwarfContext
|
||||
from reddwarf.instance.models import BuiltInstance, InstanceTasks, Instance
|
||||
from mockito import mock, when, unstub, any
|
||||
from reddwarf.taskmanager import api
|
||||
|
||||
|
||||
def _prep_conf(current_time):
|
||||
|
@ -28,6 +31,7 @@ def _prep_conf(current_time):
|
|||
BACKUP_NAME = 'WORKS'
|
||||
BACKUP_NAME_2 = 'IT-WORKS'
|
||||
BACKUP_STATE = "NEW"
|
||||
BACKUP_DESC = 'Backup test'
|
||||
|
||||
|
||||
class BackupCreateTest(testtools.TestCase):
|
||||
|
@ -39,17 +43,89 @@ class BackupCreateTest(testtools.TestCase):
|
|||
|
||||
def tearDown(self):
|
||||
super(BackupCreateTest, self).tearDown()
|
||||
unstub()
|
||||
if self.created:
|
||||
models.DBBackup.find_by(
|
||||
tenant_id=self.context.tenant).delete()
|
||||
|
||||
def test_create(self):
|
||||
models.Backup.create(
|
||||
self.context, self.instance_id, BACKUP_NAME)
|
||||
instance = mock(Instance)
|
||||
when(BuiltInstance).load(any(), any()).thenReturn(instance)
|
||||
when(instance).validate_can_perform_action().thenReturn(None)
|
||||
when(models.Backup).verify_swift_auth_token(any()).thenReturn(
|
||||
None)
|
||||
when(api.API).create_backup(any()).thenReturn(None)
|
||||
|
||||
bu = models.Backup.create(self.context, self.instance_id,
|
||||
BACKUP_NAME, BACKUP_DESC)
|
||||
self.created = True
|
||||
db_record = models.DBBackup.find_by(
|
||||
tenant_id=self.context.tenant)
|
||||
|
||||
self.assertEqual(BACKUP_NAME, bu.name)
|
||||
self.assertEqual(BACKUP_DESC, bu.description)
|
||||
self.assertEqual(self.instance_id, bu.instance_id)
|
||||
self.assertEqual(models.BackupState.NEW, bu.state)
|
||||
|
||||
db_record = models.DBBackup.find_by(id=bu.id)
|
||||
self.assertEqual(bu.id, db_record['id'])
|
||||
self.assertEqual(BACKUP_NAME, db_record['name'])
|
||||
self.assertEqual(BACKUP_DESC, db_record['description'])
|
||||
self.assertEqual(self.instance_id, db_record['instance_id'])
|
||||
self.assertEqual(models.BackupState.NEW, db_record['state'])
|
||||
|
||||
def test_create_instance_not_found(self):
|
||||
self.assertRaises(exception.NotFound, models.Backup.create,
|
||||
self.context, self.instance_id,
|
||||
BACKUP_NAME, BACKUP_DESC)
|
||||
|
||||
def test_create_instance_not_active(self):
|
||||
instance = mock(Instance)
|
||||
when(BuiltInstance).load(any(), any()).thenReturn(instance)
|
||||
when(instance).validate_can_perform_action().thenRaise(
|
||||
exception.UnprocessableEntity)
|
||||
self.assertRaises(exception.UnprocessableEntity, models.Backup.create,
|
||||
self.context, self.instance_id,
|
||||
BACKUP_NAME, BACKUP_DESC)
|
||||
|
||||
def test_create_backup_swift_token_invalid(self):
|
||||
instance = mock(Instance)
|
||||
when(BuiltInstance).load(any(), any()).thenReturn(instance)
|
||||
when(instance).validate_can_perform_action().thenReturn(None)
|
||||
when(models.Backup).verify_swift_auth_token(any()).thenRaise(
|
||||
exception.SwiftAuthError)
|
||||
self.assertRaises(exception.SwiftAuthError, models.Backup.create,
|
||||
self.context, self.instance_id,
|
||||
BACKUP_NAME, BACKUP_DESC)
|
||||
|
||||
|
||||
class BackupDeleteTest(testtools.TestCase):
|
||||
def setUp(self):
|
||||
super(BackupDeleteTest, self).setUp()
|
||||
util.init_db()
|
||||
self.context, self.instance_id = _prep_conf(utils.utcnow())
|
||||
|
||||
def tearDown(self):
|
||||
super(BackupDeleteTest, self).tearDown()
|
||||
unstub()
|
||||
|
||||
def test_delete_backup_not_found(self):
|
||||
self.assertRaises(exception.NotFound, models.Backup.delete,
|
||||
self.context, 'backup-id')
|
||||
|
||||
def test_delete_backup_is_running(self):
|
||||
backup = mock()
|
||||
backup.is_running = True
|
||||
when(models.Backup).get_by_id(any()).thenReturn(backup)
|
||||
self.assertRaises(exception.UnprocessableEntity,
|
||||
models.Backup.delete, self.context, 'backup_id')
|
||||
|
||||
def test_delete_backup_swift_token_invalid(self):
|
||||
backup = mock()
|
||||
backup.is_running = False
|
||||
when(models.Backup).get_by_id(any()).thenReturn(backup)
|
||||
when(models.Backup).verify_swift_auth_token(any()).thenRaise(
|
||||
exception.SwiftAuthError)
|
||||
self.assertRaises(exception.SwiftAuthError, models.Backup.delete,
|
||||
self.context, 'backup_id')
|
||||
|
||||
|
||||
class BackupORMTest(testtools.TestCase):
|
||||
|
@ -66,6 +142,7 @@ class BackupORMTest(testtools.TestCase):
|
|||
|
||||
def tearDown(self):
|
||||
super(BackupORMTest, self).tearDown()
|
||||
unstub()
|
||||
if not self.deleted:
|
||||
models.DBBackup.find_by(tenant_id=self.context.tenant).delete()
|
||||
|
||||
|
@ -112,7 +189,8 @@ class BackupORMTest(testtools.TestCase):
|
|||
self.assertFalse(self.backup.is_done)
|
||||
|
||||
def test_backup_delete(self):
|
||||
models.Backup.delete(self.backup.id)
|
||||
backup = models.DBBackup.find_by(id=self.backup.id)
|
||||
backup.delete()
|
||||
query = models.Backup.list_for_instance(self.instance_id)
|
||||
self.assertEqual(query.count(), 0)
|
||||
|
||||
|
|
|
@ -0,0 +1,214 @@
|
|||
from mockito import mock, when, unstub
|
||||
import testtools
|
||||
from testtools.matchers import *
|
||||
|
||||
import swiftclient.client
|
||||
|
||||
from reddwarf.tests.fakes.swift import SwiftClientStub
|
||||
from reddwarf.common.context import ReddwarfContext
|
||||
from reddwarf.common import remote
|
||||
|
||||
|
||||
class TestRemote(testtools.TestCase):
|
||||
def setUp(self):
|
||||
super(TestRemote, self).setUp()
|
||||
|
||||
def tearDown(self):
|
||||
super(TestRemote, self).tearDown()
|
||||
unstub()
|
||||
|
||||
def test_creation(self):
|
||||
when(swiftclient.client.Connection).get_auth().thenReturn(None)
|
||||
conn = swiftclient.client.Connection()
|
||||
self.assertIsNone(conn.get_auth())
|
||||
|
||||
def test_create_swift_client(self):
|
||||
mock_resp = mock(dict)
|
||||
when(swiftclient.client.Connection).get_container('bob').thenReturn(
|
||||
["text", mock_resp])
|
||||
client = remote.create_swift_client(ReddwarfContext(tenant='123'))
|
||||
headers, container = client.get_container('bob')
|
||||
self.assertIs(headers, "text")
|
||||
self.assertIs(container, mock_resp)
|
||||
|
||||
def test_empty_account(self):
|
||||
"""
|
||||
this is an account with no containers and no objects
|
||||
"""
|
||||
# setup expectation
|
||||
swift_stub = SwiftClientStub()
|
||||
swift_stub.with_account('123223')
|
||||
# interact
|
||||
conn = swiftclient.client.Connection()
|
||||
account_info = conn.get_account()
|
||||
self.assertThat(account_info, Not(Is(None)))
|
||||
self.assertThat(len(account_info), Is(2))
|
||||
self.assertThat(account_info, IsInstance(tuple))
|
||||
self.assertThat(account_info[0], IsInstance(dict))
|
||||
self.assertThat(account_info[0],
|
||||
KeysEqual('content-length', 'accept-ranges',
|
||||
'x-timestamp', 'x-trans-id', 'date',
|
||||
'x-account-bytes-used',
|
||||
'x-account-container-count', 'content-type',
|
||||
'x-account-object-count'))
|
||||
self.assertThat(account_info[1], IsInstance(list))
|
||||
self.assertThat(len(account_info[1]), Is(0))
|
||||
|
||||
def test_one_container(self):
|
||||
"""
|
||||
tests to ensure behavior is normal with one container
|
||||
"""
|
||||
# setup expectation
|
||||
swift_stub = SwiftClientStub()
|
||||
swift_stub.with_account('123223')
|
||||
cont_name = 'a-container-name'
|
||||
swift_stub.with_container(cont_name)
|
||||
# interact
|
||||
conn = swiftclient.client.Connection()
|
||||
conn.get_auth()
|
||||
conn.put_container(cont_name)
|
||||
# get headers plus container metadata
|
||||
self.assertThat(len(conn.get_account()), Is(2))
|
||||
# verify container details
|
||||
account_containers = conn.get_account()[1]
|
||||
self.assertThat(len(account_containers), Is(1))
|
||||
self.assertThat(account_containers[0],
|
||||
KeysEqual('count', 'bytes', 'name'))
|
||||
self.assertThat(account_containers[0]['name'], Is(cont_name))
|
||||
# get container details
|
||||
cont_info = conn.get_container(cont_name)
|
||||
self.assertIsNotNone(cont_info)
|
||||
self.assertThat(cont_info[0], KeysEqual('content-length',
|
||||
"x-container-object-count",
|
||||
'accept-ranges',
|
||||
'x-container-bytes-used',
|
||||
'x-timestamp', 'x-trans-id',
|
||||
'date', 'content-type'))
|
||||
self.assertThat(len(cont_info[1]), Equals(0))
|
||||
# remove container
|
||||
swift_stub.without_container(cont_name)
|
||||
with testtools.ExpectedException(swiftclient.ClientException):
|
||||
conn.get_container(cont_name)
|
||||
# ensure there are no more containers in account
|
||||
self.assertThat(len(conn.get_account()[1]), Is(0))
|
||||
|
||||
def test_one_object(self):
|
||||
swift_stub = SwiftClientStub()
|
||||
swift_stub.with_account('123223')
|
||||
swift_stub.with_container('bob')
|
||||
swift_stub.with_object('bob', 'test', 'test_contents')
|
||||
# create connection
|
||||
conn = swiftclient.client.Connection()
|
||||
# test container lightly
|
||||
cont_info = conn.get_container('bob')
|
||||
self.assertIsNotNone(cont_info)
|
||||
self.assertThat(cont_info[0],
|
||||
KeysEqual('content-length', 'x-container-object-count',
|
||||
'accept-ranges', 'x-container-bytes-used',
|
||||
'x-timestamp', 'x-trans-id', 'date',
|
||||
'content-type'))
|
||||
cont_objects = cont_info[1]
|
||||
self.assertThat(len(cont_objects), Equals(1))
|
||||
obj_1 = cont_objects[0]
|
||||
self.assertThat(obj_1, Equals(
|
||||
{'bytes': 13, 'last_modified': '2013-03-15T22:10:49.361950',
|
||||
'hash': 'ccc55aefbf92aa66f42b638802c5e7f6', 'name': 'test',
|
||||
'content_type': 'application/octet-stream'}))
|
||||
# test object api - not much to do here
|
||||
self.assertThat(conn.get_object('bob', 'test')[1], Is('test_contents'))
|
||||
|
||||
# test remove object
|
||||
swift_stub.without_object('bob', 'test')
|
||||
# interact
|
||||
conn.delete_object('bob', 'test')
|
||||
with testtools.ExpectedException(swiftclient.ClientException):
|
||||
conn.delete_object('bob', 'test')
|
||||
self.assertThat(len(conn.get_container('bob')[1]), Is(0))
|
||||
|
||||
def test_two_objects(self):
|
||||
swift_stub = SwiftClientStub()
|
||||
swift_stub.with_account('123223')
|
||||
swift_stub.with_container('bob')
|
||||
swift_stub.with_container('bob2')
|
||||
swift_stub.with_object('bob', 'test', 'test_contents')
|
||||
swift_stub.with_object('bob', 'test2', 'test_contents2')
|
||||
|
||||
conn = swiftclient.client.Connection()
|
||||
|
||||
self.assertIs(len(conn.get_account()), 2)
|
||||
cont_info = conn.get_container('bob')
|
||||
self.assertIsNotNone(cont_info)
|
||||
self.assertThat(cont_info[0],
|
||||
KeysEqual('content-length', 'x-container-object-count',
|
||||
'accept-ranges', 'x-container-bytes-used',
|
||||
'x-timestamp', 'x-trans-id', 'date',
|
||||
'content-type'))
|
||||
self.assertThat(len(cont_info[1]), Equals(2))
|
||||
self.assertThat(cont_info[1][0], Equals(
|
||||
{'bytes': 13, 'last_modified': '2013-03-15T22:10:49.361950',
|
||||
'hash': 'ccc55aefbf92aa66f42b638802c5e7f6', 'name': 'test',
|
||||
'content_type': 'application/octet-stream'}))
|
||||
self.assertThat(conn.get_object('bob', 'test')[1], Is('test_contents'))
|
||||
self.assertThat(conn.get_object('bob', 'test2')[1],
|
||||
Is('test_contents2'))
|
||||
|
||||
swift_stub.without_object('bob', 'test')
|
||||
conn.delete_object('bob', 'test')
|
||||
with testtools.ExpectedException(swiftclient.ClientException):
|
||||
conn.delete_object('bob', 'test')
|
||||
self.assertThat(len(conn.get_container('bob')[1]), Is(1))
|
||||
|
||||
swift_stub.without_container('bob')
|
||||
with testtools.ExpectedException(swiftclient.ClientException):
|
||||
conn.get_container('bob')
|
||||
|
||||
self.assertThat(len(conn.get_account()), Is(2))
|
||||
|
||||
def test_nonexisting_container(self):
|
||||
"""
|
||||
when a container does not exist and is accessed then a 404 is returned
|
||||
"""
|
||||
from reddwarf.tests.fakes.swift import SwiftClientStub
|
||||
|
||||
swift_stub = SwiftClientStub()
|
||||
swift_stub.with_account('123223')
|
||||
swift_stub.with_container('existing')
|
||||
|
||||
conn = swiftclient.client.Connection()
|
||||
|
||||
with testtools.ExpectedException(swiftclient.ClientException):
|
||||
conn.get_container('nonexisting')
|
||||
|
||||
def test_replace_object(self):
|
||||
"""
|
||||
Test to ensure that if an object is updated the container object
|
||||
count is the same and the contents of the object are updated
|
||||
"""
|
||||
swift_stub = SwiftClientStub()
|
||||
swift_stub.with_account('1223df2')
|
||||
swift_stub.with_container('new-container')
|
||||
swift_stub.with_object('new-container', 'new-object',
|
||||
'new-object-contents')
|
||||
|
||||
conn = swiftclient.client.Connection()
|
||||
|
||||
conn.put_object('new-container', 'new-object', 'new-object-contents')
|
||||
obj_resp = conn.get_object('new-container', 'new-object')
|
||||
self.assertThat(obj_resp, Not(Is(None)))
|
||||
self.assertThat(len(obj_resp), Is(2))
|
||||
self.assertThat(obj_resp[1], Is('new-object-contents'))
|
||||
|
||||
# set expected behavior - trivial here since it is the intended
|
||||
# behavior however keep in mind this is just to support testing of
|
||||
# reddwarf components
|
||||
swift_stub.with_object('new-container', 'new-object',
|
||||
'updated-object-contents')
|
||||
|
||||
conn.put_object('new-container', 'new-object',
|
||||
'updated-object-contents')
|
||||
obj_resp = conn.get_object('new-container', 'new-object')
|
||||
self.assertThat(obj_resp, Not(Is(None)))
|
||||
self.assertThat(len(obj_resp), Is(2))
|
||||
self.assertThat(obj_resp[1], Is('updated-object-contents'))
|
||||
# ensure object count has not increased
|
||||
self.assertThat(len(conn.get_container('new-container')[1]), Is(1))
|
|
@ -23,7 +23,7 @@ from reddwarf.db.models import DatabaseModelBase
|
|||
from reddwarf.extensions.mgmt.quota.service import QuotaController
|
||||
from reddwarf.common import exception
|
||||
from reddwarf.common import cfg
|
||||
from reddwarf.instance.models import run_with_quotas
|
||||
from reddwarf.quota.quota import run_with_quotas
|
||||
from reddwarf.quota.quota import QUOTAS
|
||||
"""
|
||||
Unit tests for the classes and functions in DbQuotaDriver.py.
|
||||
|
|
|
@ -123,6 +123,7 @@ if __name__ == "__main__":
|
|||
test_config_file = parse_args_for_test_config()
|
||||
CONFIG.load_from_file(test_config_file)
|
||||
|
||||
from reddwarf.tests.api import backups
|
||||
from reddwarf.tests.api import header
|
||||
from reddwarf.tests.api import limits
|
||||
from reddwarf.tests.api import flavors
|
||||
|
|
|
@ -13,5 +13,6 @@ httplib2
|
|||
lxml
|
||||
python-novaclient
|
||||
python-keystoneclient
|
||||
python-swiftclient
|
||||
iso8601
|
||||
oslo.config>=1.1.0
|
||||
|
|
Loading…
Reference in New Issue