summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZuul <zuul@review.openstack.org>2018-07-25 00:39:14 +0000
committerGerrit Code Review <review@openstack.org>2018-07-25 00:39:14 +0000
commitac378e02547e633f0244b5db0d1cbfd0ac9305c3 (patch)
tree9d922da00bbe0e1f46bba4fdf80c8774f5a548ec
parentec559c5fbdca9dda67c810081d45534ddaca7491 (diff)
parent71bfd7bfad045a88811ef2868601ee72fc8667e3 (diff)
downloadpython-glanceclient-ac378e02547e633f0244b5db0d1cbfd0ac9305c3.tar.gz
Merge "Add multi-store support"
-rw-r--r--glanceclient/tests/unit/v2/test_shell_v2.py10
-rw-r--r--glanceclient/v2/images.py29
-rw-r--r--glanceclient/v2/shell.py85
3 files changed, 113 insertions, 11 deletions
diff --git a/glanceclient/tests/unit/v2/test_shell_v2.py b/glanceclient/tests/unit/v2/test_shell_v2.py
index 535d006..b51249d 100644
--- a/glanceclient/tests/unit/v2/test_shell_v2.py
+++ b/glanceclient/tests/unit/v2/test_shell_v2.py
@@ -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):
diff --git a/glanceclient/v2/images.py b/glanceclient/v2/images.py
index fbb67a9..be804a2 100644
--- a/glanceclient/v2/images.py
+++ b/glanceclient/v2/images.py
@@ -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
@@ -240,6 +245,13 @@ class Controller(object):
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)
diff --git a/glanceclient/v2/shell.py b/glanceclient/v2/shell.py
index c03648a..ddaca53 100644
--- a/glanceclient/v2/shell.py
+++ b/glanceclient/v2/shell.py
@@ -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)