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: I51e43065c7b0ed2a59f44aca5ab9c06a8a858945changes/91/134891/1
parent
7caf4f39db
commit
e211f7efef
|
@ -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"
|
||||
|
|
|
@ -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)
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -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()
|
|
@ -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']
|
Loading…
Reference in New Issue