Merge "Add support for DB2 datastore in Trove"

This commit is contained in:
Jenkins 2015-03-28 08:57:25 +00:00 committed by Gerrit Code Review
commit 7c300679d5
10 changed files with 1140 additions and 2 deletions

View File

@ -317,7 +317,8 @@ common_opts = [
'mongodb': 'c8c907af-7375-456f-b929-b637ff9209ee',
'postgresql': 'ac277e0d-4f21-40aa-b347-1ea31e571720',
'couchdb': 'f0a9ab7b-66f7-4352-93d7-071521d44c7c',
'vertica': 'a8d805ae-a3b2-c4fd-gb23-b62cee5201ae'},
'vertica': 'a8d805ae-a3b2-c4fd-gb23-b62cee5201ae',
'db2': 'e040cd37-263d-4869-aaa6-c62aa97523b5'},
help='Unique ID to tag notification events.'),
cfg.StrOpt('nova_proxy_admin_user', default='',
help="Admin username used to connect to Nova.", secret=True),
@ -873,6 +874,47 @@ vertica_opts = [
'logic.'),
]
# DB2
db2_group = cfg.OptGroup(
'db2', title='DB2 options',
help="Oslo option group designed for DB2 datastore")
db2_opts = [
cfg.ListOpt('tcp_ports',
default=["50000"],
help='List of TCP ports and/or port ranges to open '
'in the security group (only applicable '
'if trove_security_groups_support is True).'),
cfg.ListOpt('udp_ports', default=[],
help='List of UDP ports and/or port ranges to open '
'in the security group (only applicable '
'if trove_security_groups_support is True).'),
cfg.StrOpt('mount_point', default="/home/db2inst1/db2inst1",
help="Filesystem path for mounting "
"volumes if volume support is enabled."),
cfg.BoolOpt('volume_support', default=True,
help='Whether to provision a Cinder volume for datadir.'),
cfg.StrOpt('device_path', default='/dev/vdb',
help='Device path for volume if volume support is enabled.'),
cfg.StrOpt('backup_strategy', default=None,
help='Default strategy to perform backups.'),
cfg.StrOpt('replication_strategy', default=None,
help='Default strategy for replication.'),
cfg.BoolOpt('root_on_create', default=False,
help='Enable the automatic creation of the root user for the '
'service during instance-create. The generated password for '
'the root user is immediately returned in the response of '
"instance-create as the 'password' field."),
cfg.StrOpt('backup_namespace', default=None,
help='Namespace to load backup strategies from.'),
cfg.StrOpt('restore_namespace', default=None,
help='Namespace to load restore strategies from.'),
cfg.DictOpt('backup_incremental_strategy', default={},
help='Incremental Backup Runner based on the default '
'strategy. For strategies that do not implement an '
'incremental, the runner will use the default full backup.'),
cfg.ListOpt('ignore_users', default=['PUBLIC', 'DB2INST1']),
]
# RPC version groups
upgrade_levels = cfg.OptGroup(
'upgrade_levels',
@ -910,6 +952,7 @@ CONF.register_group(mongodb_group)
CONF.register_group(postgresql_group)
CONF.register_group(couchdb_group)
CONF.register_group(vertica_group)
CONF.register_group(db2_group)
CONF.register_opts(mysql_opts, mysql_group)
CONF.register_opts(percona_opts, percona_group)
@ -920,6 +963,7 @@ CONF.register_opts(mongodb_opts, mongodb_group)
CONF.register_opts(postgresql_opts, postgresql_group)
CONF.register_opts(couchdb_opts, couchdb_group)
CONF.register_opts(vertica_opts, vertica_group)
CONF.register_opts(db2_opts, db2_group)
CONF.register_opts(rpcapi_cap_opts, upgrade_levels)

View File

@ -0,0 +1,205 @@
# Copyright 2015 IBM Corp
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from trove.common import cfg
from trove.common import exception
from trove.openstack.common import log as logging
from trove.openstack.common import periodic_task
from trove.guestagent import dbaas
from trove.guestagent import volume
from trove.guestagent.datastore.experimental.db2 import service
from trove.openstack.common.gettextutils import _
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
MANAGER = CONF.datastore_manager
class Manager(periodic_task.PeriodicTasks):
"""
This is DB2 Manager class. It is dynamically loaded
based off of the datastore of the Trove instance.
"""
def __init__(self):
self.appStatus = service.DB2AppStatus()
self.app = service.DB2App(self.appStatus)
self.admin = service.DB2Admin()
@periodic_task.periodic_task(ticks_between_runs=3)
def update_status(self, context):
"""
Updates the status of DB2 Trove instance. It is decorated
with perodic task so it is automatically called every 3 ticks.
"""
self.appStatus.update()
def prepare(self, context, packages, databases, memory_mb, users,
device_path=None, mount_point=None, backup_info=None,
config_contents=None, root_password=None, overrides=None,
cluster_config=None, snapshot=None):
"""
This is called when the Trove instance first comes online.
It is the first rpc message passed from the task manager.
prepare handles all the base configuration of the DB2 instance.
"""
LOG.debug("Preparing the guest agent for DB2.")
self.appStatus.begin_install()
if device_path:
device = volume.VolumeDevice(device_path)
device.unmount_device(device_path)
device.format()
device.mount(mount_point)
LOG.debug('Mounted the volume.')
self.app.change_ownership(mount_point)
self.app.start_db()
if databases:
self.create_database(context, databases)
if users:
self.create_user(context, users)
self.update_status(context)
self.app.complete_install_or_restart()
LOG.info(_('Completed setup of DB2 database instance.'))
def restart(self, context):
"""
Restart this DB2 instance.
This method is called when the guest agent
gets a restart message from the taskmanager.
"""
LOG.debug("Restart a DB2 server instance.")
self.app.restart()
def stop_db(self, context, do_not_start_on_reboot=False):
"""
Stop this DB2 instance.
This method is called when the guest agent
gets a stop message from the taskmanager.
"""
LOG.debug("Stop a given DB2 server instance.")
self.app.stop_db(do_not_start_on_reboot=do_not_start_on_reboot)
def get_filesystem_stats(self, context, fs_path):
"""Gets the filesystem stats for the path given."""
LOG.debug("Get the filesystem stats.")
mount_point = CONF.get(
'db2' if not MANAGER else MANAGER).mount_point
return dbaas.get_filesystem_volume_stats(mount_point)
def create_database(self, context, databases):
LOG.debug("Creating database(s)." % databases)
self.admin.create_database(databases)
def delete_database(self, context, database):
LOG.debug("Deleting database %s." % database)
return self.admin.delete_database(database)
def list_databases(self, context, limit=None, marker=None,
include_marker=False):
LOG.debug("Listing all databases.")
return self.admin.list_databases(limit, marker, include_marker)
def create_user(self, context, users):
LOG.debug("Create user(s).")
self.admin.create_user(users)
def delete_user(self, context, user):
LOG.debug("Delete a user %s." % user)
self.admin.delete_user(user)
def get_user(self, context, username, hostname):
LOG.debug("Show details of user %s." % username)
return self.admin.get_user(username, hostname)
def list_users(self, context, limit=None, marker=None,
include_marker=False):
LOG.debug("List all users.")
return self.admin.list_users(limit, marker, include_marker)
def list_access(self, context, username, hostname):
LOG.debug("List all the databases the user has access to.")
return self.admin.list_access(username, hostname)
def mount_volume(self, context, device_path=None, mount_point=None):
device = volume.VolumeDevice(device_path)
device.mount(mount_point, write_to_fstab=False)
LOG.debug("Mounted the device %s at the mount point %s." %
(device_path, mount_point))
def unmount_volume(self, context, device_path=None, mount_point=None):
device = volume.VolumeDevice(device_path)
device.unmount(mount_point)
LOG.debug("Unmounted the device %s from the mount point %s." %
(device_path, mount_point))
def resize_fs(self, context, device_path=None, mount_point=None):
device = volume.VolumeDevice(device_path)
device.resize_fs(mount_point)
LOG.debug("Resized the filesystem %s." % mount_point)
def start_db_with_conf_changes(self, context, config_contents):
LOG.debug("Starting DB2 with configuration changes.")
self.app.start_db_with_conf_changes(config_contents)
def grant_access(self, context, username, hostname, databases):
LOG.debug("Granting acccess.")
raise exception.DatastoreOperationNotSupported(
operation='grant_access', datastore=MANAGER)
def revoke_access(self, context, username, hostname, database):
LOG.debug("Revoking access.")
raise exception.DatastoreOperationNotSupported(
operation='revoke_access', datastore=MANAGER)
def reset_configuration(self, context, configuration):
LOG.debug("Resetting DB2 configuration.")
raise exception.DatastoreOperationNotSupported(
operation='change_passwords', datastore=MANAGER)
def change_passwords(self, context, users):
LOG.debug("Changing password.")
raise exception.DatastoreOperationNotSupported(
operation='change_passwords', datastore=MANAGER)
def update_attributes(self, context, username, hostname, user_attrs):
LOG.debug("Updating database attributes.")
raise exception.DatastoreOperationNotSupported(
operation='update_attributes', datastore=MANAGER)
def enable_root(self, context):
LOG.debug("Enabling root.")
raise exception.DatastoreOperationNotSupported(
operation='enable_root', datastore=MANAGER)
def is_root_enabled(self, context):
LOG.debug("Checking if root is enabled.")
raise exception.DatastoreOperationNotSupported(
operation='is_root_enabled', datastore=MANAGER)
def _perform_restore(self, backup_info, context, restore_location, app):
raise exception.DatastoreOperationNotSupported(
operation='_perform_restore', datastore=MANAGER)
def create_backup(self, context, backup_info):
LOG.debug("Creating backup.")
raise exception.DatastoreOperationNotSupported(
operation='create_backup', datastore=MANAGER)
def get_config_changes(self, cluster_config, mount_point=None):
LOG.debug("Get configuration changes")
raise exception.DatastoreOperationNotSupported(
operation='get_configuration_changes', datastore=MANAGER)

View File

@ -0,0 +1,440 @@
# Copyright 2015 IBM Corp.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from trove.common import cfg
from trove.common import exception
from trove.common import instance as rd_instance
from trove.common import utils as utils
from trove.guestagent.datastore import service
from trove.guestagent.datastore.experimental.db2 import system
from trove.guestagent.db import models
from trove.openstack.common import log as logging
from trove.openstack.common.gettextutils import _
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
IGNORE_USERS_LIST = CONF.db2.ignore_users
class DB2App(object):
"""
Handles installation and configuration of DB2
on a Trove instance.
"""
def __init__(self, status, state_change_wait_time=None):
LOG.debug("Initialize DB2App.")
self.state_change_wait_time = (
state_change_wait_time if state_change_wait_time else
CONF.state_change_wait_time
)
LOG.debug("state_change_wait_time = %s." % self.state_change_wait_time)
self.status = status
def complete_install_or_restart(self):
self.status.end_install_or_restart()
def change_ownership(self, mount_point):
"""
When DB2 server instance is installed, it does not have the
DB2 local database directory created (/home/db2inst1/db2inst1).
This gets created when we mount the cinder volume. So we need
to change ownership of this directory to the DB2 instance user
- db2inst1.
"""
LOG.debug("Changing ownership of the DB2 data directory.")
try:
utils.execute_with_timeout(
system.CHANGE_DB_DIR_OWNER % {'datadir': mount_point},
shell=True)
utils.execute_with_timeout(
system.CHANGE_DB_DIR_GROUP_OWNER % {'datadir': mount_point},
shell=True)
except exception.ProcessExecutionError:
raise RuntimeError(_(
"Command to change ownership of DB2 data directory failed."))
def _enable_db_on_boot(self):
LOG.debug("Enable DB on boot.")
try:
run_command(system.ENABLE_AUTOSTART)
except exception.ProcessExecutionError:
raise RuntimeError(_(
"Command to enable DB2 server on boot failed."))
def _disable_db_on_boot(self):
LOG.debug("Disable DB2 on boot.")
try:
run_command(system.DISABLE_AUTOSTART)
except exception.ProcessExecutionError:
raise RuntimeError(_(
"Command to disable DB2 server on boot failed."))
def start_db_with_conf_changes(self, config_contents):
'''
Will not be implementing configuration change API for DB2 in
the Kilo release. Currently all that this method does is to start
the DB2 server without any configuration changes. Looks like
this needs to be implemented to enable volume resize on the guest
agent side.
'''
LOG.info(_("Starting DB2 with configuration changes."))
self.start_db(True)
def start_db(self, update_db=False):
LOG.debug("Start the DB2 server instance.")
self._enable_db_on_boot()
try:
run_command(system.START_DB2)
except exception.ProcessExecutionError:
pass
if not self.status.wait_for_real_status_to_change_to(
rd_instance.ServiceStatuses.RUNNING,
self.state_change_wait_time, update_db):
LOG.error(_("Start of DB2 server instance failed."))
self.status.end_install_or_restart()
raise RuntimeError(_("Could not start DB2."))
def stop_db(self, update_db=False, do_not_start_on_reboot=False):
LOG.debug("Stop the DB2 server instance.")
if do_not_start_on_reboot:
self._disable_db_on_boot()
try:
run_command(system.STOP_DB2)
except exception.ProcessExecutionError:
pass
if not (self.status.wait_for_real_status_to_change_to(
rd_instance.ServiceStatuses.SHUTDOWN,
self.state_change_wait_time, update_db)):
LOG.error(_("Could not stop DB2."))
self.status.end_install_or_restart()
raise RuntimeError(_("Could not stop DB2."))
def restart(self):
LOG.debug("Restarting DB2 server instance.")
try:
self.status.begin_restart()
self.stop_db()
self.start_db()
finally:
self.status.end_install_or_restart()
class DB2AppStatus(service.BaseDbStatus):
"""
Handles all of the status updating for the DB2 guest agent.
"""
def _get_actual_db_status(self):
LOG.debug("Getting the status of the DB2 server instance.")
try:
out, err = utils.execute_with_timeout(
system.DB2_STATUS, shell=True)
if "0" not in out:
return rd_instance.ServiceStatuses.RUNNING
else:
return rd_instance.ServiceStatuses.SHUTDOWN
except exception.ProcessExecutionError:
LOG.exception(_("Error getting the DB2 server status."))
return rd_instance.ServiceStatuses.CRASHED
def run_command(command, superuser=system.DB2_INSTANCE_OWNER,
timeout=system.TIMEOUT):
return utils.execute_with_timeout("sudo", "su", "-", superuser, "-c",
command, timeout=timeout)
class DB2Admin(object):
"""
Handles administrative tasks on the DB2 instance.
"""
def create_database(self, databases):
"""Create the given database(s)."""
dbName = None
db_create_failed = []
LOG.debug("Creating DB2 databases.")
for item in databases:
mydb = models.ValidatedMySQLDatabase()
mydb.deserialize(item)
dbName = mydb.name
LOG.debug("Creating DB2 database: %s." % dbName)
try:
run_command(system.CREATE_DB_COMMAND % {'dbname': dbName})
except exception.ProcessExecutionError:
LOG.exception(_(
"There was an error creating database: %s.") % dbName)
db_create_failed.append(dbName)
pass
if len(db_create_failed) > 0:
LOG.exception(_("Creating the following databases failed: %s.") %
db_create_failed)
def delete_database(self, database):
"""Delete the specified database."""
dbName = None
try:
mydb = models.ValidatedMySQLDatabase()
mydb.deserialize(database)
dbName = mydb.name
LOG.debug("Deleting DB2 database: %s." % dbName)
run_command(system.DELETE_DB_COMMAND % {'dbname': dbName})
except exception.ProcessExecutionError:
LOG.exception(_(
"There was an error while deleting database:%s.") % dbName)
raise exception.GuestError(_("Unable to delete database: %s.") %
dbName)
def list_databases(self, limit=None, marker=None, include_marker=False):
LOG.debug("Listing all the DB2 databases.")
databases = []
next_marker = None
try:
out, err = run_command(system.LIST_DB_COMMAND)
dblist = out.split()
result = iter(dblist)
count = 0
if marker is not None:
try:
item = result.next()
while item != marker:
item = result.next()
if item == marker:
marker = None
except StopIteration:
pass
try:
item = result.next()
while item:
count = count + 1
if (limit and count <= limit) or limit is None:
db2_db = models.MySQLDatabase()
db2_db.name = item
LOG.debug("database = %s ." % item)
db2_db.character_set = None
db2_db.collate = None
next_marker = db2_db.name
databases.append(db2_db.serialize())
item = result.next()
else:
next_marker = None
break
except StopIteration:
next_marker = None
LOG.debug("databases = %s." % str(databases))
except exception.ProcessExecutionError as pe:
LOG.exception(_("An error occured listing databases: %s.") %
pe.message)
pass
return databases, next_marker
def create_user(self, users):
LOG.debug("Creating user(s) for accessing DB2 database(s).")
try:
for item in users:
user = models.MySQLUser()
user.deserialize(item)
try:
LOG.debug("Creating OS user: %s." % user.name)
utils.execute_with_timeout(
system.CREATE_USER_COMMAND % {
'login': user.name, 'login': user.name,
'passwd': user.password}, shell=True)
except exception.ProcessExecutionError as pe:
LOG.exception(_("Error creating user: %s.") % user.name)
continue
for database in user.databases:
mydb = models.ValidatedMySQLDatabase()
mydb.deserialize(database)
try:
LOG.debug("Granting user: %s access to database: %s."
% (user.name, mydb.name))
run_command(system.GRANT_USER_ACCESS % {
'dbname': mydb.name, 'login': user.name})
except exception.ProcessExecutionError as pe:
LOG.debug(
"Error granting user: %s access to database: %s."
% (user.name, mydb.name))
LOG.debug(pe)
pass
except exception.ProcessExecutionError as pe:
LOG.exception(_("An error occured creating users: %s.") %
pe.message)
pass
def delete_user(self, user):
LOG.debug("Delete a given user.")
db2_user = models.MySQLUser()
db2_user.deserialize(user)
userName = db2_user.name
user_dbs = db2_user.databases
LOG.debug("For user %s, databases to be deleted = %r." % (
userName, user_dbs))
if len(user_dbs) == 0:
databases = self.list_access(db2_user.name, None)
else:
databases = user_dbs
LOG.debug("databases for user = %r." % databases)
for database in databases:
mydb = models.ValidatedMySQLDatabase()
mydb.deserialize(database)
try:
run_command(system.REVOKE_USER_ACCESS % {
'dbname': mydb.name,
'login': userName})
LOG.debug("Revoked access for user:%s on database:%s." % (
userName, mydb.name))
except exception.ProcessExecutionError as pe:
LOG.debug("Error occured while revoking access to %s." %
mydb.name)
pass
try:
utils.execute_with_timeout(system.DELETE_USER_COMMAND % {
'login': db2_user.name.lower()}, shell=True)
except exception.ProcessExecutionError as pe:
LOG.exception(_(
"There was an error while deleting user: %s.") % pe)
raise exception.GuestError(_("Unable to delete user: %s.") %
userName)
def list_users(self, limit=None, marker=None, include_marker=False):
LOG.debug(
"List all users for all the databases in a DB2 server instance.")
users = []
user_map = {}
next_marker = None
count = 0
databases, marker = self.list_databases()
for database in databases:
db2_db = models.MySQLDatabase()
db2_db.deserialize(database)
out = None
try:
out, err = run_command(
system.LIST_DB_USERS % {'dbname': db2_db.name})
except exception.ProcessExecutionError:
LOG.debug(
"There was an error while listing users for database: %s."
% db2_db.name)
continue
userlist = []
for item in out.split('\n'):
LOG.debug("item = %r" % item)
user = item.split() if item != "" else None
LOG.debug("user = %r" % (user))
if user is not None and user[0] not in IGNORE_USERS_LIST \
and user[1] == 'Y':
userlist.append(user[0])
result = iter(userlist)
if marker is not None:
try:
item = result.next()
while item != marker:
item = result.next()
if item == marker:
marker = None
except StopIteration:
pass
try:
item = result.next()
db2db = models.MySQLDatabase()
db2db.name = db2_db.name
while item:
'''
Check if the user has already been discovered. If so,
add this database to the database list for this user.
'''
if item in user_map:
db2user = user_map.get(item)
db2user.databases.append(db2db.serialize())
item = result.next()
continue
'''
If this user was not previously discovered, then add
this to the user's list.
'''
count = count + 1
if (limit and count <= limit) or limit is None:
db2_user = models.MySQLUser()
db2_user.name = item
db2_user.databases.append(db2db.serialize())
users.append(db2_user.serialize())
user_map.update({item: db2_user})
item = result.next()
else:
next_marker = None
break
except StopIteration:
next_marker = None
if count == limit:
break
return users, next_marker
def get_user(self, username, hostname):
LOG.debug("Get details of a given database user.")
user = self._get_user(username, hostname)
if not user:
return None
return user.serialize()
def _get_user(self, username, hostname):
LOG.debug("Get details of a given database user %s." % username)
user = models.MySQLUser()
user.name = username
databases, marker = self.list_databases()
out = None
for database in databases:
db2_db = models.MySQLDatabase()
db2_db.deserialize(database)
try:
out, err = run_command(
system.LIST_DB_USERS % {'dbname': db2_db.name})
except exception.ProcessExecutionError:
LOG.debug(
"Error while trying to get the users for database: %s." %
db2_db.name)
continue
for item in out.split('\n'):
user_access = item.split() if item != "" else None
if (user_access is not None and
user_access[0].lower() == username.lower() and
user_access[1] == 'Y'):
user.databases = db2_db.name
break
return user
def list_access(self, username, hostname):
"""
Show all the databases to which the user has more than
USAGE granted.
"""
LOG.debug("Listing databases that user: %s has access to." % username)
user = self._get_user(username, hostname)
return user.databases

View File

@ -0,0 +1,49 @@
# Copyright 2015 IBM Corp.
# 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.
TIMEOUT = 1200
DB2_INSTANCE_OWNER = "db2inst1"
ENABLE_AUTOSTART = (
"/opt/ibm/db2/V10.5/instance/db2iauto -on " + DB2_INSTANCE_OWNER)
DISABLE_AUTOSTART = (
"/opt/ibm/db2/V10.5/instance/db2iauto -off " + DB2_INSTANCE_OWNER)
START_DB2 = "db2start"
STOP_DB2 = "db2 force application all; db2 terminate; db2stop"
DB2_STATUS = ("ps -ef | grep " + DB2_INSTANCE_OWNER + " | grep db2sysc |"
"grep -v grep | wc -l")
CHANGE_DB_DIR_OWNER = "sudo chown " + DB2_INSTANCE_OWNER + " %(datadir)s"
CHANGE_DB_DIR_GROUP_OWNER = (
"sudo chgrp " + DB2_INSTANCE_OWNER + " %(datadir)s")
CREATE_DB_COMMAND = "db2 create database %(dbname)s"
DELETE_DB_COMMAND = "db2 drop database %(dbname)s"
LIST_DB_COMMAND = (
"db2 list database directory | grep -B6 -i indirect | "
"grep 'Database name' | sed 's/.*= //'")
CREATE_USER_COMMAND = (
'sudo useradd -m -d /home/%(login)s %(login)s;'
'sudo echo %(login)s:%(passwd)s |sudo chpasswd')
GRANT_USER_ACCESS = (
"db2 connect to %(dbname)s; "
"db2 GRANT DBADM,CREATETAB,BINDADD,CONNECT,DATAACCESS "
"ON DATABASE TO USER %(login)s; db2 connect reset")
DELETE_USER_COMMAND = 'sudo userdel -r %(login)s'
REVOKE_USER_ACCESS = (
"db2 connect to %(dbname)s; "
"db2 REVOKE DBADM,CREATETAB,BINDADD,CONNECT,DATAACCESS "
"ON DATABASE FROM USER %(login)s; db2 connect reset")
LIST_DB_USERS = (
"db2 +o connect to %(dbname)s; "
"db2 -x select grantee, dataaccessauth from sysibm.sysdbauth; "
"db2 connect reset")

View File

@ -51,6 +51,8 @@ defaults = {
'trove.guestagent.datastore.experimental.couchdb.manager.Manager',
'vertica':
'trove.guestagent.datastore.experimental.vertica.manager.Manager',
'db2':
'trove.guestagent.datastore.experimental.db2.manager.Manager',
}
CONF = cfg.CONF

View File

View File

@ -0,0 +1,201 @@
# Copyright 2015 IBM Corp.
#
# 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 testtools
from mock import MagicMock
from testtools.matchers import Is, Equals, Not
from trove.common.context import TroveContext
from trove.common.instance import ServiceStatuses
from trove.guestagent import volume
from trove.guestagent.datastore.experimental.db2 import (
service as db2_service)
from trove.guestagent.datastore.experimental.db2 import (
manager as db2_manager)
from trove.guestagent import pkg as pkg
class GuestAgentDB2ManagerTest(testtools.TestCase):
def setUp(self):
super(GuestAgentDB2ManagerTest, self).setUp()
self.real_status = db2_service.DB2AppStatus.set_status
class FakeInstanceServiceStatus(object):
status = ServiceStatuses.NEW
def save(self):
pass
db2_service.DB2AppStatus.set_status = MagicMock(
return_value=FakeInstanceServiceStatus())
self.context = TroveContext()
self.manager = db2_manager.Manager()
self.real_db_app_status = db2_service.DB2AppStatus
self.origin_format = volume.VolumeDevice.format
self.origin_mount = volume.VolumeDevice.mount
self.origin_mount_points = volume.VolumeDevice.mount_points
self.origin_stop_db = db2_service.DB2App.stop_db
self.origin_start_db = db2_service.DB2App.start_db
self.orig_change_ownership = (db2_service.DB2App.change_ownership)
self.orig_create_databases = db2_service.DB2Admin.create_database
self.orig_list_databases = db2_service.DB2Admin.list_databases
self.orig_delete_database = db2_service.DB2Admin.delete_database
self.orig_create_users = db2_service.DB2Admin.create_user
self.orig_list_users = db2_service.DB2Admin.list_users
self.orig_delete_user = db2_service.DB2Admin.delete_user
def tearDown(self):
super(GuestAgentDB2ManagerTest, self).tearDown()
db2_service.DB2AppStatus.set_status = self.real_db_app_status
volume.VolumeDevice.format = self.origin_format
volume.VolumeDevice.mount = self.origin_mount
volume.VolumeDevice.mount_points = self.origin_mount_points
db2_service.DB2App.stop_db = self.origin_stop_db
db2_service.DB2App.start_db = self.origin_start_db
db2_service.DB2App.change_ownership = self.orig_change_ownership
db2_service.DB2Admin.create_database = self.orig_create_databases
db2_service.DB2Admin.create_user = self.orig_create_users
db2_service.DB2Admin.create_database = self.orig_create_databases
db2_service.DB2Admin.list_databases = self.orig_list_databases
db2_service.DB2Admin.delete_database = self.orig_delete_database
db2_service.DB2Admin.create_user = self.orig_create_users
db2_service.DB2Admin.list_users = self.orig_list_users
db2_service.DB2Admin.delete_user = self.orig_delete_user
def test_update_status(self):
mock_status = MagicMock()
self.manager.appStatus = mock_status
self.manager.update_status(self.context)
mock_status.update.assert_any_call()
def test_prepare_device_path_true(self):
self._prepare_dynamic()
def test_prepare_device_path_false(self):
self._prepare_dynamic(device_path=None)
def test_prepare_database(self):
self._prepare_dynamic(databases=['db1'])
def _prepare_dynamic(self, packages=None, databases=None, users=None,
config_content=None, device_path='/dev/vdb',
is_db_installed=True, backup_id=None, overrides=None):
mock_status = MagicMock()
mock_app = MagicMock()
self.manager.appStatus = mock_status
self.manager.app = mock_app
mock_status.begin_install = MagicMock(return_value=None)
pkg.Package.pkg_is_installed = MagicMock(return_value=is_db_installed)
mock_app.change_ownership = MagicMock(return_value=None)
mock_app.restart = MagicMock(return_value=None)
mock_app.start_db = MagicMock(return_value=None)
mock_app.stop_db = MagicMock(return_value=None)
volume.VolumeDevice.format = MagicMock(return_value=None)
volume.VolumeDevice.mount = MagicMock(return_value=None)
volume.VolumeDevice.mount_points = MagicMock(return_value=[])
db2_service.DB2Admin.create_user = MagicMock(return_value=None)
db2_service.DB2Admin.create_database = MagicMock(return_value=None)
self.manager.prepare(context=self.context, packages=packages,
config_contents=config_content,
databases=databases,
memory_mb='2048', users=users,
device_path=device_path,
mount_point="/home/db2inst1/db2inst1",
backup_info=None,
overrides=None,
cluster_config=None)
mock_status.begin_install.assert_any_call()
self.assertEqual(mock_app.change_ownership.call_count, 1)
if databases:
self.assertTrue(db2_service.DB2Admin.create_database.called)
else:
self.assertFalse(db2_service.DB2Admin.create_database.called)
if users:
self.assertTrue(db2_service.DB2Admin.create_user.called)
else:
self.assertFalse(db2_service.DB2Admin.create_user.called)
def test_restart(self):
mock_status = MagicMock()
self.manager.appStatus = mock_status
db2_service.DB2App.restart = MagicMock(return_value=None)
self.manager.restart(self.context)
db2_service.DB2App.restart.assert_any_call()
def test_stop_db(self):
mock_status = MagicMock()
self.manager.appStatus = mock_status
db2_service.DB2App.stop_db = MagicMock(return_value=None)
self.manager.stop_db(self.context)
db2_service.DB2App.stop_db.assert_any_call(
do_not_start_on_reboot=False)
def test_create_database(self):
mock_status = MagicMock()
self.manager.appStatus = mock_status
db2_service.DB2Admin.create_database = MagicMock(return_value=None)
self.manager.create_database(self.context, ['db1'])
db2_service.DB2Admin.create_database.assert_any_call(['db1'])
def test_create_user(self):
mock_status = MagicMock()
self.manager.appStatus = mock_status
db2_service.DB2Admin.create_user = MagicMock(return_value=None)
self.manager.create_user(self.context, ['user1'])
db2_service.DB2Admin.create_user.assert_any_call(['user1'])
def test_delete_database(self):
databases = ['db1']
mock_status = MagicMock()
self.manager.appStatus = mock_status
db2_service.DB2Admin.delete_database = MagicMock(return_value=None)
self.manager.delete_database(self.context, databases)
db2_service.DB2Admin.delete_database.assert_any_call(databases)
def test_delete_user(self):
user = ['user1']
mock_status = MagicMock()
self.manager.appStatus = mock_status
db2_service.DB2Admin.delete_user = MagicMock(return_value=None)
self.manager.delete_user(self.context, user)
db2_service.DB2Admin.delete_user.assert_any_call(user)
def test_list_databases(self):
mock_status = MagicMock()
self.manager.appStatus = mock_status
db2_service.DB2Admin.list_databases = MagicMock(
return_value=['database1'])
databases = self.manager.list_databases(self.context)
self.assertThat(databases, Not(Is(None)))
self.assertThat(databases, Equals(['database1']))
db2_service.DB2Admin.list_databases.assert_any_call(None, None, False)
def test_list_users(self):
db2_service.DB2Admin.list_users = MagicMock(return_value=['user1'])
users = self.manager.list_users(self.context)
self.assertThat(users, Equals(['user1']))
db2_service.DB2Admin.list_users.assert_any_call(None, None, False)
def test_get_users(self):
username = ['user1']
hostname = ['host']
mock_status = MagicMock()
self.manager.appStatus = mock_status
db2_service.DB2Admin.get_user = MagicMock(return_value=['user1'])
users = self.manager.get_user(self.context, username, hostname)
self.assertThat(users, Equals(['user1']))
db2_service.DB2Admin.get_user.assert_any_call(username, hostname)

View File

@ -27,6 +27,7 @@ from testtools.matchers import Is
from testtools.matchers import Equals
from testtools.matchers import Not
from trove.common.exception import ProcessExecutionError
from trove.common.exception import GuestError
from trove.common import utils
from trove.common import instance as rd_instance
from trove.conductor import api as conductor_api
@ -62,6 +63,8 @@ from trove.guestagent.datastore.experimental.vertica.service import (
VerticaAppStatus)
from trove.guestagent.datastore.experimental.vertica import (
system as vertica_system)
from trove.guestagent.datastore.experimental.db2 import (
service as db2service)
from trove.guestagent.db import models
from trove.guestagent.volume import VolumeDevice
from trove.instance.models import InstanceServiceStatus
@ -79,7 +82,6 @@ FAKE_DB_2 = {"_name": "testDB2", "_character_set": "latin2",
FAKE_USER = [{"_name": "random", "_password": "guesswhat",
"_databases": [FAKE_DB]}]
conductor_api.API.get_client = Mock()
conductor_api.API.heartbeat = Mock()
@ -1005,6 +1007,9 @@ class ServiceRegistryTest(testtools.TestCase):
self.assertEqual(test_dict.get('couchdb'),
'trove.guestagent.datastore.experimental.couchdb.'
'manager.Manager')
self.assertEqual('trove.guestagent.datastore.experimental.db2.'
'manager.Manager',
test_dict.get('db2'))
def test_datastore_registry_with_existing_manager(self):
datastore_registry_ext_test = {
@ -1038,6 +1043,9 @@ class ServiceRegistryTest(testtools.TestCase):
self.assertEqual('trove.guestagent.datastore.experimental.vertica.'
'manager.Manager',
test_dict.get('vertica'))
self.assertEqual('trove.guestagent.datastore.experimental.db2.'
'manager.Manager',
test_dict.get('db2'))
def test_datastore_registry_with_blank_dict(self):
datastore_registry_ext_test = dict()
@ -1068,6 +1076,9 @@ class ServiceRegistryTest(testtools.TestCase):
self.assertEqual('trove.guestagent.datastore.experimental.vertica.'
'manager.Manager',
test_dict.get('vertica'))
self.assertEqual('trove.guestagent.datastore.experimental.db2.'
'manager.Manager',
test_dict.get('db2'))
class KeepAliveConnectionTest(testtools.TestCase):
@ -2314,3 +2325,189 @@ class VerticaAppTest(testtools.TestCase):
# Verifying nu,ber of shell calls,
# as command has already been tested in preceeding tests
self.assertEqual(vertica_system.shell_execute.call_count, 5)
class DB2AppTest(testtools.TestCase):
def setUp(self):
super(DB2AppTest, self).setUp()
self.orig_utils_execute_with_timeout = (
db2service.utils.execute_with_timeout)
util.init_db()
self.FAKE_ID = str(uuid4())
InstanceServiceStatus.create(instance_id=self.FAKE_ID,
status=rd_instance.ServiceStatuses.NEW)
self.appStatus = FakeAppStatus(self.FAKE_ID,
rd_instance.ServiceStatuses.NEW)
self.db2App = db2service.DB2App(self.appStatus)
dbaas.CONF.guest_id = self.FAKE_ID
def tearDown(self):
super(DB2AppTest, self).tearDown()
db2service.utils.execute_with_timeout = (
self.orig_utils_execute_with_timeout)
InstanceServiceStatus.find_by(instance_id=self.FAKE_ID).delete()
dbaas.CONF.guest_id = None
self.db2App = None
def assert_reported_status(self, expected_status):
service_status = InstanceServiceStatus.find_by(
instance_id=self.FAKE_ID)
self.assertEqual(expected_status, service_status.status)
def test_stop_db(self):
db2service.utils.execute_with_timeout = MagicMock(return_value=None)
self.appStatus.set_next_status(rd_instance.ServiceStatuses.SHUTDOWN)
self.db2App.stop_db()
self.assert_reported_status(rd_instance.ServiceStatuses.NEW)
def test_restart_server(self):
self.appStatus.set_next_status(rd_instance.ServiceStatuses.RUNNING)
mock_status = MagicMock(return_value=None)
app = db2service.DB2App(mock_status)
mock_status.begin_restart = MagicMock(return_value=None)
app.stop_db = MagicMock(return_value=None)
app.start_db = MagicMock(return_value=None)
app.restart()
self.assertTrue(mock_status.begin_restart.called)
self.assertTrue(app.stop_db.called)
self.assertTrue(app.start_db.called)
def test_start_db(self):
db2service.utils.execute_with_timeout = MagicMock(return_value=None)
self.appStatus.set_next_status(rd_instance.ServiceStatuses.RUNNING)
with patch.object(self.db2App, '_enable_db_on_boot',
return_value=None):
self.db2App.start_db()
self.assert_reported_status(rd_instance.ServiceStatuses.NEW)
class DB2AdminTest(testtools.TestCase):
def setUp(self):
super(DB2AdminTest, self).setUp()
self.db2Admin = db2service.DB2Admin()
self.orig_utils_execute_with_timeout = (
db2service.utils.execute_with_timeout)
def tearDown(self):
super(DB2AdminTest, self).tearDown()
db2service.utils.execute_with_timeout = (
self.orig_utils_execute_with_timeout)
def test_delete_database(self):
with patch.object(
db2service, 'run_command',
MagicMock(
return_value=None,
side_effect=ProcessExecutionError('Error'))):
self.assertRaises(GuestError,
self.db2Admin.delete_database,
FAKE_DB)
self.assertTrue(db2service.run_command.called)
args, _ = db2service.run_command.call_args_list[0]
expected = "db2 drop database testDB"
self.assertEqual(args[0], expected,
"Delete database queries are not the same")
def test_list_databases(self):
with patch.object(db2service, 'run_command', MagicMock(
side_effect=ProcessExecutionError('Error'))):
self.db2Admin.list_databases()
self.assertTrue(db2service.run_command.called)
args, _ = db2service.run_command.call_args_list[0]
expected = "db2 list database directory " \
"| grep -B6 -i indirect | grep 'Database name' | " \
"sed 's/.*= //'"
self.assertEqual(args[0], expected,
"Delete database queries are not the same")
def test_create_users(self):
with patch.object(db2service, 'run_command', MagicMock(
return_value=None)):
db2service.utils.execute_with_timeout = MagicMock(
return_value=None)
self.db2Admin.create_user(FAKE_USER)
self.assertTrue(db2service.utils.execute_with_timeout.called)
self.assertTrue(db2service.run_command.called)
args, _ = db2service.run_command.call_args_list[0]
expected = "db2 connect to testDB; " \
"db2 GRANT DBADM,CREATETAB,BINDADD,CONNECT,DATAACCESS " \
"ON DATABASE TO USER random; db2 connect reset"
self.assertEqual(
args[0], expected,
"Granting database access queries are not the same")
self.assertEqual(db2service.run_command.call_count, 1)
def test_delete_users_with_db(self):
with patch.object(db2service, 'run_command',
MagicMock(return_value=None)):
with patch.object(db2service.DB2Admin, 'list_access',
MagicMock(return_value=None)):
utils.execute_with_timeout = MagicMock(return_value=None)
self.db2Admin.delete_user(FAKE_USER[0])
self.assertTrue(db2service.run_command.called)
self.assertTrue(db2service.utils.execute_with_timeout.called)
self.assertFalse(db2service.DB2Admin.list_access.called)
args, _ = db2service.run_command.call_args_list[0]
expected = "db2 connect to testDB; " \
"db2 REVOKE DBADM,CREATETAB,BINDADD,CONNECT,DATAACCESS " \
"ON DATABASE FROM USER random; db2 connect reset"
self.assertEqual(
args[0], expected,
"Revoke database access queries are not the same")
self.assertEqual(db2service.run_command.call_count, 1)
def test_delete_users_without_db(self):
FAKE_USER.append(
{"_name": "random2", "_password": "guesswhat", "_databases": []})
with patch.object(db2service, 'run_command',
MagicMock(return_value=None)):
with patch.object(db2service.DB2Admin, 'list_access',
MagicMock(return_value=[FAKE_DB])):
utils.execute_with_timeout = MagicMock(return_value=None)
self.db2Admin.delete_user(FAKE_USER[1])
self.assertTrue(db2service.run_command.called)
self.assertTrue(db2service.DB2Admin.list_access.called)
self.assertTrue(
db2service.utils.execute_with_timeout.called)
args, _ = db2service.run_command.call_args_list[0]
expected = "db2 connect to testDB; " \
"db2 REVOKE DBADM,CREATETAB,BINDADD,CONNECT," \
"DATAACCESS ON DATABASE FROM USER random2; " \
"db2 connect reset"
self.assertEqual(
args[0], expected,
"Revoke database access queries are not the same")
self.assertEqual(db2service.run_command.call_count, 1)
def test_list_users(self):
databases = []
databases.append(FAKE_DB)
with patch.object(db2service, 'run_command', MagicMock(
side_effect=ProcessExecutionError('Error'))):
with patch.object(self.db2Admin, "list_databases",
MagicMock(return_value=(databases, None))):
self.db2Admin.list_users()
self.assertTrue(db2service.run_command.called)
args, _ = db2service.run_command.call_args_list[0]
expected = "db2 +o connect to testDB; " \
"db2 -x select grantee, dataaccessauth " \
"from sysibm.sysdbauth; db2 connect reset"
self.assertEqual(args[0], expected,
"List database queries are not the same")
def test_get_user(self):
databases = []
databases.append(FAKE_DB)
with patch.object(db2service, 'run_command', MagicMock(
side_effect=ProcessExecutionError('Error'))):
with patch.object(self.db2Admin, "list_databases",
MagicMock(return_value=(databases, None))):
self.db2Admin._get_user('random', None)
self.assertTrue(db2service.run_command.called)
args, _ = db2service.run_command.call_args_list[0]
expected = "db2 +o connect to testDB; " \
"db2 -x select grantee, dataaccessauth " \
"from sysibm.sysdbauth; db2 connect reset"
self.assertEqual(args[0], expected,
"Delete database queries are not the same")