summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2014-03-05 02:02:19 +0000
committerGerrit Code Review <review@openstack.org>2014-03-05 02:02:19 +0000
commitfba8cabea326527bacdeca56760a97e14cdcc18f (patch)
tree93fdfed7e5b50d6c3f5fbe8ee2bee9c8372d6a1b
parent3c94c46d0f48d8bdf7a56276f1cf9cc16644ba31 (diff)
parentbaecc0465bcb60cfebdd71aaea5bd95919109d05 (diff)
downloadtrove-2014.1.b3.tar.gz
Merge "Initial support for single instance MongoDB support"2014.1.b3
-rw-r--r--trove/common/cfg.py31
-rw-r--r--trove/guestagent/common/operating_system.py1
-rw-r--r--trove/guestagent/datastore/mongodb/__init__.py0
-rw-r--r--trove/guestagent/datastore/mongodb/manager.py164
-rw-r--r--trove/guestagent/datastore/mongodb/service.py229
-rw-r--r--trove/guestagent/datastore/mongodb/system.py30
-rw-r--r--trove/guestagent/dbaas.py1
-rw-r--r--trove/templates/mongodb/config.template95
-rw-r--r--trove/templates/mongodb/heat.template93
-rw-r--r--trove/templates/mongodb/override.config.template0
-rw-r--r--trove/tests/unittests/common/test_template.py13
-rw-r--r--trove/tests/unittests/guestagent/test_dbaas.py159
-rw-r--r--trove/tests/unittests/guestagent/test_mongodb_manager.py95
13 files changed, 901 insertions, 10 deletions
diff --git a/trove/common/cfg.py b/trove/common/cfg.py
index 7ae2a6ac..e9a5831d 100644
--- a/trove/common/cfg.py
+++ b/trove/common/cfg.py
@@ -219,7 +219,8 @@ common_opts = [
default={'mysql': '2f3ff068-2bfb-4f70-9a9d-a6bb65bc084b',
'redis': 'b216ffc5-1947-456c-a4cf-70f94c05f7d0',
'cassandra': '459a230d-4e97-4344-9067-2a54a310b0ed',
- 'couchbase': 'fa62fe68-74d9-4779-a24e-36f19602c415'},
+ 'couchbase': 'fa62fe68-74d9-4779-a24e-36f19602c415',
+ 'mongodb': 'c8c907af-7375-456f-b929-b637ff9209ee'},
help='Unique ID to tag notification events.'),
cfg.StrOpt('nova_proxy_admin_user', default='',
help="Admin username used to connect to nova.", secret=True),
@@ -259,6 +260,12 @@ common_opts = [
'large tokens (typically those generated by the '
'Keystone v3 API with big service catalogs'),
]
+
+CONF = cfg.CONF
+
+CONF.register_opts(path_opts)
+CONF.register_opts(common_opts)
+
# Datastore specific option groups
# Mysql
@@ -363,6 +370,26 @@ couchbase_opts = [
"volumes if volume support is enabled"),
]
+# MongoDB
+mongodb_group = cfg.OptGroup(
+ 'mongodb', title='MongoDB options',
+ help="Oslo option group designed for MongoDB datastore")
+mongodb_opts = [
+ cfg.ListOpt('tcp_ports', default=["2500", "27017"],
+ 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 UPD ports and/or port ranges to open'
+ ' in the security group (only applicable '
+ 'if trove_security_groups_support is True)'),
+ cfg.StrOpt('backup_strategy', default=None,
+ help='Default strategy to perform backups.'),
+ cfg.StrOpt('mount_point', default='/var/lib/mongodb',
+ help="Filesystem path for mounting "
+ "volumes if volume support is enabled"),
+]
+
CONF = cfg.CONF
CONF.register_opts(path_opts)
@@ -373,12 +400,14 @@ CONF.register_group(percona_group)
CONF.register_group(redis_group)
CONF.register_group(cassandra_group)
CONF.register_group(couchbase_group)
+CONF.register_group(mongodb_group)
CONF.register_opts(mysql_opts, mysql_group)
CONF.register_opts(percona_opts, percona_group)
CONF.register_opts(redis_opts, redis_group)
CONF.register_opts(cassandra_opts, cassandra_group)
CONF.register_opts(couchbase_opts, couchbase_group)
+CONF.register_opts(mongodb_opts, mongodb_group)
def custom_parser(parsername, parser):
diff --git a/trove/guestagent/common/operating_system.py b/trove/guestagent/common/operating_system.py
index a24f8593..3e5374b2 100644
--- a/trove/guestagent/common/operating_system.py
+++ b/trove/guestagent/common/operating_system.py
@@ -12,6 +12,7 @@
# 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
import fcntl
import struct
diff --git a/trove/guestagent/datastore/mongodb/__init__.py b/trove/guestagent/datastore/mongodb/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/trove/guestagent/datastore/mongodb/__init__.py
diff --git a/trove/guestagent/datastore/mongodb/manager.py b/trove/guestagent/datastore/mongodb/manager.py
new file mode 100644
index 00000000..255f10bf
--- /dev/null
+++ b/trove/guestagent/datastore/mongodb/manager.py
@@ -0,0 +1,164 @@
+# Copyright (c) 2014 Mirantis, Inc.
+# 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 trove.common import cfg
+from trove.common import exception
+from trove.guestagent import dbaas
+from trove.guestagent import volume
+from trove.guestagent.common import operating_system
+from trove.guestagent.datastore.mongodb import service as mongo_service
+from trove.guestagent.datastore.mongodb import system
+from trove.openstack.common import log as logging
+from trove.openstack.common.gettextutils import _
+from trove.openstack.common import periodic_task
+
+
+LOG = logging.getLogger(__name__)
+CONF = cfg.CONF
+ERROR_MSG = _("Not supported")
+
+
+class Manager(periodic_task.PeriodicTasks):
+
+ def __init__(self):
+ self.status = mongo_service.MongoDbAppStatus()
+ self.app = mongo_service.MongoDBApp(self.status)
+
+ @periodic_task.periodic_task(ticks_between_runs=3)
+ def update_status(self, context):
+ """Update the status of the MongoDB service"""
+ self.status.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):
+ """Makes ready DBAAS on a Guest container."""
+
+ LOG.debug(_("Prepare MongoDB instance"))
+
+ self.status.begin_install()
+ self.app.install_if_needed(packages)
+ self.app.stop_db()
+ self.app.clear_storage()
+ mount_point = system.MONGODB_MOUNT_POINT
+ if device_path:
+ device = volume.VolumeDevice(device_path)
+ device.format()
+ if os.path.exists(system.MONGODB_MOUNT_POINT):
+ device.migrate_data(mount_point)
+ device.mount(mount_point)
+ self.app.update_owner(mount_point)
+
+ LOG.debug(_("Mounted the volume %(path)s as %(mount)s") %
+ {'path': device_path, "mount": mount_point})
+
+ if mount_point:
+ config_contents = self.app.update_config_contents(
+ config_contents, {
+ 'dbpath': mount_point,
+ 'bind_ip': operating_system.get_ip_address()
+ })
+
+ self.app.start_db_with_conf_changes(config_contents)
+ LOG.info(_('"prepare" call has finished.'))
+
+ def restart(self, context):
+ self.app.restart()
+
+ def start_db_with_conf_changes(self, context, config_contents):
+ self.app.start_db_with_conf_changes(config_contents)
+
+ def stop_db(self, context, do_not_start_on_reboot=False):
+ self.app.stop_db(do_not_start_on_reboot=do_not_start_on_reboot)
+
+ def reset_configuration(self, context, configuration):
+ self.app.reset_configuration(configuration)
+
+ def get_filesystem_stats(self, context, fs_path):
+ """Gets the filesystem stats for the path given """
+ return dbaas.get_filesystem_volume_stats(system.MONGODB_MOUNT_POINT)
+
+ def change_passwords(self, context, users):
+ raise exception.TroveError(ERROR_MSG)
+
+ def update_attributes(self, context, username, hostname, user_attrs):
+ raise exception.TroveError(ERROR_MSG)
+
+ def create_database(self, context, databases):
+ raise exception.TroveError(ERROR_MSG)
+
+ def create_user(self, context, users):
+ raise exception.TroveError(ERROR_MSG)
+
+ def delete_database(self, context, database):
+ raise exception.TroveError(ERROR_MSG)
+
+ def delete_user(self, context, user):
+ raise exception.TroveError(ERROR_MSG)
+
+ def get_user(self, context, username, hostname):
+ raise exception.TroveError(ERROR_MSG)
+
+ def grant_access(self, context, username, hostname, databases):
+ raise exception.TroveError(ERROR_MSG)
+
+ def revoke_access(self, context, username, hostname, database):
+ raise exception.TroveError(ERROR_MSG)
+
+ def list_access(self, context, username, hostname):
+ raise exception.TroveError(ERROR_MSG)
+
+ def list_databases(self, context, limit=None, marker=None,
+ include_marker=False):
+ raise exception.TroveError(ERROR_MSG)
+
+ def list_users(self, context, limit=None, marker=None,
+ include_marker=False):
+ raise exception.TroveError(ERROR_MSG)
+
+ def enable_root(self, context):
+ raise exception.TroveError(ERROR_MSG)
+
+ def is_root_enabled(self, context):
+ raise exception.TroveError(ERROR_MSG)
+
+ def _perform_restore(self, backup_info, context, restore_location, app):
+ raise exception.TroveError(ERROR_MSG)
+
+ def create_backup(self, context, backup_info):
+ raise exception.TroveError(ERROR_MSG)
+
+ 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 volume."))
+
+ def unmount_volume(self, context, device_path=None, mount_point=None):
+ device = volume.VolumeDevice(device_path)
+ device.unmount(mount_point)
+ LOG.debug(_("Unmounted the volume."))
+
+ 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"))
+
+ def update_overrides(self, context, overrides, remove=False):
+ raise exception.TroveError(ERROR_MSG)
+
+ def apply_overrides(self, context, overrides):
+ raise exception.TroveError(ERROR_MSG)
diff --git a/trove/guestagent/datastore/mongodb/service.py b/trove/guestagent/datastore/mongodb/service.py
new file mode 100644
index 00000000..10da0858
--- /dev/null
+++ b/trove/guestagent/datastore/mongodb/service.py
@@ -0,0 +1,229 @@
+# Copyright (c) 2014 Mirantis, Inc.
+# 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 re
+
+from trove.common import cfg
+from trove.common import utils as utils
+from trove.common import exception
+from trove.common import instance as rd_instance
+from trove.common.exception import ProcessExecutionError
+from trove.guestagent.datastore import service
+from trove.guestagent.datastore.mongodb import system
+from trove.openstack.common import log as logging
+from trove.guestagent.common import operating_system
+from trove.openstack.common.gettextutils import _
+
+LOG = logging.getLogger(__name__)
+CONF = cfg.CONF
+
+
+class MongoDBApp(object):
+ """Prepares DBaaS on a Guest container."""
+
+ def __init__(self, status):
+ self.state_change_wait_time = CONF.state_change_wait_time
+ self.status = status
+
+ def install_if_needed(self, packages):
+ """Prepare the guest machine with a MongoDB installation"""
+ LOG.info(_("Preparing Guest as MongoDB"))
+ if not system.PACKAGER.pkg_is_installed(packages):
+ LOG.debug(_("Installing packages: %s") % str(packages))
+ system.PACKAGER.pkg_install(packages, {}, system.TIME_OUT)
+ LOG.info(_("Finished installing MongoDB server"))
+
+ def _enable_db_on_boot(self):
+ LOG.info(_("Enabling MongoDB on boot"))
+ try:
+ mongodb_service = operating_system.service_discovery(
+ system.SERVICE_CANDIDATES)
+ utils.execute_with_timeout(mongodb_service['cmd_enable'],
+ shell=True)
+ except KeyError:
+ raise RuntimeError(_("MongoDB service is not discovered."))
+
+ def _disable_db_on_boot(self):
+ LOG.info(_("Disabling MongoDB on boot"))
+ try:
+ mongodb_service = operating_system.service_discovery(
+ system.SERVICE_CANDIDATES)
+ utils.execute_with_timeout(mongodb_service['cmd_disable'],
+ shell=True)
+ except KeyError:
+ raise RuntimeError("MongoDB service is not discovered.")
+
+ def stop_db(self, update_db=False, do_not_start_on_reboot=False):
+ LOG.info(_("Stopping MongoDB"))
+ if do_not_start_on_reboot:
+ self._disable_db_on_boot()
+
+ try:
+ mongodb_service = operating_system.service_discovery(
+ system.SERVICE_CANDIDATES)
+ utils.execute_with_timeout(mongodb_service['cmd_stop'],
+ shell=True)
+ except KeyError:
+ raise RuntimeError(_("MongoDB service is not discovered."))
+
+ 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 MongoDB"))
+ self.status.end_install_or_restart()
+ raise RuntimeError(_("Could not stop MongoDB"))
+
+ def restart(self):
+ LOG.info(_("Restarting MongoDB"))
+ try:
+ self.status.begin_restart()
+ self.stop_db()
+ self.start_db()
+ finally:
+ self.status.end_install_or_restart()
+
+ def start_db(self, update_db=False):
+ LOG.info(_("Starting MongoDB"))
+
+ self._enable_db_on_boot()
+
+ try:
+ mongodb_service = operating_system.service_discovery(
+ system.SERVICE_CANDIDATES)
+ utils.execute_with_timeout(mongodb_service['cmd_start'],
+ shell=True)
+ except ProcessExecutionError:
+ pass
+ except KeyError:
+ raise RuntimeError("MongoDB service is not discovered.")
+
+ 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 up of MongoDB failed"))
+ # If it won't start, but won't die either, kill it by hand so we
+ # don't let a rouge process wander around.
+ try:
+ out, err = utils.execute_with_timeout(
+ system.FIND_PID, shell=True)
+ pid = "".join(out.split(" ")[1:2])
+ utils.execute_with_timeout(
+ system.MONGODB_KILL % pid, shell=True)
+ except exception.ProcessExecutionError as p:
+ LOG.error("Error killing stalled MongoDB start command.")
+ LOG.error(p)
+ # There's nothing more we can do...
+ self.status.end_install_or_restart()
+ raise RuntimeError("Could not start MongoDB")
+
+ def start_db_with_conf_changes(self, config_contents):
+ LOG.info(_("Starting MongoDB with configuration changes"))
+ LOG.info(_("Configuration contents:\n %s") % config_contents)
+ if self.status.is_running:
+ LOG.error(_("Cannot start MongoDB with configuration changes. "
+ "MongoDB state == %s!") % self.status)
+ raise RuntimeError("MongoDB is not stopped.")
+ self._write_config(config_contents)
+ self.start_db(True)
+
+ def reset_configuration(self, configuration):
+ config_contents = configuration['config_contents']
+ LOG.info(_("Resetting configuration"))
+ self._write_config(config_contents)
+
+ def update_config_contents(self, config_contents, parameters):
+ if not config_contents:
+ config_contents = self._read_config()
+
+ contents = self._delete_config_parameters(config_contents,
+ parameters.keys())
+ for param, value in parameters.iteritems():
+ if param and value:
+ contents = self._add_config_parameter(contents,
+ param, value)
+
+ return contents
+
+ def _write_config(self, config_contents):
+ """
+ Update contents of MongoDB configuration file
+ """
+ LOG.info(_("Updating MongoDB config"))
+ if config_contents:
+ LOG.info(_("Writing %s") % system.TMP_CONFIG)
+ with open(system.TMP_CONFIG, 'w') as t:
+ t.write(config_contents)
+
+ LOG.info(_("Moving %(a)s to %(b)s")
+ % {'a': system.TMP_CONFIG, 'b': system.CONFIG})
+ utils.execute_with_timeout("mv", system.TMP_CONFIG, system.CONFIG,
+ run_as_root=True, root_helper="sudo")
+ else:
+ LOG.info(_("Empty config_contents. Do nothing"))
+
+ def _read_config(self):
+ try:
+ with open(system.CONFIG, 'r') as f:
+ return f.read()
+ except IOError:
+ LOG.info(_("Config file %s not found") % system.CONFIG)
+ return ''
+
+ def _delete_config_parameters(self, config_contents, parameters):
+ if not config_contents:
+ return None
+
+ params_as_string = '|'.join(parameters)
+ p = re.compile("\\s*#?\\s*(%s)\\s*=" % params_as_string)
+ contents_as_list = config_contents.splitlines()
+ filtered = filter(lambda line: not p.match(line), contents_as_list)
+ return '\n'.join(filtered)
+
+ def _add_config_parameter(self, config_contents, parameter, value):
+ return (config_contents or '') + "\n%s = %s" % (parameter, value)
+
+ def update_owner(self, path):
+ LOG.info(_("Set owner to 'mongodb' for %s ") % system.CONFIG)
+ utils.execute_with_timeout("chown", "-R", "mongodb", path,
+ run_as_root=True, root_helper="sudo")
+ LOG.info(_("Set group to 'mongodb' for %s ") % system.CONFIG)
+ utils.execute_with_timeout("chgrp", "-R", "mongodb", path,
+ run_as_root=True, root_helper="sudo")
+
+ def clear_storage(self):
+ mount_point = "/var/lib/mongodb/*"
+ try:
+ cmd = "sudo rm -rf %s" % mount_point
+ utils.execute_with_timeout(cmd, shell=True)
+ except exception.ProcessExecutionError as e:
+ LOG.error(_("Process execution %s") % e)
+
+
+class MongoDbAppStatus(service.BaseDbStatus):
+ def _get_actual_db_status(self):
+ try:
+ status_check = (system.CMD_STATUS %
+ operating_system.get_ip_address())
+ out, err = utils.execute_with_timeout(status_check, shell=True)
+ if not err and "connected to:" in out:
+ return rd_instance.ServiceStatuses.RUNNING
+ else:
+ return rd_instance.ServiceStatuses.SHUTDOWN
+ except exception.ProcessExecutionError as e:
+ LOG.error(_("Process execution %s") % e)
+ return rd_instance.ServiceStatuses.SHUTDOWN
+ except OSError as e:
+ LOG.error(_("OS Error %s") % e)
+ return rd_instance.ServiceStatuses.SHUTDOWN
diff --git a/trove/guestagent/datastore/mongodb/system.py b/trove/guestagent/datastore/mongodb/system.py
new file mode 100644
index 00000000..ab945247
--- /dev/null
+++ b/trove/guestagent/datastore/mongodb/system.py
@@ -0,0 +1,30 @@
+# Copyright (c) 2014 Mirantis, Inc.
+# 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.guestagent import pkg
+
+MONGODB_MOUNT_POINT = "/var/lib/mongodb"
+# After changing bind address mongodb accepts connection
+# on real IP, not on the localhost
+CMD_STATUS = "mongostat --host %s -n 1 | grep connected"
+
+TMP_CONFIG = "/tmp/mongodb.conf.tmp"
+CONFIG = "/etc/mongodb.conf"
+SERVICE_CANDIDATES = ["mongodb", "mongod"]
+MONGODB_KILL = "sudo kill %s"
+FIND_PID = "ps xau | grep mongod"
+TIME_OUT = 1000
+
+PACKAGER = pkg.Package()
diff --git a/trove/guestagent/dbaas.py b/trove/guestagent/dbaas.py
index 45d4bdad..3bad2f30 100644
--- a/trove/guestagent/dbaas.py
+++ b/trove/guestagent/dbaas.py
@@ -37,6 +37,7 @@ defaults = {
'redis': 'trove.guestagent.datastore.redis.manager.Manager',
'cassandra': 'trove.guestagent.datastore.cassandra.manager.Manager',
'couchbase': 'trove.guestagent.datastore.couchbase.manager.Manager',
+ 'mongodb': 'trove.guestagent.datastore.mongodb.manager.Manager',
}
CONF = cfg.CONF
diff --git a/trove/templates/mongodb/config.template b/trove/templates/mongodb/config.template
new file mode 100644
index 00000000..e22f0c86
--- /dev/null
+++ b/trove/templates/mongodb/config.template
@@ -0,0 +1,95 @@
+# mongodb.conf
+
+smallfiles = false
+
+# Where to store the data.
+dbpath=/var/lib/mongodb
+
+#where to log
+logpath=/var/log/mongodb/mongodb.log
+
+logappend=true
+
+bind_ip = 127.0.0.1
+#port = 27017
+
+# Enable journaling, http://www.mongodb.org/display/DOCS/Journaling
+journal=true
+
+# Enables periodic logging of CPU utilization and I/O wait
+#cpu = true
+
+# Turn on/off security. Off is currently the default
+#noauth = true
+#auth = true
+
+# Verbose logging output.
+#verbose = true
+
+# Inspect all client data for validity on receipt (useful for
+# developing drivers)
+#objcheck = true
+
+# Enable db quota management
+#quota = true
+
+# Set oplogging level where n is
+# 0=off (default)
+# 1=W
+# 2=R
+# 3=both
+# 7=W+some reads
+#oplog = 0
+
+# Diagnostic/debugging option
+#nocursors = true
+
+# Ignore query hints
+#nohints = true
+
+# Disable the HTTP interface (Defaults to localhost:27018).
+#nohttpinterface = true
+
+# Turns off server-side scripting. This will result in greatly limited
+# functionality
+#noscripting = true
+
+# Turns off table scans. Any query that would do a table scan fails.
+#notablescan = true
+
+# Disable data file preallocation.
+#noprealloc = true
+
+# Specify .ns file size for new databases.
+# nssize = <size>
+
+# Accout token for Mongo monitoring server.
+#mms-token = <token>
+
+# Server name for Mongo monitoring server.
+#mms-name = <server-name>
+
+# Ping interval for Mongo monitoring server.
+#mms-interval = <seconds>
+
+# Replication Options
+
+# in replicated mongo databases, specify here whether this is a slave or master
+#slave = true
+#source = master.example.com
+# Slave only: specify a single database to replicate
+#only = master.example.com
+# or
+#master = true
+#source = slave.example.com
+
+# Address of a server to pair with.
+#pairwith = <server:port>
+# Address of arbiter server.
+#arbiter = <server:port>
+# Automatically resync if slave data is stale
+#autoresync
+# Custom size for replication operation log.
+#oplogSize = <MB>
+# Size limit for in-memory storage of op ids.
+#opIdMem = <bytes>
diff --git a/trove/templates/mongodb/heat.template b/trove/templates/mongodb/heat.template
new file mode 100644
index 00000000..91a0f49c
--- /dev/null
+++ b/trove/templates/mongodb/heat.template
@@ -0,0 +1,93 @@
+HeatTemplateFormatVersion: '2012-12-12'
+Description: Instance creation template for mongodb
+Parameters:
+ Flavor:
+ Type: String
+ VolumeSize:
+ Type: Number
+ Default : '1'
+ InstanceId:
+ Type: String
+ ImageId:
+ Type: String
+ DatastoreManager:
+ Type: String
+ AvailabilityZone:
+ Type: String
+ Default: nova
+ TenantId:
+ Type: String
+Resources:
+{% for port in ports %}
+ {{ port.name }}:
+ Type: OS::Neutron::Port
+ Properties:
+ network_id: "{{ port.net_id }}"
+ security_groups: [{Ref: DBaaSSG}]
+ {% if port.fixed_ip %}
+ fixed_ips: [{"ip_address": "{{ port.fixed_ip }}"}]
+ {% endif %}
+{% endfor %}
+ BaseInstance:
+ Type: AWS::EC2::Instance
+ Metadata:
+ AWS::CloudFormation::Init:
+ config:
+ files:
+ /etc/guest_info:
+ content:
+ Fn::Join:
+ - ''
+ - ["[DEFAULT]\nguest_id=", {Ref: InstanceId},
+ "\ndatastore_manager=", {Ref: DatastoreManager},
+ "\ntenant_id=", {Ref: TenantId}]
+ mode: '000644'
+ owner: root
+ group: root
+ Properties:
+ ImageId: {Ref: ImageId}
+ InstanceType: {Ref: Flavor}
+ AvailabilityZone: {Ref: AvailabilityZone}
+ {% if ifaces %}
+ NetworkInterfaces: [{{ ifaces|join(', ') }}]
+ {% else %}
+ SecurityGroups: [{Ref: DBaaSSG}]
+ {% endif %}
+ UserData:
+ Fn::Base64:
+ Fn::Join:
+ - ''
+ - ["#!/bin/bash -v\n",
+ "/opt/aws/bin/cfn-init\n",
+ "sudo service trove-guest start\n"]
+{% if volume_support %}
+ DataVolume:
+ Type: AWS::EC2::Volume
+ Properties:
+ Size: {Ref: VolumeSize}
+ AvailabilityZone: {Ref: AvailabilityZone}
+ Tags:
+ - {Key: Usage, Value: Test}
+ MountPoint:
+ Type: AWS::EC2::VolumeAttachment
+ Properties:
+ InstanceId: {Ref: BaseInstance}
+ VolumeId: {Ref: DataVolume}
+ Device: /dev/vdb
+{% endif %}
+ DBaaSSG:
+ Type: AWS::EC2::SecurityGroup
+ Properties:
+ GroupDescription: Default Security group for MongoDB
+ SecurityGroupIngress:
+ - IpProtocol: "tcp"
+ FromPort: "27017"
+ ToPort: "27017"
+ CidrIp: "0.0.0.0/0"
+ DatabaseIPAddress:
+ Type: AWS::EC2::EIP
+ DatabaseIPAssoc :
+ Type: AWS::EC2::EIPAssociation
+ Properties:
+ InstanceId: {Ref: BaseInstance}
+ EIP: {Ref: DatabaseIPAddress}
diff --git a/trove/templates/mongodb/override.config.template b/trove/templates/mongodb/override.config.template
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/trove/templates/mongodb/override.config.template
diff --git a/trove/tests/unittests/common/test_template.py b/trove/tests/unittests/common/test_template.py
index a13c2d71..8c77885d 100644
--- a/trove/tests/unittests/common/test_template.py
+++ b/trove/tests/unittests/common/test_template.py
@@ -12,7 +12,6 @@
import testtools
-import mock
import re
from trove.common import template
@@ -68,13 +67,6 @@ class HeatTemplateLoadTest(testtools.TestCase):
def setUp(self):
super(HeatTemplateLoadTest, self).setUp()
- self.fException = mock.Mock(side_effect=
- lambda *args, **kwargs:
- _raise(template.jinja2.
- TemplateNotFound("Test")))
-
- def _raise(ex):
- raise ex
def tearDown(self):
super(HeatTemplateLoadTest, self).tearDown()
@@ -86,6 +78,11 @@ class HeatTemplateLoadTest(testtools.TestCase):
def test_heat_template_load_success(self):
mysql_tmpl = template.load_heat_template('mysql')
+ #TODO(denis_makogon): use it when redis template would be added
+ #redis_tmplt = template.load_heat_template('redis')
cassandra_tmpl = template.load_heat_template('cassandra')
+ mongo_tmpl = template.load_heat_template('mongodb')
self.assertIsNotNone(mysql_tmpl)
self.assertIsNotNone(cassandra_tmpl)
+ self.assertIsNotNone(mongo_tmpl)
+ # self.assertIsNotNone(redis_tmpl)
diff --git a/trove/tests/unittests/guestagent/test_dbaas.py b/trove/tests/unittests/guestagent/test_dbaas.py
index 9aeee610..cdc77e08 100644
--- a/trove/tests/unittests/guestagent/test_dbaas.py
+++ b/trove/tests/unittests/guestagent/test_dbaas.py
@@ -51,6 +51,8 @@ from trove.guestagent.datastore.mysql.service import MySqlApp
from trove.guestagent.datastore.mysql.service import MySqlAppStatus
from trove.guestagent.datastore.mysql.service import KeepAliveConnection
from trove.guestagent.datastore.couchbase import service as couchservice
+from trove.guestagent.datastore.mongodb import service as mongo_service
+from trove.guestagent.datastore.mongodb import system as mongo_system
from trove.guestagent.db import models
from trove.instance.models import InstanceServiceStatus
from trove.tests.unittests.util import util
@@ -71,7 +73,7 @@ FAKE_USER = [{"_name": "random", "_password": "guesswhat",
conductor_api.API.heartbeat = Mock()
-class FakeAppStatus(MySqlAppStatus):
+class FakeAppStatus(BaseDbStatus):
def __init__(self, id, status):
self.id = id
@@ -929,6 +931,9 @@ class ServiceRegistryTest(testtools.TestCase):
self.assertEqual(test_dict.get('couchbase'),
'trove.guestagent.datastore.couchbase.manager'
'.Manager')
+ self.assertEqual('trove.guestagent.datastore.mongodb.'
+ 'manager.Manager',
+ test_dict.get('mongodb'))
def test_datastore_registry_with_existing_manager(self):
datastore_registry_ext_test = {
@@ -952,6 +957,8 @@ class ServiceRegistryTest(testtools.TestCase):
self.assertEqual(test_dict.get('couchbase'),
'trove.guestagent.datastore.couchbase.manager'
'.Manager')
+ self.assertEqual('trove.guestagent.datastore.mongodb.manager.Manager',
+ test_dict.get('mongodb'))
def test_datastore_registry_with_blank_dict(self):
datastore_registry_ext_test = dict()
@@ -972,6 +979,8 @@ class ServiceRegistryTest(testtools.TestCase):
self.assertEqual(test_dict.get('couchbase'),
'trove.guestagent.datastore.couchbase.manager'
'.Manager')
+ self.assertEqual('trove.guestagent.datastore.mongodb.manager.Manager',
+ test_dict.get('mongodb'))
class KeepAliveConnectionTest(testtools.TestCase):
@@ -1621,3 +1630,151 @@ class CouchbaseAppTest(testtools.TestCase):
self.assertTrue(couchservice.packager.pkg_is_installed.called)
self.assertTrue(self.couchbaseApp.initial_setup.called)
self.assert_reported_status(rd_instance.ServiceStatuses.NEW)
+
+
+class MongoDBAppTest(testtools.TestCase):
+
+ def fake_mongodb_service_discovery(self, candidates):
+ return {
+ 'cmd_start': 'start',
+ 'cmd_stop': 'stop',
+ 'cmd_enable': 'enable',
+ 'cmd_disable': 'disable'
+ }
+
+ def setUp(self):
+ super(MongoDBAppTest, self).setUp()
+ self.orig_utils_execute_with_timeout = (mongo_service.
+ utils.execute_with_timeout)
+ self.orig_time_sleep = time.sleep
+ self.orig_packager = mongo_system.PACKAGER
+ self.orig_service_discovery = operating_system.service_discovery
+
+ operating_system.service_discovery = (
+ self.fake_mongodb_service_discovery)
+ 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.mongoDbApp = mongo_service.MongoDBApp(self.appStatus)
+ time.sleep = Mock()
+
+ def tearDown(self):
+ super(MongoDBAppTest, self).tearDown()
+ mongo_service.utils.execute_with_timeout = (
+ self.orig_utils_execute_with_timeout)
+ time.sleep = self.orig_time_sleep
+ mongo_system.PACKAGER = self.orig_packager
+ operating_system.service_discovery = self.orig_service_discovery
+ InstanceServiceStatus.find_by(instance_id=self.FAKE_ID).delete()
+
+ 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_stopdb(self):
+ mongo_service.utils.execute_with_timeout = Mock()
+ self.appStatus.set_next_status(
+ rd_instance.ServiceStatuses.SHUTDOWN)
+
+ self.mongoDbApp.stop_db()
+ self.assert_reported_status(rd_instance.ServiceStatuses.NEW)
+
+ def test_stop_db_with_db_update(self):
+
+ mongo_service.utils.execute_with_timeout = Mock()
+ self.appStatus.set_next_status(
+ rd_instance.ServiceStatuses.SHUTDOWN)
+
+ self.mongoDbApp.stop_db(True)
+ self.assertTrue(conductor_api.API.heartbeat.called_once_with(
+ self.FAKE_ID, {'service_status': 'shutdown'}))
+
+ def test_stop_db_error(self):
+
+ mongo_service.utils.execute_with_timeout = Mock()
+ self.appStatus.set_next_status(rd_instance.ServiceStatuses.RUNNING)
+ self.mongoDbApp.state_change_wait_time = 1
+ self.assertRaises(RuntimeError, self.mongoDbApp.stop_db)
+
+ def test_restart(self):
+
+ self.appStatus.set_next_status(rd_instance.ServiceStatuses.RUNNING)
+ self.mongoDbApp.stop_db = Mock()
+ self.mongoDbApp.start_db = Mock()
+
+ self.mongoDbApp.restart()
+
+ self.assertTrue(self.mongoDbApp.stop_db.called)
+ self.assertTrue(self.mongoDbApp.start_db.called)
+
+ self.assertTrue(conductor_api.API.heartbeat.called_once_with(
+ self.FAKE_ID, {'service_status': 'shutdown'}))
+
+ self.assertTrue(conductor_api.API.heartbeat.called_once_with(
+ self.FAKE_ID, {'service_status': 'running'}))
+
+ def test_start_db(self):
+
+ mongo_service.utils.execute_with_timeout = Mock()
+ self.appStatus.set_next_status(rd_instance.ServiceStatuses.RUNNING)
+
+ self.mongoDbApp.start_db()
+ self.assert_reported_status(rd_instance.ServiceStatuses.NEW)
+
+ def test_start_db_with_update(self):
+
+ mongo_service.utils.execute_with_timeout = Mock()
+ self.appStatus.set_next_status(rd_instance.ServiceStatuses.RUNNING)
+
+ self.mongoDbApp.start_db(True)
+ self.assertTrue(conductor_api.API.heartbeat.called_once_with(
+ self.FAKE_ID, {'service_status': 'running'}))
+
+ def test_start_db_runs_forever(self):
+
+ mongo_service.utils.execute_with_timeout = Mock(
+ return_value=["ubuntu 17036 0.0 0.1 618960 "
+ "29232 pts/8 Sl+ Jan29 0:07 mongod", ""])
+ self.mongoDbApp.state_change_wait_time = 1
+ self.appStatus.set_next_status(rd_instance.ServiceStatuses.SHUTDOWN)
+
+ self.assertRaises(RuntimeError, self.mongoDbApp.start_db)
+ self.assertTrue(conductor_api.API.heartbeat.called_once_with(
+ self.FAKE_ID, {'service_status': 'shutdown'}))
+
+ def test_start_db_error(self):
+
+ self.mongoDbApp._enable_db_on_boot = Mock()
+ from trove.common.exception import ProcessExecutionError
+ mocked = Mock(side_effect=ProcessExecutionError('Error'))
+ mongo_service.utils.execute_with_timeout = mocked
+
+ self.assertRaises(RuntimeError, self.mongoDbApp.start_db)
+
+ def test_start_db_with_conf_changes_db_is_running(self):
+
+ self.mongoDbApp.start_db = Mock()
+
+ self.appStatus.status = rd_instance.ServiceStatuses.RUNNING
+ self.assertRaises(RuntimeError,
+ self.mongoDbApp.start_db_with_conf_changes,
+ Mock())
+
+ def test_install_when_db_installed(self):
+ packager_mock = mock()
+ when(packager_mock).pkg_is_installed(any()).thenReturn(True)
+ mongo_system.PACKAGER = packager_mock
+ self.mongoDbApp.install_if_needed(['package'])
+ self.assert_reported_status(rd_instance.ServiceStatuses.NEW)
+
+ def test_install_when_db_not_installed(self):
+ packager_mock = mock()
+ when(packager_mock).pkg_is_installed(any()).thenReturn(False)
+ mongo_system.PACKAGER = packager_mock
+ self.mongoDbApp.install_if_needed(['package'])
+ verify(packager_mock).pkg_install(any(), {}, any())
+ self.assert_reported_status(rd_instance.ServiceStatuses.NEW)
diff --git a/trove/tests/unittests/guestagent/test_mongodb_manager.py b/trove/tests/unittests/guestagent/test_mongodb_manager.py
new file mode 100644
index 00000000..2e4dd69d
--- /dev/null
+++ b/trove/tests/unittests/guestagent/test_mongodb_manager.py
@@ -0,0 +1,95 @@
+# 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.
+
+import os
+
+import testtools
+from mockito import verify, when, unstub, any, mock
+from trove.common.context import TroveContext
+from trove.guestagent import volume
+from trove.guestagent.datastore.mongodb import service as mongo_service
+from trove.guestagent.datastore.mongodb import manager as mongo_manager
+from trove.guestagent.volume import VolumeDevice
+
+
+class GuestAgentMongoDBManagerTest(testtools.TestCase):
+
+ def setUp(self):
+ super(GuestAgentMongoDBManagerTest, self).setUp()
+ self.context = TroveContext()
+ self.manager = mongo_manager.Manager()
+ self.origin_MongoDbAppStatus = mongo_service.MongoDbAppStatus
+ self.origin_os_path_exists = os.path.exists
+ self.origin_format = volume.VolumeDevice.format
+ self.origin_migrate_data = volume.VolumeDevice.migrate_data
+ self.origin_mount = volume.VolumeDevice.mount
+ self.origin_stop_db = mongo_service.MongoDBApp.stop_db
+ self.origin_start_db = mongo_service.MongoDBApp.start_db
+
+ def tearDown(self):
+ super(GuestAgentMongoDBManagerTest, self).tearDown()
+ mongo_service.MongoDbAppStatus = self.origin_MongoDbAppStatus
+ os.path.exists = self.origin_os_path_exists
+ volume.VolumeDevice.format = self.origin_format
+ volume.VolumeDevice.migrate_data = self.origin_migrate_data
+ volume.VolumeDevice.mount = self.origin_mount
+ mongo_service.MongoDBApp.stop_db = self.origin_stop_db
+ mongo_service.MongoDBApp.start_db = self.origin_start_db
+ unstub()
+
+ def test_update_status(self):
+ self.manager.status = mock()
+ self.manager.update_status(self.context)
+ verify(self.manager.status).update()
+
+ def test_prepare_from_backup(self):
+ self._prepare_dynamic(backup_id='backup_id_123abc')
+
+ def _prepare_dynamic(self, device_path='/dev/vdb', is_db_installed=True,
+ backup_id=None):
+
+ # covering all outcomes is starting to cause trouble here
+ backup_info = {'id': backup_id,
+ 'location': 'fake-location',
+ 'type': 'MongoDBDump',
+ 'checksum': 'fake-checksum'} if backup_id else None
+
+ mock_status = mock()
+ self.manager.status = mock_status
+ when(mock_status).begin_install().thenReturn(None)
+
+ when(VolumeDevice).format().thenReturn(None)
+ when(VolumeDevice).migrate_data(any()).thenReturn(None)
+ when(VolumeDevice).mount().thenReturn(None)
+
+ mock_app = mock()
+ self.manager.app = mock_app
+ when(mock_app).stop_db().thenReturn(None)
+ when(mock_app).start_db().thenReturn(None)
+ when(mock_app).clear_storage().thenReturn(None)
+ when(os.path).exists(any()).thenReturn(is_db_installed)
+
+ # invocation
+ self.manager.prepare(context=self.context, databases=None,
+ packages=['package'],
+ memory_mb='2048', users=None,
+ device_path=device_path,
+ mount_point='/var/lib/mongodb',
+ backup_info=backup_info)
+ # verification/assertion
+ verify(mock_status).begin_install()
+ verify(VolumeDevice).format()
+ verify(mock_app).stop_db()
+ verify(VolumeDevice).migrate_data(any())
+ verify(mock_app).install_if_needed(any())