Sync image when first instance launched

When instance launched in the cascaded openstack and no corresponding
image found, then the image will be synced to cascaded firstly, then
vm is launched by this image.
Next time when launching instance using the same image, there is no
need to sync.

Implements: blueprint sync-image-first-vm-launch
Change-Id: I51e43065c7b0ed2a59f44aca5ab9c06a8a858945
This commit is contained in:
joey5678 2014-11-17 17:46:29 +08:00
parent 7caf4f39db
commit e211f7efef
5 changed files with 306 additions and 0 deletions

View File

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

View File

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

View File

@ -0,0 +1 @@

View File

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

View File

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