image: Add 'image stage' command

This is the equivalent of the 'image-stage' glanceclient command.

Change-Id: I10b01ef145740a2f7ffe5a8c7ce0296df0ece0bd
Signed-off-by: Stephen Finucane <sfinucan@redhat.com>
This commit is contained in:
Stephen Finucane 2022-10-19 18:07:41 +01:00
parent 3d9a9df935
commit 1fb8d1f48b
6 changed files with 164 additions and 26 deletions

View File

@ -8,7 +8,7 @@ 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.
image-stage,,Upload data for a specific image to staging.
image-stage,image stage,Upload data for a specific image to staging.
image-tag-delete,image unset --tag <tag>,Delete the tag associated with the given image.
image-tag-update,image set --tag <tag>,Update an image with the given tag.
image-update,image set,Update an existing image.

1 explain WONTFIX Describe a specific model.
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.
11 image-stage image stage Upload data for a specific image to staging.
12 image-tag-delete image unset --tag <tag> Delete the tag associated with the given image.
13 image-tag-update image set --tag <tag> Update an image with the given tag.
14 image-update image set Update an existing image.

View File

@ -1484,3 +1484,80 @@ class UnsetImage(command.Command):
"Failed to unset %(propret)s of %(proptotal)s" " properties."
) % {'propret': propret, 'proptotal': proptotal}
raise exceptions.CommandError(msg)
class StageImage(command.Command):
_description = _(
"Upload data for a specific image to staging.\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(
'--file',
metavar='<file>',
dest='filename',
help=_(
'Local file that contains disk image to be uploaded. '
'Alternatively, images can be passed via stdin.'
),
)
# NOTE(stephenfin): glanceclient had a --size argument but it didn't do
# anything so we have chosen not to port this
parser.add_argument(
'--progress',
action='store_true',
default=False,
help=_(
'Show upload progress bar '
'(ignored if passing data via stdin)'
),
)
parser.add_argument(
'image',
metavar='<image>',
help=_('Image to upload data for (name or ID)'),
)
return parser
def take_action(self, parsed_args):
image_client = self.app.client_manager.image
image = image_client.find_image(
parsed_args.image,
ignore_missing=False,
)
# open the file first to ensure any failures are handled before the
# image is created. Get the file name (if it is file, and not stdin)
# for easier further handling.
if parsed_args.filename:
try:
fp = open(parsed_args.filename, 'rb')
except FileNotFoundError:
raise exceptions.CommandError(
'%r is not a valid file' % parsed_args.filename,
)
else:
fp = get_data_from_stdin()
kwargs = {}
if parsed_args.progress and parsed_args.filename:
# NOTE(stephenfin): we only show a progress bar if the user
# requested it *and* we're reading from a file (not stdin)
filesize = os.path.getsize(parsed_args.filename)
if filesize is not None:
kwargs['data'] = progressbar.VerboseFileWrapper(fp, filesize)
else:
kwargs['data'] = fp
elif parsed_args.filename:
kwargs['filename'] = parsed_args.filename
elif fp:
kwargs['data'] = fp
image_client.stage_image(image, **kwargs)

View File

@ -38,6 +38,7 @@ class FakeImagev2Client:
self.download_image = mock.Mock()
self.reactivate_image = mock.Mock()
self.deactivate_image = mock.Mock()
self.stage_image = mock.Mock()
self.members = mock.Mock()
self.add_member = mock.Mock()

View File

@ -22,7 +22,7 @@ from openstack import exceptions as sdk_exceptions
from osc_lib.cli import format_columns
from osc_lib import exceptions
from openstackclient.image.v2 import image
from openstackclient.image.v2 import image as _image
from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes
from openstackclient.tests.unit.image.v2 import fakes as image_fakes
from openstackclient.tests.unit.volume.v3 import fakes as volume_fakes
@ -73,10 +73,10 @@ class TestImageCreate(TestImage):
self.client.update_image.return_value = self.new_image
(self.expected_columns, self.expected_data) = zip(
*sorted(image._format_image(self.new_image).items()))
*sorted(_image._format_image(self.new_image).items()))
# Get the command object to test
self.cmd = image.CreateImage(self.app, None)
self.cmd = _image.CreateImage(self.app, None)
@mock.patch("sys.stdin", side_effect=[None])
def test_image_reserve_no_options(self, raw_input):
@ -84,8 +84,8 @@ class TestImageCreate(TestImage):
self.new_image.name
]
verifylist = [
('container_format', image.DEFAULT_CONTAINER_FORMAT),
('disk_format', image.DEFAULT_DISK_FORMAT),
('container_format', _image.DEFAULT_CONTAINER_FORMAT),
('disk_format', _image.DEFAULT_DISK_FORMAT),
('name', self.new_image.name),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
@ -99,8 +99,8 @@ class TestImageCreate(TestImage):
self.client.create_image.assert_called_with(
name=self.new_image.name,
allow_duplicates=True,
container_format=image.DEFAULT_CONTAINER_FORMAT,
disk_format=image.DEFAULT_DISK_FORMAT,
container_format=_image.DEFAULT_CONTAINER_FORMAT,
disk_format=_image.DEFAULT_DISK_FORMAT,
)
self.assertEqual(self.expected_columns, columns)
@ -224,8 +224,8 @@ class TestImageCreate(TestImage):
self.client.create_image.assert_called_with(
name=self.new_image.name,
allow_duplicates=True,
container_format=image.DEFAULT_CONTAINER_FORMAT,
disk_format=image.DEFAULT_DISK_FORMAT,
container_format=_image.DEFAULT_CONTAINER_FORMAT,
disk_format=_image.DEFAULT_DISK_FORMAT,
is_protected=self.new_image.is_protected,
visibility=self.new_image.visibility,
Alpha='1',
@ -245,7 +245,7 @@ class TestImageCreate(TestImage):
def test_image_create__progress_ignore_with_stdin(
self, mock_get_data_from_stdin,
):
fake_stdin = io.StringIO('fake-image-data')
fake_stdin = io.BytesIO(b'some fake data')
mock_get_data_from_stdin.return_value = fake_stdin
arglist = [
@ -263,8 +263,8 @@ class TestImageCreate(TestImage):
self.client.create_image.assert_called_with(
name=self.new_image.name,
allow_duplicates=True,
container_format=image.DEFAULT_CONTAINER_FORMAT,
disk_format=image.DEFAULT_DISK_FORMAT,
container_format=_image.DEFAULT_CONTAINER_FORMAT,
disk_format=_image.DEFAULT_DISK_FORMAT,
data=fake_stdin,
validate_checksum=False,
)
@ -305,8 +305,8 @@ class TestImageCreate(TestImage):
self.client.create_image.assert_called_with(
name=self.new_image.name,
allow_duplicates=True,
container_format=image.DEFAULT_CONTAINER_FORMAT,
disk_format=image.DEFAULT_DISK_FORMAT,
container_format=_image.DEFAULT_CONTAINER_FORMAT,
disk_format=_image.DEFAULT_DISK_FORMAT,
use_import=True
)
@ -445,7 +445,7 @@ class TestAddProjectToImage(TestImage):
self.project_mock.get.return_value = self.project
self.domain_mock.get.return_value = self.domain
# Get the command object to test
self.cmd = image.AddProjectToImage(self.app, None)
self.cmd = _image.AddProjectToImage(self.app, None)
def test_add_project_to_image_no_option(self):
arglist = [
@ -504,7 +504,7 @@ class TestImageDelete(TestImage):
self.client.delete_image.return_value = None
# Get the command object to test
self.cmd = image.DeleteImage(self.app, None)
self.cmd = _image.DeleteImage(self.app, None)
def test_image_delete_no_options(self):
images = self.setup_images_mock(count=1)
@ -595,7 +595,7 @@ class TestImageList(TestImage):
self.client.images.side_effect = [[self._image], []]
# Get the command object to test
self.cmd = image.ListImage(self.app, None)
self.cmd = _image.ListImage(self.app, None)
def test_image_list_no_options(self):
arglist = []
@ -993,7 +993,7 @@ class TestListImageProjects(TestImage):
self.client.find_image.return_value = self._image
self.client.members.return_value = [self.member]
self.cmd = image.ListImageProjects(self.app, None)
self.cmd = _image.ListImageProjects(self.app, None)
def test_image_member_list(self):
arglist = [
@ -1028,7 +1028,7 @@ class TestRemoveProjectImage(TestImage):
self.domain_mock.get.return_value = self.domain
self.client.remove_member.return_value = None
# Get the command object to test
self.cmd = image.RemoveProjectImage(self.app, None)
self.cmd = _image.RemoveProjectImage(self.app, None)
def test_remove_project_image_no_options(self):
arglist = [
@ -1095,7 +1095,7 @@ class TestImageSet(TestImage):
)
# Get the command object to test
self.cmd = image.SetImage(self.app, None)
self.cmd = _image.SetImage(self.app, None)
def test_image_set_no_options(self):
arglist = [
@ -1624,7 +1624,7 @@ class TestImageShow(TestImage):
self.client.find_image = mock.Mock(return_value=self._data)
# Get the command object to test
self.cmd = image.ShowImage(self.app, None)
self.cmd = _image.ShowImage(self.app, None)
def test_image_show(self):
arglist = [
@ -1689,7 +1689,7 @@ class TestImageUnset(TestImage):
self.client.update_image.return_value = self.image
# Get the command object to test
self.cmd = image.UnsetImage(self.app, None)
self.cmd = _image.UnsetImage(self.app, None)
def test_image_unset_no_options(self):
arglist = [
@ -1769,6 +1769,60 @@ class TestImageUnset(TestImage):
self.assertIsNone(result)
class TestImageStage(TestImage):
image = image_fakes.create_one_image({})
def setUp(self):
super().setUp()
self.client.find_image.return_value = self.image
self.cmd = _image.StageImage(self.app, None)
def test_stage_image__from_file(self):
imagefile = tempfile.NamedTemporaryFile(delete=False)
imagefile.write(b'\0')
imagefile.close()
arglist = [
'--file', imagefile.name,
self.image.name,
]
verifylist = [
('filename', imagefile.name),
('image', self.image.name),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
self.client.stage_image.assert_called_once_with(
self.image,
filename=imagefile.name,
)
@mock.patch('openstackclient.image.v2.image.get_data_from_stdin')
def test_stage_image__from_stdin(self, mock_get_data_from_stdin):
fake_stdin = io.BytesIO(b"some initial binary data: \x00\x01")
mock_get_data_from_stdin.return_value = fake_stdin
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.stage_image.assert_called_once_with(
self.image,
data=fake_stdin,
)
class TestImageSave(TestImage):
image = image_fakes.create_one_image({})
@ -1780,7 +1834,7 @@ class TestImageSave(TestImage):
self.client.download_image.return_value = self.image
# Get the command object to test
self.cmd = image.SaveImage(self.app, None)
self.cmd = _image.SaveImage(self.app, None)
def test_save_data(self):
@ -1810,7 +1864,7 @@ class TestImageGetData(TestImage):
stdin.isatty.return_value = False
stdin.buffer = fd
test_fd = image.get_data_from_stdin()
test_fd = _image.get_data_from_stdin()
# Ensure data written to temp file is correct
self.assertEqual(fd, test_fd)
@ -1822,6 +1876,6 @@ class TestImageGetData(TestImage):
# There is stdin, but interactive
stdin.return_value = fd
test_fd = image.get_data_from_stdin()
test_fd = _image.get_data_from_stdin()
self.assertIsNone(test_fd)

View File

@ -0,0 +1,5 @@
---
features:
- |
Added a new command, ``image stage``, that will allow users to upload data
for an image to staging.

View File

@ -383,6 +383,7 @@ openstack.image.v2 =
image_show = openstackclient.image.v2.image:ShowImage
image_set = openstackclient.image.v2.image:SetImage
image_unset = openstackclient.image.v2.image:UnsetImage
image_stage = openstackclient.image.v2.image:StageImage
image_task_show = openstackclient.image.v2.task:ShowTask
image_task_list = openstackclient.image.v2.task:ListTask
image_metadef_namespace_list = openstackclient.image.v2.metadef_namespaces:ListMetadefNameSpaces