Merge "Add multi-store support"

This commit is contained in:
Zuul 2018-07-25 00:39:14 +00:00 committed by Gerrit Code Review
commit ac378e0254
3 changed files with 113 additions and 11 deletions

View File

@ -95,6 +95,7 @@ class ShellV2Test(testtools.TestCase):
# dict directly, it throws an AttributeError.
class Args(object):
def __init__(self, entries):
self.backend = None
self.__dict__.update(entries)
return Args(args)
@ -1178,7 +1179,8 @@ class ShellV2Test(testtools.TestCase):
utils.get_data_file = mock.Mock(return_value='testfile')
mocked_upload.return_value = None
test_shell.do_image_upload(self.gc, args)
mocked_upload.assert_called_once_with('IMG-01', 'testfile', 1024)
mocked_upload.assert_called_once_with('IMG-01', 'testfile', 1024,
backend=None)
@mock.patch('glanceclient.common.utils.exit')
def test_neg_image_import_not_available(self, mock_utils_exit):
@ -1339,7 +1341,7 @@ class ShellV2Test(testtools.TestCase):
mock_import.return_value = None
test_shell.do_image_import(self.gc, args)
mock_import.assert_called_once_with(
'IMG-01', 'glance-direct', None)
'IMG-01', 'glance-direct', None, backend=None)
def test_image_import_web_download(self):
args = self._make_args(
@ -1357,7 +1359,7 @@ class ShellV2Test(testtools.TestCase):
test_shell.do_image_import(self.gc, args)
mock_import.assert_called_once_with(
'IMG-01', 'web-download',
'http://example.com/image.qcow')
'http://example.com/image.qcow', backend=None)
@mock.patch('glanceclient.common.utils.print_image')
def test_image_import_no_print_image(self, mocked_utils_print_image):
@ -1375,7 +1377,7 @@ class ShellV2Test(testtools.TestCase):
mock_import.return_value = None
test_shell.do_image_import(self.gc, args)
mock_import.assert_called_once_with(
'IMG-02', 'glance-direct', None)
'IMG-02', 'glance-direct', None, backend=None)
mocked_utils_print_image.assert_not_called()
def test_image_download(self):

View File

@ -218,16 +218,21 @@ class Controller(object):
return utils.IterableWithLength(body, content_length), resp
@utils.add_req_id_to_object()
def upload(self, image_id, image_data, image_size=None, u_url=None):
def upload(self, image_id, image_data, image_size=None, u_url=None,
backend=None):
"""Upload the data for an image.
:param image_id: ID of the image to upload data for.
:param image_data: File-like object supplying the data to upload.
:param image_size: Unused - present for backwards compatibility
:param u_url: Upload url to upload the data to.
:param backend: Backend store to upload image to.
"""
url = u_url or '/v2/images/%s/file' % image_id
hdrs = {'Content-Type': 'application/octet-stream'}
if backend is not None:
hdrs['x-image-meta-store'] = backend
body = image_data
resp, body = self.http_client.put(url, headers=hdrs, data=body)
return (resp, body), resp
@ -239,6 +244,13 @@ class Controller(object):
resp, body = self.http_client.get(url)
return body, resp
@utils.add_req_id_to_object()
def get_stores_info(self):
"""Get available stores info from discovery endpoint."""
url = '/v2/info/stores'
resp, body = self.http_client.get(url)
return body, resp
@utils.add_req_id_to_object()
def stage(self, image_id, image_data, image_size=None):
"""Upload the data to image staging.
@ -254,17 +266,22 @@ class Controller(object):
return body, resp
@utils.add_req_id_to_object()
def image_import(self, image_id, method='glance-direct', uri=None):
def image_import(self, image_id, method='glance-direct', uri=None,
backend=None):
"""Import Image via method."""
headers = {}
url = '/v2/images/%s/import' % image_id
data = {'method': {'name': method}}
if backend is not None:
headers['x-image-meta-store'] = backend
if uri:
if method == 'web-download':
data['method']['uri'] = uri
else:
raise exc.HTTPBadRequest('URI is only supported with method: '
'"web-download"')
resp, body = self.http_client.post(url, data=data)
resp, body = self.http_client.post(url, data=data, headers=headers)
return body, resp
@utils.add_req_id_to_object()
@ -277,7 +294,11 @@ class Controller(object):
@utils.add_req_id_to_object()
def create(self, **kwargs):
"""Create an image."""
headers = {}
url = '/v2/images'
backend = kwargs.pop('backend', None)
if backend is not None:
headers['x-image-meta-store'] = backend
image = self.model()
for (key, value) in kwargs.items():
@ -286,7 +307,7 @@ class Controller(object):
except warlock.InvalidOperation as e:
raise TypeError(encodeutils.exception_to_unicode(e))
resp, body = self.http_client.post(url, data=image)
resp, body = self.http_client.post(url, headers=headers, data=image)
# NOTE(esheffield): remove 'self' for now until we have an elegant
# way to pass it into the model constructor without conflict
body.pop('self', None)

View File

@ -60,6 +60,9 @@ def get_image_schema():
'passed to the client via stdin.'))
@utils.arg('--progress', action='store_true', default=False,
help=_('Show upload progress bar.'))
@utils.arg('--backend', metavar='<STORE>',
default=utils.env('OS_IMAGE_BACKEND', default=None),
help='Backend store to upload image to.')
@utils.on_data_require_fields(DATA_FIELDS)
def do_image_create(gc, args):
"""Create a new image."""
@ -75,13 +78,25 @@ def do_image_create(gc, args):
key, value = datum.split('=', 1)
fields[key] = value
backend = args.backend
file_name = fields.pop('file', None)
using_stdin = not sys.stdin.isatty()
if args.backend and not (file_name or using_stdin):
utils.exit("--backend option should only be provided with --file "
"option or stdin.")
if backend:
# determine if backend is valid
_validate_backend(backend, gc)
if file_name is not None and os.access(file_name, os.R_OK) is False:
utils.exit("File %s does not exist or user does not have read "
"privileges to it" % file_name)
image = gc.images.create(**fields)
try:
if utils.get_data_file(args) is not None:
backend = fields.get('backend', None)
args.id = image['id']
args.size = None
do_image_upload(gc, args)
@ -114,6 +129,9 @@ def do_image_create(gc, args):
'record if no import-method and no data is supplied'))
@utils.arg('--uri', metavar='<IMAGE_URL>', default=None,
help=_('URI to download the external image.'))
@utils.arg('--backend', metavar='<STORE>',
default=utils.env('OS_IMAGE_BACKEND', default=None),
help='Backend store to upload image to.')
@utils.on_data_require_fields(DATA_FIELDS)
def do_image_create_via_import(gc, args):
"""EXPERIMENTAL: Create a new image via image import.
@ -159,12 +177,21 @@ def do_image_create_via_import(gc, args):
"Valid values can be retrieved with import-info command." %
args.import_method)
# determine if backend is valid
backend = None
if args.backend:
backend = args.backend
_validate_backend(backend, gc)
# make sure we have all and only correct inputs for the requested method
if args.import_method is None:
if args.uri:
utils.exit("You cannot use --uri without specifying an import "
"method.")
if args.import_method == 'glance-direct':
if backend and not (file_name or using_stdin):
utils.exit("--backend option should only be provided with --file "
"option or stdin for the glance-direct import method.")
if args.uri:
utils.exit("You cannot specify a --uri with the glance-direct "
"import method.")
@ -178,6 +205,9 @@ def do_image_create_via_import(gc, args):
utils.exit("You must specify a --file or provide data via stdin "
"for the glance-direct import method.")
if args.import_method == 'web-download':
if backend and not args.uri:
utils.exit("--backend option should only be provided with --uri "
"option for the web-download import method.")
if not args.uri:
utils.exit("URI is required for web-download import method. "
"Please use '--uri <uri>'.")
@ -203,6 +233,26 @@ def do_image_create_via_import(gc, args):
utils.print_image(image)
def _validate_backend(backend, gc):
try:
enabled_backends = gc.images.get_stores_info().get('stores')
except exc.HTTPNotFound:
# NOTE(abhishekk): To maintain backward compatibility
return
if backend:
valid_backend = False
for available_backend in enabled_backends:
if available_backend['id'] == backend:
valid_backend = True
break
if not valid_backend:
utils.exit("Backend '%s' is not valid for this cloud. Valid "
"values can be retrieved with stores-info command." %
backend)
@utils.arg('id', metavar='<IMAGE_ID>', help=_('ID of image to update.'))
@utils.schema_args(get_image_schema, omit=['id', 'locations', 'created_at',
'updated_at', 'file', 'checksum',
@ -394,6 +444,16 @@ def do_import_info(gc, args):
utils.print_dict(import_info)
def do_stores_info(gc, args):
"""Print available backends from Glance."""
try:
stores_info = gc.images.get_stores_info()
except exc.HTTPNotFound:
utils.exit('Multi Backend support is not enabled')
else:
utils.print_dict(stores_info)
@utils.arg('--file', metavar='<FILE>',
help=_('Local file to save downloaded image data to. '
'If this is not specified and there is no redirection '
@ -438,8 +498,17 @@ def do_image_download(gc, args):
help=_('Show upload progress bar.'))
@utils.arg('id', metavar='<IMAGE_ID>',
help=_('ID of image to upload data to.'))
@utils.arg('--backend', metavar='<STORE>',
default=utils.env('OS_IMAGE_BACKEND', default=None),
help='Backend store to upload image to.')
def do_image_upload(gc, args):
"""Upload data for a specific image."""
backend = None
if args.backend:
backend = args.backend
# determine if backend is valid
_validate_backend(backend, gc)
image_data = utils.get_data_file(args)
if args.progress:
filesize = utils.get_file_size(image_data)
@ -447,7 +516,7 @@ def do_image_upload(gc, args):
# NOTE(kragniz): do not show a progress bar if the size of the
# input is unknown (most likely a piped input)
image_data = progressbar.VerboseFileWrapper(image_data, filesize)
gc.images.upload(args.id, image_data, args.size)
gc.images.upload(args.id, image_data, args.size, backend=backend)
@utils.arg('--file', metavar='<FILE>',
@ -484,13 +553,22 @@ def do_image_stage(gc, args):
help=_('URI to download the external image.'))
@utils.arg('id', metavar='<IMAGE_ID>',
help=_('ID of image to import.'))
@utils.arg('--backend', metavar='<STORE>',
default=utils.env('OS_IMAGE_BACKEND', default=None),
help='Backend store to upload image to.')
def do_image_import(gc, args):
"""Initiate the image import taskflow."""
backend = None
if args.backend:
backend = args.backend
# determine if backend is valid
_validate_backend(backend, gc)
if getattr(args, 'from_create', False):
# this command is being called "internally" so we can skip
# validation -- just do the import and get out of here
gc.images.image_import(args.id, args.import_method, args.uri)
gc.images.image_import(args.id, args.import_method, args.uri,
backend=backend)
return
# do input validation
@ -529,7 +607,8 @@ def do_image_import(gc, args):
"to an image in status 'queued'")
# finally, do the import
gc.images.image_import(args.id, args.import_method, args.uri)
gc.images.image_import(args.id, args.import_method, args.uri,
backend=backend)
image = gc.images.get(args.id)
utils.print_image(image)