Merge "Initial support for single instance MongoDB support"

This commit is contained in:
Jenkins 2014-03-05 02:02:19 +00:00 committed by Gerrit Code Review
commit fba8cabea3
13 changed files with 901 additions and 10 deletions

View File

@ -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):

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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>

View File

@ -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}

View File

@ -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)

View File

@ -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)

View File

@ -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())