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:
parent
3d9a9df935
commit
1fb8d1f48b
@ -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.
|
||||
|
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
5
releasenotes/notes/image-stage-ac19c47e6a52ffeb.yaml
Normal file
5
releasenotes/notes/image-stage-ac19c47e6a52ffeb.yaml
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Added a new command, ``image stage``, that will allow users to upload data
|
||||
for an image to staging.
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user