No longer require a volume.

* Added a VolumeCreationFailure exception, which is raised on Instance creation if the volume doesn't prov.
* Casted the flavorid in the flavor view code to a string to ensure it works in fake mode.
* Changed the Guest code to save the volume info in fstab.
* Split the VolumeHelper into VolumeDevice and VolumeMountPoint classes.
* Changed fake mode to work when no volume is given.
This commit is contained in:
Tim Simpson 2012-05-08 15:28:54 -05:00
parent 3103efbb43
commit 532633cd4b
8 changed files with 201 additions and 98 deletions

View File

@ -108,3 +108,8 @@ class VolumeAttachmentsNotFound(NotFound):
message = _("Cannot find the volumes attached to compute "
"instance %(server_id)")
class VolumeCreationFailure(ReddwarfError):
message = _("Failed to create a volume in Nova.")

View File

@ -40,7 +40,8 @@ instances = Table('instances', meta,
Column('compute_instance_id', String(36)),
Column('task_id', Integer()),
Column('task_description', String(32)),
Column('task_start_time', DateTime()))
Column('task_start_time', DateTime()),
Column('volume_id', String(36)))
def upgrade(migrate_engine):

View File

@ -40,7 +40,7 @@ class FlavorView(object):
detailed = '/detail'
splitpath.pop(-1)
flavorid = self.flavor.id
if splitpath[-1] == flavorid:
if str(splitpath[-1]) == str(flavorid):
splitpath.pop(-1)
href_template = "%(scheme)s://%(endpoint)s%(path)s/%(flavorid)s"
for link in self.flavor.links:

View File

@ -46,7 +46,7 @@ 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.guestagent.volume import VolumeDevice
from reddwarf.instance import models as rd_models
@ -503,17 +503,16 @@ class DBaaSAgent(object):
app = MySqlApp(self.status)
restart_mysql = False
if device_path:
VolumeHelper.format(device_path)
device = VolumeDevice(device_path)
device.format()
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)
device.migrate_data(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
device.mount(mount_point)
LOG.debug(_("Mounted the volume."))
#check mysql was installed and stopped
if restart_mysql:

View File

@ -30,26 +30,24 @@ LOG = logging.getLogger(__name__)
CONFIG = config.Config
class VolumeHelper(object):
class VolumeDevice(object):
@staticmethod
def _has_volume_device(device_path):
return not device_path is None
def __init__(self, device_path):
self.device_path = device_path
@staticmethod
def migrate_data(device_path, mysql_base):
def migrate_data(self, mysql_base):
""" Synchronize the data from the mysql directory to the new volume """
# Use sudo to have access to this spot.
utils.execute("sudo", "mkdir", "-p", TMP_MOUNT_POINT)
VolumeHelper.mount(device_path, TMP_MOUNT_POINT)
self._tmp_mount(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)
self.unmount()
@staticmethod
def _check_device_exists(device_path):
def _check_device_exists(self):
"""Check that the device path exists.
Verify that the device path has actually been created and can report
@ -58,72 +56,100 @@ class VolumeHelper(object):
"""
try:
num_tries = CONFIG.get('num_tries', 3)
utils.execute('sudo', 'blockdev', '--getsize64', device_path,
utils.execute('sudo', 'blockdev', '--getsize64', self.device_path,
attempts=num_tries)
except ProcessExecutionError:
raise GuestError("InvalidDevicePath(path=%s)" % device_path)
raise GuestError("InvalidDevicePath(path=%s)" % self.device_path)
@staticmethod
def _check_format(device_path):
def _check_format(self):
"""Checks that an unmounted volume is formatted."""
child = pexpect.spawn("sudo dumpe2fs %s" % device_path)
child = pexpect.spawn("sudo dumpe2fs %s" % self.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))
(self.device_path, volume_fstype))
except pexpect.EOF:
raise IOError("Volume was not formatted.")
child.expect(pexpect.EOF)
@staticmethod
def _format(device_path):
def _format(self):
"""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)
format_options, self.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):
def format(self):
"""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)
self._check_device_exists()
self._format()
self._check_format()
@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)
def mount(self, mount_point):
"""Mounts, and writes to fstab."""
mount_point = VolumeMountPoint(self.device_path, mount_point)
mount_point.mount()
mount_point.write_to_fstab()
@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):
#TODO(tim.simpson): Are we using this?
def resize_fs(self):
"""Resize the filesystem on the specified device"""
VolumeHelper._check_device_exists(device_path)
self._check_device_exists()
try:
utils.execute("sudo", "resize2fs", device_path)
utils.execute("sudo", "resize2fs", self.device_path)
except ProcessExecutionError as err:
LOG.error(err)
raise GuestError("Error resizing the filesystem: %s"
% device_path)
% self.device_path)
def _tmp_mount(self, mount_point):
"""Mounts, but doesn't save to fstab."""
mount_point = VolumeMountPoint(self.device_path, mount_point)
mount_point.mount() # Don't save to fstab.
def unmount(self):
if os.path.exists(self.device_path):
cmd = "sudo umount %s" % self.device_path
child = pexpect.spawn(cmd)
child.expect(pexpect.EOF)
class VolumeMountPoint(object):
def __init__(self, device_path, mount_point):
self.device_path = device_path
self.mount_point = mount_point
self.volume_fstype = CONFIG.get('volume_fstype', 'ext3')
self.mount_options = CONFIG.get('mount_options', 'defaults,noatime')
def mount(self):
if not os.path.exists(self.mount_point):
os.makedirs(self.mount_point)
LOG.debug("Adding volume. Device path:%s, mount_point:%s, "
"volume_type:%s, mount options:%s" %
(self.device_path, self.mount_point, self.volume_fstype,
self.mount_options))
cmd = "sudo mount -t %s -o %s %s %s" % (self.volume_fstype,
self.mount_options, self.device_path, self.mount_point)
child = pexpect.spawn(cmd)
child.expect(pexpect.EOF)
def write_to_fstab(self):
fstab_line = "%s\t%s\t%s\t%s\t0\t0" % (self.device_path,
self.mount_point, self.volume_fstype, self.mount_options)
LOG.debug("Writing new line to fstab:%s" % fstab_line)
utils.execute("sudo", "cp", "/etc/fstab", "/etc/fstab.orig")
utils.execute("sudo", "cp", "/etc/fstab", "/tmp/newfstab")
utils.execute("sudo", "chmod", "666", "/tmp/newfstab")
with open("/tmp/newfstab", 'a') as new_fstab:
new_fstab.write("\n" + fstab_line)
utils.execute("sudo", "chmod", "640", "/tmp/newfstab")
utils.execute("sudo", "mv", "/tmp/newfstab", "/etc/fstab")

View File

@ -43,7 +43,7 @@ CONFIG = config.Config
LOG = logging.getLogger(__name__)
def load_server(context, instance_id, server_id):
def load_server_with_volumes(context, instance_id, server_id):
"""Loads a server or raises an exception."""
client = create_nova_client(context)
try:
@ -168,8 +168,8 @@ class Instance(object):
db_info = DBInstance.find_by(id=id)
except rd_exceptions.NotFound:
raise rd_exceptions.NotFound(uuid=id)
server, volumes = load_server(context, db_info.id,
db_info.compute_instance_id)
server, volumes = load_server_with_volumes(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)
@ -208,6 +208,9 @@ class Instance(object):
volume_size,
display_name="mysql-%s" % db_info.id,
display_description=volume_desc)
# Record the volume ID in case something goes wrong.
db_info.volume_id = volume_ref.id
db_info.save()
#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
@ -219,8 +222,7 @@ class Instance(object):
v_ref = volume_client.volumes.get(volume_ref.id)
if v_ref.status in ['error']:
raise rd_exceptions.ReddwarfError(
_("Could not create volume"))
raise rd_exceptions.VolumeCreationFailure()
LOG.debug(_("Created volume %s") % v_ref)
# The mapping is in the format:
# <id>:[<type>]:[<size(GB)>]:[<delete_on_terminate>]
@ -232,10 +234,10 @@ class Instance(object):
# 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,
volumes = [{'id': v_ref.id,
'size': v_ref.size}]
LOG.debug("block_device = %s" % block_device)
LOG.debug("volume = %s" % volume)
LOG.debug("volume = %s" % volumes)
device_path = CONFIG.get('device_path')
mount_point = CONFIG.get('mount_point')
@ -246,12 +248,16 @@ class Instance(object):
block_device = None
device_path = None
mount_point = None
volume = None
volumes = None
#end volume_support
#block_device = ""
#device_path = /dev/vdb
#mount_point = /var/lib/mysql
volume_info = {'block_device': block_device,
'device_path': device_path,
'mount_point': mount_point}
return volume, volume_info
'mount_point': mount_point,
'volumes': volumes}
return volume_info
@classmethod
def create(cls, context, name, flavor_ref, image_id,
@ -259,15 +265,25 @@ class Instance(object):
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)
if volume_size:
volume_info = cls._create_volume(context, db_info, volume_size)
block_device_mapping = volume_info['block_device']
device_path=volume_info['device_path']
mount_point=volume_info['mount_point']
volumes = volume_info['volumes']
else:
block_device_mapping = None
device_path=None
mount_point=None
volumes = []
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,
block_device_mapping=volume_info['block_device'])
block_device_mapping=block_device_mapping)
LOG.debug(_("Created new compute instance %s.") % server.id)
db_info.compute_instance_id = server.id
@ -281,9 +297,9 @@ class Instance(object):
# 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)
device_path=device_path,
mount_point=mount_point)
return Instance(context, db_info, server, service_status, volumes)
def get_guest(self):
return create_guest_client(self.context, self.db_info.id)
@ -377,9 +393,10 @@ class Instance(object):
def _refresh_compute_server_info(self):
"""Refreshes the compute server field."""
server = load_server(self.context, self.db_info.id,
self.db_info.compute_instance_id)
server, volumes = load_server_with_volumes(self.context,
self.db_info.id, self.db_info.compute_instance_id)
self.server = server
self.volumes = volumes
return server
def resize_flavor(self, new_flavor_id):
@ -631,7 +648,8 @@ class DBInstance(DatabaseModelBase):
#TODO(tim.simpson): Add start time.
_data_fields = ['name', 'created', 'compute_instance_id',
'task_id', 'task_description', 'task_start_time']
'task_id', 'task_description', 'task_start_time',
'volume_id']
def __init__(self, task_status=None, **kwargs):
kwargs["task_id"] = task_status.code

View File

@ -262,7 +262,10 @@ class InstanceController(BaseController):
databases = body['instance'].get('databases')
if databases is None:
databases = []
volume_size = body['instance']['volume']['size']
if body['instance'].get('volume', None) is not None:
volume_size = body['instance']['volume']['size']
else:
volume_size = None
instance = models.Instance.create(context, name, flavor_ref,
image_id, databases,
service_type, volume_size)
@ -284,8 +287,8 @@ class InstanceController(BaseController):
volume_size = float(size)
except (ValueError, TypeError) as err:
LOG.error(err)
msg = ("Required element/key - instance volume"
"'size' was not specified as a number")
msg = ("Required element/key - instance volume 'size' was not "
"specified as a number (value was %s)." % size)
raise rd_exceptions.ReddwarfError(msg)
if int(volume_size) != volume_size or int(volume_size) < 1:
msg = ("Volume 'size' needs to be a positive "
@ -308,12 +311,13 @@ class InstanceController(BaseController):
body['instance']
body['instance']['flavorRef']
# TODO(cp16net) add in volume to the mix
volume_size = body['instance']['volume']['size']
if 'volume' in body['instance'] and \
body['instance']['volume'] is not None:
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)
InstanceController._validate_volume_size(volume_size)
@staticmethod
def _validate_resize_instance(body):

View File

@ -59,6 +59,7 @@ class FakeFlavors(object):
self._add(3, 10, "m1.medium", 4096)
self._add(4, 10, "m1.large", 8192)
self._add(5, 10, "m1.xlarge", 16384)
self._add(6, 0, "tinier", 506)
def _add(self, *args, **kwargs):
new_flavor = FakeFlavor(*args, **kwargs)
@ -86,7 +87,7 @@ class FakeFlavors(object):
class FakeServer(object):
def __init__(self, parent, owner, id, name, image_id, flavor_ref,
block_device_mapping):
block_device_mapping, volumes):
self.owner = owner # This is a context.
self.id = id
self.parent = parent
@ -95,8 +96,7 @@ class FakeServer(object):
self.flavor_ref = flavor_ref
self.events = EventSimulator()
self.schedule_status("BUILD", 0.0)
LOG.debug("block_device_mapping = %s" % block_device_mapping)
self.block_device_mapping = block_device_mapping
self.volumes = volumes
@property
def addresses(self):
@ -176,13 +176,32 @@ class FakeServers(object):
def create(self, name, image_id, flavor_ref, files, block_device_mapping):
id = "FAKE_%d" % self.next_id
self.next_id += 1
volumes = self._get_volumes_from_bdm(block_device_mapping)
server = FakeServer(self, self.context, id, name, image_id, flavor_ref,
block_device_mapping)
block_device_mapping, volumes)
self.db[id] = server
server.schedule_status("ACTIVE", 1)
LOG.info("FAKE_SERVERS_DB : %s" % str(FAKE_SERVERS_DB))
return server
def _get_volumes_from_bdm(self, block_device_mapping):
volumes = []
if block_device_mapping is not None:
# block_device_mapping is a dictionary, where the key is the
# device name on the compute instance and the mapping info is a
# set of fields in a string, seperated by colons.
# For each device, find the volume, and record the mapping info
# to another fake object and attach it to the volume
# so that the fake API can later retrieve this.
for device in block_device_mapping:
mapping = block_device_mapping[device]
(id, _type, size, delete_on_terminate) = mapping.split(":")
volume = self.volumes.get(id)
volume.mapping = FakeBlockDeviceMappingInfo(id, device,
_type, size, delete_on_terminate)
volumes.append(volume)
return volumes
def get(self, id):
if id not in self.db:
LOG.error("Couldn't find server id %s, collection=%s" % (id,
@ -194,6 +213,11 @@ class FakeServers(object):
else:
raise nova_exceptions.NotFound(404, "Bad permissions")
def get_server_volumes(self, server_id):
return [volume.mapping
for volume in self.get(server_id).volumes
if volume.mapping is not None]
def list(self):
return [v for (k, v) in self.db.items() if self.can_see(v.id)]
@ -223,20 +247,8 @@ class FakeServerVolumes(object):
return [ServerVolumes(server.block_device_mapping)]
FLAVORS = FakeFlavors()
class FakeClient(object):
def __init__(self, context):
self.context = context
self.flavors = FLAVORS
self.servers = FakeServers(context, self.flavors)
self.volumes = FakeServerVolumes(context)
def fake_create_nova_client(context):
return FakeClient(context)
class FakeVolume(object):
@ -268,6 +280,16 @@ class FakeVolume(object):
return self._current_status
class FakeBlockDeviceMappingInfo(object):
def __init__(self, id, device, _type, size, delete_on_terminate):
self.volumeId = id
self.device = device
self.type = _type
self.size = size
self.delete_on_terminate = delete_on_terminate
FAKE_VOLUMES_DB = {}
@ -307,12 +329,40 @@ class FakeVolumes(object):
return volume
class FakeVolumeClient(object):
FLAVORS = FakeFlavors()
class FakeClient(object):
def __init__(self, context):
self.context = context
self.flavors = FLAVORS
self.servers = FakeServers(context, self.flavors)
self.volumes = FakeVolumes(context)
self.servers.volumes = self.volumes
def get_server_volumes(self, server_id):
return self.servers.get_server_volumes(server_id)
CLIENT_DATA = {}
def get_client_data(context):
if context not in CLIENT_DATA:
nova_client = FakeClient(context)
volume_client = FakeClient(context)
nova_client.volumes = volume_client
volume_client.servers = nova_client
CLIENT_DATA[context] = {
'nova': nova_client,
'volume': volume_client
}
return CLIENT_DATA[context]
def fake_create_nova_client(context):
return get_client_data(context)['nova']
def fake_create_nova_volume_client(context):
return FakeVolumeClient(context)
return get_client_data(context)['volume']