Merge "image: Add 'image import' command"

This commit is contained in:
Zuul 2022-12-05 12:59:52 +00:00 committed by Gerrit Code Review
commit 29129a7715
5 changed files with 473 additions and 6 deletions

View File

@ -4,7 +4,7 @@ image-create-via-import,,EXPERIMENTAL: Create a new image via image import.
image-deactivate,image set --deactivate,Deactivate specified image.
image-delete,image delete,Delete specified image.
image-download,image save,Download a specific image.
image-import,,Initiate the image import taskflow.
image-import,image import,Initiate the image import taskflow.
image-list,image list,List images you can access.
image-reactivate,image set --activate,Reactivate specified image.
image-show,image show,Describe a specific image.

1 explain WONTFIX Describe a specific model.
4 image-deactivate image set --deactivate Deactivate specified image.
5 image-delete image delete Delete specified image.
6 image-download image save Download a specific image.
7 image-import image import Initiate the image import taskflow.
8 image-list image list List images you can access.
9 image-reactivate image set --activate Reactivate specified image.
10 image-show image show Describe a specific image.

View File

@ -22,6 +22,7 @@ import os
import sys
from cinderclient import api_versions
from openstack import exceptions as sdk_exceptions
from openstack.image import image_signer
from osc_lib.api import utils as api_utils
from osc_lib.cli import format_columns
@ -1557,3 +1558,245 @@ class StageImage(command.Command):
kwargs['data'] = fp
image_client.stage_image(image, **kwargs)
class ImportImage(command.ShowOne):
_description = _(
"Initiate the image import process.\n"
"This requires support for the interoperable image import process, "
"which was first introduced in Image API version 2.6 "
"(Glance 16.0.0 (Queens))"
)
def get_parser(self, prog_name):
parser = super().get_parser(prog_name)
parser.add_argument(
'image',
metavar='<image>',
help=_('Image to initiate import process for (name or ID)'),
)
# TODO(stephenfin): Uncomment help text when we have this command
# implemented
parser.add_argument(
'--method',
metavar='<method>',
default='glance-direct',
dest='import_method',
choices=[
'glance-direct',
'web-download',
'glance-download',
'copy-image',
],
help=_(
"Import method used for image import process. "
"Not all deployments will support all methods. "
# "Valid values can be retrieved with the 'image import "
# "methods' command. "
"The 'glance-direct' method (default) requires images be "
"first staged using the 'image-stage' command."
),
)
parser.add_argument(
'--uri',
metavar='<uri>',
help=_(
"URI to download the external image "
"(only valid with the 'web-download' import method)"
),
)
parser.add_argument(
'--remote-image',
metavar='<REMOTE_IMAGE>',
help=_(
"The image of remote glance (ID only) to be imported "
"(only valid with the 'glance-download' import method)"
),
)
parser.add_argument(
'--remote-region',
metavar='<REMOTE_GLANCE_REGION>',
help=_(
"The remote Glance region to download the image from "
"(only valid with the 'glance-download' import method)"
),
)
parser.add_argument(
'--remote-service-interface',
metavar='<REMOTE_SERVICE_INTERFACE>',
help=_(
"The remote Glance service interface to use when importing "
"images "
"(only valid with the 'glance-download' import method)"
),
)
stores_group = parser.add_mutually_exclusive_group()
stores_group.add_argument(
'--store',
metavar='<STORE>',
dest='stores',
nargs='*',
help=_(
"Backend store to upload image to "
"(specify multiple times to upload to multiple stores) "
"(either '--store' or '--all-stores' required with the "
"'copy-image' import method)"
),
)
stores_group.add_argument(
'--all-stores',
help=_(
"Make image available to all stores "
"(either '--store' or '--all-stores' required with the "
"'copy-image' import method)"
),
)
parser.add_argument(
'--allow-failure',
action='store_true',
dest='allow_failure',
default=True,
help=_(
'When uploading to multiple stores, indicate that the import '
'should be continue should any of the uploads fail. '
'Only usable with --stores or --all-stores'
),
)
parser.add_argument(
'--disallow-failure',
action='store_true',
dest='allow_failure',
default=True,
help=_(
'When uploading to multiple stores, indicate that the import '
'should be reverted should any of the uploads fail. '
'Only usable with --stores or --all-stores'
),
)
parser.add_argument(
'--wait',
action='store_true',
help=_('Wait for operation to complete'),
)
return parser
def take_action(self, parsed_args):
image_client = self.app.client_manager.image
try:
import_info = image_client.get_import_info()
except sdk_exceptions.ResourceNotFound:
msg = _(
'The Image Import feature is not supported by this deployment'
)
raise exceptions.CommandError(msg)
import_methods = import_info.import_methods['value']
if parsed_args.import_method not in import_methods:
msg = _(
"The '%s' import method is not supported by this deployment. "
"Supported: %s"
)
raise exceptions.CommandError(
msg % (parsed_args.import_method, ', '.join(import_methods)),
)
if parsed_args.import_method == 'web-download':
if not parsed_args.uri:
msg = _(
"The '--uri' option is required when using "
"'--method=web-download'"
)
raise exceptions.CommandError(msg)
else:
if parsed_args.uri:
msg = _(
"The '--uri' option is only supported when using "
"'--method=web-download'"
)
raise exceptions.CommandError(msg)
if parsed_args.import_method == 'glance-download':
if not (parsed_args.remote_region and parsed_args.remote_image):
msg = _(
"The '--remote-region' and '--remote-image' options are "
"required when using '--method=web-download'"
)
raise exceptions.CommandError(msg)
else:
if parsed_args.remote_region:
msg = _(
"The '--remote-region' option is only supported when "
"using '--method=glance-download'"
)
raise exceptions.CommandError(msg)
if parsed_args.remote_image:
msg = _(
"The '--remote-image' option is only supported when using "
"'--method=glance-download'"
)
raise exceptions.CommandError(msg)
if parsed_args.remote_service_interface:
msg = _(
"The '--remote-service-interface' option is only "
"supported when using '--method=glance-download'"
)
raise exceptions.CommandError(msg)
if parsed_args.import_method == 'copy-image':
if not (parsed_args.stores or parsed_args.all_stores):
msg = _(
"The '--stores' or '--all-stores' options are required "
"when using '--method=copy-image'"
)
raise exceptions.CommandError(msg)
image = image_client.find_image(parsed_args.image)
if not image.container_format and not image.disk_format:
msg = _(
"The 'container_format' and 'disk_format' properties "
"must be set on an image before it can be imported"
)
raise exceptions.CommandError(msg)
if parsed_args.import_method == 'glance-direct':
if image.status != 'uploading':
msg = _(
"The 'glance-direct' import method can only be used with "
"an image in status 'uploading'"
)
raise exceptions.CommandError(msg)
elif parsed_args.import_method == 'web-download':
if image.status != 'queued':
msg = _(
"The 'web-download' import method can only be used with "
"an image in status 'queued'"
)
raise exceptions.CommandError(msg)
elif parsed_args.import_method == 'copy-image':
if image.status != 'active':
msg = _(
"The 'copy-image' import method can only be used with "
"an image in status 'active'"
)
raise exceptions.CommandError(msg)
image_client.import_image(
image,
method=parsed_args.import_method,
# uri=parsed_args.uri,
# remote_region=parsed_args.remote_region,
# remote_image=parsed_args.remote_image,
# remote_service_interface=parsed_args.remote_service_interface,
stores=parsed_args.stores,
all_stores=parsed_args.all_stores,
all_stores_must_succeed=not parsed_args.allow_failure,
)
info = _format_image(image)
return zip(*sorted(info.items()))

View File

@ -19,6 +19,7 @@ import uuid
from openstack.image.v2 import image
from openstack.image.v2 import member
from openstack.image.v2 import metadef_namespace
from openstack.image.v2 import service_info as _service_info
from openstack.image.v2 import task
from openstackclient.tests.unit import fakes
@ -39,6 +40,7 @@ class FakeImagev2Client:
self.reactivate_image = mock.Mock()
self.deactivate_image = mock.Mock()
self.stage_image = mock.Mock()
self.import_image = mock.Mock()
self.members = mock.Mock()
self.add_member = mock.Mock()
@ -49,17 +51,15 @@ class FakeImagev2Client:
self.metadef_namespaces = mock.Mock()
self.tasks = mock.Mock()
self.tasks.resource_class = fakes.FakeResource(None, {})
self.get_task = mock.Mock()
self.get_import_info = mock.Mock()
self.auth_token = kwargs['token']
self.management_url = kwargs['endpoint']
self.version = 2.0
self.tasks = mock.Mock()
self.tasks.resource_class = fakes.FakeResource(None, {})
self.metadef_namespaces = mock.Mock()
class TestImagev2(utils.TestCommand):
@ -143,6 +143,33 @@ def create_one_image_member(attrs=None):
return member.Member(**image_member_info)
def create_one_import_info(attrs=None):
"""Create a fake import info.
:param attrs: A dictionary with all attributes of import info
:type attrs: dict
:return: A fake Import object.
:rtype: `openstack.image.v2.service_info.Import`
"""
attrs = attrs or {}
import_info = {
'import-methods': {
'description': 'Import methods available.',
'type': 'array',
'value': [
'glance-direct',
'web-download',
'glance-download',
'copy-image',
]
}
}
import_info.update(attrs)
return _service_info.Import(**import_info)
def create_one_task(attrs=None):
"""Create a fake task.

View File

@ -1823,6 +1823,197 @@ class TestImageStage(TestImage):
)
class TestImageImport(TestImage):
image = image_fakes.create_one_image(
{
'container_format': 'bare',
'disk_format': 'qcow2',
}
)
import_info = image_fakes.create_one_import_info()
def setUp(self):
super().setUp()
self.client.find_image.return_value = self.image
self.client.get_import_info.return_value = self.import_info
self.cmd = _image.ImportImage(self.app, None)
def test_import_image__glance_direct(self):
self.image.status = 'uploading'
arglist = [
self.image.name,
]
verifylist = [
('image', self.image.name),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
self.client.import_image.assert_called_once_with(
self.image,
method='glance-direct',
stores=None,
all_stores=None,
all_stores_must_succeed=False,
)
def test_import_image__web_download(self):
self.image.status = 'queued'
arglist = [
self.image.name,
'--method', 'web-download',
'--uri', 'https://example.com/',
]
verifylist = [
('image', self.image.name),
('import_method', 'web-download'),
('uri', 'https://example.com/'),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
self.client.import_image.assert_called_once_with(
self.image,
method='web-download',
# uri='https://example.com/',
stores=None,
all_stores=None,
all_stores_must_succeed=False,
)
# NOTE(stephenfin): We don't do this for all combinations since that would
# be tedious af. You get the idea...
def test_import_image__web_download_missing_options(self):
arglist = [
self.image.name,
'--method', 'web-download',
]
verifylist = [
('image', self.image.name),
('import_method', 'web-download'),
('uri', None),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
exc = self.assertRaises(
exceptions.CommandError,
self.cmd.take_action,
parsed_args,
)
self.assertIn("The '--uri' option is required ", str(exc))
self.client.import_image.assert_not_called()
# NOTE(stephenfin): Ditto
def test_import_image__web_download_invalid_options(self):
arglist = [
self.image.name,
'--method', 'glance-direct', # != web-download
'--uri', 'https://example.com/',
]
verifylist = [
('image', self.image.name),
('import_method', 'glance-direct'),
('uri', 'https://example.com/'),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
exc = self.assertRaises(
exceptions.CommandError,
self.cmd.take_action,
parsed_args,
)
self.assertIn("The '--uri' option is only supported ", str(exc))
self.client.import_image.assert_not_called()
def test_import_image__web_download_invalid_image_state(self):
self.image.status = 'uploading' # != 'queued'
arglist = [
self.image.name,
'--method', 'web-download',
'--uri', 'https://example.com/',
]
verifylist = [
('image', self.image.name),
('import_method', 'web-download'),
('uri', 'https://example.com/'),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
exc = self.assertRaises(
exceptions.CommandError,
self.cmd.take_action,
parsed_args,
)
self.assertIn(
"The 'web-download' import method can only be used with "
"an image in status 'queued'",
str(exc),
)
self.client.import_image.assert_not_called()
def test_import_image__copy_image(self):
self.image.status = 'active'
arglist = [
self.image.name,
'--method', 'copy-image',
'--store', 'fast',
]
verifylist = [
('image', self.image.name),
('import_method', 'copy-image'),
('stores', ['fast']),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
self.client.import_image.assert_called_once_with(
self.image,
method='copy-image',
stores=['fast'],
all_stores=None,
all_stores_must_succeed=False,
)
def test_import_image__glance_download(self):
arglist = [
self.image.name,
'--method', 'glance-download',
'--remote-region', 'eu/dublin',
'--remote-image', 'remote-image-id',
'--remote-service-interface', 'private',
]
verifylist = [
('image', self.image.name),
('import_method', 'glance-download'),
('remote_region', 'eu/dublin'),
('remote_image', 'remote-image-id'),
('remote_service_interface', 'private'),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
self.client.import_image.assert_called_once_with(
self.image,
method='glance-download',
# remote_region='eu/dublin',
# remote_image='remote-image-id',
# remote_service_interface='private',
stores=None,
all_stores=None,
all_stores_must_succeed=False,
)
class TestImageSave(TestImage):
image = image_fakes.create_one_image({})

View File

@ -0,0 +1,6 @@
---
features:
- |
Add ``image import`` command, allowing users to take advantage of the
interoperable image import functionality first introduced in Glance 16.0.0
(Queens).