Skip to content

Commit 56b0f6d

Browse files
Zuulopenstack-gerrit
authored andcommitted
Merge "image: Add 'image stage' command"
2 parents 222ea8d + 1fb8d1f commit 56b0f6d

6 files changed

Lines changed: 164 additions & 26 deletions

File tree

doc/source/cli/data/glance.csv

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ image-import,,Initiate the image import taskflow.
88
image-list,image list,List images you can access.
99
image-reactivate,image set --activate,Reactivate specified image.
1010
image-show,image show,Describe a specific image.
11-
image-stage,,Upload data for a specific image to staging.
11+
image-stage,image stage,Upload data for a specific image to staging.
1212
image-tag-delete,image unset --tag <tag>,Delete the tag associated with the given image.
1313
image-tag-update,image set --tag <tag>,Update an image with the given tag.
1414
image-update,image set,Update an existing image.

openstackclient/image/v2/image.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1484,3 +1484,80 @@ def take_action(self, parsed_args):
14841484
"Failed to unset %(propret)s of %(proptotal)s" " properties."
14851485
) % {'propret': propret, 'proptotal': proptotal}
14861486
raise exceptions.CommandError(msg)
1487+
1488+
1489+
class StageImage(command.Command):
1490+
_description = _(
1491+
"Upload data for a specific image to staging.\n"
1492+
"This requires support for the interoperable image import process, "
1493+
"which was first introduced in Image API version 2.6 "
1494+
"(Glance 16.0.0 (Queens))"
1495+
)
1496+
1497+
def get_parser(self, prog_name):
1498+
parser = super().get_parser(prog_name)
1499+
1500+
parser.add_argument(
1501+
'--file',
1502+
metavar='<file>',
1503+
dest='filename',
1504+
help=_(
1505+
'Local file that contains disk image to be uploaded. '
1506+
'Alternatively, images can be passed via stdin.'
1507+
),
1508+
)
1509+
# NOTE(stephenfin): glanceclient had a --size argument but it didn't do
1510+
# anything so we have chosen not to port this
1511+
parser.add_argument(
1512+
'--progress',
1513+
action='store_true',
1514+
default=False,
1515+
help=_(
1516+
'Show upload progress bar '
1517+
'(ignored if passing data via stdin)'
1518+
),
1519+
)
1520+
parser.add_argument(
1521+
'image',
1522+
metavar='<image>',
1523+
help=_('Image to upload data for (name or ID)'),
1524+
)
1525+
1526+
return parser
1527+
1528+
def take_action(self, parsed_args):
1529+
image_client = self.app.client_manager.image
1530+
1531+
image = image_client.find_image(
1532+
parsed_args.image,
1533+
ignore_missing=False,
1534+
)
1535+
# open the file first to ensure any failures are handled before the
1536+
# image is created. Get the file name (if it is file, and not stdin)
1537+
# for easier further handling.
1538+
if parsed_args.filename:
1539+
try:
1540+
fp = open(parsed_args.filename, 'rb')
1541+
except FileNotFoundError:
1542+
raise exceptions.CommandError(
1543+
'%r is not a valid file' % parsed_args.filename,
1544+
)
1545+
else:
1546+
fp = get_data_from_stdin()
1547+
1548+
kwargs = {}
1549+
1550+
if parsed_args.progress and parsed_args.filename:
1551+
# NOTE(stephenfin): we only show a progress bar if the user
1552+
# requested it *and* we're reading from a file (not stdin)
1553+
filesize = os.path.getsize(parsed_args.filename)
1554+
if filesize is not None:
1555+
kwargs['data'] = progressbar.VerboseFileWrapper(fp, filesize)
1556+
else:
1557+
kwargs['data'] = fp
1558+
elif parsed_args.filename:
1559+
kwargs['filename'] = parsed_args.filename
1560+
elif fp:
1561+
kwargs['data'] = fp
1562+
1563+
image_client.stage_image(image, **kwargs)

openstackclient/tests/unit/image/v2/fakes.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ def __init__(self, **kwargs):
3838
self.download_image = mock.Mock()
3939
self.reactivate_image = mock.Mock()
4040
self.deactivate_image = mock.Mock()
41+
self.stage_image = mock.Mock()
4142

4243
self.members = mock.Mock()
4344
self.add_member = mock.Mock()

openstackclient/tests/unit/image/v2/test_image.py

Lines changed: 79 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from osc_lib.cli import format_columns
2323
from osc_lib import exceptions
2424

25-
from openstackclient.image.v2 import image
25+
from openstackclient.image.v2 import image as _image
2626
from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes
2727
from openstackclient.tests.unit.image.v2 import fakes as image_fakes
2828
from openstackclient.tests.unit.volume.v3 import fakes as volume_fakes
@@ -73,19 +73,19 @@ def setUp(self):
7373
self.client.update_image.return_value = self.new_image
7474

7575
(self.expected_columns, self.expected_data) = zip(
76-
*sorted(image._format_image(self.new_image).items()))
76+
*sorted(_image._format_image(self.new_image).items()))
7777

7878
# Get the command object to test
79-
self.cmd = image.CreateImage(self.app, None)
79+
self.cmd = _image.CreateImage(self.app, None)
8080

8181
@mock.patch("sys.stdin", side_effect=[None])
8282
def test_image_reserve_no_options(self, raw_input):
8383
arglist = [
8484
self.new_image.name
8585
]
8686
verifylist = [
87-
('container_format', image.DEFAULT_CONTAINER_FORMAT),
88-
('disk_format', image.DEFAULT_DISK_FORMAT),
87+
('container_format', _image.DEFAULT_CONTAINER_FORMAT),
88+
('disk_format', _image.DEFAULT_DISK_FORMAT),
8989
('name', self.new_image.name),
9090
]
9191
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
@@ -99,8 +99,8 @@ def test_image_reserve_no_options(self, raw_input):
9999
self.client.create_image.assert_called_with(
100100
name=self.new_image.name,
101101
allow_duplicates=True,
102-
container_format=image.DEFAULT_CONTAINER_FORMAT,
103-
disk_format=image.DEFAULT_DISK_FORMAT,
102+
container_format=_image.DEFAULT_CONTAINER_FORMAT,
103+
disk_format=_image.DEFAULT_DISK_FORMAT,
104104
)
105105

106106
self.assertEqual(self.expected_columns, columns)
@@ -224,8 +224,8 @@ def test_image_create_file(self):
224224
self.client.create_image.assert_called_with(
225225
name=self.new_image.name,
226226
allow_duplicates=True,
227-
container_format=image.DEFAULT_CONTAINER_FORMAT,
228-
disk_format=image.DEFAULT_DISK_FORMAT,
227+
container_format=_image.DEFAULT_CONTAINER_FORMAT,
228+
disk_format=_image.DEFAULT_DISK_FORMAT,
229229
is_protected=self.new_image.is_protected,
230230
visibility=self.new_image.visibility,
231231
Alpha='1',
@@ -245,7 +245,7 @@ def test_image_create_file(self):
245245
def test_image_create__progress_ignore_with_stdin(
246246
self, mock_get_data_from_stdin,
247247
):
248-
fake_stdin = io.StringIO('fake-image-data')
248+
fake_stdin = io.BytesIO(b'some fake data')
249249
mock_get_data_from_stdin.return_value = fake_stdin
250250

251251
arglist = [
@@ -263,8 +263,8 @@ def test_image_create__progress_ignore_with_stdin(
263263
self.client.create_image.assert_called_with(
264264
name=self.new_image.name,
265265
allow_duplicates=True,
266-
container_format=image.DEFAULT_CONTAINER_FORMAT,
267-
disk_format=image.DEFAULT_DISK_FORMAT,
266+
container_format=_image.DEFAULT_CONTAINER_FORMAT,
267+
disk_format=_image.DEFAULT_DISK_FORMAT,
268268
data=fake_stdin,
269269
validate_checksum=False,
270270
)
@@ -305,8 +305,8 @@ def test_image_create_import(self, raw_input):
305305
self.client.create_image.assert_called_with(
306306
name=self.new_image.name,
307307
allow_duplicates=True,
308-
container_format=image.DEFAULT_CONTAINER_FORMAT,
309-
disk_format=image.DEFAULT_DISK_FORMAT,
308+
container_format=_image.DEFAULT_CONTAINER_FORMAT,
309+
disk_format=_image.DEFAULT_DISK_FORMAT,
310310
use_import=True
311311
)
312312

@@ -445,7 +445,7 @@ def setUp(self):
445445
self.project_mock.get.return_value = self.project
446446
self.domain_mock.get.return_value = self.domain
447447
# Get the command object to test
448-
self.cmd = image.AddProjectToImage(self.app, None)
448+
self.cmd = _image.AddProjectToImage(self.app, None)
449449

450450
def test_add_project_to_image_no_option(self):
451451
arglist = [
@@ -504,7 +504,7 @@ def setUp(self):
504504
self.client.delete_image.return_value = None
505505

506506
# Get the command object to test
507-
self.cmd = image.DeleteImage(self.app, None)
507+
self.cmd = _image.DeleteImage(self.app, None)
508508

509509
def test_image_delete_no_options(self):
510510
images = self.setup_images_mock(count=1)
@@ -595,7 +595,7 @@ def setUp(self):
595595
self.client.images.side_effect = [[self._image], []]
596596

597597
# Get the command object to test
598-
self.cmd = image.ListImage(self.app, None)
598+
self.cmd = _image.ListImage(self.app, None)
599599

600600
def test_image_list_no_options(self):
601601
arglist = []
@@ -993,7 +993,7 @@ def setUp(self):
993993
self.client.find_image.return_value = self._image
994994
self.client.members.return_value = [self.member]
995995

996-
self.cmd = image.ListImageProjects(self.app, None)
996+
self.cmd = _image.ListImageProjects(self.app, None)
997997

998998
def test_image_member_list(self):
999999
arglist = [
@@ -1028,7 +1028,7 @@ def setUp(self):
10281028
self.domain_mock.get.return_value = self.domain
10291029
self.client.remove_member.return_value = None
10301030
# Get the command object to test
1031-
self.cmd = image.RemoveProjectImage(self.app, None)
1031+
self.cmd = _image.RemoveProjectImage(self.app, None)
10321032

10331033
def test_remove_project_image_no_options(self):
10341034
arglist = [
@@ -1095,7 +1095,7 @@ def setUp(self):
10951095
)
10961096

10971097
# Get the command object to test
1098-
self.cmd = image.SetImage(self.app, None)
1098+
self.cmd = _image.SetImage(self.app, None)
10991099

11001100
def test_image_set_no_options(self):
11011101
arglist = [
@@ -1624,7 +1624,7 @@ def setUp(self):
16241624
self.client.find_image = mock.Mock(return_value=self._data)
16251625

16261626
# Get the command object to test
1627-
self.cmd = image.ShowImage(self.app, None)
1627+
self.cmd = _image.ShowImage(self.app, None)
16281628

16291629
def test_image_show(self):
16301630
arglist = [
@@ -1689,7 +1689,7 @@ def setUp(self):
16891689
self.client.update_image.return_value = self.image
16901690

16911691
# Get the command object to test
1692-
self.cmd = image.UnsetImage(self.app, None)
1692+
self.cmd = _image.UnsetImage(self.app, None)
16931693

16941694
def test_image_unset_no_options(self):
16951695
arglist = [
@@ -1769,6 +1769,60 @@ def test_image_unset_mixed_option(self):
17691769
self.assertIsNone(result)
17701770

17711771

1772+
class TestImageStage(TestImage):
1773+
1774+
image = image_fakes.create_one_image({})
1775+
1776+
def setUp(self):
1777+
super().setUp()
1778+
1779+
self.client.find_image.return_value = self.image
1780+
1781+
self.cmd = _image.StageImage(self.app, None)
1782+
1783+
def test_stage_image__from_file(self):
1784+
imagefile = tempfile.NamedTemporaryFile(delete=False)
1785+
imagefile.write(b'\0')
1786+
imagefile.close()
1787+
1788+
arglist = [
1789+
'--file', imagefile.name,
1790+
self.image.name,
1791+
]
1792+
verifylist = [
1793+
('filename', imagefile.name),
1794+
('image', self.image.name),
1795+
]
1796+
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
1797+
1798+
self.cmd.take_action(parsed_args)
1799+
1800+
self.client.stage_image.assert_called_once_with(
1801+
self.image,
1802+
filename=imagefile.name,
1803+
)
1804+
1805+
@mock.patch('openstackclient.image.v2.image.get_data_from_stdin')
1806+
def test_stage_image__from_stdin(self, mock_get_data_from_stdin):
1807+
fake_stdin = io.BytesIO(b"some initial binary data: \x00\x01")
1808+
mock_get_data_from_stdin.return_value = fake_stdin
1809+
1810+
arglist = [
1811+
self.image.name,
1812+
]
1813+
verifylist = [
1814+
('image', self.image.name),
1815+
]
1816+
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
1817+
1818+
self.cmd.take_action(parsed_args)
1819+
1820+
self.client.stage_image.assert_called_once_with(
1821+
self.image,
1822+
data=fake_stdin,
1823+
)
1824+
1825+
17721826
class TestImageSave(TestImage):
17731827

17741828
image = image_fakes.create_one_image({})
@@ -1780,7 +1834,7 @@ def setUp(self):
17801834
self.client.download_image.return_value = self.image
17811835

17821836
# Get the command object to test
1783-
self.cmd = image.SaveImage(self.app, None)
1837+
self.cmd = _image.SaveImage(self.app, None)
17841838

17851839
def test_save_data(self):
17861840

@@ -1810,7 +1864,7 @@ def test_get_data_from_stdin(self):
18101864
stdin.isatty.return_value = False
18111865
stdin.buffer = fd
18121866

1813-
test_fd = image.get_data_from_stdin()
1867+
test_fd = _image.get_data_from_stdin()
18141868

18151869
# Ensure data written to temp file is correct
18161870
self.assertEqual(fd, test_fd)
@@ -1822,6 +1876,6 @@ def test_get_data_from_stdin__interactive(self):
18221876
# There is stdin, but interactive
18231877
stdin.return_value = fd
18241878

1825-
test_fd = image.get_data_from_stdin()
1879+
test_fd = _image.get_data_from_stdin()
18261880

18271881
self.assertIsNone(test_fd)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
features:
3+
- |
4+
Added a new command, ``image stage``, that will allow users to upload data
5+
for an image to staging.

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,7 @@ openstack.image.v2 =
383383
image_show = openstackclient.image.v2.image:ShowImage
384384
image_set = openstackclient.image.v2.image:SetImage
385385
image_unset = openstackclient.image.v2.image:UnsetImage
386+
image_stage = openstackclient.image.v2.image:StageImage
386387
image_task_show = openstackclient.image.v2.task:ShowTask
387388
image_task_list = openstackclient.image.v2.task:ListTask
388389
image_metadef_namespace_list = openstackclient.image.v2.metadef_namespaces:ListMetadefNameSpaces

0 commit comments

Comments
 (0)