summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAbhishek Kekane <akekane@redhat.com>2018-05-30 09:38:37 +0000
committerAbhishek Kekane <akekane@redhat.com>2018-07-24 10:54:18 +0000
commit71bfd7bfad045a88811ef2868601ee72fc8667e3 (patch)
tree8969c8dad80c025c0c644af88dfe27ca4313ed0a
parent71abbfca2a5927d19ad5a7187bb2112e907b301a (diff)
downloadpython-glanceclient-71bfd7bfad045a88811ef2868601ee72fc8667e3.tar.gz
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
-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 f2f30e0..19e176f 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)
@@ -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):
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 d837ba1..46a92cc 100644
--- a/glanceclient/v2/shell.py
+++ b/glanceclient/v2/shell.py
@@ -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)