Merge "Add support of datastore types"

This commit is contained in:
Jenkins
2013-11-20 03:47:13 +00:00
committed by Gerrit Code Review
48 changed files with 1427 additions and 502 deletions

View File

@@ -53,9 +53,10 @@ if __name__ == '__main__':
try: try:
get_db_api().configure_db(CONF) get_db_api().configure_db(CONF)
manager = dbaas.service_registry().get(CONF.service_type) manager = dbaas.datastore_registry().get(CONF.datastore_manager)
if not manager: if not manager:
msg = "Manager not found for service type " + CONF.service_type msg = ("Manager class not registered for datastore manager " +
CONF.datastore_manager)
raise RuntimeError(msg) raise RuntimeError(msg)
server = rpc_service.RpcService(manager=manager, host=CONF.guest_id) server = rpc_service.RpcService(manager=manager, host=CONF.guest_id)
launcher = openstack_service.launch(server) launcher = openstack_service.launch(server)

View File

@@ -36,11 +36,13 @@ if os.path.exists(os.path.join(possible_topdir, 'trove', '__init__.py')):
from trove import version from trove import version
from trove.common import cfg from trove.common import cfg
from trove.common import exception
from trove.common import utils from trove.common import utils
from trove.db import get_db_api from trove.db import get_db_api
from trove.openstack.common import log as logging from trove.openstack.common import log as logging
from trove.openstack.common import uuidutils from trove.openstack.common import uuidutils
from trove.instance import models as instance_models from trove.instance import models as instance_models
from trove.datastore import models as datastore_models
CONF = cfg.CONF CONF = cfg.CONF
@@ -69,28 +71,30 @@ class Commands(object):
kwargs[arg] = getattr(CONF.action, arg) kwargs[arg] = getattr(CONF.action, arg)
exec_method(**kwargs) exec_method(**kwargs)
def image_update(self, service_name, image_id): def datastore_update(self, datastore_name, manager, default_version):
self.db_api.configure_db(CONF) try:
image = self.db_api.find_by(instance_models.ServiceImage, datastore_models.update_datastore(datastore_name, manager,
service_name=service_name) default_version)
if image is None: print("Datastore '%s' updated." % datastore_name)
# Create a new one except exception.DatastoreVersionNotFound as e:
image = instance_models.ServiceImage() print(e)
image.id = uuidutils.generate_uuid()
image.service_name = service_name
image.image_id = image_id
self.db_api.save(image)
def db_wipe(self, repo_path, service_name, image_id): def datastore_version_update(self, datastore, version_name, image_id,
packages, active):
try:
datastore_models.update_datastore_version(datastore,
version_name, image_id,
packages, active)
print("Datastore version '%s' updated." % version_name)
except exception.DatastoreNotFound as e:
print(e)
def db_wipe(self, repo_path):
"""Drops the database and recreates it.""" """Drops the database and recreates it."""
from trove.instance import models from trove.instance import models
from trove.db.sqlalchemy import session from trove.db.sqlalchemy import session
self.db_api.drop_db(CONF) self.db_api.drop_db(CONF)
self.db_sync() self.db_sync()
# Sets up database engine, so the next line will work...
session.configure_db(CONF)
models.ServiceImage.create(service_name=service_name,
image_id=image_id)
def params_of(self, command_name): def params_of(self, command_name):
if Commands.has(command_name): if Commands.has(command_name):
@@ -108,13 +112,18 @@ if __name__ == '__main__':
parser = subparser.add_parser('db_downgrade') parser = subparser.add_parser('db_downgrade')
parser.add_argument('version') parser.add_argument('version')
parser.add_argument('--repo_path') parser.add_argument('--repo_path')
parser = subparser.add_parser('image_update') parser = subparser.add_parser('datastore_update')
parser.add_argument('service_name') parser.add_argument('datastore_name')
parser.add_argument('manager')
parser.add_argument('default_version')
parser = subparser.add_parser('datastore_version_update')
parser.add_argument('datastore')
parser.add_argument('version_name')
parser.add_argument('image_id') parser.add_argument('image_id')
parser.add_argument('packages')
parser.add_argument('active')
parser = subparser.add_parser('db_wipe') parser = subparser.add_parser('db_wipe')
parser.add_argument('repo_path') parser.add_argument('repo_path')
parser.add_argument('service_name')
parser.add_argument('image_id')
cfg.custom_parser('action', actions) cfg.custom_parser('action', actions)
cfg.parse_args(sys.argv) cfg.parse_args(sys.argv)

View File

@@ -92,6 +92,9 @@ http_post_rate = 200
http_put_rate = 200 http_put_rate = 200
http_delete_rate = 200 http_delete_rate = 200
# default datastore
default_datastore = a00000a0-00a0-0a00-00a0-000a000000aa
# Auth # Auth
admin_roles = admin admin_roles = admin

View File

@@ -52,16 +52,41 @@ def initialize_trove(config_file):
return pastedeploy.paste_deploy_app(config_file, 'trove', {}) return pastedeploy.paste_deploy_app(config_file, 'trove', {})
def datastore_init():
# Adds the datastore for mysql (needed to make most calls work).
from trove.datastore import models
models.DBDatastore.create(id="a00000a0-00a0-0a00-00a0-000a000000aa",
name=CONFIG.dbaas_datastore, manager='mysql',
default_version_id=
"b00000b0-00b0-0b00-00b0-000b000000bb")
models.DBDatastore.create(id="e00000e0-00e0-0e00-00e0-000e000000ee",
name='Test_Datastore_1', manager='manager1',
default_version_id=None)
models.DBDatastoreVersion.create(id="b00000b0-00b0-0b00-00b0-000b000000bb",
datastore_id=
"a00000a0-00a0-0a00-00a0-000a000000aa",
name=CONFIG.dbaas_datastore_version,
image_id=
'c00000c0-00c0-0c00-00c0-000c000000cc',
packages='test packages',
active=1)
models.DBDatastoreVersion.create(id="d00000d0-00d0-0d00-00d0-000d000000dd",
datastore_id=
"a00000a0-00a0-0a00-00a0-000a000000aa",
name='mysql_inactive_version',
image_id=
'c00000c0-00c0-0c00-00c0-000c000000cc',
packages=None, active=0)
def initialize_database(): def initialize_database():
from trove.db import get_db_api from trove.db import get_db_api
from trove.instance import models
from trove.db.sqlalchemy import session from trove.db.sqlalchemy import session
db_api = get_db_api() db_api = get_db_api()
db_api.drop_db(CONF) # Destroys the database, if it exists. db_api.drop_db(CONF) # Destroys the database, if it exists.
db_api.db_sync(CONF) db_api.db_sync(CONF)
session.configure_db(CONF) session.configure_db(CONF)
# Adds the image for mysql (needed to make most calls work). datastore_init()
models.ServiceImage.create(service_name="mysql", image_id="fake")
db_api.configure_db(CONF) db_api.configure_db(CONF)
@@ -125,6 +150,7 @@ if __name__ == "__main__":
from trove.tests.api import instances_mysql_down from trove.tests.api import instances_mysql_down
from trove.tests.api import instances_resize from trove.tests.api import instances_resize
from trove.tests.api import databases from trove.tests.api import databases
from trove.tests.api import datastores
from trove.tests.api import root from trove.tests.api import root
from trove.tests.api import root_on_create from trove.tests.api import root_on_create
from trove.tests.api import users from trove.tests.api import users

View File

@@ -20,6 +20,7 @@ from trove.instance.service import InstanceController
from trove.limits.service import LimitsController from trove.limits.service import LimitsController
from trove.backup.service import BackupController from trove.backup.service import BackupController
from trove.versions import VersionsController from trove.versions import VersionsController
from trove.datastore.service import DatastoreController
class API(wsgi.Router): class API(wsgi.Router):
@@ -28,6 +29,7 @@ class API(wsgi.Router):
mapper = routes.Mapper() mapper = routes.Mapper()
super(API, self).__init__(mapper) super(API, self).__init__(mapper)
self._instance_router(mapper) self._instance_router(mapper)
self._datastore_router(mapper)
self._flavor_router(mapper) self._flavor_router(mapper)
self._versions_router(mapper) self._versions_router(mapper)
self._limits_router(mapper) self._limits_router(mapper)
@@ -37,6 +39,17 @@ class API(wsgi.Router):
versions_resource = VersionsController().create_resource() versions_resource = VersionsController().create_resource()
mapper.connect("/", controller=versions_resource, action="show") mapper.connect("/", controller=versions_resource, action="show")
def _datastore_router(self, mapper):
datastore_resource = DatastoreController().create_resource()
mapper.resource("datastore", "/{tenant_id}/datastores",
controller=datastore_resource)
mapper.connect("/{tenant_id}/datastores/{datastore}/versions",
controller=datastore_resource,
action="version_index")
mapper.connect("/{tenant_id}/datastores/{datastore}/versions/{id}",
controller=datastore_resource,
action="version_show")
def _instance_router(self, mapper): def _instance_router(self, mapper):
instance_resource = InstanceController().create_resource() instance_resource = InstanceController().create_resource()
path = "/{tenant_id}/instances" path = "/{tenant_id}/instances"

View File

@@ -183,7 +183,6 @@ instance = {
"volume": volume, "volume": volume,
"databases": databases_def, "databases": databases_def,
"users": users_list, "users": users_list,
"service_type": non_empty_string,
"restorePoint": { "restorePoint": {
"type": "object", "type": "object",
"required": ["backupRef"], "required": ["backupRef"],
@@ -192,7 +191,15 @@ instance = {
"backupRef": uuid "backupRef": uuid
} }
}, },
"availability_zone": non_empty_string "availability_zone": non_empty_string,
"datastore": {
"type": "object",
"additionalProperties": True,
"properties": {
"type": non_empty_string,
"version": non_empty_string
}
}
} }
} }
} }

View File

@@ -64,8 +64,6 @@ common_opts = [
cfg.IntOpt('periodic_interval', default=60), cfg.IntOpt('periodic_interval', default=60),
cfg.BoolOpt('trove_dns_support', default=False), cfg.BoolOpt('trove_dns_support', default=False),
cfg.StrOpt('db_api_implementation', default='trove.db.sqlalchemy.api'), cfg.StrOpt('db_api_implementation', default='trove.db.sqlalchemy.api'),
cfg.StrOpt('mysql_pkg', default='mysql-server-5.5'),
cfg.StrOpt('percona_pkg', default='percona-server-server-5.5'),
cfg.StrOpt('dns_driver', default='trove.dns.driver.DnsDriver'), cfg.StrOpt('dns_driver', default='trove.dns.driver.DnsDriver'),
cfg.StrOpt('dns_instance_entry_factory', cfg.StrOpt('dns_instance_entry_factory',
default='trove.dns.driver.DnsInstanceEntryFactory'), default='trove.dns.driver.DnsInstanceEntryFactory'),
@@ -111,13 +109,18 @@ common_opts = [
cfg.BoolOpt('use_heat', default=False), cfg.BoolOpt('use_heat', default=False),
cfg.StrOpt('device_path', default='/dev/vdb'), cfg.StrOpt('device_path', default='/dev/vdb'),
cfg.StrOpt('mount_point', default='/var/lib/mysql'), cfg.StrOpt('mount_point', default='/var/lib/mysql'),
cfg.StrOpt('service_type', default='mysql'), cfg.StrOpt('default_datastore', default=None,
help="The default datastore id or name to use if one is not "
"provided by the user. If the default value is None, the field"
" becomes required in the instance-create request."),
cfg.StrOpt('datastore_manager', default=None,
help='manager class in guestagent, setup by taskmanager on '
'instance provision'),
cfg.StrOpt('block_device_mapping', default='vdb'), cfg.StrOpt('block_device_mapping', default='vdb'),
cfg.IntOpt('server_delete_time_out', default=60), cfg.IntOpt('server_delete_time_out', default=60),
cfg.IntOpt('volume_time_out', default=60), cfg.IntOpt('volume_time_out', default=60),
cfg.IntOpt('heat_time_out', default=60), cfg.IntOpt('heat_time_out', default=60),
cfg.IntOpt('reboot_time_out', default=60 * 2), cfg.IntOpt('reboot_time_out', default=60 * 2),
cfg.StrOpt('service_options', default=['mysql']),
cfg.IntOpt('dns_time_out', default=60 * 2), cfg.IntOpt('dns_time_out', default=60 * 2),
cfg.IntOpt('resize_time_out', default=60 * 10), cfg.IntOpt('resize_time_out', default=60 * 10),
cfg.IntOpt('revert_time_out', default=60 * 10), cfg.IntOpt('revert_time_out', default=60 * 10),
@@ -212,10 +215,10 @@ common_opts = [
cfg.StrOpt('guest_config', cfg.StrOpt('guest_config',
default='$pybasedir/etc/trove/trove-guestagent.conf.sample', default='$pybasedir/etc/trove/trove-guestagent.conf.sample',
help="Path to guestagent config file"), help="Path to guestagent config file"),
cfg.DictOpt('service_registry_ext', default=dict(), cfg.DictOpt('datastore_registry_ext', default=dict(),
help='Extention for default service managers.' help='Extention for default datastore managers.'
' Allows to use custom managers for each of' ' Allows to use custom managers for each of'
' service type supported in trove'), ' datastore supported in trove'),
cfg.StrOpt('template_path', cfg.StrOpt('template_path',
default='/etc/trove/templates/', default='/etc/trove/templates/',
help='Path which leads to datastore templates'), help='Path which leads to datastore templates'),

View File

@@ -96,6 +96,41 @@ class DnsRecordNotFound(NotFound):
message = _("DnsRecord with name= %(name)s not found.") message = _("DnsRecord with name= %(name)s not found.")
class DatastoreNotFound(NotFound):
message = _("Datastore '%(datastore)s' cannot be found.")
class DatastoreVersionNotFound(NotFound):
message = _("Datastore version '%(version)s' cannot be found.")
class DatastoresNotFound(NotFound):
message = _("Datastores cannot be found.")
class DatastoreNoVersion(TroveError):
message = _("Datastore '%(datastore)s' has no version '%(version)s'.")
class DatastoreVersionInactive(TroveError):
message = _("Datastore version '%(version)s' is not active.")
class DatastoreDefaultDatastoreNotFound(TroveError):
message = _("Please specify datastore.")
class DatastoreDefaultVersionNotFound(TroveError):
message = _("Default version for datastore '%(datastore)s' not found.")
class OverLimit(TroveError): class OverLimit(TroveError):
internal_message = _("The server rejected the request due to its size or " internal_message = _("The server rejected the request due to its size or "

View File

@@ -30,10 +30,10 @@ class SingleInstanceConfigTemplate(object):
""" This class selects a single configuration file by database type for """ This class selects a single configuration file by database type for
rendering on the guest """ rendering on the guest """
def __init__(self, datastore_type, flavor_dict, instance_id): def __init__(self, datastore_manager, flavor_dict, instance_id):
""" Constructor """ Constructor
:param datastore_type: The database type. :param datastore_manager: The datastore manager.
:type name: str. :type name: str.
:param flavor_dict: dict containing flavor details for use in jinja. :param flavor_dict: dict containing flavor details for use in jinja.
:type flavor_dict: dict. :type flavor_dict: dict.
@@ -42,7 +42,7 @@ class SingleInstanceConfigTemplate(object):
""" """
self.flavor_dict = flavor_dict self.flavor_dict = flavor_dict
template_filename = "%s/config.template" % datastore_type template_filename = "%s/config.template" % datastore_manager
self.template = ENV.get_template(template_filename) self.template = ENV.get_template(template_filename)
self.instance_id = instance_id self.instance_id = instance_id
@@ -66,12 +66,12 @@ class SingleInstanceConfigTemplate(object):
return abs(hash(self.instance_id) % (2 ** 31)) return abs(hash(self.instance_id) % (2 ** 31))
def load_heat_template(datastore_type): def load_heat_template(datastore_manager):
template_filename = "%s/heat.template" % datastore_type template_filename = "%s/heat.template" % datastore_manager
try: try:
template_obj = ENV.get_template(template_filename) template_obj = ENV.get_template(template_filename)
return template_obj return template_obj
except jinja2.TemplateNotFound: except jinja2.TemplateNotFound:
msg = "Missing heat template for %s" % datastore_type msg = "Missing heat template for %s" % datastore_manager
LOG.error(msg) LOG.error(msg)
raise exception.TroveError(msg) raise exception.TroveError(msg)

View File

185
trove/datastore/models.py Normal file
View File

@@ -0,0 +1,185 @@
from trove.common import cfg
from trove.common import exception
from trove.db import models as dbmodels
from trove.db import get_db_api
from trove.openstack.common import uuidutils
CONF = cfg.CONF
db_api = get_db_api()
def persisted_models():
return {
'datastore': DBDatastore,
'datastore_version': DBDatastoreVersion,
}
class DBDatastore(dbmodels.DatabaseModelBase):
_data_fields = ['id', 'name', 'manager', 'default_version_id']
class DBDatastoreVersion(dbmodels.DatabaseModelBase):
_data_fields = ['id', 'datastore_id', 'name', 'image_id', 'packages',
'active']
class Datastore(object):
def __init__(self, db_info):
self.db_info = db_info
@classmethod
def load(cls, id_or_name):
try:
return cls(DBDatastore.find_by(id=id_or_name))
except exception.ModelNotFoundError:
try:
return cls(DBDatastore.find_by(name=id_or_name))
except exception.ModelNotFoundError:
raise exception.DatastoreNotFound(datastore=id_or_name)
@property
def id(self):
return self.db_info.id
@property
def name(self):
return self.db_info.name
@property
def manager(self):
return self.db_info.manager
@property
def default_version_id(self):
return self.db_info.default_version_id
class Datastores(object):
def __init__(self, db_info):
self.db_info = db_info
@classmethod
def load(cls):
return cls(DBDatastore.find_all())
def __iter__(self):
for item in self.db_info:
yield item
class DatastoreVersion(object):
def __init__(self, db_info):
self.db_info = db_info
@classmethod
def load(cls, id_or_name):
try:
return cls(DBDatastoreVersion.find_by(id=id_or_name))
except exception.ModelNotFoundError:
try:
return cls(DBDatastoreVersion.find_by(name=id_or_name))
except exception.ModelNotFoundError:
raise exception.DatastoreVersionNotFound(version=id_or_name)
@property
def id(self):
return self.db_info.id
@property
def datastore_id(self):
return self.db_info.datastore_id
@property
def name(self):
return self.db_info.name
@property
def image_id(self):
return self.db_info.image_id
@property
def packages(self):
return self.db_info.packages
@property
def active(self):
return self.db_info.active
class DatastoreVersions(object):
def __init__(self, db_info):
self.db_info = db_info
@classmethod
def load(cls, id_or_name, active=True):
datastore = Datastore.load(id_or_name)
return cls(DBDatastoreVersion.find_all(datastore_id=datastore.id,
active=active))
def __iter__(self):
for item in self.db_info:
yield item
def get_datastore_version(type=None, version=None):
datastore = type or CONF.default_datastore
if not datastore:
raise exception.DatastoreDefaultDatastoreNotFound()
datastore = Datastore.load(datastore)
version = version or datastore.default_version_id
if not version:
raise exception.DatastoreDefaultVersionNotFound(datastore=
datastore.name)
datastore_version = DatastoreVersion.load(version)
if datastore_version.datastore_id != datastore.id:
raise exception.DatastoreNoVersion(datastore=datastore.name,
version=datastore_version.name)
if not datastore_version.active:
raise exception.DatastoreVersionInactive(version=
datastore_version.name)
return (datastore, datastore_version)
def update_datastore(name, manager, default_version):
db_api.configure_db(CONF)
if default_version:
version = DatastoreVersion.load(default_version)
if not version.active:
raise exception.DatastoreVersionInactive(version=
version.name)
try:
datastore = DBDatastore.find_by(name=name)
except exception.ModelNotFoundError:
# Create a new one
datastore = DBDatastore()
datastore.id = uuidutils.generate_uuid()
datastore.name = name
datastore.manager = manager
if default_version:
datastore.default_version_id = version.id
db_api.save(datastore)
def update_datastore_version(datastore, name, image_id, packages, active):
db_api.configure_db(CONF)
datastore = Datastore.load(datastore)
try:
version = DBDatastoreVersion.find_by(name=name)
except exception.ModelNotFoundError:
# Create a new one
version = DBDatastoreVersion()
version.id = uuidutils.generate_uuid()
version.name = name
version.datastore_id = datastore.id
version.image_id = image_id
version.packages = packages
version.active = active
db_api.save(version)

View File

@@ -0,0 +1,31 @@
from trove.common import cfg
from trove.common import exception
from trove.common import utils
from trove.common import wsgi
from trove.datastore import models, views
class DatastoreController(wsgi.Controller):
def show(self, req, tenant_id, id):
datastore = models.Datastore.load(id)
return wsgi.Result(views.
DatastoreView(datastore, req).data(), 200)
def index(self, req, tenant_id):
datastores = models.Datastores.load()
return wsgi.Result(views.
DatastoresView(datastores, req).data(),
200)
def version_show(self, req, tenant_id, datastore, id):
datastore, datastore_version = models.get_datastore_version(datastore,
id)
return wsgi.Result(views.DatastoreVersionView(datastore_version,
req).data(), 200)
def version_index(self, req, tenant_id, datastore):
datastore_versions = models.DatastoreVersions.load(datastore)
return wsgi.Result(views.
DatastoreVersionsView(datastore_versions,
req).data(), 200)

76
trove/datastore/views.py Normal file
View File

@@ -0,0 +1,76 @@
from trove.common.views import create_links
class DatastoreView(object):
def __init__(self, datastore, req=None):
self.datastore = datastore
self.req = req
def data(self):
datastore_dict = {
"id": self.datastore.id,
"name": self.datastore.name,
"links": self._build_links(),
}
return {"datastore": datastore_dict}
def _build_links(self):
return create_links("datastores", self.req,
self.datastore.id)
class DatastoresView(object):
def __init__(self, datastores, req=None):
self.datastores = datastores
self.req = req
def data(self):
data = []
for datastore in self.datastores:
data.append(self.data_for_datastore(datastore))
return {'datastores': data}
def data_for_datastore(self, datastore):
view = DatastoreView(datastore, req=self.req)
return view.data()['datastore']
class DatastoreVersionView(object):
def __init__(self, datastore_version, req=None):
self.datastore_version = datastore_version
self.req = req
def data(self):
datastore_version_dict = {
"id": self.datastore_version.id,
"name": self.datastore_version.name,
"links": self._build_links(),
}
return {"version": datastore_version_dict}
def _build_links(self):
return create_links("datastores/versions",
self.req, self.datastore_version.id)
class DatastoreVersionsView(object):
def __init__(self, datastore_versions, req=None):
self.datastore_versions = datastore_versions
self.req = req
def data(self):
data = []
for datastore_version in self.datastore_versions:
data.append(self.
data_for_datastore_version(datastore_version))
return {'versions': data}
def data_for_datastore_version(self, datastore_version):
view = DatastoreVersionView(datastore_version, req=self.req)
return view.data()['version']

View File

@@ -51,6 +51,10 @@ class Query(object):
return self.db_api.count(self._query_func, self._model, return self.db_api.count(self._query_func, self._model,
**self._conditions) **self._conditions)
def first(self):
return self.db_api.first(self._query_func, self._model,
**self._conditions)
def __iter__(self): def __iter__(self):
return iter(self.all()) return iter(self.all())

View File

@@ -35,6 +35,10 @@ def count(query, *args, **kwargs):
return query(*args, **kwargs).count() return query(*args, **kwargs).count()
def first(query, *args, **kwargs):
return query(*args, **kwargs).first()
def find_all(model, **conditions): def find_all(model, **conditions):
return _query_by(model, **conditions) return _query_by(model, **conditions)

View File

@@ -30,8 +30,10 @@ def map(engine, models):
orm.mapper(models['instance'], Table('instances', meta, autoload=True)) orm.mapper(models['instance'], Table('instances', meta, autoload=True))
orm.mapper(models['root_enabled_history'], orm.mapper(models['root_enabled_history'],
Table('root_enabled_history', meta, autoload=True)) Table('root_enabled_history', meta, autoload=True))
orm.mapper(models['service_image'], orm.mapper(models['datastore'],
Table('service_images', meta, autoload=True)) Table('datastores', meta, autoload=True))
orm.mapper(models['datastore_version'],
Table('datastore_versions', meta, autoload=True))
orm.mapper(models['service_statuses'], orm.mapper(models['service_statuses'],
Table('service_statuses', meta, autoload=True)) Table('service_statuses', meta, autoload=True))
orm.mapper(models['dns_records'], orm.mapper(models['dns_records'],

View File

@@ -0,0 +1,76 @@
# Copyright 2012 OpenStack Foundation
#
# 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 sqlalchemy import ForeignKey
from sqlalchemy.schema import Column
from sqlalchemy.schema import MetaData
from sqlalchemy.schema import UniqueConstraint
from trove.db.sqlalchemy.migrate_repo.schema import Boolean
from trove.db.sqlalchemy.migrate_repo.schema import create_tables
from trove.db.sqlalchemy.migrate_repo.schema import DateTime
from trove.db.sqlalchemy.migrate_repo.schema import drop_tables
from trove.db.sqlalchemy.migrate_repo.schema import Integer
from trove.db.sqlalchemy.migrate_repo.schema import BigInteger
from trove.db.sqlalchemy.migrate_repo.schema import String
from trove.db.sqlalchemy.migrate_repo.schema import Table
meta = MetaData()
datastores = Table(
'datastores',
meta,
Column('id', String(36), primary_key=True, nullable=False),
Column('name', String(255), unique=True),
Column('manager', String(255), nullable=False),
Column('default_version_id', String(36)),
)
datastore_versions = Table(
'datastore_versions',
meta,
Column('id', String(36), primary_key=True, nullable=False),
Column('datastore_id', String(36), ForeignKey('datastores.id')),
Column('name', String(255), unique=True),
Column('image_id', String(36), nullable=False),
Column('packages', String(511)),
Column('active', Boolean(), nullable=False),
UniqueConstraint('datastore_id', 'name', name='ds_versions')
)
def upgrade(migrate_engine):
meta.bind = migrate_engine
create_tables([datastores, datastore_versions])
instances = Table('instances', meta, autoload=True)
datastore_version_id = Column('datastore_version_id', String(36),
ForeignKey('datastore_versions.id'))
instances.create_column(datastore_version_id)
instances.drop_column('service_type')
# Table 'service_images' is deprecated since this version.
# Leave it for few releases.
#drop_tables([service_images])
def downgrade(migrate_engine):
meta.bind = migrate_engine
drop_tables([datastores, datastore_versions])
instances = Table('instances', meta, autoload=True)
instances.drop_column('datastore_version_id')
service_type = Column('service_type', String(36))
instances.create_column(service_type)
instances.update().values({'service_type': 'mysql'}).execute()

View File

@@ -42,6 +42,7 @@ def configure_db(options, models_mapper=None):
models_mapper.map(_ENGINE) models_mapper.map(_ENGINE)
else: else:
from trove.instance import models as base_models from trove.instance import models as base_models
from trove.datastore import models as datastores_models
from trove.dns import models as dns_models from trove.dns import models as dns_models
from trove.extensions.mysql import models as mysql_models from trove.extensions.mysql import models as mysql_models
from trove.guestagent import models as agent_models from trove.guestagent import models as agent_models
@@ -51,6 +52,7 @@ def configure_db(options, models_mapper=None):
model_modules = [ model_modules = [
base_models, base_models,
datastores_models,
dns_models, dns_models,
mysql_models, mysql_models,
agent_models, agent_models,

View File

@@ -180,14 +180,14 @@ class NotificationTransformer(object):
subsecond=True) subsecond=True)
return audit_start, audit_end return audit_start, audit_end
def _get_service_id(self, service_type, id_map): def _get_service_id(self, datastore_manager, id_map):
if service_type in id_map: if datastore_manager in id_map:
service_type_id = id_map[service_type] datastore_manager_id = id_map[datastore_manager]
else: else:
service_type_id = cfg.UNKNOWN_SERVICE_ID datastore_manager_id = cfg.UNKNOWN_SERVICE_ID
LOG.error("Service ID for Type (%s) is not configured" LOG.error("Datastore ID for Manager (%s) is not configured"
% service_type) % datastore_manager)
return service_type_id return datastore_manager_id
def transform_instance(self, instance, audit_start, audit_end): def transform_instance(self, instance, audit_start, audit_end):
payload = { payload = {
@@ -206,7 +206,7 @@ class NotificationTransformer(object):
'tenant_id': instance.tenant_id 'tenant_id': instance.tenant_id
} }
payload['service_id'] = self._get_service_id( payload['service_id'] = self._get_service_id(
instance.service_type, CONF.notification_service_id) instance.datastore.manager, CONF.notification_service_id)
return payload return payload
def __call__(self): def __call__(self):

View File

@@ -212,7 +212,7 @@ class API(proxy.RpcProxy):
LOG.debug(_("Check diagnostics on Instance %s"), self.id) LOG.debug(_("Check diagnostics on Instance %s"), self.id)
return self._call("get_diagnostics", AGENT_LOW_TIMEOUT) return self._call("get_diagnostics", AGENT_LOW_TIMEOUT)
def prepare(self, memory_mb, databases, users, def prepare(self, memory_mb, packages, databases, users,
device_path='/dev/vdb', mount_point='/mnt/volume', device_path='/dev/vdb', mount_point='/mnt/volume',
backup_id=None, config_contents=None, root_password=None): backup_id=None, config_contents=None, root_password=None):
"""Make an asynchronous call to prepare the guest """Make an asynchronous call to prepare the guest
@@ -220,10 +220,10 @@ class API(proxy.RpcProxy):
""" """
LOG.debug(_("Sending the call to prepare the Guest")) LOG.debug(_("Sending the call to prepare the Guest"))
self._cast_with_consumer( self._cast_with_consumer(
"prepare", databases=databases, memory_mb=memory_mb, "prepare", packages=packages, databases=databases,
users=users, device_path=device_path, mount_point=mount_point, memory_mb=memory_mb, users=users, device_path=device_path,
backup_id=backup_id, config_contents=config_contents, mount_point=mount_point, backup_id=backup_id,
root_password=root_password) config_contents=config_contents, root_password=root_password)
def restart(self): def restart(self):
"""Restart the MySQL server.""" """Restart the MySQL server."""

View File

@@ -25,3 +25,46 @@ def get_os():
return REDHAT return REDHAT
else: else:
return DEBIAN return DEBIAN
def service_discovery(service_candidates):
"""
This function discovering how to start, stop, enable, disable service
in current environment. "service_candidates" is array with possible
system service names. Works for upstart, systemd, sysvinit.
"""
result = {}
for service in service_candidates:
# check upstart
if os.path.isfile("/etc/init/%s.conf" % service):
# upstart returns error code when service already started/stopped
result['cmd_start'] = "sudo start %s || true" % service
result['cmd_stop'] = "sudo stop %s || true" % service
result['cmd_enable'] = ("sudo sed -i '/^manual$/d' "
"/etc/init/%s.conf" % service)
result['cmd_disable'] = ("sudo sh -c 'echo manual >> "
"/etc/init/%s.conf'" % service)
break
# check sysvinit
if os.path.isfile("/etc/init.d/%s" % service):
result['cmd_start'] = "sudo service %s start" % service
result['cmd_stop'] = "sudo service %s stop" % service
if os.path.isfile("/usr/sbin/update-rc.d"):
result['cmd_enable'] = "sudo update-rc.d %s defaults; sudo " \
"update-rc.d %s enable" % (service,
service)
result['cmd_disable'] = "sudo update-rc.d %s defaults; sudo " \
"update-rc.d %s disable" % (service,
service)
elif os.path.isfile("/sbin/chkconfig"):
result['cmd_enable'] = "sudo chkconfig %s on" % service
result['cmd_disable'] = "sudo chkconfig %s off" % service
break
# check systemd
if os.path.isfile("/lib/systemd/system/%s.service" % service):
result['cmd_start'] = "sudo systemctl start %s" % service
result['cmd_stop'] = "sudo systemctl stop %s" % service
result['cmd_enable'] = "sudo systemctl enable %s" % service
result['cmd_disable'] = "sudo systemctl disable %s" % service
break
return result

View File

@@ -84,32 +84,26 @@ class Manager(periodic_task.PeriodicTasks):
raise raise
LOG.info(_("Restored database successfully")) LOG.info(_("Restored database successfully"))
def prepare(self, context, databases, memory_mb, users, device_path=None, def prepare(self, context, packages, databases, memory_mb, users,
mount_point=None, backup_id=None, config_contents=None, device_path=None, mount_point=None, backup_id=None,
root_password=None): config_contents=None, root_password=None):
"""Makes ready DBAAS on a Guest container.""" """Makes ready DBAAS on a Guest container."""
MySqlAppStatus.get().begin_install() MySqlAppStatus.get().begin_install()
# status end_mysql_install set with secure() # status end_mysql_install set with secure()
app = MySqlApp(MySqlAppStatus.get()) app = MySqlApp(MySqlAppStatus.get())
restart_mysql = False app.install_if_needed(packages)
if device_path: if device_path:
#stop and do not update database
app.stop_db()
device = volume.VolumeDevice(device_path) device = volume.VolumeDevice(device_path)
device.format() device.format()
#if a /var/lib/mysql folder exists, back it up.
if os.path.exists(CONF.mount_point): if os.path.exists(CONF.mount_point):
#stop and do not update database
app.stop_db()
#rsync exiting data #rsync exiting data
if not backup_id: device.migrate_data(CONF.mount_point)
restart_mysql = True
device.migrate_data(CONF.mount_point)
#mount the volume #mount the volume
device.mount(mount_point) device.mount(mount_point)
LOG.debug(_("Mounted the volume.")) LOG.debug(_("Mounted the volume."))
#check mysql was installed and stopped app.start_mysql()
if restart_mysql:
app.start_mysql()
app.install_if_needed()
if backup_id: if backup_id:
self._perform_restore(backup_id, context, CONF.mount_point, app) self._perform_restore(backup_id, context, CONF.mount_point, app)
LOG.info(_("Securing mysql now.")) LOG.info(_("Securing mysql now."))

View File

@@ -13,11 +13,11 @@ from trove.common import cfg
from trove.common import utils as utils from trove.common import utils as utils
from trove.common import exception from trove.common import exception
from trove.common import instance as rd_instance from trove.common import instance as rd_instance
from trove.guestagent.common import operating_system
from trove.guestagent.common import sql_query from trove.guestagent.common import sql_query
from trove.guestagent.db import models from trove.guestagent.db import models
from trove.guestagent import pkg from trove.guestagent import pkg
from trove.guestagent.datastore import service from trove.guestagent.datastore import service
from trove.guestagent.datastore.mysql import system
from trove.openstack.common import log as logging from trove.openstack.common import log as logging
from trove.openstack.common.gettextutils import _ from trove.openstack.common.gettextutils import _
from trove.extensions.mysql.models import RootHistory from trove.extensions.mysql.models import RootHistory
@@ -26,7 +26,6 @@ ADMIN_USER_NAME = "os_admin"
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
FLUSH = text(sql_query.FLUSH) FLUSH = text(sql_query.FLUSH)
ENGINE = None ENGINE = None
MYSQLD_ARGS = None
PREPARING = False PREPARING = False
UUID = False UUID = False
@@ -39,6 +38,11 @@ INCLUDE_MARKER_OPERATORS = {
False: ">" False: ">"
} }
MYSQL_CONFIG = "/etc/mysql/my.cnf"
MYSQL_SERVICE_CANDIDATES = ["mysql", "mysqld", "mysql-server"]
MYSQL_BIN_CANDIDATES = ["/usr/sbin/mysqld", "/usr/libexec/mysqld"]
# Create a package impl # Create a package impl
packager = pkg.Package() packager = pkg.Package()
@@ -47,12 +51,41 @@ def generate_random_password():
return passlib.utils.generate_password(size=CONF.default_password_length) return passlib.utils.generate_password(size=CONF.default_password_length)
def clear_expired_password():
"""
Some mysql installations generating random root password
and save it in /root/.mysql_secret, this password is
expired and should be changed by client that supports expired passwords.
"""
LOG.debug("Removing expired password.")
secret_file = "/root/.mysql_secret"
try:
out, err = utils.execute("cat", secret_file,
run_as_root=True, root_helper="sudo")
except exception.ProcessExecutionError:
LOG.debug("/root/.mysql_secret is not exists.")
return
m = re.match('# The random password set for the root user at .*: (.*)',
out)
if m:
try:
out, err = utils.execute("mysqladmin", "-p%s" % m.group(1),
"password", "", run_as_root=True,
root_helper="sudo")
except exception.ProcessExecutionError:
LOG.error("Cannot change mysql password.")
return
utils.execute("rm", "-f", secret_file, run_as_root=True,
root_helper="sudo")
LOG.debug("Expired password removed.")
def get_auth_password(): def get_auth_password():
pwd, err = utils.execute_with_timeout( pwd, err = utils.execute_with_timeout(
"sudo", "sudo",
"awk", "awk",
"/password\\t=/{print $3; exit}", "/password\\t=/{print $3; exit}",
system.MYSQL_CONFIG) MYSQL_CONFIG)
if err: if err:
LOG.error(err) LOG.error(err)
raise RuntimeError("Problem reading my.cnf! : %s" % err) raise RuntimeError("Problem reading my.cnf! : %s" % err)
@@ -76,8 +109,13 @@ def get_engine():
def load_mysqld_options(): def load_mysqld_options():
#find mysqld bin
for bin in MYSQL_BIN_CANDIDATES:
if os.path.isfile(bin):
mysqld_bin = bin
break
try: try:
out, err = utils.execute(system.MYSQL_BIN, "--print-defaults", out, err = utils.execute(mysqld_bin, "--print-defaults",
run_as_root=True, root_helper="sudo") run_as_root=True, root_helper="sudo")
arglist = re.split("\n", out)[1].split() arglist = re.split("\n", out)[1].split()
args = {} args = {}
@@ -89,7 +127,7 @@ def load_mysqld_options():
args[item.lstrip("--")] = None args[item.lstrip("--")] = None
return args return args
except exception.ProcessExecutionError: except exception.ProcessExecutionError:
return None return {}
class MySqlAppStatus(service.BaseDbStatus): class MySqlAppStatus(service.BaseDbStatus):
@@ -100,7 +138,6 @@ class MySqlAppStatus(service.BaseDbStatus):
return cls._instance return cls._instance
def _get_actual_db_status(self): def _get_actual_db_status(self):
global MYSQLD_ARGS
try: try:
out, err = utils.execute_with_timeout( out, err = utils.execute_with_timeout(
"/usr/bin/mysqladmin", "/usr/bin/mysqladmin",
@@ -119,10 +156,9 @@ class MySqlAppStatus(service.BaseDbStatus):
LOG.info("Service Status is BLOCKED.") LOG.info("Service Status is BLOCKED.")
return rd_instance.ServiceStatuses.BLOCKED return rd_instance.ServiceStatuses.BLOCKED
except exception.ProcessExecutionError: except exception.ProcessExecutionError:
if not MYSQLD_ARGS: mysql_args = load_mysqld_options()
MYSQLD_ARGS = load_mysqld_options() pid_file = mysql_args.get('pid_file',
pid_file = MYSQLD_ARGS.get('pid_file', '/var/run/mysqld/mysqld.pid')
'/var/run/mysqld/mysqld.pid')
if os.path.exists(pid_file): if os.path.exists(pid_file):
LOG.info("Service Status is CRASHED.") LOG.info("Service Status is CRASHED.")
return rd_instance.ServiceStatuses.CRASHED return rd_instance.ServiceStatuses.CRASHED
@@ -492,10 +528,6 @@ class MySqlApp(object):
"""Prepares DBaaS on a Guest container.""" """Prepares DBaaS on a Guest container."""
TIME_OUT = 1000 TIME_OUT = 1000
if CONF.service_type == "mysql":
MYSQL_PACKAGE_VERSION = CONF.mysql_pkg
elif CONF.service_type == "percona":
MYSQL_PACKAGE_VERSION = CONF.percona_pkg
def __init__(self, status): def __init__(self, status):
""" By default login with root no password for initial setup. """ """ By default login with root no password for initial setup. """
@@ -522,11 +554,19 @@ class MySqlApp(object):
t = text(str(uu)) t = text(str(uu))
client.execute(t) client.execute(t)
def install_if_needed(self): def install_if_needed(self, packages):
"""Prepare the guest machine with a secure mysql server installation""" """Prepare the guest machine with a secure mysql server installation"""
LOG.info(_("Preparing Guest as MySQL Server")) LOG.info(_("Preparing Guest as MySQL Server"))
if not self.is_installed(): if not packager.pkg_is_installed(packages):
self._install_mysql() LOG.debug(_("Installing mysql server"))
self._clear_mysql_config()
# set blank password on pkg configuration stage
pkg_opts = {'root_password': '',
'root_password_again': ''}
packager.pkg_install(packages, pkg_opts, self.TIME_OUT)
self._create_mysql_confd_dir()
LOG.debug(_("Finished installing mysql server"))
self.start_mysql()
LOG.info(_("Dbaas install_if_needed complete")) LOG.info(_("Dbaas install_if_needed complete"))
def complete_install_or_restart(self): def complete_install_or_restart(self):
@@ -535,7 +575,7 @@ class MySqlApp(object):
def secure(self, config_contents): def secure(self, config_contents):
LOG.info(_("Generating admin password...")) LOG.info(_("Generating admin password..."))
admin_password = generate_random_password() admin_password = generate_random_password()
clear_expired_password()
engine = sqlalchemy.create_engine("mysql://root:@localhost:3306", engine = sqlalchemy.create_engine("mysql://root:@localhost:3306",
echo=True) echo=True)
with LocalSqlClient(engine) as client: with LocalSqlClient(engine) as client:
@@ -549,22 +589,25 @@ class MySqlApp(object):
LOG.info(_("Dbaas secure complete.")) LOG.info(_("Dbaas secure complete."))
def secure_root(self, secure_remote_root=True): def secure_root(self, secure_remote_root=True):
engine = sqlalchemy.create_engine("mysql://root:@localhost:3306", with LocalSqlClient(get_engine()) as client:
echo=True)
with LocalSqlClient(engine) as client:
LOG.info(_("Preserving root access from restore")) LOG.info(_("Preserving root access from restore"))
self._generate_root_password(client) self._generate_root_password(client)
if secure_remote_root: if secure_remote_root:
self._remove_remote_root_access(client) self._remove_remote_root_access(client)
def _install_mysql(self): def _clear_mysql_config(self):
"""Install mysql server. The current version is 5.5""" """Clear old configs, which can be incompatible with new version """
LOG.debug(_("Installing mysql server")) LOG.debug("Clearing old mysql config")
self._create_mysql_confd_dir() random_uuid = str(uuid.uuid4())
packager.pkg_install(self.MYSQL_PACKAGE_VERSION, self.TIME_OUT) configs = ["/etc/my.cnf", "/etc/mysql/conf.d", "/etc/mysql/my.cnf"]
self.start_mysql() for config in configs:
LOG.debug(_("Finished installing mysql server")) command = "mv %s %s_%s" % (config, config, random_uuid)
#TODO(rnirmal): Add checks to make sure the package got installed try:
utils.execute_with_timeout(command, shell=True,
root_helper="sudo")
LOG.debug("%s saved to %s_%s" % (config, config, random_uuid))
except exception.ProcessExecutionError:
pass
def _create_mysql_confd_dir(self): def _create_mysql_confd_dir(self):
conf_dir = "/etc/mysql/conf.d" conf_dir = "/etc/mysql/conf.d"
@@ -573,44 +616,33 @@ class MySqlApp(object):
utils.execute_with_timeout(command, shell=True) utils.execute_with_timeout(command, shell=True)
def _enable_mysql_on_boot(self): def _enable_mysql_on_boot(self):
"""
There is a difference between the init.d mechanism and the upstart
The stock mysql uses the upstart mechanism, therefore, there is a
mysql.conf file responsible for the job. to toggle enable/disable
on boot one needs to modify this file. Percona uses the init.d
mechanism and there is no mysql.conf file. Instead, the update-rc.d
command needs to be used to modify the /etc/rc#.d/[S/K]##mysql links
"""
LOG.info("Enabling mysql on boot.") LOG.info("Enabling mysql on boot.")
conf = "/etc/init/mysql.conf" try:
if os.path.isfile(conf): mysql_service = operating_system.service_discovery(
command = "sudo sed -i '/^manual$/d' %(conf)s" % {'conf': conf} MYSQL_SERVICE_CANDIDATES)
else: utils.execute_with_timeout(mysql_service['cmd_enable'], shell=True)
command = system.MYSQL_CMD_ENABLE except KeyError:
utils.execute_with_timeout(command, shell=True) raise RuntimeError("Service is not discovered.")
def _disable_mysql_on_boot(self): def _disable_mysql_on_boot(self):
""" try:
There is a difference between the init.d mechanism and the upstart mysql_service = operating_system.service_discovery(
The stock mysql uses the upstart mechanism, therefore, there is a MYSQL_SERVICE_CANDIDATES)
mysql.conf file responsible for the job. to toggle enable/disable utils.execute_with_timeout(mysql_service['cmd_disable'],
on boot one needs to modify this file. Percona uses the init.d shell=True)
mechanism and there is no mysql.conf file. Instead, the update-rc.d except KeyError:
command needs to be used to modify the /etc/rc#.d/[S/K]##mysql links raise RuntimeError("Service is not discovered.")
"""
LOG.info("Disabling mysql on boot.")
conf = "/etc/init/mysql.conf"
if os.path.isfile(conf):
command = "sudo sh -c 'echo manual >> %(conf)s'" % {'conf': conf}
else:
command = system.MYSQL_CMD_DISABLE
utils.execute_with_timeout(command, shell=True)
def stop_db(self, update_db=False, do_not_start_on_reboot=False): def stop_db(self, update_db=False, do_not_start_on_reboot=False):
LOG.info(_("Stopping mysql...")) LOG.info(_("Stopping mysql..."))
if do_not_start_on_reboot: if do_not_start_on_reboot:
self._disable_mysql_on_boot() self._disable_mysql_on_boot()
utils.execute_with_timeout(system.MYSQL_CMD_STOP, shell=True) try:
mysql_service = operating_system.service_discovery(
MYSQL_SERVICE_CANDIDATES)
utils.execute_with_timeout(mysql_service['cmd_stop'], shell=True)
except KeyError:
raise RuntimeError("Service is not discovered.")
if not self.status.wait_for_real_status_to_change_to( if not self.status.wait_for_real_status_to_change_to(
rd_instance.ServiceStatuses.SHUTDOWN, rd_instance.ServiceStatuses.SHUTDOWN,
self.state_change_wait_time, update_db): self.state_change_wait_time, update_db):
@@ -696,13 +728,13 @@ class MySqlApp(object):
with open(TMP_MYCNF, 'w') as t: with open(TMP_MYCNF, 'w') as t:
t.write(config_contents) t.write(config_contents)
utils.execute_with_timeout("sudo", "mv", TMP_MYCNF, utils.execute_with_timeout("sudo", "mv", TMP_MYCNF,
system.MYSQL_CONFIG) MYSQL_CONFIG)
self._write_temp_mycnf_with_admin_account(system.MYSQL_CONFIG, self._write_temp_mycnf_with_admin_account(MYSQL_CONFIG,
TMP_MYCNF, TMP_MYCNF,
admin_password) admin_password)
utils.execute_with_timeout("sudo", "mv", TMP_MYCNF, utils.execute_with_timeout("sudo", "mv", TMP_MYCNF,
system.MYSQL_CONFIG) MYSQL_CONFIG)
self.wipe_ib_logfiles() self.wipe_ib_logfiles()
@@ -715,8 +747,11 @@ class MySqlApp(object):
self._enable_mysql_on_boot() self._enable_mysql_on_boot()
try: try:
utils.execute_with_timeout(system. mysql_service = operating_system.service_discovery(
MYSQL_CMD_START, shell=True) MYSQL_SERVICE_CANDIDATES)
utils.execute_with_timeout(mysql_service['cmd_start'], shell=True)
except KeyError:
raise RuntimeError("Service is not discovered.")
except exception.ProcessExecutionError: except exception.ProcessExecutionError:
# it seems mysql (percona, at least) might come back with [Fail] # it seems mysql (percona, at least) might come back with [Fail]
# but actually come up ok. we're looking into the timing issue on # but actually come up ok. we're looking into the timing issue on
@@ -756,11 +791,6 @@ class MySqlApp(object):
LOG.info(_("Resetting configuration")) LOG.info(_("Resetting configuration"))
self._write_mycnf(None, config_contents) self._write_mycnf(None, config_contents)
def is_installed(self):
#(cp16net) could raise an exception, does it need to be handled here?
version = packager.pkg_version(self.MYSQL_PACKAGE_VERSION)
return not version is None
class MySqlRootAccess(object): class MySqlRootAccess(object):
@classmethod @classmethod

View File

@@ -1,51 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2011 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Determines operating system version and os depended commands.
"""
import os.path
from trove.common import cfg
CONF = cfg.CONF
REDHAT = 'redhat'
DEBIAN = 'debian'
# The default is debian
OS = DEBIAN
MYSQL_CONFIG = "/etc/mysql/my.cnf"
MYSQL_BIN = "/usr/sbin/mysqld"
MYSQL_CMD_ENABLE = "sudo update-rc.d mysql enable"
MYSQL_CMD_DISABLE = "sudo update-rc.d mysql disable"
MYSQL_CMD_START = "sudo service mysql start || /bin/true"
MYSQL_CMD_STOP = "sudo service mysql stop || /bin/true"
if os.path.isfile("/etc/redhat-release"):
OS = REDHAT
MYSQL_CONFIG = "/etc/my.cnf"
if CONF.service_type == 'percona':
MYSQL_CMD_ENABLE = "sudo chkconfig mysql on"
MYSQL_CMD_DISABLE = "sudo chkconfig mysql off"
MYSQL_CMD_START = "sudo service mysql start"
MYSQL_CMD_STOP = "sudo service mysql stop"
else:
MYSQL_BIN = "/usr/libexec/mysqld"
MYSQL_CMD_ENABLE = "sudo chkconfig mysqld on"
MYSQL_CMD_DISABLE = "sudo chkconfig mysqld off"
MYSQL_CMD_START = "sudo service mysqld start"
MYSQL_CMD_STOP = "sudo service mysqld stop"

View File

@@ -42,10 +42,10 @@ CONF = cfg.CONF
def get_custom_managers(): def get_custom_managers():
return CONF.service_registry_ext return CONF.datastore_registry_ext
def service_registry(): def datastore_registry():
return dict(chain(defaults.iteritems(), return dict(chain(defaults.iteritems(),
get_custom_managers().iteritems())) get_custom_managers().iteritems()))

View File

@@ -20,6 +20,7 @@ Manages packages on the Guest VM.
""" """
import commands import commands
import re import re
from tempfile import NamedTemporaryFile
import pexpect import pexpect
@@ -35,6 +36,7 @@ LOG = logging.getLogger(__name__)
OK = 0 OK = 0
RUN_DPKG_FIRST = 1 RUN_DPKG_FIRST = 1
REINSTALL_FIRST = 2 REINSTALL_FIRST = 2
CONFLICT_REMOVED = 3
class PkgAdminLockError(exception.TroveError): class PkgAdminLockError(exception.TroveError):
@@ -61,11 +63,15 @@ class PkgScriptletError(exception.TroveError):
pass pass
class PkgTransactionCheckError(exception.TroveError): class PkgDownloadError(exception.TroveError):
pass pass
class PkgDownloadError(exception.TroveError): class PkgSignError(exception.TroveError):
pass
class PkgBrokenError(exception.TroveError):
pass pass
@@ -84,16 +90,29 @@ class BasePackagerMixin:
child = pexpect.spawn(cmd, timeout=time_out) child = pexpect.spawn(cmd, timeout=time_out)
try: try:
i = child.expect(output_expects) i = child.expect(output_expects)
match = child.match
self.pexpect_wait_and_close_proc(child) self.pexpect_wait_and_close_proc(child)
except pexpect.TIMEOUT: except pexpect.TIMEOUT:
self.pexpect_kill_proc(child) self.pexpect_kill_proc(child)
raise PkgTimeout("Process timeout after %i seconds." % time_out) raise PkgTimeout("Process timeout after %i seconds." % time_out)
return i return (i, match)
class RedhatPackagerMixin(BasePackagerMixin): class RedhatPackagerMixin(BasePackagerMixin):
def _install(self, package_name, time_out): def _rpm_remove_nodeps(self, package_name):
"""
Sometimes transaction errors happens, easy way is to remove
conflicted package without dependencies and hope it will replaced
by anoter package
"""
try:
utils.execute("rpm", "-e", "--nodeps", package_name,
run_as_root=True, root_helper="sudo")
except ProcessExecutionError:
LOG.error(_("Error removing conflict %s") % package_name)
def _install(self, packages, time_out):
"""Attempts to install a package. """Attempts to install a package.
Returns OK if the package installs fine or a result code if a Returns OK if the package installs fine or a result code if a
@@ -101,27 +120,35 @@ class RedhatPackagerMixin(BasePackagerMixin):
Raises an exception if a non-recoverable error or time out occurs. Raises an exception if a non-recoverable error or time out occurs.
""" """
cmd = "sudo yum --color=never -y install %s" % package_name cmd = "sudo yum --color=never -y install %s" % packages
output_expects = ['\[sudo\] password for .*:', output_expects = ['\[sudo\] password for .*:',
'No package %s available.' % package_name, 'No package (.*) available.',
'Transaction Check Error:', ('file .* from install of .* conflicts with file'
' from package (.*?)\r\n'),
'Error: (.*?) conflicts with .*?\r\n',
'Processing Conflict: .* conflicts (.*?)\r\n',
'.*scriptlet failed*', '.*scriptlet failed*',
'HTTP Error', 'HTTP Error',
'No more mirrors to try.', 'No more mirrors to try.',
'GPG key retrieval failed:',
'.*already installed and latest version', '.*already installed and latest version',
'Updated:', 'Updated:',
'Installed:'] 'Installed:']
i = self.pexpect_run(cmd, output_expects, time_out) LOG.debug("Running package install command: %s" % cmd)
i, match = self.pexpect_run(cmd, output_expects, time_out)
if i == 0: if i == 0:
raise PkgPermissionError("Invalid permissions.") raise PkgPermissionError("Invalid permissions.")
elif i == 1: elif i == 1:
raise PkgNotFoundError("Could not find pkg %s" % package_name) raise PkgNotFoundError("Could not find pkg %s" % match.group(1))
elif i == 2: elif i == 2 or i == 3 or i == 4:
raise PkgTransactionCheckError("Transaction Check Error") self._rpm_remove_nodeps(match.group(1))
elif i == 3: return CONFLICT_REMOVED
elif i == 5:
raise PkgScriptletError("Package scriptlet failed") raise PkgScriptletError("Package scriptlet failed")
elif i == 4 or i == 5: elif i == 6 or i == 7:
raise PkgDownloadError("Package download problem") raise PkgDownloadError("Package download problem")
elif i == 8:
raise PkgSignError("GPG key retrieval failed")
return OK return OK
def _remove(self, package_name, time_out): def _remove(self, package_name, time_out):
@@ -136,18 +163,35 @@ class RedhatPackagerMixin(BasePackagerMixin):
output_expects = ['\[sudo\] password for .*:', output_expects = ['\[sudo\] password for .*:',
'No Packages marked for removal', 'No Packages marked for removal',
'Removed:'] 'Removed:']
i = self.pexpect_run(cmd, output_expects, time_out) i, match = self.pexpect_run(cmd, output_expects, time_out)
if i == 0: if i == 0:
raise PkgPermissionError("Invalid permissions.") raise PkgPermissionError("Invalid permissions.")
elif i == 1: elif i == 1:
raise PkgNotFoundError("Could not find pkg %s" % package_name) raise PkgNotFoundError("Could not find pkg %s" % package_name)
return OK return OK
def pkg_install(self, package_name, time_out): def pkg_install(self, packages, config_opts, time_out):
result = self._install(package_name, time_out) result = self._install(packages, time_out)
if result != OK: if result != OK:
raise PkgPackageStateError("Package %s is in a bad state." while result == CONFLICT_REMOVED:
% package_name) result = self._install(packages, time_out)
if result != OK:
raise PkgPackageStateError("Cannot install packages.")
def pkg_is_installed(self, packages):
pkg_list = packages.split()
cmd = "rpm -qa"
p = commands.getstatusoutput(cmd)
std_out = p[1]
for pkg in pkg_list:
found = False
for line in std_out.split("\n"):
if line.find(pkg) != -1:
found = True
break
if not found:
return False
return True
def pkg_version(self, package_name): def pkg_version(self, package_name):
cmd_list = ["rpm", "-qa", "--qf", "'%{VERSION}-%{RELEASE}\n'", cmd_list = ["rpm", "-qa", "--qf", "'%{VERSION}-%{RELEASE}\n'",
@@ -185,33 +229,71 @@ class DebianPackagerMixin(BasePackagerMixin):
except ProcessExecutionError: except ProcessExecutionError:
LOG.error(_("Error fixing dpkg")) LOG.error(_("Error fixing dpkg"))
def _install(self, package_name, time_out): def _fix_package_selections(self, packages, config_opts):
"""Attempts to install a package. """
Sometimes you have to run this command before a pkg will install.
This command sets package selections to configure package.
"""
selections = ""
for package in packages:
m = re.match('(.+)=(.+)', package)
if m:
package_name = m.group(1)
else:
package_name = package
command = "sudo debconf-show %s" % package_name
p = commands.getstatusoutput(command)
std_out = p[1]
for line in std_out.split("\n"):
for selection, value in config_opts.items():
m = re.match(".* (.*/%s):.*" % selection, line)
if m:
selections += ("%s %s string '%s'\n" %
(package_name, m.group(1), value))
if selections:
with NamedTemporaryFile(delete=False) as f:
fname = f.name
f.write(selections)
utils.execute("debconf-set-selections %s && dpkg --configure -a"
% fname, run_as_root=True, root_helper="sudo",
shell=True)
os.remove(fname)
def _install(self, packages, time_out):
"""Attempts to install a packages.
Returns OK if the package installs fine or a result code if a Returns OK if the package installs fine or a result code if a
recoverable-error occurred. recoverable-error occurred.
Raises an exception if a non-recoverable error or time out occurs. Raises an exception if a non-recoverable error or time out occurs.
""" """
cmd = "sudo -E DEBIAN_FRONTEND=noninteractive " \ cmd = "sudo -E DEBIAN_FRONTEND=noninteractive apt-get -y " \
"apt-get -y --allow-unauthenticated install %s" % package_name "--force-yes --allow-unauthenticated -o " \
"DPkg::options::=--force-confmiss --reinstall " \
"install %s" % packages
output_expects = ['.*password*', output_expects = ['.*password*',
'E: Unable to locate package %s' % package_name, 'E: Unable to locate package (.*)',
"Couldn't find package % s" % package_name, "Couldn't find package (.*)",
"E: Version '.*' for '(.*)' was not found",
("dpkg was interrupted, you must manually run " ("dpkg was interrupted, you must manually run "
"'sudo dpkg --configure -a'"), "'sudo dpkg --configure -a'"),
"Unable to lock the administration directory", "Unable to lock the administration directory",
"Setting up %s*" % package_name, ("E: Unable to correct problems, you have held "
"broken packages."),
"Setting up (.*)",
"is already the newest version"] "is already the newest version"]
i = self.pexpect_run(cmd, output_expects, time_out) LOG.debug("Running package install command: %s" % cmd)
i, match = self.pexpect_run(cmd, output_expects, time_out)
if i == 0: if i == 0:
raise PkgPermissionError("Invalid permissions.") raise PkgPermissionError("Invalid permissions.")
elif i == 1 or i == 2: elif i == 1 or i == 2 or i == 3:
raise PkgNotFoundError("Could not find apt %s" % package_name) raise PkgNotFoundError("Could not find apt %s" % match.group(1))
elif i == 3:
return RUN_DPKG_FIRST
elif i == 4: elif i == 4:
return RUN_DPKG_FIRST
elif i == 5:
raise PkgAdminLockError() raise PkgAdminLockError()
elif i == 6:
raise PkgBrokenError()
return OK return OK
def _remove(self, package_name, time_out): def _remove(self, package_name, time_out):
@@ -232,7 +314,7 @@ class DebianPackagerMixin(BasePackagerMixin):
"'sudo dpkg --configure -a'"), "'sudo dpkg --configure -a'"),
"Unable to lock the administration directory", "Unable to lock the administration directory",
"Removing %s*" % package_name] "Removing %s*" % package_name]
i = self.pexpect_run(cmd, output_expects, time_out) i, match = self.pexpect_run(cmd, output_expects, time_out)
if i == 0: if i == 0:
raise PkgPermissionError("Invalid permissions.") raise PkgPermissionError("Invalid permissions.")
elif i == 1: elif i == 1:
@@ -245,58 +327,54 @@ class DebianPackagerMixin(BasePackagerMixin):
raise PkgAdminLockError() raise PkgAdminLockError()
return OK return OK
def pkg_install(self, package_name, time_out): def pkg_install(self, packages, config_opts, time_out):
"""Installs a package.""" """Installs a packages."""
try: try:
utils.execute("apt-get", "update", run_as_root=True, utils.execute("apt-get", "update", run_as_root=True,
root_helper="sudo") root_helper="sudo")
except ProcessExecutionError: except ProcessExecutionError:
LOG.error(_("Error updating the apt sources")) LOG.error(_("Error updating the apt sources"))
result = self._install(package_name, time_out) result = self._install(packages, time_out)
if result != OK: if result != OK:
if result == RUN_DPKG_FIRST: if result == RUN_DPKG_FIRST:
self._fix(time_out) self._fix(time_out)
result = self._install(package_name, time_out) result = self._install(packages, time_out)
if result != OK: if result != OK:
raise PkgPackageStateError("Package %s is in a bad state." raise PkgPackageStateError("Packages is in a bad state.")
% package_name) # even after successful install, packages can stay unconfigured
# config_opts - is dict with name/value for questions asked by
# interactive configure script
self._fix_package_selections(packages, config_opts)
def pkg_version(self, package_name): def pkg_version(self, package_name):
cmd_list = ["dpkg", "-l", package_name] p = commands.getstatusoutput("apt-cache policy %s" % package_name)
p = commands.getstatusoutput(' '.join(cmd_list))
# check the command status code
if not p[0] == 0:
return None
# Need to capture the version string
# check the command output
std_out = p[1] std_out = p[1]
patterns = ['.*No packages found matching.*',
"\w\w\s+(\S+)\s+(\S+)\s+(.*)$"]
for line in std_out.split("\n"): for line in std_out.split("\n"):
for p in patterns: m = re.match("\s+Installed: (.*)", line)
regex = re.compile(p) if m:
matches = regex.match(line) version = m.group(1)
if matches: if version == "(none)":
line = matches.group() version = None
parts = line.split() return version
if not parts:
msg = _("returned nothing") def pkg_is_installed(self, packages):
LOG.error(msg) pkg_list = packages.split()
raise exception.GuestError(msg) for pkg in pkg_list:
if len(parts) <= 2: m = re.match('(.+)=(.+)', pkg)
msg = _("Unexpected output.") if m:
LOG.error(msg) package_name = m.group(1)
raise exception.GuestError(msg) package_version = m.group(2)
if parts[1] != package_name: else:
msg = _("Unexpected output:[1] = %s") % str(parts[1]) package_name = pkg
LOG.error(msg) package_version = None
raise exception.GuestError(msg) installed_version = self.pkg_version(package_name)
if parts[0] == 'un' or parts[2] == '<none>': if ((package_version and installed_version == package_version) or
return None (installed_version and not package_version)):
return parts[2] LOG.debug(_("Package %s already installed.") % package_name)
msg = _("version() saw unexpected output from dpkg!") else:
LOG.error(msg) return False
return True
def pkg_remove(self, package_name, time_out): def pkg_remove(self, package_name, time_out):
"""Removes a package.""" """Removes a package."""

View File

@@ -30,6 +30,7 @@ from trove.common import utils
from trove.extensions.security_group.models import SecurityGroup from trove.extensions.security_group.models import SecurityGroup
from trove.db import get_db_api from trove.db import get_db_api
from trove.db import models as dbmodels from trove.db import models as dbmodels
from trove.datastore import models as datastore_models
from trove.backup.models import Backup from trove.backup.models import Backup
from trove.quota.quota import run_with_quotas from trove.quota.quota import run_with_quotas
from trove.instance.tasks import InstanceTask from trove.instance.tasks import InstanceTask
@@ -121,6 +122,10 @@ class SimpleInstance(object):
self.db_info = db_info self.db_info = db_info
self.service_status = service_status self.service_status = service_status
self.root_pass = root_password self.root_pass = root_password
self.ds_version = (datastore_models.DatastoreVersion.
load(self.db_info.datastore_version_id))
self.ds = (datastore_models.Datastore.
load(self.ds_version.datastore_id))
@property @property
def addresses(self): def addresses(self):
@@ -227,8 +232,12 @@ class SimpleInstance(object):
return self.db_info.volume_size return self.db_info.volume_size
@property @property
def service_type(self): def datastore_version(self):
return self.db_info.service_type return self.ds_version
@property
def datastore(self):
return self.ds
@property @property
def root_password(self): def root_password(self):
@@ -426,8 +435,8 @@ class Instance(BuiltInstance):
""" """
@classmethod @classmethod
def create(cls, context, name, flavor_id, image_id, def create(cls, context, name, flavor_id, image_id, databases, users,
databases, users, service_type, volume_size, backup_id, datastore, datastore_version, volume_size, backup_id,
availability_zone=None): availability_zone=None):
client = create_nova_client(context) client = create_nova_client(context)
@@ -463,7 +472,8 @@ class Instance(BuiltInstance):
db_info = DBInstance.create(name=name, flavor_id=flavor_id, db_info = DBInstance.create(name=name, flavor_id=flavor_id,
tenant_id=context.tenant, tenant_id=context.tenant,
volume_size=volume_size, volume_size=volume_size,
service_type=service_type, datastore_version_id=
datastore_version.id,
task_status=InstanceTasks.BUILDING) task_status=InstanceTasks.BUILDING)
LOG.debug(_("Tenant %(tenant)s created new " LOG.debug(_("Tenant %(tenant)s created new "
"Trove instance %(db)s...") % "Trove instance %(db)s...") %
@@ -485,8 +495,9 @@ class Instance(BuiltInstance):
task_api.API(context).create_instance(db_info.id, name, flavor, task_api.API(context).create_instance(db_info.id, name, flavor,
image_id, databases, users, image_id, databases, users,
service_type, volume_size, datastore.manager,
backup_id, datastore_version.packages,
volume_size, backup_id,
availability_zone, availability_zone,
root_password) root_password)
@@ -694,7 +705,8 @@ class DBInstance(dbmodels.DatabaseModelBase):
_data_fields = ['name', 'created', 'compute_instance_id', _data_fields = ['name', 'created', 'compute_instance_id',
'task_id', 'task_description', 'task_start_time', 'task_id', 'task_description', 'task_start_time',
'volume_id', 'deleted', 'tenant_id', 'service_type'] 'volume_id', 'deleted', 'tenant_id',
'datastore_version_id']
def __init__(self, task_status, **kwargs): def __init__(self, task_status, **kwargs):
kwargs["task_id"] = task_status.code kwargs["task_id"] = task_status.code
@@ -719,12 +731,6 @@ class DBInstance(dbmodels.DatabaseModelBase):
task_status = property(get_task_status, set_task_status) task_status = property(get_task_status, set_task_status)
class ServiceImage(dbmodels.DatabaseModelBase):
"""Defines the status of the service being run."""
_data_fields = ['service_name', 'image_id']
class InstanceServiceStatus(dbmodels.DatabaseModelBase): class InstanceServiceStatus(dbmodels.DatabaseModelBase):
_data_fields = ['instance_id', 'status_id', 'status_description', _data_fields = ['instance_id', 'status_id', 'status_description',
'updated_at'] 'updated_at']
@@ -758,7 +764,6 @@ class InstanceServiceStatus(dbmodels.DatabaseModelBase):
def persisted_models(): def persisted_models():
return { return {
'instance': DBInstance, 'instance': DBInstance,
'service_image': ServiceImage,
'service_statuses': InstanceServiceStatus, 'service_statuses': InstanceServiceStatus,
} }

View File

@@ -25,6 +25,7 @@ from trove.common import wsgi
from trove.extensions.mysql.common import populate_validated_databases from trove.extensions.mysql.common import populate_validated_databases
from trove.extensions.mysql.common import populate_users from trove.extensions.mysql.common import populate_users
from trove.instance import models, views from trove.instance import models, views
from trove.datastore import models as datastore_models
from trove.backup.models import Backup as backup_model from trove.backup.models import Backup as backup_model
from trove.backup import views as backup_views from trove.backup import views as backup_views
from trove.openstack.common import log as logging from trove.openstack.common import log as logging
@@ -178,11 +179,10 @@ class InstanceController(wsgi.Controller):
LOG.info(_("req : '%s'\n\n") % req) LOG.info(_("req : '%s'\n\n") % req)
LOG.info(_("body : '%s'\n\n") % body) LOG.info(_("body : '%s'\n\n") % body)
context = req.environ[wsgi.CONTEXT_KEY] context = req.environ[wsgi.CONTEXT_KEY]
# Set the service type to mysql if its not in the request datastore_args = body['instance'].get('datastore', {})
service_type = (body['instance'].get('service_type') or datastore, datastore_version = (
CONF.service_type) datastore_models.get_datastore_version(**datastore_args))
service = models.ServiceImage.find_by(service_name=service_type) image_id = datastore_version.image_id
image_id = service['image_id']
name = body['instance']['name'] name = body['instance']['name']
flavor_ref = body['instance']['flavorRef'] flavor_ref = body['instance']['flavorRef']
flavor_id = utils.get_id_from_href(flavor_ref) flavor_id = utils.get_id_from_href(flavor_ref)
@@ -214,8 +214,9 @@ class InstanceController(wsgi.Controller):
instance = models.Instance.create(context, name, flavor_id, instance = models.Instance.create(context, name, flavor_id,
image_id, databases, users, image_id, databases, users,
service_type, volume_size, datastore, datastore_version,
backup_id, availability_zone) volume_size, backup_id,
availability_zone)
view = views.InstanceDetailView(instance, req=req) view = views.InstanceDetailView(instance, req=req)
return wsgi.Result(view.data(), 200) return wsgi.Result(view.data(), 200)

View File

@@ -55,6 +55,7 @@ class InstanceView(object):
"status": self.instance.status, "status": self.instance.status,
"links": self._build_links(), "links": self._build_links(),
"flavor": self._build_flavor_info(), "flavor": self._build_flavor_info(),
"datastore": {"type": self.instance.datastore.name},
} }
if CONF.trove_volume_support: if CONF.trove_volume_support:
instance_dict['volume'] = {'size': self.instance.volume_size} instance_dict['volume'] = {'size': self.instance.volume_size}
@@ -88,6 +89,9 @@ class InstanceDetailView(InstanceView):
result['instance']['created'] = self.instance.created result['instance']['created'] = self.instance.created
result['instance']['updated'] = self.instance.updated result['instance']['updated'] = self.instance.updated
result['instance']['datastore']['version'] = (self.instance.
datastore_version.name)
dns_support = CONF.trove_dns_support dns_support = CONF.trove_dns_support
if dns_support: if dns_support:
result['instance']['hostname'] = self.instance.hostname result['instance']['hostname'] = self.instance.hostname

View File

@@ -99,10 +99,10 @@ class API(proxy.RpcProxy):
self.cast(self.context, self.make_msg("delete_backup", self.cast(self.context, self.make_msg("delete_backup",
backup_id=backup_id)) backup_id=backup_id))
def create_instance(self, instance_id, name, flavor, image_id, def create_instance(self, instance_id, name, flavor,
databases, users, service_type, volume_size, image_id, databases, users, datastore_manager,
backup_id=None, availability_zone=None, packages, volume_size, backup_id=None,
root_password=None): availability_zone=None, root_password=None):
LOG.debug("Making async call to create instance %s " % instance_id) LOG.debug("Making async call to create instance %s " % instance_id)
self.cast(self.context, self.cast(self.context,
self.make_msg("create_instance", self.make_msg("create_instance",
@@ -111,7 +111,8 @@ class API(proxy.RpcProxy):
image_id=image_id, image_id=image_id,
databases=databases, databases=databases,
users=users, users=users,
service_type=service_type, datastore_manager=datastore_manager,
packages=packages,
volume_size=volume_size, volume_size=volume_size,
backup_id=backup_id, backup_id=backup_id,
availability_zone=availability_zone, availability_zone=availability_zone,

View File

@@ -81,12 +81,13 @@ class Manager(periodic_task.PeriodicTasks):
instance_tasks.create_backup(backup_id) instance_tasks.create_backup(backup_id)
def create_instance(self, context, instance_id, name, flavor, def create_instance(self, context, instance_id, name, flavor,
image_id, databases, users, service_type, image_id, databases, users, datastore_manager,
volume_size, backup_id, availability_zone, packages, volume_size, backup_id, availability_zone,
root_password): root_password):
instance_tasks = FreshInstanceTasks.load(context, instance_id) instance_tasks = FreshInstanceTasks.load(context, instance_id)
instance_tasks.create_instance(flavor, image_id, databases, users, instance_tasks.create_instance(flavor, image_id, databases, users,
service_type, volume_size, backup_id, datastore_manager, packages,
volume_size, backup_id,
availability_zone, root_password) availability_zone, root_password)
if CONF.exists_notification_transformer: if CONF.exists_notification_transformer:

View File

@@ -69,14 +69,14 @@ class NotifyMixin(object):
This adds the ability to send usage events to an Instance object. This adds the ability to send usage events to an Instance object.
""" """
def _get_service_id(self, service_type, id_map): def _get_service_id(self, datastore_manager, id_map):
if service_type in id_map: if datastore_manager in id_map:
service_type_id = id_map[service_type] datastore_manager_id = id_map[datastore_manager]
else: else:
service_type_id = cfg.UNKNOWN_SERVICE_ID datastore_manager_id = cfg.UNKNOWN_SERVICE_ID
LOG.error(_("Service ID for Type (%s) is not configured") LOG.error("Datastore ID for Manager (%s) is not configured"
% service_type) % datastore_manager)
return service_type_id return datastore_manager_id
def send_usage_event(self, event_type, **kwargs): def send_usage_event(self, event_type, **kwargs):
event_type = 'trove.instance.%s' % event_type event_type = 'trove.instance.%s' % event_type
@@ -117,7 +117,7 @@ class NotifyMixin(object):
}) })
payload['service_id'] = self._get_service_id( payload['service_id'] = self._get_service_id(
self.service_type, CONF.notification_service_id) self.datastore.manager, CONF.notification_service_id)
# Update payload with all other kwargs # Update payload with all other kwargs
payload.update(kwargs) payload.update(kwargs)
@@ -133,17 +133,17 @@ class ConfigurationMixin(object):
Configuration related tasks for instances and resizes. Configuration related tasks for instances and resizes.
""" """
def _render_config(self, service_type, flavor, instance_id): def _render_config(self, datastore_manager, flavor, instance_id):
config = template.SingleInstanceConfigTemplate( config = template.SingleInstanceConfigTemplate(
service_type, flavor, instance_id) datastore_manager, flavor, instance_id)
config.render() config.render()
return config return config
class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin): class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin):
def create_instance(self, flavor, image_id, databases, users, def create_instance(self, flavor, image_id, databases, users,
service_type, volume_size, backup_id, datastore_manager, packages, volume_size,
availability_zone, root_password): backup_id, availability_zone, root_password):
LOG.debug(_("begin create_instance for id: %s") % self.id) LOG.debug(_("begin create_instance for id: %s") % self.id)
security_groups = None security_groups = None
@@ -170,7 +170,7 @@ class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin):
flavor, flavor,
image_id, image_id,
security_groups, security_groups,
service_type, datastore_manager,
volume_size, volume_size,
availability_zone) availability_zone)
elif use_nova_server_volume: elif use_nova_server_volume:
@@ -178,7 +178,7 @@ class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin):
flavor['id'], flavor['id'],
image_id, image_id,
security_groups, security_groups,
service_type, datastore_manager,
volume_size, volume_size,
availability_zone) availability_zone)
else: else:
@@ -186,15 +186,15 @@ class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin):
flavor['id'], flavor['id'],
image_id, image_id,
security_groups, security_groups,
service_type, datastore_manager,
volume_size, volume_size,
availability_zone) availability_zone)
config = self._render_config(service_type, flavor, self.id) config = self._render_config(datastore_manager, flavor, self.id)
if server: if server:
self._guest_prepare(server, flavor['ram'], volume_info, self._guest_prepare(server, flavor['ram'], volume_info,
databases, users, backup_id, packages, databases, users, backup_id,
config.config_contents, root_password) config.config_contents, root_password)
if not self.db_info.task_status.is_error: if not self.db_info.task_status.is_error:
@@ -285,15 +285,15 @@ class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin):
return False return False
def _create_server_volume(self, flavor_id, image_id, security_groups, def _create_server_volume(self, flavor_id, image_id, security_groups,
service_type, volume_size, datastore_manager, volume_size,
availability_zone): availability_zone):
LOG.debug(_("begin _create_server_volume for id: %s") % self.id) LOG.debug(_("begin _create_server_volume for id: %s") % self.id)
server = None server = None
try: try:
files = {"/etc/guest_info": ("[DEFAULT]\n--guest_id=%s\n" files = {"/etc/guest_info": ("[DEFAULT]\n--guest_id="
"--service_type=%s\n" "%s\n--datastore_manager=%s\n"
"--tenant_id=%s\n" % "--tenant_id=%s\n" %
(self.id, service_type, (self.id, datastore,
self.tenant_id))} self.tenant_id))}
name = self.hostname or self.name name = self.hostname or self.name
volume_desc = ("mysql volume for %s" % self.id) volume_desc = ("mysql volume for %s" % self.id)
@@ -332,14 +332,14 @@ class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin):
return server, volume_info return server, volume_info
def _create_server_volume_heat(self, flavor, image_id, def _create_server_volume_heat(self, flavor, image_id,
security_groups, service_type, security_groups, datastore_manager,
volume_size, availability_zone): volume_size, availability_zone):
LOG.debug(_("begin _create_server_volume_heat for id: %s") % self.id) LOG.debug(_("begin _create_server_volume_heat for id: %s") % self.id)
client = create_heat_client(self.context) client = create_heat_client(self.context)
novaclient = create_nova_client(self.context) novaclient = create_nova_client(self.context)
cinderclient = create_cinder_client(self.context) cinderclient = create_cinder_client(self.context)
template_obj = template.load_heat_template(service_type) template_obj = template.load_heat_template(datastore_manager)
heat_template_unicode = template_obj.render() heat_template_unicode = template_obj.render()
try: try:
heat_template = heat_template_unicode.encode('ascii') heat_template = heat_template_unicode.encode('ascii')
@@ -351,6 +351,7 @@ class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin):
"VolumeSize": volume_size, "VolumeSize": volume_size,
"InstanceId": self.id, "InstanceId": self.id,
"ImageId": image_id, "ImageId": image_id,
"DatastoreManager": datastore_manager,
"AvailabilityZone": availability_zone} "AvailabilityZone": availability_zone}
stack_name = 'trove-%s' % self.id stack_name = 'trove-%s' % self.id
client.stacks.create(stack_name=stack_name, client.stacks.create(stack_name=stack_name,
@@ -377,7 +378,7 @@ class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin):
return server, volume_info return server, volume_info
def _create_server_volume_individually(self, flavor_id, image_id, def _create_server_volume_individually(self, flavor_id, image_id,
security_groups, service_type, security_groups, datastore_manager,
volume_size, volume_size,
availability_zone): availability_zone):
LOG.debug(_("begin _create_server_volume_individually for id: %s") % LOG.debug(_("begin _create_server_volume_individually for id: %s") %
@@ -387,7 +388,8 @@ class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin):
block_device_mapping = volume_info['block_device'] block_device_mapping = volume_info['block_device']
try: try:
server = self._create_server(flavor_id, image_id, security_groups, server = self._create_server(flavor_id, image_id, security_groups,
service_type, block_device_mapping, datastore_manager,
block_device_mapping,
availability_zone) availability_zone)
server_id = server.id server_id = server.id
# Save server ID. # Save server ID.
@@ -477,17 +479,19 @@ class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin):
return volume_info return volume_info
def _create_server(self, flavor_id, image_id, security_groups, def _create_server(self, flavor_id, image_id, security_groups,
service_type, block_device_mapping, datastore_manager, block_device_mapping,
availability_zone): availability_zone):
files = {"/etc/guest_info": ("[DEFAULT]\nguest_id=%s\n" files = {"/etc/guest_info": ("[DEFAULT]\nguest_id=%s\n"
"service_type=%s\n" "tenant_id=%s\n" % "datastore_manager=%s\n"
(self.id, service_type, self.tenant_id))} "tenant_id=%s\n" %
(self.id, datastore_manager,
self.tenant_id))}
if os.path.isfile(CONF.get('guest_config')): if os.path.isfile(CONF.get('guest_config')):
with open(CONF.get('guest_config'), "r") as f: with open(CONF.get('guest_config'), "r") as f:
files["/etc/trove-guestagent.conf"] = f.read() files["/etc/trove-guestagent.conf"] = f.read()
userdata = None userdata = None
cloudinit = os.path.join(CONF.get('cloudinit_location'), cloudinit = os.path.join(CONF.get('cloudinit_location'),
"%s.cloudinit" % service_type) "%s.cloudinit" % datastore_manager)
if os.path.isfile(cloudinit): if os.path.isfile(cloudinit):
with open(cloudinit, "r") as f: with open(cloudinit, "r") as f:
userdata = f.read() userdata = f.read()
@@ -503,11 +507,11 @@ class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin):
return server return server
def _guest_prepare(self, server, flavor_ram, volume_info, def _guest_prepare(self, server, flavor_ram, volume_info,
databases, users, backup_id=None, packages, databases, users, backup_id=None,
config_contents=None, root_password=None): config_contents=None, root_password=None):
LOG.info(_("Entering guest_prepare")) LOG.info(_("Entering guest_prepare"))
# Now wait for the response from the create to do additional work # Now wait for the response from the create to do additional work
self.guest.prepare(flavor_ram, databases, users, self.guest.prepare(flavor_ram, packages, databases, users,
device_path=volume_info['device_path'], device_path=volume_info['device_path'],
mount_point=volume_info['mount_point'], mount_point=volume_info['mount_point'],
backup_id=backup_id, backup_id=backup_id,
@@ -1007,7 +1011,7 @@ class ResizeAction(ResizeActionBase):
% self.instance.id) % self.instance.id)
LOG.debug(_("Repairing config.")) LOG.debug(_("Repairing config."))
try: try:
config = self._render_config(self.instance.service_type, config = self._render_config(self.instance.datastore.manager,
self.old_flavor, self.instance.id) self.old_flavor, self.instance.id)
config = {'config_contents': config.config_contents} config = {'config_contents': config.config_contents}
self.instance.guest.reset_configuration(config) self.instance.guest.reset_configuration(config)
@@ -1028,7 +1032,7 @@ class ResizeAction(ResizeActionBase):
modify_at=timeutils.isotime(self.instance.updated)) modify_at=timeutils.isotime(self.instance.updated))
def _start_mysql(self): def _start_mysql(self):
config = self._render_config(self.instance.service_type, config = self._render_config(self.instance.datastore.manager,
self.new_flavor, self.instance.id) self.new_flavor, self.instance.id)
self.instance.guest.start_db_with_conf_changes(config.config_contents) self.instance.guest.start_db_with_conf_changes(config.config_contents)

View File

@@ -10,6 +10,8 @@ Parameters:
Type: String Type: String
ImageId: ImageId:
Type: String Type: String
DatastoreManager:
Type: String
AvailabilityZone: AvailabilityZone:
Type: String Type: String
Default: nova Default: nova
@@ -25,7 +27,7 @@ Resources:
Fn::Join: Fn::Join:
- '' - ''
- ["[DEFAULT]\nguest_id=", {Ref: InstanceId}, - ["[DEFAULT]\nguest_id=", {Ref: InstanceId},
"\nservice_type=mysql"] "\\ndatastore_manager=", {Ref: DatastoreManager}]
mode: '000644' mode: '000644'
owner: root owner: root
group: root group: root
@@ -69,4 +71,4 @@ Resources:
Type: AWS::EC2::EIPAssociation Type: AWS::EC2::EIPAssociation
Properties: Properties:
InstanceId: {Ref: BaseInstance} InstanceId: {Ref: BaseInstance}
EIP: {Ref: DatabaseIPAddress} EIP: {Ref: DatabaseIPAddress}

View File

@@ -0,0 +1,114 @@
# Copyright (c) 2011 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import os
from nose.tools import assert_equal
from nose.tools import assert_false
from nose.tools import assert_true
from troveclient.compat import exceptions
from proboscis import before_class
from proboscis import test
from proboscis.asserts import assert_raises
from proboscis import SkipTest
from trove import tests
from trove.tests.util import create_dbaas_client
from trove.tests.util import test_config
from trove.tests.util.users import Requirements
from trove.tests.util.check import TypeCheck
GROUP = "dbaas.api.datastores"
NAME = "nonexistent"
@test(groups=[tests.DBAAS_API, GROUP, tests.PRE_INSTANCES],
depends_on_groups=["services.initialize"])
class Datastores(object):
@before_class
def setUp(self):
rd_user = test_config.users.find_user(
Requirements(is_admin=False, services=["trove"]))
self.rd_client = create_dbaas_client(rd_user)
@test
def test_datastore_list_attrs(self):
datastores = self.rd_client.datastores.list()
for datastore in datastores:
with TypeCheck('Datastore', datastore) as check:
check.has_field("id", basestring)
check.has_field("name", basestring)
check.has_field("links", list)
@test
def test_datastore_get_attrs(self):
datastore = self.rd_client.datastores.get(test_config.
dbaas_datastore)
with TypeCheck('Datastore', datastore) as check:
check.has_field("id", basestring)
check.has_field("name", basestring)
check.has_field("links", list)
assert_equal(datastore.name, test_config.dbaas_datastore)
@test
def test_datastore_not_found(self):
try:
assert_raises(exceptions.NotFound,
self.rd_client.datastores.get, NAME)
except exceptions.BadRequest as e:
assert_equal(e.message,
"Datastore '%s' cannot be found." % NAME)
@test
def test_datastore_version_list_attrs(self):
versions = self.rd_client.datastore_versions.list(test_config.
dbaas_datastore)
for version in versions:
with TypeCheck('DatastoreVersion', version) as check:
check.has_field("id", basestring)
check.has_field("name", basestring)
check.has_field("links", list)
@test
def test_datastore_version_get_attrs(self):
version = self.rd_client.datastore_versions.get(
test_config.dbaas_datastore, test_config.dbaas_datastore_version)
with TypeCheck('DatastoreVersion', version) as check:
check.has_field("id", basestring)
check.has_field("name", basestring)
check.has_field("links", list)
assert_equal(version.name, test_config.dbaas_datastore_version)
@test
def test_datastore_version_datastore_not_found(self):
try:
assert_raises(exceptions.NotFound,
self.rd_client.datastore_versions.get,
NAME, NAME)
except exceptions.BadRequest as e:
assert_equal(e.message,
"Datastore '%s' cannot be found." % NAME)
@test
def test_datastore_version_not_found(self):
try:
assert_raises(exceptions.NotFound,
self.rd_client.datastore_versions.get,
test_config.dbaas_datastore, NAME)
except exceptions.BadRequest as e:
assert_equal(e.message,
"Datastore version '%s' cannot be found." % NAME)

View File

@@ -36,6 +36,7 @@ GROUP_SECURITY_GROUPS = "dbaas.api.security_groups"
from datetime import datetime from datetime import datetime
from time import sleep from time import sleep
from trove.datastore import models as datastore_models
from trove.common import exception as rd_exceptions from trove.common import exception as rd_exceptions
from troveclient.compat import exceptions from troveclient.compat import exceptions
@@ -65,6 +66,9 @@ from trove.tests.util import string_in_list
from trove.common.utils import poll_until from trove.common.utils import poll_until
from trove.tests.util.check import AttrCheck from trove.tests.util.check import AttrCheck
from trove.tests.util.check import TypeCheck from trove.tests.util.check import TypeCheck
from trove.tests.util import test_config
FAKE = test_config.values['fake_mode']
class InstanceTestInfo(object): class InstanceTestInfo(object):
@@ -75,8 +79,8 @@ class InstanceTestInfo(object):
self.dbaas_admin = None # The rich client with admin access. self.dbaas_admin = None # The rich client with admin access.
self.dbaas_flavor = None # The flavor object of the instance. self.dbaas_flavor = None # The flavor object of the instance.
self.dbaas_flavor_href = None # The flavor of the instance. self.dbaas_flavor_href = None # The flavor of the instance.
self.dbaas_image = None # The image used to create the instance. self.dbaas_datastore = None # The datastore id
self.dbaas_image_href = None # The link of the image. self.dbaas_datastore_version = None # The datastore version id
self.id = None # The ID of the instance in the database. self.id = None # The ID of the instance in the database.
self.local_id = None self.local_id = None
self.address = None self.address = None
@@ -162,7 +166,7 @@ def clear_messages_off_queue():
class InstanceSetup(object): class InstanceSetup(object):
"""Makes sure the client can hit the ReST service. """Makes sure the client can hit the ReST service.
This test also uses the API to find the image and flavor to use. This test also uses the API to find the flavor to use.
""" """
@@ -223,6 +227,7 @@ class CreateInstanceQuotaTest(unittest.TestCase):
import copy import copy
self.test_info = copy.deepcopy(instance_info) self.test_info = copy.deepcopy(instance_info)
self.test_info.dbaas_datastore = CONFIG.dbaas_datastore
def tearDown(self): def tearDown(self):
quota_dict = {'instances': CONFIG.trove_max_instances_per_user} quota_dict = {'instances': CONFIG.trove_max_instances_per_user}
@@ -323,6 +328,7 @@ class CreateInstance(object):
users.append({"name": "lite", "password": "litepass", users.append({"name": "lite", "password": "litepass",
"databases": [{"name": "firstdb"}]}) "databases": [{"name": "firstdb"}]})
instance_info.users = users instance_info.users = users
instance_info.dbaas_datastore = CONFIG.dbaas_datastore
if VOLUME_SUPPORT: if VOLUME_SUPPORT:
instance_info.volume = {'size': 1} instance_info.volume = {'size': 1}
else: else:
@@ -335,7 +341,9 @@ class CreateInstance(object):
instance_info.volume, instance_info.volume,
databases, databases,
users, users,
availability_zone="nova") availability_zone="nova",
datastore=instance_info.dbaas_datastore,
datastore_version=instance_info.dbaas_datastore_version)
assert_equal(200, dbaas.last_http_code) assert_equal(200, dbaas.last_http_code)
else: else:
id = existing_instance() id = existing_instance()
@@ -355,7 +363,7 @@ class CreateInstance(object):
# Check these attrs only are returned in create response # Check these attrs only are returned in create response
expected_attrs = ['created', 'flavor', 'addresses', 'id', 'links', expected_attrs = ['created', 'flavor', 'addresses', 'id', 'links',
'name', 'status', 'updated'] 'name', 'status', 'updated', 'datastore']
if ROOT_ON_CREATE: if ROOT_ON_CREATE:
expected_attrs.append('password') expected_attrs.append('password')
if VOLUME_SUPPORT: if VOLUME_SUPPORT:
@@ -369,6 +377,7 @@ class CreateInstance(object):
msg="Create response") msg="Create response")
# Don't CheckInstance if the instance already exists. # Don't CheckInstance if the instance already exists.
check.flavor() check.flavor()
check.datastore()
check.links(result._info['links']) check.links(result._info['links'])
if VOLUME_SUPPORT: if VOLUME_SUPPORT:
check.volume() check.volume()
@@ -454,7 +463,7 @@ class CreateInstance(object):
result = dbaas_admin.management.show(instance_info.id) result = dbaas_admin.management.show(instance_info.id)
expected_attrs = ['account_id', 'addresses', 'created', expected_attrs = ['account_id', 'addresses', 'created',
'databases', 'flavor', 'guest_status', 'host', 'databases', 'flavor', 'guest_status', 'host',
'hostname', 'id', 'name', 'hostname', 'id', 'name', 'datastore',
'server_state_description', 'status', 'updated', 'server_state_description', 'status', 'updated',
'users', 'volume', 'root_enabled_at', 'users', 'volume', 'root_enabled_at',
'root_enabled_by'] 'root_enabled_by']
@@ -462,8 +471,122 @@ class CreateInstance(object):
check.attrs_exist(result._info, expected_attrs, check.attrs_exist(result._info, expected_attrs,
msg="Mgmt get instance") msg="Mgmt get instance")
check.flavor() check.flavor()
check.datastore()
check.guest_status() check.guest_status()
@test
def test_create_failure_with_datastore_default_notfound(self):
if not FAKE:
raise SkipTest("This test only for fake mode.")
if VOLUME_SUPPORT:
volume = {'size': 1}
else:
volume = None
instance_name = "datastore_default_notfound"
databases = []
users = []
origin_default_datastore = (datastore_models.CONF.
default_datastore)
datastore_models.CONF.default_datastore = ""
try:
assert_raises(exceptions.NotFound,
dbaas.instances.create, instance_name,
instance_info.dbaas_flavor_href,
volume, databases, users)
except exceptions.BadRequest as e:
assert_equal(e.message,
"Please specify datastore.")
datastore_models.CONF.default_datastore = \
origin_default_datastore
@test
def test_create_failure_with_datastore_default_version_notfound(self):
if VOLUME_SUPPORT:
volume = {'size': 1}
else:
volume = None
instance_name = "datastore_default_version_notfound"
databases = []
users = []
datastore = "Test_Datastore_1"
try:
assert_raises(exceptions.NotFound,
dbaas.instances.create, instance_name,
instance_info.dbaas_flavor_href,
volume, databases, users,
datastore=datastore)
except exceptions.BadRequest as e:
assert_equal(e.message,
"Default version for datastore '%s' not found." %
datastore)
@test
def test_create_failure_with_datastore_notfound(self):
if VOLUME_SUPPORT:
volume = {'size': 1}
else:
volume = None
instance_name = "datastore_notfound"
databases = []
users = []
datastore = "nonexistent"
try:
assert_raises(exceptions.NotFound,
dbaas.instances.create, instance_name,
instance_info.dbaas_flavor_href,
volume, databases, users,
datastore=datastore)
except exceptions.BadRequest as e:
assert_equal(e.message,
"Datastore '%s' cannot be found." %
datastore)
@test
def test_create_failure_with_datastore_version_notfound(self):
if VOLUME_SUPPORT:
volume = {'size': 1}
else:
volume = None
instance_name = "datastore_version_notfound"
databases = []
users = []
datastore = "Test_Mysql"
datastore_version = "nonexistent"
try:
assert_raises(exceptions.NotFound,
dbaas.instances.create, instance_name,
instance_info.dbaas_flavor_href,
volume, databases, users,
datastore=datastore,
datastore_version=datastore_version)
except exceptions.BadRequest as e:
assert_equal(e.message,
"Datastore version '%s' cannot be found." %
datastore_version)
@test
def test_create_failure_with_datastore_version_inactive(self):
if VOLUME_SUPPORT:
volume = {'size': 1}
else:
volume = None
instance_name = "datastore_version_inactive"
databases = []
users = []
datastore = "Test_Mysql"
datastore_version = "mysql_inactive_version"
try:
assert_raises(exceptions.NotFound,
dbaas.instances.create, instance_name,
instance_info.dbaas_flavor_href,
volume, databases, users,
datastore=datastore,
datastore_version=datastore_version)
except exceptions.BadRequest as e:
assert_equal(e.message,
"Datastore version '%s' is not active." %
datastore_version)
def assert_unprocessable(func, *args): def assert_unprocessable(func, *args):
try: try:
@@ -730,7 +853,8 @@ class TestInstanceListing(object):
@test @test
def test_index_list(self): def test_index_list(self):
expected_attrs = ['id', 'links', 'name', 'status', 'flavor'] expected_attrs = ['id', 'links', 'name', 'status', 'flavor',
'datastore']
if VOLUME_SUPPORT: if VOLUME_SUPPORT:
expected_attrs.append('volume') expected_attrs.append('volume')
instances = dbaas.instances.list() instances = dbaas.instances.list()
@@ -743,12 +867,14 @@ class TestInstanceListing(object):
msg="Instance Index") msg="Instance Index")
check.links(instance_dict['links']) check.links(instance_dict['links'])
check.flavor() check.flavor()
check.datastore()
check.volume() check.volume()
@test @test
def test_get_instance(self): def test_get_instance(self):
expected_attrs = ['created', 'databases', 'flavor', 'hostname', 'id', expected_attrs = ['created', 'databases', 'flavor', 'hostname', 'id',
'links', 'name', 'status', 'updated', 'ip'] 'links', 'name', 'status', 'updated', 'ip',
'datastore']
if VOLUME_SUPPORT: if VOLUME_SUPPORT:
expected_attrs.append('volume') expected_attrs.append('volume')
else: else:
@@ -761,6 +887,7 @@ class TestInstanceListing(object):
check.attrs_exist(instance_dict, expected_attrs, check.attrs_exist(instance_dict, expected_attrs,
msg="Get Instance") msg="Get Instance")
check.flavor() check.flavor()
check.datastore()
check.links(instance_dict['links']) check.links(instance_dict['links'])
check.used_volume() check.used_volume()
@@ -835,12 +962,13 @@ class TestInstanceListing(object):
expected_attrs = ['account_id', 'addresses', 'created', 'databases', expected_attrs = ['account_id', 'addresses', 'created', 'databases',
'flavor', 'guest_status', 'host', 'hostname', 'id', 'flavor', 'guest_status', 'host', 'hostname', 'id',
'name', 'root_enabled_at', 'root_enabled_by', 'name', 'root_enabled_at', 'root_enabled_by',
'server_state_description', 'status', 'server_state_description', 'status', 'datastore',
'updated', 'users', 'volume'] 'updated', 'users', 'volume']
with CheckInstance(result._info) as check: with CheckInstance(result._info) as check:
check.attrs_exist(result._info, expected_attrs, check.attrs_exist(result._info, expected_attrs,
msg="Mgmt get instance") msg="Mgmt get instance")
check.flavor() check.flavor()
check.datastore()
check.guest_status() check.guest_status()
check.addresses() check.addresses()
check.volume_mgmt() check.volume_mgmt()
@@ -1063,6 +1191,14 @@ class CheckInstance(AttrCheck):
msg="Flavor") msg="Flavor")
self.links(self.instance['flavor']['links']) self.links(self.instance['flavor']['links'])
def datastore(self):
if 'datastore' not in self.instance:
self.fail("'datastore' not found in instance.")
else:
expected_attrs = ['type', 'version']
self.attrs_exist(self.instance['datastore'], expected_attrs,
msg="datastore")
def volume_key_exists(self): def volume_key_exists(self):
if 'volume' not in self.instance: if 'volume' not in self.instance:
self.fail("'volume' not found in instance.") self.fail("'volume' not found in instance.")

View File

@@ -31,6 +31,7 @@ from trove.instance.tasks import InstanceTasks
from trove.openstack.common.rpc.common import RPCException from trove.openstack.common.rpc.common import RPCException
from trove.taskmanager import models as models from trove.taskmanager import models as models
from trove.tests.fakes import nova from trove.tests.fakes import nova
from trove.tests.util import test_config
GROUP = 'dbaas.api.instances.resize' GROUP = 'dbaas.api.instances.resize'
@@ -51,7 +52,7 @@ class ResizeTestBase(TestCase):
flavor_id=OLD_FLAVOR_ID, flavor_id=OLD_FLAVOR_ID,
tenant_id=999, tenant_id=999,
volume_size=None, volume_size=None,
service_type='mysql', datastore_version_id=test_config.dbaas_datastore_version,
task_status=InstanceTasks.RESIZING) task_status=InstanceTasks.RESIZING)
self.server = self.mock.CreateMock(Server) self.server = self.mock.CreateMock(Server)
self.instance = models.BuiltInstanceTasks(context, self.instance = models.BuiltInstanceTasks(context,

View File

@@ -53,6 +53,12 @@ def flavor_check(flavor):
check.has_element("links", list) check.has_element("links", list)
def datastore_check(datastore):
with CollectionCheck("datastore", datastore) as check:
check.has_element("type", basestring)
check.has_element("version", basestring)
def guest_status_check(guest_status): def guest_status_check(guest_status):
with CollectionCheck("guest_status", guest_status) as check: with CollectionCheck("guest_status", guest_status) as check:
check.has_element("state_description", basestring) check.has_element("state_description", basestring)
@@ -87,6 +93,7 @@ def mgmt_instance_get():
# lets avoid creating more ordering work. # lets avoid creating more ordering work.
instance.has_field('deleted_at', (basestring, None)) instance.has_field('deleted_at', (basestring, None))
instance.has_field('flavor', dict, flavor_check) instance.has_field('flavor', dict, flavor_check)
instance.has_field('datastore', dict, datastore_check)
instance.has_field('guest_status', dict, guest_status_check) instance.has_field('guest_status', dict, guest_status_check)
instance.has_field('id', basestring) instance.has_field('id', basestring)
instance.has_field('links', list) instance.has_field('links', list)
@@ -175,6 +182,7 @@ class WhenMgmtInstanceGetIsCalledButServerIsNotReady(object):
# lets avoid creating more ordering work. # lets avoid creating more ordering work.
instance.has_field('deleted_at', (basestring, None)) instance.has_field('deleted_at', (basestring, None))
instance.has_field('flavor', dict, flavor_check) instance.has_field('flavor', dict, flavor_check)
instance.has_field('datastore', dict, datastore_check)
instance.has_field('guest_status', dict, guest_status_check) instance.has_field('guest_status', dict, guest_status_check)
instance.has_field('id', basestring) instance.has_field('id', basestring)
instance.has_field('links', list) instance.has_field('links', list)
@@ -211,6 +219,7 @@ class MgmtInstancesIndex(object):
'deleted', 'deleted',
'deleted_at', 'deleted_at',
'flavor', 'flavor',
'datastore',
'id', 'id',
'links', 'links',
'name', 'name',

View File

@@ -52,9 +52,9 @@ class MgmtInstanceBase(object):
self.db_info = DBInstance.create( self.db_info = DBInstance.create(
name="instance", name="instance",
flavor_id=1, flavor_id=1,
datastore_version_id=test_config.dbaas_datastore_version,
tenant_id=self.tenant_id, tenant_id=self.tenant_id,
volume_size=None, volume_size=None,
service_type='mysql',
task_status=InstanceTasks.NONE) task_status=InstanceTasks.NONE)
self.server = self.mock.CreateMock(Server) self.server = self.mock.CreateMock(Server)
self.instance = imodels.Instance(self.context, self.instance = imodels.Instance(self.context,

View File

@@ -41,8 +41,7 @@ class MalformedJson(object):
users = "bar" users = "bar"
try: try:
self.dbaas.instances.create("bad_instance", 3, 3, self.dbaas.instances.create("bad_instance", 3, 3,
databases=databases, databases=databases, users=users)
users=users)
except Exception as e: except Exception as e:
resp, body = self.dbaas.client.last_response resp, body = self.dbaas.client.last_response
httpCode = resp.status httpCode = resp.status
@@ -260,6 +259,33 @@ class MalformedJson(object):
"instance['volume'] 2 is not of type 'object'" % "instance['volume'] 2 is not of type 'object'" %
(flavorId, flavorId, flavorId, flavorId)) (flavorId, flavorId, flavorId, flavorId))
@test
def test_bad_body_datastore_create_instance(self):
tests_utils.skip_if_xml()
datastore = "*"
datastore_version = "*"
try:
self.dbaas.instances.create("test_instance",
3, {"size": 2},
datastore=datastore,
datastore_version=datastore_version)
except Exception as e:
resp, body = self.dbaas.client.last_response
httpCode = resp.status
assert_equal(httpCode, 400,
"Create instance failed with code %s, exception %s" %
(httpCode, e))
if not isinstance(self.dbaas.client,
troveclient.compat.xml.TroveXmlClient):
assert_equal(e.message,
"Validation error: instance['datastore']['type']"
" u'%s' does not match '^.*[0-9a-zA-Z]+.*$'; "
"instance['datastore']['version'] u'%s' does not"
" match '^.*[0-9a-zA-Z]+.*$'" %
(datastore, datastore_version))
@test @test
def test_bad_body_volsize_create_instance(self): def test_bad_body_volsize_create_instance(self):
volsize = "h3ll0" volsize = "h3ll0"

View File

@@ -70,8 +70,9 @@ class TestConfig(object):
'dbaas_url': "http://localhost:8775/v1.0/dbaas", 'dbaas_url': "http://localhost:8775/v1.0/dbaas",
'version_url': "http://localhost:8775/", 'version_url': "http://localhost:8775/",
'nova_url': "http://localhost:8774/v1.1", 'nova_url': "http://localhost:8774/v1.1",
'dbaas_datastore': "Test_Mysql",
'dbaas_datastore_version': "mysql_test_version",
'instance_create_time': 16 * 60, 'instance_create_time': 16 * 60,
'dbaas_image': None,
'mysql_connection_method': {"type": "direct"}, 'mysql_connection_method': {"type": "direct"},
'typical_nova_image_name': None, 'typical_nova_image_name': None,
'white_box': os.environ.get("WHITE_BOX", "False") == "True", 'white_box': os.environ.get("WHITE_BOX", "False") == "True",

View File

@@ -207,7 +207,7 @@ class FakeGuest(object):
% (username, hostname)) % (username, hostname))
return self.users.get((username, hostname), None) return self.users.get((username, hostname), None)
def prepare(self, memory_mb, databases, users, device_path=None, def prepare(self, memory_mb, packages, databases, users, device_path=None,
mount_point=None, backup_id=None, config_contents=None, mount_point=None, backup_id=None, config_contents=None,
root_password=None): root_password=None):
from trove.instance.models import DBInstance from trove.instance.models import DBInstance

View File

@@ -252,14 +252,15 @@ class ApiTest(testtools.TestCase):
mock_conn = mock() mock_conn = mock()
when(rpc).create_connection(new=True).thenReturn(mock_conn) when(rpc).create_connection(new=True).thenReturn(mock_conn)
when(mock_conn).create_consumer(any(), any(), any()).thenReturn(None) when(mock_conn).create_consumer(any(), any(), any()).thenReturn(None)
exp_msg = RpcMsgMatcher('prepare', 'memory_mb', 'databases', 'users', exp_msg = RpcMsgMatcher('prepare', 'memory_mb', 'packages',
'device_path', 'mount_point', 'backup_id', 'databases', 'users', 'device_path',
'config_contents', 'root_password') 'mount_point', 'backup_id', 'config_contents',
'root_password')
when(rpc).cast(any(), any(), exp_msg).thenReturn(None) when(rpc).cast(any(), any(), exp_msg).thenReturn(None)
self.api.prepare('2048', 'db1', 'user1', '/dev/vdt', '/mnt/opt', self.api.prepare('2048', 'package1', 'db1', 'user1', '/dev/vdt',
'bkup-1232', 'cont', '1-2-3-4') '/mnt/opt', 'bkup-1232', 'cont', '1-2-3-4')
self._verify_rpc_connection_and_cast(rpc, mock_conn, exp_msg) self._verify_rpc_connection_and_cast(rpc, mock_conn, exp_msg)
@@ -267,13 +268,14 @@ class ApiTest(testtools.TestCase):
mock_conn = mock() mock_conn = mock()
when(rpc).create_connection(new=True).thenReturn(mock_conn) when(rpc).create_connection(new=True).thenReturn(mock_conn)
when(mock_conn).create_consumer(any(), any(), any()).thenReturn(None) when(mock_conn).create_consumer(any(), any(), any()).thenReturn(None)
exp_msg = RpcMsgMatcher('prepare', 'memory_mb', 'databases', 'users', exp_msg = RpcMsgMatcher('prepare', 'memory_mb', 'packages',
'device_path', 'mount_point', 'backup_id', 'databases', 'users', 'device_path',
'config_contents', 'root_password') 'mount_point', 'backup_id', 'config_contents',
'root_password')
when(rpc).cast(any(), any(), exp_msg).thenReturn(None) when(rpc).cast(any(), any(), exp_msg).thenReturn(None)
self.api.prepare('2048', 'db1', 'user1', '/dev/vdt', '/mnt/opt', self.api.prepare('2048', 'package1', 'db1', 'user1', '/dev/vdt',
'backup_id_123', 'cont', '1-2-3-4') '/mnt/opt', 'backup_id_123', 'cont', '1-2-3-4')
self._verify_rpc_connection_and_cast(rpc, mock_conn, exp_msg) self._verify_rpc_connection_and_cast(rpc, mock_conn, exp_msg)
@@ -291,11 +293,13 @@ class ApiTest(testtools.TestCase):
def test_rpc_cast_with_consumer_exception(self): def test_rpc_cast_with_consumer_exception(self):
mock_conn = mock() mock_conn = mock()
when(rpc).create_connection(new=True).thenRaise(IOError('host down')) when(rpc).create_connection(new=True).thenRaise(IOError('host down'))
exp_msg = RpcMsgMatcher('prepare', 'memory_mb', 'databases', 'users', exp_msg = RpcMsgMatcher('prepare', 'memory_mb', 'packages',
'device_path', 'mount_point') 'databases', 'users', 'device_path',
'mount_point')
with testtools.ExpectedException(exception.GuestError, '.* host down'): with testtools.ExpectedException(exception.GuestError, '.* host down'):
self.api.prepare('2048', 'db1', 'user1', '/dev/vdt', '/mnt/opt') self.api.prepare('2048', 'package1', 'db1', 'user1', '/dev/vdt',
'/mnt/opt')
verify(rpc).create_connection(new=True) verify(rpc).create_connection(new=True)
verifyZeroInteractions(mock_conn) verifyZeroInteractions(mock_conn)

View File

@@ -39,6 +39,7 @@ from trove.common import utils
from trove.common import instance as rd_instance from trove.common import instance as rd_instance
import trove.guestagent.datastore.mysql.service as dbaas import trove.guestagent.datastore.mysql.service as dbaas
from trove.guestagent import dbaas as dbaas_sr from trove.guestagent import dbaas as dbaas_sr
from trove.guestagent import pkg
from trove.guestagent.dbaas import to_gb from trove.guestagent.dbaas import to_gb
from trove.guestagent.dbaas import get_filesystem_volume_stats from trove.guestagent.dbaas import get_filesystem_volume_stats
from trove.guestagent.datastore.service import BaseDbStatus from trove.guestagent.datastore.service import BaseDbStatus
@@ -108,11 +109,18 @@ class DbaasTest(testtools.TestCase):
self.assertRaises(RuntimeError, dbaas.get_auth_password) self.assertRaises(RuntimeError, dbaas.get_auth_password)
def test_service_discovery(self):
when(os.path).isfile(any()).thenReturn(True)
mysql_service = dbaas.operating_system.service_discovery(["mysql"])
self.assertIsNotNone(mysql_service['cmd_start'])
self.assertIsNotNone(mysql_service['cmd_enable'])
def test_load_mysqld_options(self): def test_load_mysqld_options(self):
output = "mysqld would've been started with the these args:\n"\ output = "mysqld would've been started with the these args:\n"\
"--user=mysql --port=3306 --basedir=/usr "\ "--user=mysql --port=3306 --basedir=/usr "\
"--tmpdir=/tmp --skip-external-locking" "--tmpdir=/tmp --skip-external-locking"
when(os.path).isfile(any()).thenReturn(True)
dbaas.utils.execute = Mock(return_value=(output, None)) dbaas.utils.execute = Mock(return_value=(output, None))
options = dbaas.load_mysqld_options() options = dbaas.load_mysqld_options()
@@ -453,6 +461,13 @@ class MySqlAppTest(testtools.TestCase):
self.appStatus = FakeAppStatus(self.FAKE_ID, self.appStatus = FakeAppStatus(self.FAKE_ID,
rd_instance.ServiceStatuses.NEW) rd_instance.ServiceStatuses.NEW)
self.mySqlApp = MySqlApp(self.appStatus) self.mySqlApp = MySqlApp(self.appStatus)
mysql_service = {'cmd_start': Mock(),
'cmd_stop': Mock(),
'cmd_enable': Mock(),
'cmd_disable': Mock(),
'bin': Mock()}
dbaas.operating_system.service_discovery = Mock(return_value=
mysql_service)
dbaas.time.sleep = Mock() dbaas.time.sleep = Mock()
def tearDown(self): def tearDown(self):
@@ -564,13 +579,14 @@ class MySqlAppTest(testtools.TestCase):
dbaas.utils.execute_with_timeout = Mock() dbaas.utils.execute_with_timeout = Mock()
self.appStatus.set_next_status(rd_instance.ServiceStatuses.RUNNING) self.appStatus.set_next_status(rd_instance.ServiceStatuses.RUNNING)
self.mySqlApp._enable_mysql_on_boot = Mock()
self.mySqlApp.start_mysql() self.mySqlApp.start_mysql()
self.assert_reported_status(rd_instance.ServiceStatuses.NEW) self.assert_reported_status(rd_instance.ServiceStatuses.NEW)
def test_start_mysql_with_db_update(self): def test_start_mysql_with_db_update(self):
dbaas.utils.execute_with_timeout = Mock() dbaas.utils.execute_with_timeout = Mock()
self.mySqlApp._enable_mysql_on_boot = Mock()
self.appStatus.set_next_status(rd_instance.ServiceStatuses.RUNNING) self.appStatus.set_next_status(rd_instance.ServiceStatuses.RUNNING)
self.mySqlApp.start_mysql(True) self.mySqlApp.start_mysql(True)
@@ -579,6 +595,7 @@ class MySqlAppTest(testtools.TestCase):
def test_start_mysql_runs_forever(self): def test_start_mysql_runs_forever(self):
dbaas.utils.execute_with_timeout = Mock() dbaas.utils.execute_with_timeout = Mock()
self.mySqlApp._enable_mysql_on_boot = Mock()
self.mySqlApp.state_change_wait_time = 1 self.mySqlApp.state_change_wait_time = 1
self.appStatus.set_next_status(rd_instance.ServiceStatuses.SHUTDOWN) self.appStatus.set_next_status(rd_instance.ServiceStatuses.SHUTDOWN)
@@ -634,13 +651,19 @@ class MySqlAppInstallTest(MySqlAppTest):
def test_install(self): def test_install(self):
self.mySqlApp._install_mysql = Mock() self.mySqlApp._install_mysql = Mock()
self.mySqlApp.is_installed = Mock(return_value=False) pkg.Package.pkg_is_installed = Mock(return_value=False)
self.mySqlApp.install_if_needed() utils.execute_with_timeout = Mock()
self.assertTrue(self.mySqlApp._install_mysql.called) pkg.Package.pkg_install = Mock()
self.mySqlApp._clear_mysql_config = Mock()
self.mySqlApp._create_mysql_confd_dir = Mock()
self.mySqlApp.start_mysql = Mock()
self.mySqlApp.install_if_needed(["package"])
self.assertTrue(pkg.Package.pkg_install.called)
self.assert_reported_status(rd_instance.ServiceStatuses.NEW) self.assert_reported_status(rd_instance.ServiceStatuses.NEW)
def test_secure(self): def test_secure(self):
dbaas.clear_expired_password = Mock()
self.mySqlApp.start_mysql = Mock() self.mySqlApp.start_mysql = Mock()
self.mySqlApp.stop_db = Mock() self.mySqlApp.stop_db = Mock()
self.mySqlApp._write_mycnf = Mock() self.mySqlApp._write_mycnf = Mock()
@@ -660,17 +683,20 @@ class MySqlAppInstallTest(MySqlAppTest):
from trove.guestagent import pkg from trove.guestagent import pkg
self.mySqlApp.start_mysql = Mock() self.mySqlApp.start_mysql = Mock()
self.mySqlApp.stop_db = Mock() self.mySqlApp.stop_db = Mock()
self.mySqlApp.is_installed = Mock(return_value=False) pkg.Package.pkg_is_installed = Mock(return_value=False)
self.mySqlApp._install_mysql = Mock( self.mySqlApp._clear_mysql_config = Mock()
side_effect=pkg.PkgPackageStateError("Install error")) self.mySqlApp._create_mysql_confd_dir = Mock()
pkg.Package.pkg_install = \
Mock(side_effect=pkg.PkgPackageStateError("Install error"))
self.assertRaises(pkg.PkgPackageStateError, self.assertRaises(pkg.PkgPackageStateError,
self.mySqlApp.install_if_needed) self.mySqlApp.install_if_needed, ["package"])
self.assert_reported_status(rd_instance.ServiceStatuses.NEW) self.assert_reported_status(rd_instance.ServiceStatuses.NEW)
def test_secure_write_conf_error(self): def test_secure_write_conf_error(self):
dbaas.clear_expired_password = Mock()
self.mySqlApp.start_mysql = Mock() self.mySqlApp.start_mysql = Mock()
self.mySqlApp.stop_db = Mock() self.mySqlApp.stop_db = Mock()
self.mySqlApp._write_mycnf = Mock( self.mySqlApp._write_mycnf = Mock(
@@ -686,18 +712,6 @@ class MySqlAppInstallTest(MySqlAppTest):
self.assertFalse(self.mySqlApp.start_mysql.called) self.assertFalse(self.mySqlApp.start_mysql.called)
self.assert_reported_status(rd_instance.ServiceStatuses.NEW) self.assert_reported_status(rd_instance.ServiceStatuses.NEW)
def test_is_installed(self):
dbaas.packager.pkg_version = Mock(return_value=True)
self.assertTrue(self.mySqlApp.is_installed())
def test_is_installed_not(self):
dbaas.packager.pkg_version = Mock(return_value=None)
self.assertFalse(self.mySqlApp.is_installed())
class TextClauseMatcher(matchers.Matcher): class TextClauseMatcher(matchers.Matcher):
def __init__(self, text): def __init__(self, text):
@@ -772,8 +786,11 @@ class MySqlAppMockTest(testtools.TestCase):
mock_status = mock() mock_status = mock()
when(mock_status).wait_for_real_status_to_change_to( when(mock_status).wait_for_real_status_to_change_to(
any(), any(), any()).thenReturn(True) any(), any(), any()).thenReturn(True)
when(dbaas).clear_expired_password().thenReturn(None)
app = MySqlApp(mock_status) app = MySqlApp(mock_status)
when(app)._write_mycnf(any(), any()).thenReturn(True) when(app)._write_mycnf(any(), any()).thenReturn(True)
when(app).start_mysql().thenReturn(None)
when(app).stop_db().thenReturn(None)
app.secure('foo') app.secure('foo')
verify(mock_conn, never).execute(TextClauseMatcher('root')) verify(mock_conn, never).execute(TextClauseMatcher('root'))
@@ -883,16 +900,16 @@ class ServiceRegistryTest(testtools.TestCase):
def tearDown(self): def tearDown(self):
super(ServiceRegistryTest, self).tearDown() super(ServiceRegistryTest, self).tearDown()
def test_service_registry_with_extra_manager(self): def test_datastore_registry_with_extra_manager(self):
service_registry_ext_test = { datastore_registry_ext_test = {
'test': 'trove.guestagent.datastore.test.manager.Manager', 'test': 'trove.guestagent.datastore.test.manager.Manager',
} }
dbaas_sr.get_custom_managers = Mock(return_value= dbaas_sr.get_custom_managers = Mock(return_value=
service_registry_ext_test) datastore_registry_ext_test)
test_dict = dbaas_sr.service_registry() test_dict = dbaas_sr.datastore_registry()
self.assertEqual(3, len(test_dict)) self.assertEqual(3, len(test_dict))
self.assertEqual(test_dict.get('test'), self.assertEqual(test_dict.get('test'),
service_registry_ext_test.get('test', None)) datastore_registry_ext_test.get('test', None))
self.assertEqual(test_dict.get('mysql'), self.assertEqual(test_dict.get('mysql'),
'trove.guestagent.datastore.mysql.' 'trove.guestagent.datastore.mysql.'
'manager.Manager') 'manager.Manager')
@@ -900,14 +917,14 @@ class ServiceRegistryTest(testtools.TestCase):
'trove.guestagent.datastore.mysql.' 'trove.guestagent.datastore.mysql.'
'manager.Manager') 'manager.Manager')
def test_service_registry_with_existing_manager(self): def test_datastore_registry_with_existing_manager(self):
service_registry_ext_test = { datastore_registry_ext_test = {
'mysql': 'trove.guestagent.datastore.mysql.' 'mysql': 'trove.guestagent.datastore.mysql.'
'manager.Manager123', 'manager.Manager123',
} }
dbaas_sr.get_custom_managers = Mock(return_value= dbaas_sr.get_custom_managers = Mock(return_value=
service_registry_ext_test) datastore_registry_ext_test)
test_dict = dbaas_sr.service_registry() test_dict = dbaas_sr.datastore_registry()
self.assertEqual(2, len(test_dict)) self.assertEqual(2, len(test_dict))
self.assertEqual(test_dict.get('mysql'), self.assertEqual(test_dict.get('mysql'),
'trove.guestagent.datastore.mysql.' 'trove.guestagent.datastore.mysql.'
@@ -916,11 +933,11 @@ class ServiceRegistryTest(testtools.TestCase):
'trove.guestagent.datastore.mysql.' 'trove.guestagent.datastore.mysql.'
'manager.Manager') 'manager.Manager')
def test_service_registry_with_blank_dict(self): def test_datastore_registry_with_blank_dict(self):
service_registry_ext_test = dict() datastore_registry_ext_test = dict()
dbaas_sr.get_custom_managers = Mock(return_value= dbaas_sr.get_custom_managers = Mock(return_value=
service_registry_ext_test) datastore_registry_ext_test)
test_dict = dbaas_sr.service_registry() test_dict = dbaas_sr.datastore_registry()
self.assertEqual(2, len(test_dict)) self.assertEqual(2, len(test_dict))
self.assertEqual(test_dict.get('mysql'), self.assertEqual(test_dict.get('mysql'),
'trove.guestagent.datastore.mysql.' 'trove.guestagent.datastore.mysql.'

View File

@@ -24,6 +24,7 @@ from trove.guestagent.datastore.mysql.manager import Manager
import trove.guestagent.datastore.mysql.service as dbaas import trove.guestagent.datastore.mysql.service as dbaas
from trove.guestagent import backup from trove.guestagent import backup
from trove.guestagent.volume import VolumeDevice from trove.guestagent.volume import VolumeDevice
from trove.guestagent import pkg
class GuestAgentManagerTest(testtools.TestCase): class GuestAgentManagerTest(testtools.TestCase):
@@ -37,10 +38,10 @@ class GuestAgentManagerTest(testtools.TestCase):
self.origin_format = volume.VolumeDevice.format self.origin_format = volume.VolumeDevice.format
self.origin_migrate_data = volume.VolumeDevice.migrate_data self.origin_migrate_data = volume.VolumeDevice.migrate_data
self.origin_mount = volume.VolumeDevice.mount self.origin_mount = volume.VolumeDevice.mount
self.origin_is_installed = dbaas.MySqlApp.is_installed
self.origin_stop_mysql = dbaas.MySqlApp.stop_db self.origin_stop_mysql = dbaas.MySqlApp.stop_db
self.origin_start_mysql = dbaas.MySqlApp.start_mysql self.origin_start_mysql = dbaas.MySqlApp.start_mysql
self.origin_install_mysql = dbaas.MySqlApp._install_mysql self.origin_pkg_is_installed = pkg.Package.pkg_is_installed
self.origin_os_path_exists = os.path.exists
def tearDown(self): def tearDown(self):
super(GuestAgentManagerTest, self).tearDown() super(GuestAgentManagerTest, self).tearDown()
@@ -49,10 +50,10 @@ class GuestAgentManagerTest(testtools.TestCase):
volume.VolumeDevice.format = self.origin_format volume.VolumeDevice.format = self.origin_format
volume.VolumeDevice.migrate_data = self.origin_migrate_data volume.VolumeDevice.migrate_data = self.origin_migrate_data
volume.VolumeDevice.mount = self.origin_mount volume.VolumeDevice.mount = self.origin_mount
dbaas.MySqlApp.is_installed = self.origin_is_installed
dbaas.MySqlApp.stop_db = self.origin_stop_mysql dbaas.MySqlApp.stop_db = self.origin_stop_mysql
dbaas.MySqlApp.start_mysql = self.origin_start_mysql dbaas.MySqlApp.start_mysql = self.origin_start_mysql
dbaas.MySqlApp._install_mysql = self.origin_install_mysql pkg.Package.pkg_is_installed = self.origin_pkg_is_installed
os.path.exists = self.origin_os_path_exists
unstub() unstub()
def test_update_status(self): def test_update_status(self):
@@ -139,8 +140,6 @@ class GuestAgentManagerTest(testtools.TestCase):
# covering all outcomes is starting to cause trouble here # covering all outcomes is starting to cause trouble here
COUNT = 1 if device_path else 0 COUNT = 1 if device_path else 0
SEC_COUNT = 1 if is_mysql_installed else 0
migrate_count = 1 * COUNT if not backup_id else 0
# TODO(juice): this should stub an instance of the MySqlAppStatus # TODO(juice): this should stub an instance of the MySqlAppStatus
mock_status = mock() mock_status = mock()
@@ -155,16 +154,18 @@ class GuestAgentManagerTest(testtools.TestCase):
when(backup).restore(self.context, backup_id).thenReturn(None) when(backup).restore(self.context, backup_id).thenReturn(None)
when(dbaas.MySqlApp).secure(any()).thenReturn(None) when(dbaas.MySqlApp).secure(any()).thenReturn(None)
when(dbaas.MySqlApp).secure_root(any()).thenReturn(None) when(dbaas.MySqlApp).secure_root(any()).thenReturn(None)
when(dbaas.MySqlApp).is_installed().thenReturn(is_mysql_installed) (when(pkg.Package).pkg_is_installed(any()).
thenReturn(is_mysql_installed))
when(dbaas.MySqlAdmin).is_root_enabled().thenReturn(is_root_enabled) when(dbaas.MySqlAdmin).is_root_enabled().thenReturn(is_root_enabled)
when(dbaas.MySqlAdmin).create_user().thenReturn(None) when(dbaas.MySqlAdmin).create_user().thenReturn(None)
when(dbaas.MySqlAdmin).create_database().thenReturn(None) when(dbaas.MySqlAdmin).create_database().thenReturn(None)
when(dbaas.MySqlAdmin).report_root_enabled(self.context).thenReturn( when(dbaas.MySqlAdmin).report_root_enabled(self.context).thenReturn(
None) None)
when(os.path).exists(any()).thenReturn(is_mysql_installed) when(os.path).exists(any()).thenReturn(True)
# invocation # invocation
self.manager.prepare(context=self.context, databases=None, self.manager.prepare(context=self.context, packages=None,
databases=None,
memory_mb='2048', users=None, memory_mb='2048', users=None,
device_path=device_path, device_path=device_path,
mount_point='/var/lib/mysql', mount_point='/var/lib/mysql',
@@ -173,12 +174,11 @@ class GuestAgentManagerTest(testtools.TestCase):
verify(mock_status).begin_install() verify(mock_status).begin_install()
verify(VolumeDevice, times=COUNT).format() verify(VolumeDevice, times=COUNT).format()
verify(dbaas.MySqlApp, times=(COUNT * SEC_COUNT)).stop_db() verify(dbaas.MySqlApp, times=COUNT).stop_db()
verify(VolumeDevice, times=(migrate_count * SEC_COUNT)).migrate_data( verify(VolumeDevice, times=COUNT).migrate_data(
any()) any())
if backup_id: if backup_id:
verify(backup).restore(self.context, backup_id, '/var/lib/mysql') verify(backup).restore(self.context, backup_id, '/var/lib/mysql')
verify(dbaas.MySqlApp).install_if_needed()
# We dont need to make sure the exact contents are there # We dont need to make sure the exact contents are there
verify(dbaas.MySqlApp).secure(any()) verify(dbaas.MySqlApp).secure(any())
verify(dbaas.MySqlAdmin, never).create_database() verify(dbaas.MySqlAdmin, never).create_database()

View File

@@ -17,6 +17,7 @@
import testtools import testtools
from mock import Mock from mock import Mock
from mockito import when, any
import pexpect import pexpect
from trove.common import utils from trove.common import utils
from trove.common import exception from trove.common import exception
@@ -38,10 +39,12 @@ class PkgDEBInstallTestCase(testtools.TestCase):
self.pexpect_spawn_closed = pexpect.spawn.close self.pexpect_spawn_closed = pexpect.spawn.close
self.pkg = pkg.DebianPackagerMixin() self.pkg = pkg.DebianPackagerMixin()
self.pkg_fix = self.pkg._fix self.pkg_fix = self.pkg._fix
self.pkg_fix_package_selections = self.pkg._fix_package_selections
utils.execute = Mock() utils.execute = Mock()
pexpect.spawn.__init__ = Mock(return_value=None) pexpect.spawn.__init__ = Mock(return_value=None)
pexpect.spawn.closed = Mock(return_value=None) pexpect.spawn.closed = Mock(return_value=None)
self.pkg._fix = Mock(return_value=None) self.pkg._fix = Mock(return_value=None)
self.pkg._fix_package_selections = Mock(return_value=None)
self.pkgName = 'packageName' self.pkgName = 'packageName'
def tearDown(self): def tearDown(self):
@@ -50,53 +53,78 @@ class PkgDEBInstallTestCase(testtools.TestCase):
pexpect.spawn.__init__ = self.pexpect_spawn_init pexpect.spawn.__init__ = self.pexpect_spawn_init
pexpect.spawn.close = self.pexpect_spawn_closed pexpect.spawn.close = self.pexpect_spawn_closed
self.pkg._fix = self.pkg_fix self.pkg._fix = self.pkg_fix
self.pkg._fix_package_selections = self.pkg_fix_package_selections
def test_pkg_is_instaled_no_packages(self):
packages = ""
self.assertTrue(self.pkg.pkg_is_installed(packages))
def test_pkg_is_instaled_yes(self):
packages = "package1=1.0 package2"
when(self.pkg).pkg_version("package1").thenReturn("1.0")
when(self.pkg).pkg_version("package2").thenReturn("2.0")
self.assertTrue(self.pkg.pkg_is_installed(packages))
def test_pkg_is_instaled_no(self):
packages = "package1=1.0 package2 package3=3.1"
when(self.pkg).pkg_version("package1").thenReturn("1.0")
when(self.pkg).pkg_version("package2").thenReturn("2.0")
when(self.pkg).pkg_version("package3").thenReturn("3.0")
self.assertFalse(self.pkg.pkg_is_installed(packages))
def test_success_install(self): def test_success_install(self):
# test # test
pexpect.spawn.expect = Mock(return_value=5) pexpect.spawn.expect = Mock(return_value=7)
self.assertTrue(self.pkg.pkg_install(self.pkgName, 5000) is None) pexpect.spawn.match = False
# verify self.assertTrue(self.pkg.pkg_install(self.pkgName, {}, 5000) is None)
def test_already_instaled(self):
# test happy path
pexpect.spawn.expect = Mock(return_value=6)
self.pkg.pkg_install(self.pkgName, 5000)
def test_permission_error(self): def test_permission_error(self):
# test # test
pexpect.spawn.expect = Mock(return_value=0) pexpect.spawn.expect = Mock(return_value=0)
pexpect.spawn.match = False
# test and verify # test and verify
self.assertRaises(pkg.PkgPermissionError, self.pkg.pkg_install, self.assertRaises(pkg.PkgPermissionError, self.pkg.pkg_install,
self.pkgName, 5000) self.pkgName, {}, 5000)
def test_package_not_found_1(self): def test_package_not_found_1(self):
# test # test
pexpect.spawn.expect = Mock(return_value=1) pexpect.spawn.expect = Mock(return_value=1)
pexpect.spawn.match = re.match('(.*)', self.pkgName)
# test and verify # test and verify
self.assertRaises(pkg.PkgNotFoundError, self.pkg.pkg_install, self.assertRaises(pkg.PkgNotFoundError, self.pkg.pkg_install,
self.pkgName, 5000) self.pkgName, {}, 5000)
def test_package_not_found_2(self): def test_package_not_found_2(self):
# test # test
pexpect.spawn.expect = Mock(return_value=2) pexpect.spawn.expect = Mock(return_value=2)
pexpect.spawn.match = re.match('(.*)', self.pkgName)
# test and verify # test and verify
self.assertRaises(pkg.PkgNotFoundError, self.pkg.pkg_install, self.assertRaises(pkg.PkgNotFoundError, self.pkg.pkg_install,
self.pkgName, 5000) self.pkgName, {}, 5000)
def test_run_DPKG_bad_State(self): def test_run_DPKG_bad_State(self):
# test _fix method is called and PackageStateError is thrown # test _fix method is called and PackageStateError is thrown
pexpect.spawn.expect = Mock(return_value=3) pexpect.spawn.expect = Mock(return_value=4)
pexpect.spawn.match = False
# test and verify # test and verify
self.assertRaises(pkg.PkgPackageStateError, self.pkg.pkg_install, self.assertRaises(pkg.PkgPackageStateError, self.pkg.pkg_install,
self.pkgName, 5000) self.pkgName, {}, 5000)
self.assertTrue(self.pkg._fix.called) self.assertTrue(self.pkg._fix.called)
def test_admin_lock_error(self): def test_admin_lock_error(self):
# test 'Unable to lock the administration directory' error # test 'Unable to lock the administration directory' error
pexpect.spawn.expect = Mock(return_value=4) pexpect.spawn.expect = Mock(return_value=5)
pexpect.spawn.match = False
# test and verify # test and verify
self.assertRaises(pkg.PkgAdminLockError, self.pkg.pkg_install, self.assertRaises(pkg.PkgAdminLockError, self.pkg.pkg_install,
self.pkgName, 5000) self.pkgName, {}, 5000)
def test_package_broken_error(self):
pexpect.spawn.expect = Mock(return_value=6)
pexpect.spawn.match = False
# test and verify
self.assertRaises(pkg.PkgBrokenError, self.pkg.pkg_install,
self.pkgName, {}, 5000)
def test_timeout_error(self): def test_timeout_error(self):
# test timeout error # test timeout error
@@ -104,7 +132,7 @@ class PkgDEBInstallTestCase(testtools.TestCase):
TIMEOUT('timeout error')) TIMEOUT('timeout error'))
# test and verify # test and verify
self.assertRaises(pkg.PkgTimeout, self.pkg.pkg_install, self.assertRaises(pkg.PkgTimeout, self.pkg.pkg_install,
self.pkgName, 5000) self.pkgName, {}, 5000)
class PkgDEBRemoveTestCase(testtools.TestCase): class PkgDEBRemoveTestCase(testtools.TestCase):
@@ -140,11 +168,13 @@ class PkgDEBRemoveTestCase(testtools.TestCase):
def test_success_remove(self): def test_success_remove(self):
# test # test
pexpect.spawn.expect = Mock(return_value=6) pexpect.spawn.expect = Mock(return_value=6)
pexpect.spawn.match = False
self.assertTrue(self.pkg.pkg_remove(self.pkgName, 5000) is None) self.assertTrue(self.pkg.pkg_remove(self.pkgName, 5000) is None)
def test_permission_error(self): def test_permission_error(self):
# test # test
pexpect.spawn.expect = Mock(return_value=0) pexpect.spawn.expect = Mock(return_value=0)
pexpect.spawn.match = False
# test and verify # test and verify
self.assertRaises(pkg.PkgPermissionError, self.pkg.pkg_remove, self.assertRaises(pkg.PkgPermissionError, self.pkg.pkg_remove,
self.pkgName, 5000) self.pkgName, 5000)
@@ -152,6 +182,7 @@ class PkgDEBRemoveTestCase(testtools.TestCase):
def test_package_not_found(self): def test_package_not_found(self):
# test # test
pexpect.spawn.expect = Mock(return_value=1) pexpect.spawn.expect = Mock(return_value=1)
pexpect.spawn.match = False
# test and verify # test and verify
self.assertRaises(pkg.PkgNotFoundError, self.pkg.pkg_remove, self.assertRaises(pkg.PkgNotFoundError, self.pkg.pkg_remove,
self.pkgName, 5000) self.pkgName, 5000)
@@ -159,6 +190,7 @@ class PkgDEBRemoveTestCase(testtools.TestCase):
def test_package_reinstall_first_1(self): def test_package_reinstall_first_1(self):
# test # test
pexpect.spawn.expect = Mock(return_value=2) pexpect.spawn.expect = Mock(return_value=2)
pexpect.spawn.match = False
# test and verify # test and verify
self.assertRaises(pkg.PkgPackageStateError, self.pkg.pkg_remove, self.assertRaises(pkg.PkgPackageStateError, self.pkg.pkg_remove,
self.pkgName, 5000) self.pkgName, 5000)
@@ -168,6 +200,7 @@ class PkgDEBRemoveTestCase(testtools.TestCase):
def test_package_reinstall_first_2(self): def test_package_reinstall_first_2(self):
# test # test
pexpect.spawn.expect = Mock(return_value=3) pexpect.spawn.expect = Mock(return_value=3)
pexpect.spawn.match = False
# test and verify # test and verify
self.assertRaises(pkg.PkgPackageStateError, self.pkg.pkg_remove, self.assertRaises(pkg.PkgPackageStateError, self.pkg.pkg_remove,
self.pkgName, 5000) self.pkgName, 5000)
@@ -177,6 +210,7 @@ class PkgDEBRemoveTestCase(testtools.TestCase):
def test_package_DPKG_first(self): def test_package_DPKG_first(self):
# test # test
pexpect.spawn.expect = Mock(return_value=4) pexpect.spawn.expect = Mock(return_value=4)
pexpect.spawn.match = False
# test and verify # test and verify
self.assertRaises(pkg.PkgPackageStateError, self.pkg.pkg_remove, self.assertRaises(pkg.PkgPackageStateError, self.pkg.pkg_remove,
self.pkgName, 5000) self.pkgName, 5000)
@@ -186,6 +220,7 @@ class PkgDEBRemoveTestCase(testtools.TestCase):
def test_admin_lock_error(self): def test_admin_lock_error(self):
# test 'Unable to lock the administration directory' error # test 'Unable to lock the administration directory' error
pexpect.spawn.expect = Mock(return_value=5) pexpect.spawn.expect = Mock(return_value=5)
pexpect.spawn.match = False
# test and verify # test and verify
self.assertRaises(pkg.PkgAdminLockError, self.pkg.pkg_remove, self.assertRaises(pkg.PkgAdminLockError, self.pkg.pkg_remove,
self.pkgName, 5000) self.pkgName, 5000)
@@ -201,22 +236,6 @@ class PkgDEBRemoveTestCase(testtools.TestCase):
class PkgDEBVersionTestCase(testtools.TestCase): class PkgDEBVersionTestCase(testtools.TestCase):
@staticmethod
def build_output(packageName, packageVersion, parts=None):
if parts is None:
parts = "ii " + packageName + " " + packageVersion + \
" MySQL database server binaries "\
"and system database setup \n"
cmd_out = "Desired=Unknown/Install/Remove/Purge/Hold\n" \
"| Status=Not/Inst/Conf-files/Unpacked/halF-conf/"\
"Half-inst/trig-aWait/Trig-pend\n" \
"|/ Err?=(none)/Reinst-required "\
"(Status,Err: uppercase=bad)\n"\
"||/ Name Version Description\n" \
"+++-==============-================-=============\n" \
"=================================\n" + parts
return cmd_out
def setUp(self): def setUp(self):
super(PkgDEBVersionTestCase, self).setUp() super(PkgDEBVersionTestCase, self).setUp()
self.pkgName = 'mysql-server-5.5' self.pkgName = 'mysql-server-5.5'
@@ -228,43 +247,19 @@ class PkgDEBVersionTestCase(testtools.TestCase):
commands.getstatusoutput = self.commands_output commands.getstatusoutput = self.commands_output
def test_version_success(self): def test_version_success(self):
cmd_out = self.build_output(self.pkgName, self.pkgVersion) cmd_out = "%s:\n Installed: %s\n" % (self.pkgName, self.pkgVersion)
commands.getstatusoutput = Mock(return_value=(0, cmd_out)) commands.getstatusoutput = Mock(return_value=(0, cmd_out))
version = pkg.DebianPackagerMixin().pkg_version(self.pkgName) version = pkg.DebianPackagerMixin().pkg_version(self.pkgName)
self.assertTrue(version) self.assertTrue(version)
self.assertEqual(self.pkgVersion, version) self.assertEqual(self.pkgVersion, version)
def test_version_status_error(self):
cmd_out = self.build_output(self.pkgName, self.pkgVersion)
commands.getstatusoutput = Mock(return_value=(1, cmd_out))
self.assertFalse(pkg.DebianPackagerMixin().pkg_version(self.pkgName))
def test_version_no_output(self):
cmd_out = self.build_output(self.pkgName, self.pkgVersion, "")
commands.getstatusoutput = Mock(return_value=(0, cmd_out))
self.assertIsNone(pkg.DebianPackagerMixin().pkg_version(self.pkgName))
def test_version_unexpected_parts(self):
unexp_parts = "ii 123"
cmd_out = self.build_output(self.pkgName, self.pkgVersion, unexp_parts)
commands.getstatusoutput = Mock(return_value=(0, cmd_out))
self.assertIsNone(pkg.DebianPackagerMixin().pkg_version(self.pkgName))
def test_version_wrong_package(self):
invalid_pkg = "package_invalid_001"
cmd_out = self.build_output(invalid_pkg, self.pkgVersion)
commands.getstatusoutput = Mock(return_value=(0, cmd_out))
self.assertRaises(exception.GuestError,
pkg.DebianPackagerMixin().pkg_version, self.pkgName)
def test_version_unknown_package(self): def test_version_unknown_package(self):
unk_parts = "un " + self.pkgName + " " + self.pkgVersion + " \n" cmd_out = "N: Unable to locate package %s" % self.pkgName
cmd_out = self.build_output(self.pkgName, self.pkgVersion, unk_parts)
commands.getstatusoutput = Mock(return_value=(0, cmd_out)) commands.getstatusoutput = Mock(return_value=(0, cmd_out))
self.assertFalse(pkg.DebianPackagerMixin().pkg_version(self.pkgName)) self.assertFalse(pkg.DebianPackagerMixin().pkg_version(self.pkgName))
def test_version_no_version(self): def test_version_no_version(self):
cmd_out = self.build_output(self.pkgName, '<none>') cmd_out = "%s:\n Installed: %s\n" % (self.pkgName, "(none)")
commands.getstatusoutput = Mock(return_value=(0, cmd_out)) commands.getstatusoutput = Mock(return_value=(0, cmd_out))
self.assertFalse(pkg.DebianPackagerMixin().pkg_version(self.pkgName)) self.assertFalse(pkg.DebianPackagerMixin().pkg_version(self.pkgName))
@@ -313,73 +308,107 @@ class PkgRPMInstallTestCase(testtools.TestCase):
pexpect.spawn.__init__ = self.pexpect_spawn_init pexpect.spawn.__init__ = self.pexpect_spawn_init
pexpect.spawn.close = self.pexpect_spawn_closed pexpect.spawn.close = self.pexpect_spawn_closed
def test_pkg_is_instaled_no_packages(self):
packages = ""
self.assertTrue(self.pkg.pkg_is_installed(packages))
def test_pkg_is_instaled_yes(self):
packages = "package1=1.0 package2"
when(commands).getstatusoutput(any()).thenReturn({1: "package1=1.0\n"
"package2=2.0"})
self.assertTrue(self.pkg.pkg_is_installed(packages))
def test_pkg_is_instaled_no(self):
packages = "package1=1.0 package2 package3=3.0"
when(commands).getstatusoutput({1: "package1=1.0\npackage2=2.0"})
self.assertFalse(self.pkg.pkg_is_installed(packages))
def test_permission_error(self): def test_permission_error(self):
# test # test
pexpect.spawn.expect = Mock(return_value=0) pexpect.spawn.expect = Mock(return_value=0)
pexpect.spawn.match = False
# test and verify # test and verify
self.assertRaises(pkg.PkgPermissionError, self.pkg.pkg_install, self.assertRaises(pkg.PkgPermissionError, self.pkg.pkg_install,
self.pkgName, 5000) self.pkgName, {}, 5000)
def test_package_not_found(self): def test_package_not_found(self):
# test # test
pexpect.spawn.expect = Mock(return_value=1) pexpect.spawn.expect = Mock(return_value=1)
pexpect.spawn.match = re.match('(.*)', self.pkgName)
# test and verify # test and verify
self.assertRaises(pkg.PkgNotFoundError, self.pkg.pkg_install, self.assertRaises(pkg.PkgNotFoundError, self.pkg.pkg_install,
self.pkgName, 5000) self.pkgName, {}, 5000)
def test_transaction_check_error(self): def test_package_conflict_remove(self):
# test # test
pexpect.spawn.expect = Mock(return_value=2) pexpect.spawn.expect = Mock(return_value=2)
pexpect.spawn.match = re.match('(.*)', self.pkgName)
self.pkg._rpm_remove_nodeps = Mock()
# test and verify # test and verify
self.assertRaises(pkg.PkgTransactionCheckError, self.pkg.pkg_install, self.pkg._install(self.pkgName, 5000)
self.pkgName, 5000) self.assertTrue(self.pkg._rpm_remove_nodeps.called)
def test_package_scriptlet_error(self): def test_package_scriptlet_error(self):
# test # test
pexpect.spawn.expect = Mock(return_value=3) pexpect.spawn.expect = Mock(return_value=5)
pexpect.spawn.match = False
# test and verify # test and verify
self.assertRaises(pkg.PkgScriptletError, self.pkg.pkg_install, self.assertRaises(pkg.PkgScriptletError, self.pkg.pkg_install,
self.pkgName, 5000) self.pkgName, {}, 5000)
def test_package_http_error(self): def test_package_http_error(self):
# test # test
pexpect.spawn.expect = Mock(return_value=4) pexpect.spawn.expect = Mock(return_value=6)
pexpect.spawn.match = False
# test and verify # test and verify
self.assertRaises(pkg.PkgDownloadError, self.pkg.pkg_install, self.assertRaises(pkg.PkgDownloadError, self.pkg.pkg_install,
self.pkgName, 5000) self.pkgName, {}, 5000)
def test_package_nomirrors_error(self): def test_package_nomirrors_error(self):
# test # test
pexpect.spawn.expect = Mock(return_value=5) pexpect.spawn.expect = Mock(return_value=7)
pexpect.spawn.match = False
# test and verify # test and verify
self.assertRaises(pkg.PkgDownloadError, self.pkg.pkg_install, self.assertRaises(pkg.PkgDownloadError, self.pkg.pkg_install,
self.pkgName, 5000) self.pkgName, {}, 5000)
def test_package_sign_error(self):
# test
pexpect.spawn.expect = Mock(return_value=8)
pexpect.spawn.match = False
# test and verify
self.assertRaises(pkg.PkgSignError, self.pkg.pkg_install,
self.pkgName, {}, 5000)
def test_package_already_installed(self): def test_package_already_installed(self):
# test # test
pexpect.spawn.expect = Mock(return_value=6) pexpect.spawn.expect = Mock(return_value=9)
pexpect.spawn.match = False
# test and verify # test and verify
self.assertTrue(self.pkg.pkg_install(self.pkgName, 5000) is None) self.assertTrue(self.pkg.pkg_install(self.pkgName, {}, 5000) is None)
def test_package_success_updated(self): def test_package_success_updated(self):
# test # test
pexpect.spawn.expect = Mock(return_value=7) pexpect.spawn.expect = Mock(return_value=10)
pexpect.spawn.match = False
# test and verify # test and verify
self.assertTrue(self.pkg.pkg_install(self.pkgName, 5000) is None) self.assertTrue(self.pkg.pkg_install(self.pkgName, {}, 5000) is None)
def test_package_success_installed(self): def test_package_success_installed(self):
# test # test
pexpect.spawn.expect = Mock(return_value=8) pexpect.spawn.expect = Mock(return_value=11)
pexpect.spawn.match = False
# test and verify # test and verify
self.assertTrue(self.pkg.pkg_install(self.pkgName, 5000) is None) self.assertTrue(self.pkg.pkg_install(self.pkgName, {}, 5000) is None)
def test_timeout_error(self): def test_timeout_error(self):
# test timeout error # test timeout error
pexpect.spawn.expect = Mock(side_effect=pexpect. pexpect.spawn.expect = Mock(side_effect=pexpect.
TIMEOUT('timeout error')) TIMEOUT('timeout error'))
pexpect.spawn.match = False
# test and verify # test and verify
self.assertRaises(pkg.PkgTimeout, self.pkg.pkg_install, self.assertRaises(pkg.PkgTimeout, self.pkg.pkg_install,
self.pkgName, 5000) self.pkgName, {}, 5000)
class PkgRPMRemoveTestCase(testtools.TestCase): class PkgRPMRemoveTestCase(testtools.TestCase):

View File

@@ -24,6 +24,7 @@ from oslo.config.cfg import ConfigOpts
from trove.backup.models import Backup from trove.backup.models import Backup
from trove.common.context import TroveContext from trove.common.context import TroveContext
from trove.common import instance as rd_instance from trove.common import instance as rd_instance
from trove.datastore import models as datastore_models
from trove.db.models import DatabaseModelBase from trove.db.models import DatabaseModelBase
from trove.instance.models import DBInstance from trove.instance.models import DBInstance
from trove.instance.models import InstanceServiceStatus from trove.instance.models import InstanceServiceStatus
@@ -31,6 +32,7 @@ from trove.instance.tasks import InstanceTasks
import trove.extensions.mgmt.instances.models as mgmtmodels import trove.extensions.mgmt.instances.models as mgmtmodels
from trove.openstack.common.notifier import api as notifier from trove.openstack.common.notifier import api as notifier
from trove.common import remote from trove.common import remote
from trove.tests.util import test_config
class MockMgmtInstanceTest(TestCase): class MockMgmtInstanceTest(TestCase):
@@ -62,11 +64,12 @@ class MockMgmtInstanceTest(TestCase):
name='test_name', name='test_name',
id='1', id='1',
flavor_id='flavor_1', flavor_id='flavor_1',
datastore_version_id=
test_config.dbaas_datastore_version,
compute_instance_id='compute_id_1', compute_instance_id='compute_id_1',
server_id='server_id_1', server_id='server_id_1',
tenant_id='tenant_id_1', tenant_id='tenant_id_1',
server_status=status, server_status=status)
service_type='mysql')
class TestNotificationTransformer(MockMgmtInstanceTest): class TestNotificationTransformer(MockMgmtInstanceTest):
@@ -78,6 +81,10 @@ class TestNotificationTransformer(MockMgmtInstanceTest):
when(DatabaseModelBase).find_all(deleted=False).thenReturn( when(DatabaseModelBase).find_all(deleted=False).thenReturn(
[db_instance]) [db_instance])
stub_datastore = mock()
stub_datastore.datastore_id = "stub"
stub_datastore.manager = "mysql"
when(DatabaseModelBase).find_by(id=any()).thenReturn(stub_datastore)
when(DatabaseModelBase).find_by(instance_id='1').thenReturn( when(DatabaseModelBase).find_by(instance_id='1').thenReturn(
InstanceServiceStatus(rd_instance.ServiceStatuses.BUILDING)) InstanceServiceStatus(rd_instance.ServiceStatuses.BUILDING))
@@ -165,14 +172,17 @@ class TestNovaNotificationTransformer(MockMgmtInstanceTest):
self.assertThat(payload['user_id'], Equals('test_user_id')) self.assertThat(payload['user_id'], Equals('test_user_id'))
self.assertThat(payload['service_id'], Equals('123')) self.assertThat(payload['service_id'], Equals('123'))
def test_tranformer_invalid_service_type(self): def test_tranformer_invalid_datastore_manager(self):
status = rd_instance.ServiceStatuses.BUILDING.api_status status = rd_instance.ServiceStatuses.BUILDING.api_status
db_instance = MockMgmtInstanceTest.build_db_instance( db_instance = MockMgmtInstanceTest.build_db_instance(
status, task_status=InstanceTasks.BUILDING) status, task_status=InstanceTasks.BUILDING)
db_instance.service_type = 'm0ng0'
server = mock(Server) server = mock(Server)
server.user_id = 'test_user_id' server.user_id = 'test_user_id'
stub_datastore = mock()
stub_datastore.manager = "m0ng0"
when(datastore_models.
Datastore).load(any()).thenReturn(stub_datastore)
mgmt_instance = mgmtmodels.SimpleMgmtInstance(self.context, mgmt_instance = mgmtmodels.SimpleMgmtInstance(self.context,
db_instance, db_instance,
server, server,

View File

@@ -15,6 +15,7 @@ import testtools
from mock import Mock from mock import Mock
from testtools.matchers import Equals from testtools.matchers import Equals
from mockito import mock, when, unstub, any, verify, never from mockito import mock, when, unstub, any, verify, never
from trove.datastore import models as datastore_models
from trove.taskmanager import models as taskmanager_models from trove.taskmanager import models as taskmanager_models
import trove.common.remote as remote import trove.common.remote as remote
from trove.common.instance import ServiceStatuses from trove.common.instance import ServiceStatuses
@@ -138,6 +139,10 @@ class FreshInstanceTasksTest(testtools.TestCase):
"hostname") "hostname")
when(taskmanager_models.FreshInstanceTasks).name().thenReturn( when(taskmanager_models.FreshInstanceTasks).name().thenReturn(
'name') 'name')
when(datastore_models.
DatastoreVersion).load(any()).thenReturn(mock())
when(datastore_models.
Datastore).load(any()).thenReturn(mock())
taskmanager_models.FreshInstanceTasks.nova_client = fake_nova_client() taskmanager_models.FreshInstanceTasks.nova_client = fake_nova_client()
taskmanager_models.CONF = mock() taskmanager_models.CONF = mock()
when(taskmanager_models.CONF).get(any()).thenReturn('') when(taskmanager_models.CONF).get(any()).thenReturn('')
@@ -152,7 +157,7 @@ class FreshInstanceTasksTest(testtools.TestCase):
self.guestconfig = f.name self.guestconfig = f.name
f.write(self.guestconfig_content) f.write(self.guestconfig_content)
self.freshinstancetasks = taskmanager_models.FreshInstanceTasks( self.freshinstancetasks = taskmanager_models.FreshInstanceTasks(
None, None, None, None) None, mock(), None, None)
def tearDown(self): def tearDown(self):
super(FreshInstanceTasksTest, self).tearDown() super(FreshInstanceTasksTest, self).tearDown()
@@ -164,11 +169,12 @@ class FreshInstanceTasksTest(testtools.TestCase):
def test_create_instance_userdata(self): def test_create_instance_userdata(self):
cloudinit_location = os.path.dirname(self.cloudinit) cloudinit_location = os.path.dirname(self.cloudinit)
service_type = os.path.splitext(os.path.basename(self.cloudinit))[0] datastore_manager = os.path.splitext(os.path.basename(self.
cloudinit))[0]
when(taskmanager_models.CONF).get("cloudinit_location").thenReturn( when(taskmanager_models.CONF).get("cloudinit_location").thenReturn(
cloudinit_location) cloudinit_location)
server = self.freshinstancetasks._create_server( server = self.freshinstancetasks._create_server(
None, None, None, service_type, None, None) None, None, None, datastore_manager, None, None)
self.assertEqual(server.userdata, self.userdata) self.assertEqual(server.userdata, self.userdata)
def test_create_instance_guestconfig(self): def test_create_instance_guestconfig(self):
@@ -181,23 +187,20 @@ class FreshInstanceTasksTest(testtools.TestCase):
self.guestconfig_content) self.guestconfig_content)
def test_create_instance_with_az_kwarg(self): def test_create_instance_with_az_kwarg(self):
service_type = 'mysql'
server = self.freshinstancetasks._create_server( server = self.freshinstancetasks._create_server(
None, None, None, service_type, None, availability_zone='nova') None, None, None, None, None, availability_zone='nova')
self.assertIsNotNone(server) self.assertIsNotNone(server)
def test_create_instance_with_az(self): def test_create_instance_with_az(self):
service_type = 'mysql'
server = self.freshinstancetasks._create_server( server = self.freshinstancetasks._create_server(
None, None, None, service_type, None, 'nova') None, None, None, None, None, 'nova')
self.assertIsNotNone(server) self.assertIsNotNone(server)
def test_create_instance_with_az_none(self): def test_create_instance_with_az_none(self):
service_type = 'mysql'
server = self.freshinstancetasks._create_server( server = self.freshinstancetasks._create_server(
None, None, None, service_type, None, None) None, None, None, None, None, None)
self.assertIsNotNone(server) self.assertIsNotNone(server)

View File

@@ -102,20 +102,6 @@ class TestClient(object):
flavor_href = self.find_flavor_self_href(flavor) flavor_href = self.find_flavor_self_href(flavor)
return flavor, flavor_href return flavor, flavor_href
def find_image_and_self_href(self, image_id):
"""Given an ID, returns tuple with image and its self href."""
assert_false(image_id is None)
image = self.images.get(image_id)
assert_true(image is not None)
self_links = [link['href'] for link in image.links
if link['rel'] == 'self']
assert_true(len(self_links) > 0,
"Found image with ID %s but it had no self link!" %
str(image_id))
image_href = self_links[0]
assert_false(image_href is None, "Image link self href missing.")
return image, image_href
def __getattr__(self, item): def __getattr__(self, item):
return getattr(self.real_client, item) return getattr(self.real_client, item)