diff --git a/novaproxy/nova/compute/manager_proxy.py b/novaproxy/nova/compute/manager_proxy.py index 609b7520..68ea46b7 100644 --- a/novaproxy/nova/compute/manager_proxy.py +++ b/novaproxy/nova/compute/manager_proxy.py @@ -35,6 +35,7 @@ import nova.context from nova import exception from nova import hooks from nova.image import glance +from nova.image import cascading from nova import manager from nova import network from nova.network import model as network_model @@ -2743,6 +2744,11 @@ class ComputeManager(manager.Manager): cfg.CONF.cascaded_glance_url): cascaded_image_uuid = location['url'].split('/')[-1] return cascaded_image_uuid + #lazy sync image + sync_service = cascading.GlanceCascadingService() + return sync_service.sync_image(context, + cfg.CONF.cascaded_glance_url, + image) except Exception: with excutils.save_and_reraise_exception(): LOG.error(_("Error while trying to get cascaded" diff --git a/novaproxy/nova/image/cascading.py b/novaproxy/nova/image/cascading.py new file mode 100644 index 00000000..9806a607 --- /dev/null +++ b/novaproxy/nova/image/cascading.py @@ -0,0 +1,186 @@ +"""Implementation of an cascading image service that uses to sync the image + from cascading glance to the special cascaded glance. + """ +import logging +import os +import urlparse + +from oslo.config import cfg + +from nova.openstack.common.gettextutils import _ +from nova.image import glance +from nova.image.sync import drivers as drivermgr + + +CONF = cfg.CONF +LOG = logging.getLogger(__name__) + +glance_cascading_opt = [ + cfg.StrOpt('image_copy_dest_location_url', + default='file:///var/lib/glance/images', + help=_("The path cascaded image_data copy to."), + deprecated_opts=[cfg.DeprecatedOpt('dest_location_url', + group='DEFAULT')]), + cfg.StrOpt('image_copy_dest_host', + default='127.0.0.1', + help=_("The host name where image_data copy to."), + deprecated_opts=[cfg.DeprecatedOpt('dest_host', + group='DEFAULT')]), + cfg.StrOpt('image_copy_dest_user', + default='glance', + help=_("The user name of cascaded glance for copy."), + deprecated_opts=[cfg.DeprecatedOpt('dest_user', + group='DEFAULT')]), + cfg.StrOpt('image_copy_dest_password', + default='openstack', + help=_("The passowrd of cascaded glance for copy."), + deprecated_opts=[cfg.DeprecatedOpt('dest_password', + group='DEFAULT')]), + cfg.StrOpt('image_copy_source_location_url', + default='file:///var/lib/glance/images', + help=_("where the cascaded image data from"), + deprecated_opts=[cfg.DeprecatedOpt('source_location_url', + group='DEFAULT')]), + cfg.StrOpt('image_copy_source_host', + default='0.0.0.1', + help=_("The host name where image_data copy from."), + deprecated_opts=[cfg.DeprecatedOpt('source_host', + group='DEFAULT')]), + cfg.StrOpt('image_copy_source_user', + default='glance', + help=_("The user name of glance for copy."), + deprecated_opts=[cfg.DeprecatedOpt('source_user', + group='DEFAULT')]), + cfg.StrOpt('image_copy_source_password', + default='openstack', + help=_("The passowrd of glance for copy."), + deprecated_opts=[cfg.DeprecatedOpt('source_password', + group='DEFAULT')]), + ] + +CONF.register_opts(glance_cascading_opt) + +_V2_IMAGE_CREATE_PROPERTIES = ['container_format', 'disk_format', 'min_disk', + 'min_ram', 'name', 'protected'] + + +def get_adding_image_properties(image): + _tags = list(image.tags) or [] + kwargs = {} + for key in _V2_IMAGE_CREATE_PROPERTIES: + try: + value = getattr(image, key, None) + if value and value != 'None': + kwargs[key] = value + except KeyError: + pass + if _tags: + kwargs['tags'] = _tags + return kwargs + + +def get_candidate_path(image, scheme='file'): + locations = image.locations or [] + for loc in locations: + if loc['url'].startswith(scheme): + return loc['url'] if scheme != 'file' \ + else loc['url'][len('file://'):] + return None + + +def get_copy_driver(scheme_key): + return drivermgr.get_store_driver(scheme_key) + + +def get_host_port(url): + if not url: + return None, None + pieces = urlparse.urlparse(url) + return pieces.netloc.split(":")[0], pieces.netloc.split(":")[1] + + +class GlanceCascadingService(object): + + def __init__(self, cascading_client=None): + self._client = cascading_client or glance.GlanceClientWrapper() + + def sync_image(self, context, cascaded_url, cascading_image): + cascaded_glance_url = cascaded_url + _host, _port = get_host_port(cascaded_glance_url) + _cascaded_client = glance.GlanceClientWrapper(context=context, + host=_host, + port=_port, + version=2) + + image_meta = get_adding_image_properties(cascading_image) + cascaded_image = _cascaded_client.call(context, 2, 'create', + **image_meta) + image_id = cascading_image.id + cascaded_id = cascaded_image.id + candidate_path = get_candidate_path(cascading_image) + LOG.debug("the candidate path is %s." % (candidate_path)) + # copy image + try: + image_loc = self._copy_data(image_id, cascaded_id, candidate_path) + except Exception as e: + LOG.exception(_("copy image failed, reason=%s") % e) + raise + else: + if not image_loc: + LOG.exception(_("copy image Exception, no cascaded_loc")) + try: + # patch loc to the cascaded image + csd_locs = [{'url': image_loc, + 'metadata': {} + }] + _cascaded_client.call(context, 2, 'update', cascaded_id, + remove_props=None, + locations=csd_locs) + except Exception as e: + LOG.exception(_("patch loc to cascaded image Exception, reason: %s" + % e)) + raise + + try: + # patch glance-loc to cascading image + csg_locs = cascading_image.locations + glance_loc = '%s/v2/images/%s' % (cascaded_glance_url, + cascaded_id) + csg_locs.append({'url': glance_loc, + 'metadata': {'image_id': str(cascaded_id), + 'action': 'upload' + } + }) + self._client.call(context, 2, 'update', image_id, + remove_props=None, locations=csg_locs) + except Exception as e: + LOG.exception(_("patch loc to cascading image Exception, reason: %s" + % e)) + raise + + return cascaded_id + + @staticmethod + def _copy_data(cascading_id, cascaded_id, candidate_path): + source_pieces = urlparse.urlparse(CONF.image_copy_source_location_url) + dest_pieces = urlparse.urlparse(CONF.image_copy_dest_location_url) + source_scheme = source_pieces.scheme + dest_scheme = dest_pieces.scheme + _key = ('%s:%s' % (source_scheme, dest_scheme)) + copy_driver = get_copy_driver(_key) + source_path = os.path.join(source_pieces.path, cascading_id) + dest_path = os.path.join(dest_pieces.path, cascaded_id) + + source_location = {'host': CONF.image_copy_source_host, + 'login_user': CONF.image_copy_source_user, + 'login_password': CONF.image_copy_source_password, + 'path': source_path + } + dest_location = {'host': CONF.image_copy_dest_host, + 'login_user': CONF.image_copy_dest_user, + 'login_password': CONF.image_copy_dest_password, + 'path': dest_path + } + return copy_driver.copy_to(source_location, + dest_location, + candidate_path=candidate_path) diff --git a/novaproxy/nova/image/sync/__init__.py b/novaproxy/nova/image/sync/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/novaproxy/nova/image/sync/__init__.py @@ -0,0 +1 @@ + diff --git a/novaproxy/nova/image/sync/drivers/__init__.py b/novaproxy/nova/image/sync/drivers/__init__.py new file mode 100644 index 00000000..43d41e48 --- /dev/null +++ b/novaproxy/nova/image/sync/drivers/__init__.py @@ -0,0 +1,12 @@ +import nova.image.sync.drivers.filesystem + + +_store_drivers_map = { + 'file:file':filesystem.Store + +} + + +def get_store_driver(scheme_key): + cls = _store_drivers_map.get(scheme_key) + return cls() \ No newline at end of file diff --git a/novaproxy/nova/image/sync/drivers/filesystem.py b/novaproxy/nova/image/sync/drivers/filesystem.py new file mode 100644 index 00000000..6063d4a1 --- /dev/null +++ b/novaproxy/nova/image/sync/drivers/filesystem.py @@ -0,0 +1,101 @@ +import logging +import sys + +from oslo.config import cfg +import pxssh +import pexpect + +from nova.openstack.common.gettextutils import _ + +LOG = logging.getLogger(__name__) + +CONF = cfg.CONF + +sync_opt = [ + cfg.IntOpt('scp_copy_timeout', default=3600, + help=_('when snapshot, max wait (second)time for snapshot ' + 'status become active.'), + deprecated_opts=[cfg.DeprecatedOpt('scp_copy_timeout', + group='DEFAULT')]), + ] +CONF.register_opts(sync_opt, group='sync') + + +def _get_ssh(hostname, username, password): + s = pxssh.pxssh() + s.login(hostname, username, password, original_prompt='[#$>]') + s.logfile = sys.stdout + return s + + +class Store(object): + + def copy_to(self, from_location, to_location, candidate_path=None): + + from_store_loc = from_location + to_store_loc = to_location + LOG.debug(_('from_store_loc is: %s'), from_store_loc) + + if from_store_loc['host'] == to_store_loc['host'] and \ + from_store_loc['path'] == to_store_loc['path']: + + LOG.info(_('The from_loc is same to to_loc, no need to copy. the ' + 'host:path is %s:%s') % (from_store_loc['host'], + from_store_loc['path'])) + return 'file://%s' % to_store_loc['path'] + + to_host = r"""{username}@{host}""".format( + username=to_store_loc['login_user'], + host=to_store_loc['host']) + + to_path = r"""{to_host}:{path}""".format(to_host=to_host, + path=to_store_loc['path']) + + copy_path = from_store_loc['path'] + + try: + from_ssh = _get_ssh(from_store_loc['host'], + from_store_loc['login_user'], + from_store_loc['login_password']) + except Exception: + LOG.exception("ssh login failed.") + raise + + from_ssh.sendline('ls %s' % copy_path) + from_ssh.prompt() + if 'cannot access' in from_ssh.before or \ + 'No such file' in from_ssh.before: + if candidate_path: + from_ssh.sendline('ls %s' % candidate_path) + from_ssh.prompt() + if 'cannot access' not in from_ssh.before and \ + 'No such file' not in from_ssh.before: + copy_path = candidate_path + else: + msg = _("the image path for copy to is not exists, file copy" + "failed: path is %s" % (copy_path)) + LOG.exception(msg) + raise + + from_ssh.sendline('scp -P 22 %s %s' % (copy_path, to_path)) + while True: + scp_index = from_ssh.expect(['.yes/no.', '.assword:.', + pexpect.TIMEOUT]) + if scp_index == 0: + from_ssh.sendline('yes') + from_ssh.prompt() + elif scp_index == 1: + from_ssh.sendline(to_store_loc['login_password']) + from_ssh.prompt(timeout=CONF.sync.scp_copy_timeout) + break + else: + msg = _("scp commond execute failed, with copy_path %s and " + "to_path %s" % (copy_path, to_path)) + LOG.exception(msg) + raise + break + + if from_ssh: + from_ssh.logout() + + return 'file://%s' % to_store_loc['path']