diff --git a/trove/common/cfg.py b/trove/common/cfg.py index 7ae2a6ac58..e9a5831db7 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 a24f859304..3e5374b292 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 0000000000..e69de29bb2 diff --git a/trove/guestagent/datastore/mongodb/manager.py b/trove/guestagent/datastore/mongodb/manager.py new file mode 100644 index 0000000000..255f10bf97 --- /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 0000000000..10da0858a7 --- /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 0000000000..ab94524705 --- /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 45d4bdaded..3bad2f3070 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 0000000000..e22f0c86c4 --- /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 = + +# Accout token for Mongo monitoring server. +#mms-token = + +# Server name for Mongo monitoring server. +#mms-name = + +# Ping interval for Mongo monitoring server. +#mms-interval = + +# 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 = +# Address of arbiter server. +#arbiter = +# Automatically resync if slave data is stale +#autoresync +# Custom size for replication operation log. +#oplogSize = +# Size limit for in-memory storage of op ids. +#opIdMem = diff --git a/trove/templates/mongodb/heat.template b/trove/templates/mongodb/heat.template new file mode 100644 index 0000000000..91a0f49c91 --- /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 0000000000..e69de29bb2 diff --git a/trove/tests/unittests/common/test_template.py b/trove/tests/unittests/common/test_template.py index a13c2d7124..8c77885d5a 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 9aeee61006..cdc77e0857 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 0000000000..2e4dd69d77 --- /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())