Add multi-store support
Added multi-store support. User can now use '--backend' option to pass desired store while creating, uploading or importing image to speific store backend. Added new command 'stores-info' which will return available stores information to the user. Related to blueprint multi-store Change-Id: I7370094fc4ed47205b5a86a18b22aaa7b9457e5b
This commit is contained in:
parent
71abbfca2a
commit
71bfd7bfad
|
@ -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)
|
||||
|
@ -1102,7 +1103,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):
|
||||
|
@ -1263,7 +1265,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(
|
||||
|
@ -1281,7 +1283,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):
|
||||
|
@ -1299,7 +1301,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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -59,6 +59,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."""
|
||||
|
@ -74,13 +77,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)
|
||||
|
@ -112,6 +127,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.
|
||||
|
@ -157,12 +175,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.")
|
||||
|
@ -176,6 +203,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>'.")
|
||||
|
@ -201,6 +231,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',
|
||||
|
@ -391,6 +441,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 '
|
||||
|
@ -435,8 +495,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)
|
||||
|
@ -444,7 +513,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>',
|
||||
|
@ -481,13 +550,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
|
||||
|
@ -526,7 +604,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)
|
||||
|
|
Loading…
Reference in New Issue