summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormariamj@us.ibm.com <mariamj@us.ibm.com>2015-03-13 14:10:03 -0400
committermariamj@us.ibm.com <mariamj@us.ibm.com>2015-03-26 20:22:24 -0400
commit77c3ed6e624ce95252c6c41d069cb820da6ea988 (patch)
tree4ff7e9aeacda5b186f39bdfbd86153bc04152fc1
parent4e1fdd746e64759ccb96a1abfc2a8837c280f38e (diff)
downloadtrove-77c3ed6e624ce95252c6c41d069cb820da6ea988.tar.gz
Add support for DB2 datastore in Trove
This feature will provide support for a new SQL datastore in Trove. Users will be able to provision a DB2 server instance and perform various functions as listed below. The risk of regression is very minimal since there is no code changes to the base Trove code. The guest agent for DB2 uses the existing API's supported by Trove to provide the functionalities mentioned below. The following features have been implemented in this patchset: - Launch - Reboot - Terminate - Resize Volume/Instance - Create/Delete/List database(s) - Create/Delete/List user(s) - List Access - Included unit tests for the different methods DB2 Express-C can be installed on a server that satisfy the memory requirements of 512MB to 4GB of RAM. Another point to note is how DB2 creates and deletes users. There isn't a notion of a DB2 user in DB2 unlike in the case of MySQL. When we create a user for DB2, the process involves first creating an OS user and then granting privileges to this user for the given database(s). Currently all the DB2 queries are run from command line. One of the enhancements that can be made is to use the DB2 SQLAlchemy driver to run these queries. This requires the guest agent to know about the username and password to connect to. We can use the 'db2inst1' user (which is the instance user for the DB2 server instance created) but since DB2 does not track the passwords, there isn't an easy way to get the password unless we stored it in a file or use a similar mechanism. Since I am not sure how safe that is, I decided to go with running the commands from command line. Change-Id: If95686dd37d2730a4d00f4010fa8515f33a8e337 Implements: blueprint db2-plugin-for-trove
-rw-r--r--trove/common/cfg.py46
-rw-r--r--trove/guestagent/datastore/experimental/db2/__init__.py0
-rw-r--r--trove/guestagent/datastore/experimental/db2/manager.py205
-rw-r--r--trove/guestagent/datastore/experimental/db2/service.py440
-rw-r--r--trove/guestagent/datastore/experimental/db2/system.py49
-rw-r--r--trove/guestagent/dbaas.py2
-rw-r--r--trove/templates/db2/config.template0
-rw-r--r--trove/templates/db2/override.config.template0
-rw-r--r--trove/tests/unittests/guestagent/test_db2_manager.py201
-rw-r--r--trove/tests/unittests/guestagent/test_dbaas.py199
10 files changed, 1140 insertions, 2 deletions
diff --git a/trove/common/cfg.py b/trove/common/cfg.py
index cbe3616f..ce5185b3 100644
--- a/trove/common/cfg.py
+++ b/trove/common/cfg.py
@@ -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)
diff --git a/trove/guestagent/datastore/experimental/db2/__init__.py b/trove/guestagent/datastore/experimental/db2/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/trove/guestagent/datastore/experimental/db2/__init__.py
diff --git a/trove/guestagent/datastore/experimental/db2/manager.py b/trove/guestagent/datastore/experimental/db2/manager.py
new file mode 100644
index 00000000..f2c17bf9
--- /dev/null
+++ b/trove/guestagent/datastore/experimental/db2/manager.py
@@ -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)
diff --git a/trove/guestagent/datastore/experimental/db2/service.py b/trove/guestagent/datastore/experimental/db2/service.py
new file mode 100644
index 00000000..427118a8
--- /dev/null
+++ b/trove/guestagent/datastore/experimental/db2/service.py
@@ -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
diff --git a/trove/guestagent/datastore/experimental/db2/system.py b/trove/guestagent/datastore/experimental/db2/system.py
new file mode 100644
index 00000000..196c801f
--- /dev/null
+++ b/trove/guestagent/datastore/experimental/db2/system.py
@@ -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")
diff --git a/trove/guestagent/dbaas.py b/trove/guestagent/dbaas.py
index a04154a7..4d68a3a6 100644
--- a/trove/guestagent/dbaas.py
+++ b/trove/guestagent/dbaas.py
@@ -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
diff --git a/trove/templates/db2/config.template b/trove/templates/db2/config.template
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/trove/templates/db2/config.template
diff --git a/trove/templates/db2/override.config.template b/trove/templates/db2/override.config.template
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/trove/templates/db2/override.config.template
diff --git a/trove/tests/unittests/guestagent/test_db2_manager.py b/trove/tests/unittests/guestagent/test_db2_manager.py
new file mode 100644
index 00000000..f1d8dac4
--- /dev/null
+++ b/trove/tests/unittests/guestagent/test_db2_manager.py
@@ -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)
diff --git a/trove/tests/unittests/guestagent/test_dbaas.py b/trove/tests/unittests/guestagent/test_dbaas.py
index b528a77d..fa1e76e9 100644
--- a/trove/tests/unittests/guestagent/test_dbaas.py
+++ b/trove/tests/unittests/guestagent/test_dbaas.py
@@ -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")