diff --git a/etc/reddwarf/reddwarf.conf.sample b/etc/reddwarf/reddwarf.conf.sample index 46753133e1..acc09bf447 100644 --- a/etc/reddwarf/reddwarf.conf.sample +++ b/etc/reddwarf/reddwarf.conf.sample @@ -52,6 +52,13 @@ nova_service_name = Compute Service # Config option for showing the IP address that nova doles out add_addresses = True +# Config options for enabling volume service +reddwarf_volume_support = False +nova_volume_service_type = volume +nova_volume_service_name = Volume Service +device_path = /dev/vdb +mount_point = /var/lib/mysql + # ============ notifer queue kombu connection options ======================== notifier_queue_hostname = localhost diff --git a/etc/reddwarf/reddwarf.conf.test b/etc/reddwarf/reddwarf.conf.test index 13804a5024..301751b523 100644 --- a/etc/reddwarf/reddwarf.conf.test +++ b/etc/reddwarf/reddwarf.conf.test @@ -52,6 +52,16 @@ nova_region_name = RegionOne nova_service_type = compute nova_service_name = Compute Service +# Config option for showing the IP address that nova doles out +add_addresses = True + +# Config options for enabling volume service +reddwarf_volume_support = False +nova_volume_service_type = volume +nova_volume_service_name = Volume Service +device_path = /dev/vdb +mount_point = /var/lib/mysql + # ============ notifer queue kombu connection options ======================== notifier_queue_hostname = localhost diff --git a/reddwarf/common/exception.py b/reddwarf/common/exception.py index d498d8bbe7..c0976e5964 100644 --- a/reddwarf/common/exception.py +++ b/reddwarf/common/exception.py @@ -87,3 +87,8 @@ class UnprocessableEntity(ReddwarfError): message = _("Unable to process the contained request") + +class VolumeAttachmentsNotFound(NotFound): + + message = _("Cannot find the volumes attached to compute " + "instance %(server_id)") diff --git a/reddwarf/common/remote.py b/reddwarf/common/remote.py index 35bcb5bb70..137c66d614 100644 --- a/reddwarf/common/remote.py +++ b/reddwarf/common/remote.py @@ -19,7 +19,6 @@ from reddwarf.common import config from novaclient.v1_1.client import Client - CONFIG = config.Config @@ -40,6 +39,7 @@ def create_nova_client(context): PROXY_AUTH_URL = CONFIG.get('reddwarf_auth_url', 'http://0.0.0.0:5000/v2.0') REGION_NAME = CONFIG.get('nova_region_name', 'RegionOne') + SERVICE_TYPE = CONFIG.get('nova_service_type', 'compute') SERVICE_NAME = CONFIG.get('nova_service_name', 'Compute Service') @@ -55,10 +55,38 @@ def create_nova_client(context): return client +def create_nova_volume_client(context): + # Quite annoying but due to a paste config loading bug. + # TODO(hub-cap): talk to the openstack-common people about this + PROXY_ADMIN_USER = CONFIG.get('reddwarf_proxy_admin_user', 'admin') + PROXY_ADMIN_PASS = CONFIG.get('reddwarf_proxy_admin_pass', + '3de4922d8b6ac5a1aad9') + PROXY_ADMIN_TENANT_NAME = CONFIG.get('reddwarf_proxy_admin_tenant_name', + 'admin') + PROXY_AUTH_URL = CONFIG.get('reddwarf_auth_url', + 'http://0.0.0.0:5000/v2.0') + REGION_NAME = CONFIG.get('nova_region_name', 'RegionOne') + + SERVICE_TYPE = CONFIG.get('nova_volume_service_type', 'volume') + SERVICE_NAME = CONFIG.get('nova_volume_service_name', 'Volume Service') + + #TODO(cp16net) need to fix this proxy_tenant_id + client = Client(PROXY_ADMIN_USER, PROXY_ADMIN_PASS, + PROXY_ADMIN_TENANT_NAME, PROXY_AUTH_URL, + proxy_tenant_id=context.tenant, + proxy_token=context.auth_tok, + region_name=REGION_NAME, + service_type=SERVICE_TYPE, + service_name=SERVICE_NAME) + client.authenticate() + return client + + if CONFIG.get("remote_implementation", "real") == "fake": # Override the functions above with fakes. from reddwarf.tests.fakes.nova import fake_create_nova_client + from reddwarf.tests.fakes.nova import fake_create_nova_volume_client from reddwarf.tests.fakes.guestagent import fake_create_guest_client def create_guest_client(context, id): @@ -66,3 +94,6 @@ if CONFIG.get("remote_implementation", "real") == "fake": def create_nova_client(context): return fake_create_nova_client(context) + + def create_nova_volume_client(context): + return fake_create_nova_volume_client(context) diff --git a/reddwarf/common/utils.py b/reddwarf/common/utils.py index 235a427933..c007aafc3d 100644 --- a/reddwarf/common/utils.py +++ b/reddwarf/common/utils.py @@ -22,6 +22,7 @@ import logging import re import signal import sys +import urlparse import uuid from eventlet import event @@ -146,6 +147,7 @@ class LoopingCallDone(Exception): def __init__(self, retvalue=True): """:param retvalue: Value that LoopingCall.wait() should return.""" + super(LoopingCallDone, self).__init__() self.retvalue = retvalue @@ -208,10 +210,12 @@ def get_id_from_href(href): def execute_with_timeout(*args, **kwargs): time = kwargs.get('timeout', 30) + def cb_timeout(): - raise exception.ProcessExecutionError("Time out after waiting " - + str(time) + " seconds when running proc: " + str(args) - + str(kwargs)) + msg = _("Time out after waiting" + " %(time)s seconds when running proc: %(args)s" + " %(kwargs)s") % locals() + raise exception.ProcessExecutionError(msg) timeout = Timeout(time) try: @@ -220,8 +224,9 @@ def execute_with_timeout(*args, **kwargs): if t is not timeout: raise else: - raise exception.ProcessExecutionError("Time out after waiting " - + str(time) + " seconds when running proc: " + str(args) - + str(kwargs)) + msg = _("Time out after waiting " + "%(time)s seconds when running proc: %(args)s" + " %(kwargs)s") % locals() + raise exception.ProcessExecutionError(msg) finally: timeout.cancel() diff --git a/reddwarf/guestagent/api.py b/reddwarf/guestagent/api.py index dd2f0c1c9d..4a7f0f31c2 100644 --- a/reddwarf/guestagent/api.py +++ b/reddwarf/guestagent/api.py @@ -122,12 +122,14 @@ class API(object): LOG.debug(_("Check diagnostics on Instance %s"), self.id) return self._call("get_diagnostics") - def prepare(self, memory_mb, databases, users): + def prepare(self, memory_mb, databases, users, + device_path='/dev/vdb', mount_point='/mnt/volume'): """Make an asynchronous call to prepare the guest as a database container""" LOG.debug(_("Sending the call to prepare the Guest")) self._cast_with_consumer("prepare", databases=databases, - memory_mb=memory_mb, users=users) + memory_mb=memory_mb, users=users, device_path=device_path, + mount_point=mount_point) def restart(self): """Restart the MySQL server.""" @@ -147,5 +149,5 @@ class API(object): def upgrade(self): """Make an asynchronous call to self upgrade the guest agent""" - LOG.debug(_("Sending an upgrade call to nova-guest %s"), topic) + LOG.debug(_("Sending an upgrade call to nova-guest")) self._cast_with_consumer("upgrade") diff --git a/reddwarf/guestagent/dbaas.py b/reddwarf/guestagent/dbaas.py index 7a83555a68..e8189f6809 100644 --- a/reddwarf/guestagent/dbaas.py +++ b/reddwarf/guestagent/dbaas.py @@ -28,6 +28,7 @@ handles RPC calls relating to Platform specific operations. import logging import os +import pexpect import re import sys import time @@ -40,12 +41,15 @@ from sqlalchemy import interfaces from sqlalchemy.sql.expression import text from reddwarf import db +from reddwarf.common.exception import GuestError from reddwarf.common.exception import ProcessExecutionError from reddwarf.common import config from reddwarf.common import utils from reddwarf.guestagent.db import models +from reddwarf.guestagent.volume import VolumeHelper from reddwarf.instance import models as rd_models + ADMIN_USER_NAME = "os_admin" LOG = logging.getLogger(__name__) FLUSH = text("""FLUSH PRIVILEGES;""") @@ -61,6 +65,8 @@ TMP_MYCNF = "/tmp/my.cnf.tmp" DBAAS_MYCNF = "/etc/dbaas/my.cnf/my.cnf.%dM" MYSQL_BASE_DIR = "/var/lib/mysql" +CONFIG = config.Config + def generate_random_password(): return str(uuid.uuid4()) @@ -140,7 +146,6 @@ class MySqlAppStatus(object): self.status = self._load_status() self.restart_mode = False - def begin_mysql_install(self): """Called right before MySQL is prepared.""" self.set_status(rd_models.ServiceStatuses.BUILDING) @@ -176,8 +181,8 @@ class MySqlAppStatus(object): except ProcessExecutionError as e: LOG.error("Process execution ") try: - out, err = utils.execute_with_timeout("/bin/ps", "-C", "mysqld", - "h") + out, err = utils.execute_with_timeout("/bin/ps", "-C", + "mysqld", "h") pid = out.split()[0] # TODO(rnirmal): Need to create new statuses for instances # where the mysql service is up, but unresponsive @@ -228,7 +233,6 @@ class MySqlAppStatus(object): db_status.save() self.status = status - def update(self): """Find and report status of MySQL on this machine. @@ -488,13 +492,33 @@ class DBaaSAgent(object): def is_root_enabled(self): return MySqlAdmin().is_root_enabled() - def prepare(self, databases, memory_mb, users): + def prepare(self, databases, memory_mb, users, device_path=None, + mount_point=None): """Makes ready DBAAS on a Guest container.""" from reddwarf.guestagent.pkg import PkgAgent if not isinstance(self, PkgAgent): raise TypeError("This must also be an instance of Pkg agent.") pkg = self # Python cast. + self.status.begin_mysql_install() + # status end_mysql_install set with install_and_secure() app = MySqlApp(self.status) + restart_mysql = False + if not device_path is None: + VolumeHelper.format(device_path) + if app.is_installed(pkg): + #stop and do not update database + app.stop_mysql() + restart_mysql = True + #rsync exiting data + VolumeHelper.migrate_data(device_path, MYSQL_BASE_DIR) + #mount the volume + VolumeHelper.mount(device_path, mount_point) + #TODO(cp16net) need to update the fstab here so that on a + # restart the volume will be mounted automatically again + LOG.debug(_("Mounted the volume.")) + #check mysql was installed and stopped + if restart_mysql: + app.start_mysql() app.install_and_secure(pkg, memory_mb) LOG.info("Creating initial databases and users following successful " "prepare.") @@ -536,11 +560,12 @@ class MySqlApp(object): """Prepares DBaaS on a Guest container.""" TIME_OUT = 1000 + MYSQL_PACKAGE_VERSION = "mysql-server-5.1" def __init__(self, status): """ By default login with root no password for initial setup. """ - self.state_change_wait_time = config.Config.get( - 'state_change_wait_time', 2 * 60) + self.state_change_wait_time = int(config.Config.get( + 'state_change_wait_time', 2 * 60)) self.status = status def _create_admin_user(self, client, password): @@ -586,21 +611,21 @@ class MySqlApp(object): self._remove_remote_root_access(client) self._create_admin_user(client, admin_password) - self._internal_stop_mysql() + self.stop_mysql() self._write_mycnf(pkg, memory_mb, admin_password) - self._start_mysql() + self.start_mysql() self.status.end_install_or_restart() - LOG.info(_("Dbaas preparation complete.")) + LOG.info(_("Dbaas install_and_secure complete.")) def _install_mysql(self, pkg): """Install mysql server. The current version is 5.1""" LOG.debug(_("Installing mysql server")) - pkg.pkg_install("mysql-server-5.1", self.TIME_OUT) + pkg.pkg_install(self.MYSQL_PACKAGE_VERSION, self.TIME_OUT) LOG.debug(_("Finished installing mysql server")) #TODO(rnirmal): Add checks to make sure the package got installed - def _internal_stop_mysql(self, update_db=False): + def stop_mysql(self, update_db=False): LOG.info(_("Stopping mysql...")) utils.execute_with_timeout("sudo", "/etc/init.d/mysql", "stop") if not self.status.wait_for_real_status_to_change_to( @@ -623,8 +648,8 @@ class MySqlApp(object): def restart(self): try: self.status.begin_mysql_restart() - self._internal_stop_mysql() - self._start_mysql() + self.stop_mysql() + self.start_mysql() finally: self.status.end_install_or_restart() @@ -702,8 +727,7 @@ class MySqlApp(object): utils.execute_with_timeout("sudo", "ln", "-s", FINAL_MYCNF, ORIG_MYCNF) self.wipe_ib_logfiles() - - def _start_mysql(self, update_db=False): + def start_mysql(self, update_db=False): LOG.info(_("Starting mysql...")) # This is the site of all the trouble in the restart tests. # Essentially what happens is thaty mysql start fails, but does not @@ -736,8 +760,10 @@ class MySqlApp(object): "MySQL state == %s!") % self.status) raise RuntimeError("MySQL not stopped.") LOG.info(_("Initiating config.")) - self._write_mycnf(pkg, update_memory_mb, None) - self._start_mysql(True) + self._write_mycnf(pkg, updated_memory_mb, None) + self.start_mysql(True) - def stop_mysql(self): - self._internal_stop_mysql(True) + def is_installed(self, pkg): + #(cp16net) could raise an exception, does it need to be handled here? + version = pkg.pkg_version(self.MYSQL_PACKAGE_VERSION) + return not version is None diff --git a/reddwarf/guestagent/manager.py b/reddwarf/guestagent/manager.py index 2d5f06785b..6d9ad7148d 100644 --- a/reddwarf/guestagent/manager.py +++ b/reddwarf/guestagent/manager.py @@ -44,8 +44,9 @@ class GuestManager(service.Manager): """Manages the tasks within a Guest VM.""" def __init__(self, guest_drivers=None, *args, **kwargs): + service_type = CONFIG.get('service_type') try: - service_impl = GUEST_SERVICES[CONFIG.get('service_type')] + service_impl = GUEST_SERVICES[service_type] except KeyError as e: LOG.error(_("Could not create guest, no impl for key - %s") % service_type) diff --git a/reddwarf/guestagent/pkg.py b/reddwarf/guestagent/pkg.py index e75f5b26b4..f87349da3f 100644 --- a/reddwarf/guestagent/pkg.py +++ b/reddwarf/guestagent/pkg.py @@ -18,8 +18,10 @@ """ Manages packages on the Guest VM. """ +import commands import logging import pexpect +import re from reddwarf.common import exception from reddwarf.common.exception import ProcessExecutionError @@ -27,7 +29,6 @@ from reddwarf.common import utils LOG = logging.getLogger(__name__) -# FLAGS = flags.FLAGS class PkgAdminLockError(exception.ReddwarfError): @@ -157,7 +158,8 @@ class PkgAgent(object): def pkg_install(self, package_name, time_out): """Installs a package.""" try: - utils.execute("apt-get", "update", run_as_root=True) + utils.execute("apt-get", "update", run_as_root=True, + root_helper="sudo") except ProcessExecutionError as e: LOG.error(_("Error updating the apt sources")) @@ -171,44 +173,45 @@ class PkgAgent(object): % package_name) def pkg_version(self, package_name): - """Returns the installed version of the given package. - - It is sometimes impossible to know if a package is completely - unavailable before you attempt to install. Some packages may return - no information from the dpkg command but then install fine with apt-get - install. - - """ - child = pexpect.spawn("dpkg -l %s" % package_name) - i = child.expect([".*No packages found matching*", "\+\+\+\-"]) - if i == 0: - #raise PkgNotFoundError() + cmd_list = ["dpkg", "-l", package_name] + p = commands.getstatusoutput(' '.join(cmd_list)) + # check the command status code + if not p[0] == 0: return None # Need to capture the version string - child.expect("\n") - i = child.expect(["", ".*"]) - if i == 0: - return None - line = child.match.group() - parts = line.split() - # Should be something like: - # ['un', 'cowsay', '', '(no', 'description', 'available)'] - try: - wait_and_close_proc(child) - except pexpect.TIMEOUT: - kill_proc(child) - raise PkgTimeout("Remove process took too long.") - if len(parts) <= 2: - raise Error("Unexpected output.") - if parts[1] != package_name: - raise Error("Unexpected output:[1] == " + str(parts[1])) - if parts[0] == 'un' or parts[2] == '': - return None - return parts[2] + # check the command output + std_out = p[1] + patterns = ['.*No packages found matching.*', + "\w\w\s+(\S+)\s+(\S+)\s+(.*)$"] + for line in std_out.split("\n"): + for p in patterns: + regex = re.compile(p) + matches = regex.match(line) + if matches: + line = matches.group() + parts = line.split() + if not parts: + msg = _("returned nothing") + LOG.error(msg) + raise exception.GuestError(msg) + if len(parts) <= 2: + msg = _("Unexpected output.") + LOG.error(msg) + raise exception.GuestError(msg) + if parts[1] != package_name: + msg = _("Unexpected output:[1] = %s" % str(parts[1])) + LOG.error(msg) + raise exception.GuestError(msg) + if parts[0] == 'un' or parts[2] == '': + return None + return parts[2] + msg = _("version() saw unexpected output from dpkg!") + LOG.error(msg) + raise exception.GuestError(msg) def pkg_remove(self, package_name, time_out): """Removes a package.""" - if self.pkg_version(package_name) == None: + if self.pkg_version(package_name) is None: return result = self._remove(package_name, time_out) diff --git a/reddwarf/guestagent/volume.py b/reddwarf/guestagent/volume.py new file mode 100644 index 0000000000..3a126d0b77 --- /dev/null +++ b/reddwarf/guestagent/volume.py @@ -0,0 +1,129 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2011 OpenStack, LLC. +# 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 logging +import os +import pexpect + +from reddwarf.common import config +from reddwarf.common import utils +from reddwarf.common.exception import GuestError +from reddwarf.common.exception import ProcessExecutionError + +TMP_MOUNT_POINT = "/mnt/volume" + +LOG = logging.getLogger(__name__) +CONFIG = config.Config + + +class VolumeHelper(object): + + @staticmethod + def _has_volume_device(device_path): + return not device_path is None + + @staticmethod + def migrate_data(device_path, mysql_base): + """ Synchronize the data from the mysql directory to the new volume """ + utils.execute("sudo", "mkdir", "-p", TMP_MOUNT_POINT) + VolumeHelper.mount(device_path, TMP_MOUNT_POINT) + if not mysql_base[-1] == '/': + mysql_base = "%s/" % mysql_base + utils.execute("sudo", "rsync", "--safe-links", "--perms", + "--recursive", "--owner", "--group", "--xattrs", + "--sparse", mysql_base, TMP_MOUNT_POINT) + VolumeHelper.unmount(device_path) + + @staticmethod + def _check_device_exists(device_path): + """Check that the device path exists. + + Verify that the device path has actually been created and can report + it's size, only then can it be available for formatting, retry + num_tries to account for the time lag. + """ + try: + num_tries = CONFIG.get('num_tries', 3) + utils.execute('sudo', 'blockdev', '--getsize64', device_path, + attempts=num_tries) + except ProcessExecutionError: + raise GuestError("InvalidDevicePath(path=%s)" % device_path) + + @staticmethod + def _check_format(device_path): + """Checks that an unmounted volume is formatted.""" + child = pexpect.spawn("sudo dumpe2fs %s" % device_path) + try: + i = child.expect(['has_journal', 'Wrong magic number']) + if i == 0: + return + volume_fstype = CONFIG.get('volume_fstype', 'ext3') + raise IOError('Device path at %s did not seem to be %s.' % + (device_path, volume_fstype)) + except pexpect.EOF: + raise IOError("Volume was not formatted.") + child.expect(pexpect.EOF) + + @staticmethod + def _format(device_path): + """Calls mkfs to format the device at device_path.""" + volume_fstype = CONFIG.get('volume_fstype', 'ext3') + format_options = CONFIG.get('format_options', '-m 5') + cmd = "sudo mkfs -t %s %s %s" % (volume_fstype, + format_options, device_path) + volume_format_timeout = CONFIG.get('volume_format_timeout', 120) + child = pexpect.spawn(cmd, timeout=volume_format_timeout) + # child.expect("(y,n)") + # child.sendline('y') + child.expect(pexpect.EOF) + + @staticmethod + def format(device_path): + """Formats the device at device_path and checks the filesystem.""" + VolumeHelper._check_device_exists(device_path) + VolumeHelper._format(device_path) + VolumeHelper._check_format(device_path) + + @staticmethod + def mount(device_path, mount_point): + if not os.path.exists(mount_point): + os.makedirs(mount_point) + volume_fstype = CONFIG.get('volume_fstype', 'ext3') + mount_options = CONFIG.get('mount_options', 'noatime') + cmd = "sudo mount -t %s -o %s %s %s" % (volume_fstype, + mount_options, + device_path, mount_point) + child = pexpect.spawn(cmd) + child.expect(pexpect.EOF) + + @staticmethod + def unmount(mount_point): + if os.path.exists(mount_point): + cmd = "sudo umount %s" % mount_point + child = pexpect.spawn(cmd) + child.expect(pexpect.EOF) + + @staticmethod + def resize_fs(device_path): + """Resize the filesystem on the specified device""" + VolumeHelper._check_device_exists(device_path) + try: + utils.execute("sudo", "resize2fs", device_path) + except ProcessExecutionError as err: + LOG.error(err) + raise GuestError("Error resizing the filesystem: %s" + % device_path) diff --git a/reddwarf/instance/models.py b/reddwarf/instance/models.py index e484e51300..b8709cf56b 100644 --- a/reddwarf/instance/models.py +++ b/reddwarf/instance/models.py @@ -23,19 +23,20 @@ import netaddr from reddwarf import db from reddwarf.common import config -#from reddwarf.guestagent import api as guest_api from reddwarf.common import exception as rd_exceptions from reddwarf.common import utils from reddwarf.instance.tasks import InstanceTask from reddwarf.instance.tasks import InstanceTasks -from novaclient.v1_1.client import Client from reddwarf.common.models import ModelBase from novaclient import exceptions as nova_exceptions -from reddwarf.common.models import NovaRemoteModelBase from reddwarf.common.remote import create_nova_client +from reddwarf.common.remote import create_nova_volume_client from reddwarf.common.remote import create_guest_client +from eventlet import greenthread + + CONFIG = config.Config LOG = logging.getLogger(__name__) @@ -45,12 +46,40 @@ def load_server(context, instance_id, server_id): client = create_nova_client(context) try: server = client.servers.get(server_id) + volumes = load_volumes(context, server_id, client=client) except nova_exceptions.NotFound, e: + LOG.debug("Could not find nova server_id(%s)" % server_id) raise rd_exceptions.ComputeInstanceNotFound(instance_id=instance_id, server_id=server_id) except nova_exceptions.ClientException, e: raise rd_exceptions.ReddwarfError(str(e)) - return server + return server, volumes + + +def load_volumes(context, server_id, client=None): + volume_support = config.Config.get("reddwarf_volume_support", 'False') + if utils.bool_from_string(volume_support): + if client is None: + client = create_nova_client(context) + volume_client = create_nova_volume_client(context) + try: + volumes = [] + if utils.bool_from_string(volume_support): + volumes_info = client.volumes.get_server_volumes(server_id) + volume_ids = [attachments.volumeId for attachments in + volumes_info] + for volume_id in volume_ids: + volume_info = volume_client.volumes.get(volume_id) + volume = {'id': volume_info.id, + 'size': volume_info.size} + volumes.append(volume) + except nova_exceptions.NotFound, e: + LOG.debug("Could not find nova server_id(%s)" % server_id) + raise rd_exceptions.VolumeAttachmentsNotFound(server_id=server_id) + except nova_exceptions.ClientException, e: + raise rd_exceptions.ReddwarfError(str(e)) + return volumes + return None # This probably should not happen here. Seems like it should @@ -71,7 +100,7 @@ def populate_databases(dbs): databases.append(mydb.serialize()) return databases except ValueError as ve: - raise exception.BadRequest(ve.message) + raise rd_exceptions.BadRequest(ve.message) class InstanceStatus(object): @@ -95,13 +124,14 @@ VALID_ACTION_STATUSES = ["ACTIVE"] class Instance(object): _data_fields = ['name', 'status', 'id', 'created', 'updated', - 'flavor', 'links', 'addresses'] + 'flavor', 'links', 'addresses', 'volume'] - def __init__(self, context, db_info, server, service_status): + def __init__(self, context, db_info, server, service_status, volumes): self.context = context self.db_info = db_info self.server = server self.service_status = service_status + self.volumes = volumes @staticmethod def load(context, id): @@ -113,11 +143,12 @@ class Instance(object): db_info = DBInstance.find_by(id=id) except rd_exceptions.NotFound: raise rd_exceptions.NotFound(uuid=id) - server = load_server(context, db_info.id, db_info.compute_instance_id) + server, volumes = load_server(context, db_info.id, + db_info.compute_instance_id) task_status = db_info.task_status service_status = InstanceServiceStatus.find_by(instance_id=id) LOG.info("service status=%s" % service_status) - return Instance(context, db_info, server, service_status) + return Instance(context, db_info, server, service_status, volumes) def delete(self, force=False): if not force and self.server.status in SERVER_INVALID_ACTION_STATUSES: @@ -140,26 +171,94 @@ class Instance(object): except nova_exceptions.ClientException, e: raise rd_exceptions.ReddwarfError() + @classmethod + def _create_volume(cls, context, db_info, volume_size): + volume_support = config.Config.get("reddwarf_volume_support", 'False') + LOG.debug(_("Volume support = %s") % volume_support) + if utils.bool_from_string(volume_support): + LOG.debug(_("Starting to create the volume for the instance")) + volume_client = create_nova_volume_client(context) + volume_desc = ("mysql volume for %s" % context.tenant) + volume_ref = volume_client.volumes.create( + volume_size, + display_name="mysql-%s" % db_info.id, + display_description=volume_desc) + #TODO(cp16net) this is bad to wait here for the volume create + # before returning but this was a quick way to get it working + # for now we need this to go into the task manager + v_ref = volume_client.volumes.get(volume_ref.id) + while not v_ref.status in ['available', 'error']: + LOG.debug(_("waiting for volume [volume.status=%s]") % + v_ref.status) + greenthread.sleep(1) + v_ref = volume_client.volumes.get(volume_ref.id) + + if v_ref.status in ['error']: + raise rd_exceptions.ReddwarfError( + _("Could not create volume")) + LOG.debug(_("Created volume %s") % v_ref) + # The mapping is in the format: + # :[]:[]:[] + # setting the delete_on_terminate instance to true=1 + mapping = "%s:%s:%s:%s" % (v_ref.id, '', v_ref.size, 1) + # TODO(rnirmal) This mapping device needs to be configurable. + # and we may have to do a little more trickery here. + # We don't know what's the next device available on the + # guest. Also in cases for ovz where this is mounted on + # the host, that's not going to work for us. + block_device = {'vdb': mapping} + volume = [{'id': v_ref.id, + 'size': v_ref.size}] + LOG.debug("block_device = %s" % block_device) + LOG.debug("volume = %s" % volume) + + device_path = CONFIG.get('device_path') + mount_point = CONFIG.get('mount_point') + LOG.debug(_("device_path = %s") % device_path) + LOG.debug(_("mount_point = %s") % mount_point) + else: + LOG.debug(_("Skipping setting up the volume")) + block_device = None + device_path = None + mount_point = None + volume = None + #end volume_support + volume_info = {'block_device': block_device, + 'device_path': device_path, + 'mount_point': mount_point} + return volume, volume_info + @classmethod def create(cls, context, name, flavor_ref, image_id, - databases, service_type): + databases, service_type, volume_size): db_info = DBInstance.create(name=name, task_status=InstanceTasks.NONE) LOG.debug(_("Created new Reddwarf instance %s...") % db_info.id) + volume, volume_info = cls._create_volume(context, + db_info, + volume_size) client = create_nova_client(context) files = {"/etc/guest_info": "guest_id=%s\nservice_type=%s\n" % (db_info.id, service_type)} server = client.servers.create(name, image_id, flavor_ref, - files=files) + files=files, + block_device_mapping=volume_info['block_device']) LOG.debug(_("Created new compute instance %s.") % server.id) + db_info.compute_instance_id = server.id db_info.save() service_status = InstanceServiceStatus.create(instance_id=db_info.id, status=ServiceStatuses.NEW) # Now wait for the response from the create to do additional work + guest = create_guest_client(context, db_info.id) - guest.prepare(databases=[], memory_mb=512, users=[]) - return Instance(context, db_info, server, service_status) + + # populate the databases + model_schemas = populate_databases(databases) + guest.prepare(512, model_schemas, users=[], + device_path=volume_info['device_path'], + mount_point=volume_info['mount_point']) + return Instance(context, db_info, server, service_status, volume) def get_guest(self): return create_guest_client(self.context, self.db_info.id) @@ -201,8 +300,8 @@ class Instance(object): if self.server.status in ["ACTIVE", "SHUTDOWN"]: return InstanceStatus.SHUTDOWN else: - LOG.error(_("While shutting down instance %s: server had status " - " %s.") % (self.id, self.server.status)) + LOG.error(_("While shutting down instance (%s): server had " + " status (%s).") % (self.id, self.server.status)) return InstanceStatus.ERROR # For everything else we can look at the service status mapping. return self.service_status.status.api_status @@ -311,7 +410,6 @@ class Instance(object): raise rd_exceptions.UnprocessableEntity(msg) - def create_server_list_matcher(server_list): # Returns a method which finds a server from the given list. def find_server(instance_id, server_id): @@ -319,6 +417,9 @@ def create_server_list_matcher(server_list): if len(matches) == 1: return matches[0] elif len(matches) < 1: + # The instance was not found in the list and + # this can happen if the instance is deleted from + # nova but still in reddwarf database raise rd_exceptions.ComputeInstanceNotFound( instance_id=instance_id, server_id=server_id) else: @@ -341,7 +442,8 @@ class Instances(object): ret = [] find_server = create_server_list_matcher(servers) for db in db_infos: - status = InstanceServiceStatus.find_by(instance_id=db.id) + LOG.debug("checking for db [id=%s, compute_instance_id=%s]" % + (db.id, db.compute_instance_id)) try: # TODO(hub-cap): Figure out if this is actually correct. # We are not sure if we should be doing some validation. @@ -350,11 +452,28 @@ class Instances(object): # nova db has compared to what we have. We should have # a way to handle this. server = find_server(db.id, db.compute_instance_id) + volumes = load_volumes(context, db.compute_instance_id) + status = InstanceServiceStatus.find_by(instance_id=db.id) + LOG.info(_("Server api_status(%s)") % + (status.status.api_status)) + + if not status.status: + LOG.info(_("Server status could not be read for " + "instance id(%s)") % (db.compute_instance_id)) + continue + if status.status.api_status in ['SHUTDOWN']: + LOG.info(_("Server was shutdown id(%s)") % + (db.compute_instance_id)) + continue except rd_exceptions.ComputeInstanceNotFound: LOG.info(_("Could not find server %s") % db.compute_instance_id) continue - ret.append(Instance(context, db, server, status)) + except ModelNotFoundError: + LOG.info(_("Status entry not found either failed to start " + "or instance was deleted")) + continue + ret.append(Instance(context, db, server, status, volumes)) return ret @@ -364,6 +483,7 @@ class DatabaseModelBase(ModelBase): @classmethod def create(cls, **values): values['id'] = utils.generate_uuid() + values['created'] = utils.utcnow() instance = cls(**values).save() if not instance.is_valid(): raise InvalidModelError(instance.errors) @@ -372,10 +492,17 @@ class DatabaseModelBase(ModelBase): def save(self): if not self.is_valid(): raise InvalidModelError(self.errors) - self['updated_at'] = utils.utcnow() - LOG.debug(_("Saving %s: %s") % (self.__class__.__name__, self.__dict__)) + self['updated'] = utils.utcnow() + LOG.debug(_("Saving %s: %s") % + (self.__class__.__name__, self.__dict__)) return db.db_api.save(self) + def delete(self): + self['updated'] = utils.utcnow() + LOG.debug(_("Deleting %s: %s") % + (self.__class__.__name__, self.__dict__)) + return db.db_api.delete(self) + def __init__(self, **kwargs): self.merge_attributes(kwargs) if not self.is_valid(): diff --git a/reddwarf/instance/service.py b/reddwarf/instance/service.py index 044e659fa6..486d7ba36d 100644 --- a/reddwarf/instance/service.py +++ b/reddwarf/instance/service.py @@ -56,7 +56,10 @@ class BaseController(wsgi.Controller): } def __init__(self): - self.add_addresses = config.Config.get('add_addresses', False) + self.add_addresses = utils.bool_from_string( + config.Config.get('add_addresses', 'False')) + self.add_volumes = utils.bool_from_string( + config.Config.get('reddwarf_volume_support', 'False')) pass def _extract_required_params(self, params, model_name): @@ -166,7 +169,7 @@ class InstanceController(BaseController): def detail(self, req, tenant_id): """Return all instances.""" LOG.info(_("req : '%s'\n\n") % req) - LOG.info(_("Detailing a database instance for tenant '%s'") % tenant_id) + LOG.info(_("Detailing database instance for tenant '%s'") % tenant_id) #TODO(cp16net) return a detailed list instead of index return self.index(req, tenant_id, detailed=True) @@ -180,7 +183,8 @@ class InstanceController(BaseController): view_cls = views.InstancesDetailView if detailed \ else views.InstancesView return wsgi.Result(view_cls(servers, - add_addresses=self.add_addresses).data(), 200) + add_addresses=self.add_addresses, + add_volumes=self.add_volumes).data(), 200) def show(self, req, tenant_id, id): """Return a single instance.""" @@ -201,7 +205,8 @@ class InstanceController(BaseController): # Adding the root history, if it exists. history = models.RootHistory.load(context=context, instance_id=id) return wsgi.Result(views.InstanceDetailView(server, roothistory=history, - add_addresses=self.add_addresses).data(), 200) + add_addresses=self.add_addresses, + add_volumes=self.add_volumes).data(), 200) def delete(self, req, tenant_id, id): """Delete a single instance.""" @@ -252,10 +257,13 @@ class InstanceController(BaseController): databases = body['instance'].get('databases') if databases is None: databases = [] + volume_size = body['instance']['volume']['size'] instance = models.Instance.create(context, name, flavor_ref, - image_id, databases, service_type) + image_id, databases, + service_type, volume_size) - return wsgi.Result(views.InstanceDetailView(instance).data(), 200) + return wsgi.Result(views.InstanceDetailView(instance, + add_volumes=self.add_volumes).data(), 200) @staticmethod def _validate_body_not_empty(body): @@ -295,12 +303,12 @@ class InstanceController(BaseController): body['instance'] body['instance']['flavorRef'] # TODO(cp16net) add in volume to the mix -# volume_size = body['instance']['volume']['size'] + volume_size = body['instance']['volume']['size'] except KeyError as e: LOG.error(_("Create Instance Required field(s) - %s") % e) raise rd_exceptions.ReddwarfError("Required element/key - %s " "was not specified" % e) -# Instance._validate_volume_size(volume_size) + InstanceController._validate_volume_size(volume_size) @staticmethod def _validate_resize_instance(body): diff --git a/reddwarf/instance/views.py b/reddwarf/instance/views.py index e499c4f238..ca2019d708 100644 --- a/reddwarf/instance/views.py +++ b/reddwarf/instance/views.py @@ -15,6 +15,9 @@ # License for the specific language governing permissions and limitations # under the License. +import logging +LOG = logging.getLogger(__name__) + def get_ip_address(addresses): if addresses is not None and \ @@ -23,14 +26,22 @@ def get_ip_address(addresses): return [addr.get('addr') for addr in addresses['private']] +def get_volumes(volumes): + LOG.debug("volumes - %s" % volumes) + if volumes is not None and len(volumes) > 0: + return {'size': volumes[0].get('size')} + + class InstanceView(object): - def __init__(self, instance, add_addresses=False): + def __init__(self, instance, add_addresses=False, add_volumes=False): self.instance = instance self.add_addresses = add_addresses + self.add_volumes = add_volumes def data(self): ip = get_ip_address(self.instance.addresses) + volumes = get_volumes(self.instance.volumes) instance_dict = { "id": self.instance.id, "name": self.instance.name, @@ -39,6 +50,9 @@ class InstanceView(object): } if self.add_addresses and ip is not None and len(ip) > 0: instance_dict['ip'] = ip + if self.add_volumes and volumes is not None: + instance_dict['volume'] = volumes + LOG.debug(instance_dict) return {"instance": instance_dict} @@ -61,9 +75,10 @@ class InstanceDetailView(InstanceView): class InstancesView(object): - def __init__(self, instances, add_addresses=False): + def __init__(self, instances, add_addresses=False, add_volumes=False): self.instances = instances self.add_addresses = add_addresses + self.add_volumes = add_volumes def data(self): data = [] @@ -81,4 +96,5 @@ class InstancesDetailView(InstancesView): def data_for_instance(self, instance): return InstanceDetailView(instance, - self.add_addresses).data()['instance'] + self.add_addresses, + self.add_volumes).data()['instance'] diff --git a/reddwarf/tests/fakes/guestagent.py b/reddwarf/tests/fakes/guestagent.py index d010b79c23..6b3f21d667 100644 --- a/reddwarf/tests/fakes/guestagent.py +++ b/reddwarf/tests/fakes/guestagent.py @@ -69,9 +69,11 @@ class FakeGuest(object): def list_users(self): return [self.users[name] for name in self.users] - def prepare(self, memory_mb, databases, users): + def prepare(self, databases, memory_mb, users, device_path=None, + mount_point=None): from reddwarf.instance.models import InstanceServiceStatus from reddwarf.instance.models import ServiceStatuses + def update_db(): status = InstanceServiceStatus.find_by(instance_id=self.id) status.status = ServiceStatuses.RUNNING @@ -87,6 +89,7 @@ class FakeGuest(object): def start_mysql_with_conf_changes(self, updated_memory_size): from reddwarf.instance.models import InstanceServiceStatus from reddwarf.instance.models import ServiceStatuses + def update_db(): status = InstanceServiceStatus.find_by(instance_id=self.id) status.status = ServiceStatuses.RUNNING @@ -96,6 +99,7 @@ class FakeGuest(object): def stop_mysql(self): from reddwarf.instance.models import InstanceServiceStatus from reddwarf.instance.models import ServiceStatuses + def update_db(): status = InstanceServiceStatus.find_by(instance_id=self.id) status.status = ServiceStatuses.SHUTDOWN diff --git a/reddwarf/tests/fakes/nova.py b/reddwarf/tests/fakes/nova.py index 46b5f87477..9aa2981f7d 100644 --- a/reddwarf/tests/fakes/nova.py +++ b/reddwarf/tests/fakes/nova.py @@ -96,7 +96,7 @@ class FakeServer(object): @property def addresses(self): - return {"private":[{"addr":"123.123.123.123"}]} + return {"private": [{"addr":"123.123.123.123"}]} def delete(self): self.schedule_status = [] @@ -194,3 +194,7 @@ class FakeClient(object): def fake_create_nova_client(context): return FakeClient(context) + + +def fake_create_nova_volume_client(context): + return FakeClient(context)