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:
		| @@ -8,7 +8,7 @@ image-import,,Initiate the image import taskflow. | |||||||
| image-list,image list,List images you can access. | image-list,image list,List images you can access. | ||||||
| image-reactivate,image set --activate,Reactivate specified image. | image-reactivate,image set --activate,Reactivate specified image. | ||||||
| image-show,image show,Describe a specific 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-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-tag-update,image set --tag <tag>,Update an image with the given tag. | ||||||
| image-update,image set,Update an existing image. | 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." |                 "Failed to unset %(propret)s of %(proptotal)s" " properties." | ||||||
|             ) % {'propret': propret, 'proptotal': proptotal} |             ) % {'propret': propret, 'proptotal': proptotal} | ||||||
|             raise exceptions.CommandError(msg) |             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.download_image = mock.Mock() | ||||||
|         self.reactivate_image = mock.Mock() |         self.reactivate_image = mock.Mock() | ||||||
|         self.deactivate_image = mock.Mock() |         self.deactivate_image = mock.Mock() | ||||||
|  |         self.stage_image = mock.Mock() | ||||||
|  |  | ||||||
|         self.members = mock.Mock() |         self.members = mock.Mock() | ||||||
|         self.add_member = 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.cli import format_columns | ||||||
| from osc_lib import exceptions | 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.identity.v3 import fakes as identity_fakes | ||||||
| from openstackclient.tests.unit.image.v2 import fakes as image_fakes | from openstackclient.tests.unit.image.v2 import fakes as image_fakes | ||||||
| from openstackclient.tests.unit.volume.v3 import fakes as volume_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.client.update_image.return_value = self.new_image | ||||||
|  |  | ||||||
|         (self.expected_columns, self.expected_data) = zip( |         (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 |         # 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]) |     @mock.patch("sys.stdin", side_effect=[None]) | ||||||
|     def test_image_reserve_no_options(self, raw_input): |     def test_image_reserve_no_options(self, raw_input): | ||||||
| @@ -84,8 +84,8 @@ class TestImageCreate(TestImage): | |||||||
|             self.new_image.name |             self.new_image.name | ||||||
|         ] |         ] | ||||||
|         verifylist = [ |         verifylist = [ | ||||||
|             ('container_format', image.DEFAULT_CONTAINER_FORMAT), |             ('container_format', _image.DEFAULT_CONTAINER_FORMAT), | ||||||
|             ('disk_format', image.DEFAULT_DISK_FORMAT), |             ('disk_format', _image.DEFAULT_DISK_FORMAT), | ||||||
|             ('name', self.new_image.name), |             ('name', self.new_image.name), | ||||||
|         ] |         ] | ||||||
|         parsed_args = self.check_parser(self.cmd, arglist, verifylist) |         parsed_args = self.check_parser(self.cmd, arglist, verifylist) | ||||||
| @@ -99,8 +99,8 @@ class TestImageCreate(TestImage): | |||||||
|         self.client.create_image.assert_called_with( |         self.client.create_image.assert_called_with( | ||||||
|             name=self.new_image.name, |             name=self.new_image.name, | ||||||
|             allow_duplicates=True, |             allow_duplicates=True, | ||||||
|             container_format=image.DEFAULT_CONTAINER_FORMAT, |             container_format=_image.DEFAULT_CONTAINER_FORMAT, | ||||||
|             disk_format=image.DEFAULT_DISK_FORMAT, |             disk_format=_image.DEFAULT_DISK_FORMAT, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         self.assertEqual(self.expected_columns, columns) |         self.assertEqual(self.expected_columns, columns) | ||||||
| @@ -224,8 +224,8 @@ class TestImageCreate(TestImage): | |||||||
|         self.client.create_image.assert_called_with( |         self.client.create_image.assert_called_with( | ||||||
|             name=self.new_image.name, |             name=self.new_image.name, | ||||||
|             allow_duplicates=True, |             allow_duplicates=True, | ||||||
|             container_format=image.DEFAULT_CONTAINER_FORMAT, |             container_format=_image.DEFAULT_CONTAINER_FORMAT, | ||||||
|             disk_format=image.DEFAULT_DISK_FORMAT, |             disk_format=_image.DEFAULT_DISK_FORMAT, | ||||||
|             is_protected=self.new_image.is_protected, |             is_protected=self.new_image.is_protected, | ||||||
|             visibility=self.new_image.visibility, |             visibility=self.new_image.visibility, | ||||||
|             Alpha='1', |             Alpha='1', | ||||||
| @@ -245,7 +245,7 @@ class TestImageCreate(TestImage): | |||||||
|     def test_image_create__progress_ignore_with_stdin( |     def test_image_create__progress_ignore_with_stdin( | ||||||
|         self, mock_get_data_from_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 |         mock_get_data_from_stdin.return_value = fake_stdin | ||||||
|  |  | ||||||
|         arglist = [ |         arglist = [ | ||||||
| @@ -263,8 +263,8 @@ class TestImageCreate(TestImage): | |||||||
|         self.client.create_image.assert_called_with( |         self.client.create_image.assert_called_with( | ||||||
|             name=self.new_image.name, |             name=self.new_image.name, | ||||||
|             allow_duplicates=True, |             allow_duplicates=True, | ||||||
|             container_format=image.DEFAULT_CONTAINER_FORMAT, |             container_format=_image.DEFAULT_CONTAINER_FORMAT, | ||||||
|             disk_format=image.DEFAULT_DISK_FORMAT, |             disk_format=_image.DEFAULT_DISK_FORMAT, | ||||||
|             data=fake_stdin, |             data=fake_stdin, | ||||||
|             validate_checksum=False, |             validate_checksum=False, | ||||||
|         ) |         ) | ||||||
| @@ -305,8 +305,8 @@ class TestImageCreate(TestImage): | |||||||
|         self.client.create_image.assert_called_with( |         self.client.create_image.assert_called_with( | ||||||
|             name=self.new_image.name, |             name=self.new_image.name, | ||||||
|             allow_duplicates=True, |             allow_duplicates=True, | ||||||
|             container_format=image.DEFAULT_CONTAINER_FORMAT, |             container_format=_image.DEFAULT_CONTAINER_FORMAT, | ||||||
|             disk_format=image.DEFAULT_DISK_FORMAT, |             disk_format=_image.DEFAULT_DISK_FORMAT, | ||||||
|             use_import=True |             use_import=True | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
| @@ -445,7 +445,7 @@ class TestAddProjectToImage(TestImage): | |||||||
|         self.project_mock.get.return_value = self.project |         self.project_mock.get.return_value = self.project | ||||||
|         self.domain_mock.get.return_value = self.domain |         self.domain_mock.get.return_value = self.domain | ||||||
|         # Get the command object to test |         # 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): |     def test_add_project_to_image_no_option(self): | ||||||
|         arglist = [ |         arglist = [ | ||||||
| @@ -504,7 +504,7 @@ class TestImageDelete(TestImage): | |||||||
|         self.client.delete_image.return_value = None |         self.client.delete_image.return_value = None | ||||||
|  |  | ||||||
|         # Get the command object to test |         # 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): |     def test_image_delete_no_options(self): | ||||||
|         images = self.setup_images_mock(count=1) |         images = self.setup_images_mock(count=1) | ||||||
| @@ -595,7 +595,7 @@ class TestImageList(TestImage): | |||||||
|         self.client.images.side_effect = [[self._image], []] |         self.client.images.side_effect = [[self._image], []] | ||||||
|  |  | ||||||
|         # Get the command object to test |         # 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): |     def test_image_list_no_options(self): | ||||||
|         arglist = [] |         arglist = [] | ||||||
| @@ -993,7 +993,7 @@ class TestListImageProjects(TestImage): | |||||||
|         self.client.find_image.return_value = self._image |         self.client.find_image.return_value = self._image | ||||||
|         self.client.members.return_value = [self.member] |         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): |     def test_image_member_list(self): | ||||||
|         arglist = [ |         arglist = [ | ||||||
| @@ -1028,7 +1028,7 @@ class TestRemoveProjectImage(TestImage): | |||||||
|         self.domain_mock.get.return_value = self.domain |         self.domain_mock.get.return_value = self.domain | ||||||
|         self.client.remove_member.return_value = None |         self.client.remove_member.return_value = None | ||||||
|         # Get the command object to test |         # 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): |     def test_remove_project_image_no_options(self): | ||||||
|         arglist = [ |         arglist = [ | ||||||
| @@ -1095,7 +1095,7 @@ class TestImageSet(TestImage): | |||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         # Get the command object to test |         # 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): |     def test_image_set_no_options(self): | ||||||
|         arglist = [ |         arglist = [ | ||||||
| @@ -1624,7 +1624,7 @@ class TestImageShow(TestImage): | |||||||
|         self.client.find_image = mock.Mock(return_value=self._data) |         self.client.find_image = mock.Mock(return_value=self._data) | ||||||
|  |  | ||||||
|         # Get the command object to test |         # 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): |     def test_image_show(self): | ||||||
|         arglist = [ |         arglist = [ | ||||||
| @@ -1689,7 +1689,7 @@ class TestImageUnset(TestImage): | |||||||
|         self.client.update_image.return_value = self.image |         self.client.update_image.return_value = self.image | ||||||
|  |  | ||||||
|         # Get the command object to test |         # 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): |     def test_image_unset_no_options(self): | ||||||
|         arglist = [ |         arglist = [ | ||||||
| @@ -1769,6 +1769,60 @@ class TestImageUnset(TestImage): | |||||||
|         self.assertIsNone(result) |         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): | class TestImageSave(TestImage): | ||||||
|  |  | ||||||
|     image = image_fakes.create_one_image({}) |     image = image_fakes.create_one_image({}) | ||||||
| @@ -1780,7 +1834,7 @@ class TestImageSave(TestImage): | |||||||
|         self.client.download_image.return_value = self.image |         self.client.download_image.return_value = self.image | ||||||
|  |  | ||||||
|         # Get the command object to test |         # 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): |     def test_save_data(self): | ||||||
|  |  | ||||||
| @@ -1810,7 +1864,7 @@ class TestImageGetData(TestImage): | |||||||
|             stdin.isatty.return_value = False |             stdin.isatty.return_value = False | ||||||
|             stdin.buffer = fd |             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 |             # Ensure data written to temp file is correct | ||||||
|             self.assertEqual(fd, test_fd) |             self.assertEqual(fd, test_fd) | ||||||
| @@ -1822,6 +1876,6 @@ class TestImageGetData(TestImage): | |||||||
|             # There is stdin, but interactive |             # There is stdin, but interactive | ||||||
|             stdin.return_value = fd |             stdin.return_value = fd | ||||||
|  |  | ||||||
|             test_fd = image.get_data_from_stdin() |             test_fd = _image.get_data_from_stdin() | ||||||
|  |  | ||||||
|             self.assertIsNone(test_fd) |             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_show = openstackclient.image.v2.image:ShowImage | ||||||
|     image_set = openstackclient.image.v2.image:SetImage |     image_set = openstackclient.image.v2.image:SetImage | ||||||
|     image_unset = openstackclient.image.v2.image:UnsetImage |     image_unset = openstackclient.image.v2.image:UnsetImage | ||||||
|  |     image_stage = openstackclient.image.v2.image:StageImage | ||||||
|     image_task_show = openstackclient.image.v2.task:ShowTask |     image_task_show = openstackclient.image.v2.task:ShowTask | ||||||
|     image_task_list = openstackclient.image.v2.task:ListTask |     image_task_list = openstackclient.image.v2.task:ListTask | ||||||
|     image_metadef_namespace_list = openstackclient.image.v2.metadef_namespaces:ListMetadefNameSpaces |     image_metadef_namespace_list = openstackclient.image.v2.metadef_namespaces:ListMetadefNameSpaces | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Stephen Finucane
					Stephen Finucane