diff options
author | Abhishek Kekane <akekane@redhat.com> | 2018-05-30 09:38:37 +0000 |
---|---|---|
committer | Abhishek Kekane <akekane@redhat.com> | 2018-07-24 10:54:18 +0000 |
commit | 71bfd7bfad045a88811ef2868601ee72fc8667e3 (patch) | |
tree | 8969c8dad80c025c0c644af88dfe27ca4313ed0a | |
parent | 71abbfca2a5927d19ad5a7187bb2112e907b301a (diff) | |
download | python-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.py | 10 | ||||
-rw-r--r-- | glanceclient/v2/images.py | 29 | ||||
-rw-r--r-- | glanceclient/v2/shell.py | 85 |
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) |