summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBrian Rosmaita <rosmaita.fossdev@gmail.com>2018-03-22 15:49:44 -0400
committerBrian Rosmaita <rosmaita.fossdev@gmail.com>2018-04-05 12:11:13 +0000
commit4211b67e7aa560b97e19945d3bbb07a697628382 (patch)
treeac70e37648f09e26a02ff1fde862e664260b4534
parentba425b0b9287a2eff528bb5bc5b5a583ec280edc (diff)
downloadpython-glanceclient-4211b67e7aa560b97e19945d3bbb07a697628382.tar.gz
Make image-create-via-import fail faster
Add checks to the image-create-via-import commmand so that it provides better user feedback and doesn't begin the import workflow unless the input has a chance of succeeding. Preserves backward compatibility with the current image-create command by (1) allowing an image record only to be created when no import-method is specified AND no data is supplied, and (2) doing the glance-direct workflow when no import- method is specified AND data is provided. Also adds the ability for the import-method to be set as an env var OS_IMAGE_IMPORT_METHOD. Change-Id: I0a225f5471a9311217b5d90ebb5fd415c369129a Closes-bug: #1758149 (cherry picked from commit 79543a67edd7a15aa23426548d3be9ac5b99366d)
-rw-r--r--glanceclient/tests/unit/v2/test_shell_v2.py491
-rw-r--r--glanceclient/v2/shell.py90
2 files changed, 545 insertions, 36 deletions
diff --git a/glanceclient/tests/unit/v2/test_shell_v2.py b/glanceclient/tests/unit/v2/test_shell_v2.py
index c208f95..826c58b 100644
--- a/glanceclient/tests/unit/v2/test_shell_v2.py
+++ b/glanceclient/tests/unit/v2/test_shell_v2.py
@@ -14,10 +14,12 @@
# License for the specific language governing permissions and limitations
# under the License.
import argparse
+from copy import deepcopy
import json
import mock
import os
import six
+import sys
import tempfile
import testtools
@@ -487,7 +489,457 @@ class ShellV2Test(testtools.TestCase):
utils.print_dict.assert_called_once_with({
'id': 'pass', 'name': 'IMG-01', 'myprop': 'myval'})
- def test_do_image_create_via_import_with_web_download(self):
+ # NOTE(rosmaita): have to explicitly set to None the declared but unused
+ # arguments (the configparser does that for us normally)
+ base_args = {'name': 'Mortimer',
+ 'disk_format': 'raw',
+ 'container_format': 'bare',
+ 'progress': False,
+ 'file': None,
+ 'uri': None,
+ 'import_method': None}
+
+ import_info_response = {'import-methods': {
+ 'type': 'array',
+ 'description': 'Import methods available.',
+ 'value': ['glance-direct', 'web-download']}}
+
+ def _mock_utils_exit(self, msg):
+ sys.exit(msg)
+
+ @mock.patch('glanceclient.common.utils.exit')
+ @mock.patch('os.access')
+ @mock.patch('sys.stdin', autospec=True)
+ def test_neg_image_create_via_import_no_method_with_file_and_stdin(
+ self, mock_stdin, mock_access, mock_utils_exit):
+ expected_msg = ('You cannot use both --file and stdin with the '
+ 'glance-direct import method.')
+ my_args = self.base_args.copy()
+ my_args['file'] = 'some.file'
+ args = self._make_args(my_args)
+ mock_stdin.isatty = lambda: False
+ mock_access.return_value = True
+ mock_utils_exit.side_effect = self._mock_utils_exit
+ with mock.patch.object(self.gc.images,
+ 'get_import_info') as mocked_info:
+ mocked_info.return_value = self.import_info_response
+ try:
+ test_shell.do_image_create_via_import(self.gc, args)
+ self.fail("utils.exit should have been called")
+ except SystemExit:
+ pass
+ mock_utils_exit.assert_called_once_with(expected_msg)
+
+ @mock.patch('glanceclient.common.utils.exit')
+ @mock.patch('sys.stdin', autospec=True)
+ def test_neg_image_create_via_import_no_method_passing_uri(
+ self, mock_stdin, mock_utils_exit):
+ expected_msg = ('You cannot use --uri without specifying an import '
+ 'method.')
+ my_args = self.base_args.copy()
+ my_args['uri'] = 'http://example.com/whatever'
+ args = self._make_args(my_args)
+ mock_stdin.isatty = lambda: True
+ mock_utils_exit.side_effect = self._mock_utils_exit
+ with mock.patch.object(self.gc.images,
+ 'get_import_info') as mocked_info:
+ mocked_info.return_value = self.import_info_response
+ try:
+ test_shell.do_image_create_via_import(self.gc, args)
+ self.fail("utils.exit should have been called")
+ except SystemExit:
+ pass
+ mock_utils_exit.assert_called_once_with(expected_msg)
+
+ @mock.patch('glanceclient.common.utils.exit')
+ @mock.patch('sys.stdin', autospec=True)
+ def test_neg_image_create_via_import_glance_direct_no_data(
+ self, mock_stdin, mock_utils_exit):
+ expected_msg = ('You must specify a --file or provide data via stdin '
+ 'for the glance-direct import method.')
+ my_args = self.base_args.copy()
+ my_args['import_method'] = 'glance-direct'
+ args = self._make_args(my_args)
+ mock_stdin.isatty = lambda: True
+ mock_utils_exit.side_effect = self._mock_utils_exit
+ with mock.patch.object(self.gc.images,
+ 'get_import_info') as mocked_info:
+ mocked_info.return_value = self.import_info_response
+ try:
+ test_shell.do_image_create_via_import(self.gc, args)
+ self.fail("utils.exit should have been called")
+ except SystemExit:
+ pass
+ mock_utils_exit.assert_called_once_with(expected_msg)
+
+ @mock.patch('glanceclient.common.utils.exit')
+ @mock.patch('sys.stdin', autospec=True)
+ def test_neg_image_create_via_import_glance_direct_with_uri(
+ self, mock_stdin, mock_utils_exit):
+ expected_msg = ('You cannot specify a --uri with the glance-direct '
+ 'import method.')
+ my_args = self.base_args.copy()
+ my_args['import_method'] = 'glance-direct'
+ my_args['uri'] = 'https://example.com/some/stuff'
+ args = self._make_args(my_args)
+ mock_stdin.isatty = lambda: True
+ mock_utils_exit.side_effect = self._mock_utils_exit
+ with mock.patch.object(self.gc.images,
+ 'get_import_info') as mocked_info:
+ mocked_info.return_value = self.import_info_response
+ try:
+ test_shell.do_image_create_via_import(self.gc, args)
+ self.fail("utils.exit should have been called")
+ except SystemExit:
+ pass
+ mock_utils_exit.assert_called_once_with(expected_msg)
+
+ @mock.patch('glanceclient.common.utils.exit')
+ @mock.patch('os.access')
+ @mock.patch('sys.stdin', autospec=True)
+ def test_neg_image_create_via_import_glance_direct_with_file_and_uri(
+ self, mock_stdin, mock_access, mock_utils_exit):
+ expected_msg = ('You cannot specify a --uri with the glance-direct '
+ 'import method.')
+ my_args = self.base_args.copy()
+ my_args['import_method'] = 'glance-direct'
+ my_args['uri'] = 'https://example.com/some/stuff'
+ my_args['file'] = 'my.browncow'
+ args = self._make_args(my_args)
+ mock_stdin.isatty = lambda: True
+ mock_access.return_value = True
+ mock_utils_exit.side_effect = self._mock_utils_exit
+ with mock.patch.object(self.gc.images,
+ 'get_import_info') as mocked_info:
+ mocked_info.return_value = self.import_info_response
+ try:
+ test_shell.do_image_create_via_import(self.gc, args)
+ self.fail("utils.exit should have been called")
+ except SystemExit:
+ pass
+ mock_utils_exit.assert_called_once_with(expected_msg)
+
+ @mock.patch('glanceclient.common.utils.exit')
+ @mock.patch('sys.stdin', autospec=True)
+ def test_neg_image_create_via_import_glance_direct_with_data_and_uri(
+ self, mock_stdin, mock_utils_exit):
+ expected_msg = ('You cannot specify a --uri with the glance-direct '
+ 'import method.')
+ my_args = self.base_args.copy()
+ my_args['import_method'] = 'glance-direct'
+ my_args['uri'] = 'https://example.com/some/stuff'
+ args = self._make_args(my_args)
+ mock_stdin.isatty = lambda: False
+ mock_utils_exit.side_effect = self._mock_utils_exit
+ with mock.patch.object(self.gc.images,
+ 'get_import_info') as mocked_info:
+ mocked_info.return_value = self.import_info_response
+ try:
+ test_shell.do_image_create_via_import(self.gc, args)
+ self.fail("utils.exit should have been called")
+ except SystemExit:
+ pass
+ mock_utils_exit.assert_called_once_with(expected_msg)
+
+ @mock.patch('glanceclient.common.utils.exit')
+ @mock.patch('sys.stdin', autospec=True)
+ def test_neg_image_create_via_import_web_download_no_uri(
+ self, mock_stdin, mock_utils_exit):
+ expected_msg = ('URI is required for web-download import method. '
+ 'Please use \'--uri <uri>\'.')
+ my_args = self.base_args.copy()
+ my_args['import_method'] = 'web-download'
+ args = self._make_args(my_args)
+ mock_stdin.isatty = lambda: True
+ mock_utils_exit.side_effect = self._mock_utils_exit
+ with mock.patch.object(self.gc.images,
+ 'get_import_info') as mocked_info:
+ mocked_info.return_value = self.import_info_response
+ try:
+ test_shell.do_image_create_via_import(self.gc, args)
+ self.fail("utils.exit should have been called")
+ except SystemExit:
+ pass
+ mock_utils_exit.assert_called_once_with(expected_msg)
+
+ @mock.patch('glanceclient.common.utils.exit')
+ @mock.patch('sys.stdin', autospec=True)
+ def test_neg_image_create_via_import_web_download_no_uri_with_file(
+ self, mock_stdin, mock_utils_exit):
+ expected_msg = ('URI is required for web-download import method. '
+ 'Please use \'--uri <uri>\'.')
+ my_args = self.base_args.copy()
+ my_args['import_method'] = 'web-download'
+ my_args['file'] = 'my.browncow'
+ args = self._make_args(my_args)
+ mock_stdin.isatty = lambda: True
+ mock_utils_exit.side_effect = self._mock_utils_exit
+ with mock.patch.object(self.gc.images,
+ 'get_import_info') as mocked_info:
+ mocked_info.return_value = self.import_info_response
+ try:
+ test_shell.do_image_create_via_import(self.gc, args)
+ self.fail("utils.exit should have been called")
+ except SystemExit:
+ pass
+ mock_utils_exit.assert_called_once_with(expected_msg)
+
+ @mock.patch('glanceclient.common.utils.exit')
+ @mock.patch('sys.stdin', autospec=True)
+ def test_neg_image_create_via_import_web_download_no_uri_with_data(
+ self, mock_stdin, mock_utils_exit):
+ expected_msg = ('URI is required for web-download import method. '
+ 'Please use \'--uri <uri>\'.')
+ my_args = self.base_args.copy()
+ my_args['import_method'] = 'web-download'
+ my_args['file'] = 'my.browncow'
+ args = self._make_args(my_args)
+ mock_stdin.isatty = lambda: False
+ mock_utils_exit.side_effect = self._mock_utils_exit
+ with mock.patch.object(self.gc.images,
+ 'get_import_info') as mocked_info:
+ mocked_info.return_value = self.import_info_response
+ try:
+ test_shell.do_image_create_via_import(self.gc, args)
+ self.fail("utils.exit should have been called")
+ except SystemExit:
+ pass
+ mock_utils_exit.assert_called_once_with(expected_msg)
+
+ @mock.patch('glanceclient.common.utils.exit')
+ @mock.patch('sys.stdin', autospec=True)
+ def test_neg_image_create_via_import_web_download_with_data_and_uri(
+ self, mock_stdin, mock_utils_exit):
+ expected_msg = ('You cannot pass data via stdin with the web-download '
+ 'import method.')
+ my_args = self.base_args.copy()
+ my_args['import_method'] = 'web-download'
+ my_args['uri'] = 'https://example.com/some/stuff'
+ args = self._make_args(my_args)
+ mock_stdin.isatty = lambda: False
+ mock_utils_exit.side_effect = self._mock_utils_exit
+ with mock.patch.object(self.gc.images,
+ 'get_import_info') as mocked_info:
+ mocked_info.return_value = self.import_info_response
+ try:
+ test_shell.do_image_create_via_import(self.gc, args)
+ self.fail("utils.exit should have been called")
+ except SystemExit:
+ pass
+ mock_utils_exit.assert_called_once_with(expected_msg)
+
+ @mock.patch('glanceclient.common.utils.exit')
+ @mock.patch('sys.stdin', autospec=True)
+ def test_neg_image_create_via_import_web_download_with_file_and_uri(
+ self, mock_stdin, mock_utils_exit):
+ expected_msg = ('You cannot specify a --file with the web-download '
+ 'import method.')
+ my_args = self.base_args.copy()
+ my_args['import_method'] = 'web-download'
+ my_args['uri'] = 'https://example.com/some/stuff'
+ my_args['file'] = 'my.browncow'
+ args = self._make_args(my_args)
+ mock_stdin.isatty = lambda: True
+ mock_utils_exit.side_effect = self._mock_utils_exit
+ with mock.patch.object(self.gc.images,
+ 'get_import_info') as mocked_info:
+ mocked_info.return_value = self.import_info_response
+ try:
+ test_shell.do_image_create_via_import(self.gc, args)
+ self.fail("utils.exit should have been called")
+ except SystemExit:
+ pass
+ mock_utils_exit.assert_called_once_with(expected_msg)
+
+ @mock.patch('glanceclient.common.utils.exit')
+ @mock.patch('sys.stdin', autospec=True)
+ def test_neg_image_create_via_import_bad_method(
+ self, mock_stdin, mock_utils_exit):
+ expected_msg = ('Import method \'swift-party-time\' is not valid '
+ 'for this cloud. Valid values can be retrieved with '
+ 'import-info command.')
+ my_args = self.base_args.copy()
+ my_args['import_method'] = 'swift-party-time'
+ args = self._make_args(my_args)
+ mock_stdin.isatty = lambda: True
+ mock_utils_exit.side_effect = self._mock_utils_exit
+ with mock.patch.object(self.gc.images,
+ 'get_import_info') as mocked_info:
+ mocked_info.return_value = self.import_info_response
+ try:
+ test_shell.do_image_create_via_import(self.gc, args)
+ self.fail("utils.exit should have been called")
+ except SystemExit:
+ pass
+ mock_utils_exit.assert_called_once_with(expected_msg)
+
+ @mock.patch('glanceclient.common.utils.exit')
+ @mock.patch('sys.stdin', autospec=True)
+ def test_neg_image_create_via_import_no_method_with_data_and_method_NA(
+ self, mock_stdin, mock_utils_exit):
+ expected_msg = ('Import method \'glance-direct\' is not valid '
+ 'for this cloud. Valid values can be retrieved with '
+ 'import-info command.')
+ args = self._make_args(self.base_args)
+ # need to fake some data, or this is "just like" a
+ # create-image-record-only call
+ mock_stdin.isatty = lambda: False
+ mock_utils_exit.side_effect = self._mock_utils_exit
+ my_import_info_response = deepcopy(self.import_info_response)
+ my_import_info_response['import-methods']['value'] = ['web-download']
+ with mock.patch.object(self.gc.images,
+ 'get_import_info') as mocked_info:
+ mocked_info.return_value = my_import_info_response
+ try:
+ test_shell.do_image_create_via_import(self.gc, args)
+ self.fail("utils.exit should have been called")
+ except SystemExit:
+ pass
+ mock_utils_exit.assert_called_once_with(expected_msg)
+
+ @mock.patch('glanceclient.common.utils.exit')
+ @mock.patch('sys.stdin', autospec=True)
+ def test_neg_image_create_via_import_good_method_not_available(
+ self, mock_stdin, mock_utils_exit):
+ """Make sure the good method names aren't hard coded somewhere"""
+ expected_msg = ('Import method \'glance-direct\' is not valid for '
+ 'this cloud. Valid values can be retrieved with '
+ 'import-info command.')
+ my_args = self.base_args.copy()
+ my_args['import_method'] = 'glance-direct'
+ args = self._make_args(my_args)
+ mock_stdin.isatty = lambda: True
+ mock_utils_exit.side_effect = self._mock_utils_exit
+ my_import_info_response = deepcopy(self.import_info_response)
+ my_import_info_response['import-methods']['value'] = ['bad-bad-method']
+ with mock.patch.object(self.gc.images,
+ 'get_import_info') as mocked_info:
+ mocked_info.return_value = my_import_info_response
+ try:
+ test_shell.do_image_create_via_import(self.gc, args)
+ self.fail("utils.exit should have been called")
+ except SystemExit:
+ pass
+ mock_utils_exit.assert_called_once_with(expected_msg)
+
+ @mock.patch('glanceclient.v2.shell.do_image_import')
+ @mock.patch('glanceclient.v2.shell.do_image_stage')
+ @mock.patch('sys.stdin', autospec=True)
+ def test_image_create_via_import_no_method_with_stdin(
+ self, mock_stdin, mock_do_stage, mock_do_import):
+ """Backward compat -> handle this like a glance-direct"""
+ mock_stdin.isatty = lambda: False
+ self.mock_get_data_file.return_value = six.StringIO()
+ args = self._make_args(self.base_args)
+ with mock.patch.object(self.gc.images, 'create') as mocked_create:
+ with mock.patch.object(self.gc.images, 'get') as mocked_get:
+ with mock.patch.object(self.gc.images,
+ 'get_import_info') as mocked_info:
+
+ ignore_fields = ['self', 'access', 'schema']
+ expect_image = dict([(field, field) for field in
+ ignore_fields])
+ expect_image['id'] = 'via-stdin'
+ expect_image['name'] = 'Mortimer'
+ expect_image['disk_format'] = 'raw'
+ expect_image['container_format'] = 'bare'
+ mocked_create.return_value = expect_image
+ mocked_get.return_value = expect_image
+ mocked_info.return_value = self.import_info_response
+
+ test_shell.do_image_create_via_import(self.gc, args)
+ mocked_create.assert_called_once()
+ mock_do_stage.assert_called_once()
+ mock_do_import.assert_called_once()
+ mocked_get.assert_called_with('via-stdin')
+ utils.print_dict.assert_called_with({
+ 'id': 'via-stdin', 'name': 'Mortimer',
+ 'disk_format': 'raw', 'container_format': 'bare'})
+
+ @mock.patch('glanceclient.v2.shell.do_image_import')
+ @mock.patch('glanceclient.v2.shell.do_image_stage')
+ @mock.patch('os.access')
+ @mock.patch('sys.stdin', autospec=True)
+ def test_image_create_via_import_no_method_passing_file(
+ self, mock_stdin, mock_access, mock_do_stage, mock_do_import):
+ """Backward compat -> handle this like a glance-direct"""
+ mock_stdin.isatty = lambda: True
+ self.mock_get_data_file.return_value = six.StringIO()
+ mock_access.return_value = True
+ my_args = self.base_args.copy()
+ my_args['file'] = 'fake-image-file.browncow'
+ args = self._make_args(my_args)
+ with mock.patch.object(self.gc.images, 'create') as mocked_create:
+ with mock.patch.object(self.gc.images, 'get') as mocked_get:
+ with mock.patch.object(self.gc.images,
+ 'get_import_info') as mocked_info:
+
+ ignore_fields = ['self', 'access', 'schema']
+ expect_image = dict([(field, field) for field in
+ ignore_fields])
+ expect_image['id'] = 'via-file'
+ expect_image['name'] = 'Mortimer'
+ expect_image['disk_format'] = 'raw'
+ expect_image['container_format'] = 'bare'
+ mocked_create.return_value = expect_image
+ mocked_get.return_value = expect_image
+ mocked_info.return_value = self.import_info_response
+
+ test_shell.do_image_create_via_import(self.gc, args)
+ mocked_create.assert_called_once()
+ mock_do_stage.assert_called_once()
+ mock_do_import.assert_called_once()
+ mocked_get.assert_called_with('via-file')
+ utils.print_dict.assert_called_with({
+ 'id': 'via-file', 'name': 'Mortimer',
+ 'disk_format': 'raw', 'container_format': 'bare'})
+
+ @mock.patch('glanceclient.v2.shell.do_image_import')
+ @mock.patch('glanceclient.v2.shell.do_image_stage')
+ @mock.patch('sys.stdin', autospec=True)
+ def test_do_image_create_via_import_with_no_method_no_data(
+ self, mock_stdin, mock_do_image_stage, mock_do_image_import):
+ """Create an image record without calling do_stage or do_import"""
+ img_create_args = {'name': 'IMG-11',
+ 'os_architecture': 'powerpc',
+ 'id': 'watch-out-for-ossn-0075',
+ 'progress': False}
+ client_args = {'import_method': None,
+ 'file': None,
+ 'uri': None}
+ temp_args = img_create_args.copy()
+ temp_args.update(client_args)
+ args = self._make_args(temp_args)
+ with mock.patch.object(self.gc.images, 'create') as mocked_create:
+ with mock.patch.object(self.gc.images, 'get') as mocked_get:
+ with mock.patch.object(self.gc.images,
+ 'get_import_info') as mocked_info:
+
+ ignore_fields = ['self', 'access', 'schema']
+ expect_image = dict([(field, field) for field in
+ ignore_fields])
+ expect_image['name'] = 'IMG-11'
+ expect_image['id'] = 'watch-out-for-ossn-0075'
+ expect_image['os_architecture'] = 'powerpc'
+ mocked_create.return_value = expect_image
+ mocked_get.return_value = expect_image
+ mocked_info.return_value = self.import_info_response
+ mock_stdin.isatty = lambda: True
+
+ test_shell.do_image_create_via_import(self.gc, args)
+ mocked_create.assert_called_once_with(**img_create_args)
+ mocked_get.assert_called_with('watch-out-for-ossn-0075')
+ mock_do_image_stage.assert_not_called()
+ mock_do_image_import.assert_not_called()
+ utils.print_dict.assert_called_with({
+ 'name': 'IMG-11', 'os_architecture': 'powerpc',
+ 'id': 'watch-out-for-ossn-0075'})
+
+ @mock.patch('glanceclient.v2.shell.do_image_import')
+ @mock.patch('glanceclient.v2.shell.do_image_stage')
+ @mock.patch('sys.stdin', autospec=True)
+ def test_do_image_create_via_import_with_web_download(
+ self, mock_stdin, mock_do_image_stage, mock_do_image_import):
temp_args = {'name': 'IMG-01',
'disk_format': 'vhd',
'container_format': 'bare',
@@ -497,22 +949,29 @@ class ShellV2Test(testtools.TestCase):
args = self._make_args(temp_args)
with mock.patch.object(self.gc.images, 'create') as mocked_create:
with mock.patch.object(self.gc.images, 'get') as mocked_get:
- ignore_fields = ['self', 'access', 'schema']
- expect_image = dict([(field, field) for field in
- ignore_fields])
- expect_image['id'] = 'pass'
- expect_image['name'] = 'IMG-01'
- expect_image['disk_format'] = 'vhd'
- expect_image['container_format'] = 'bare'
- mocked_create.return_value = expect_image
- mocked_get.return_value = expect_image
- test_shell.do_image_create_via_import(self.gc, args)
+ with mock.patch.object(self.gc.images,
+ 'get_import_info') as mocked_info:
+
+ ignore_fields = ['self', 'access', 'schema']
+ expect_image = dict([(field, field) for field in
+ ignore_fields])
+ expect_image['id'] = 'pass'
+ expect_image['name'] = 'IMG-01'
+ expect_image['disk_format'] = 'vhd'
+ expect_image['container_format'] = 'bare'
+ mocked_create.return_value = expect_image
+ mocked_get.return_value = expect_image
+ mocked_info.return_value = self.import_info_response
+ mock_stdin.isatty = lambda: True
- mocked_create.assert_called_once_with(**temp_args)
- mocked_get.assert_called_with('pass')
- utils.print_dict.assert_called_with({
- 'id': 'pass', 'name': 'IMG-01', 'disk_format': 'vhd',
- 'container_format': 'bare'})
+ test_shell.do_image_create_via_import(self.gc, args)
+ mock_do_image_stage.assert_not_called()
+ mock_do_image_import.assert_called_once()
+ mocked_create.assert_called_once_with(**temp_args)
+ mocked_get.assert_called_with('pass')
+ utils.print_dict.assert_called_with({
+ 'id': 'pass', 'name': 'IMG-01', 'disk_format': 'vhd',
+ 'container_format': 'bare'})
def test_do_image_update_no_user_props(self):
args = self._make_args({'id': 'pass', 'name': 'IMG-01',
diff --git a/glanceclient/v2/shell.py b/glanceclient/v2/shell.py
index baf1754..cbc9033 100644
--- a/glanceclient/v2/shell.py
+++ b/glanceclient/v2/shell.py
@@ -102,14 +102,34 @@ def do_image_create(gc, args):
'passed to the client via stdin.'))
@utils.arg('--progress', action='store_true', default=False,
help=_('Show upload progress bar.'))
-@utils.arg('--import-method', metavar='<METHOD>', default='glance-direct',
+@utils.arg('--import-method', metavar='<METHOD>',
+ default=utils.env('OS_IMAGE_IMPORT_METHOD', default=None),
help=_('Import method used for Image Import workflow. '
- 'Valid values can be retrieved with import-info command.'))
+ 'Valid values can be retrieved with import-info command. '
+ 'Defaults to env[OS_IMAGE_IMPORT_METHOD] or if that is '
+ 'undefined uses \'glance-direct\' if data is provided using '
+ '--file or stdin. Otherwise, simply creates an image '
+ 'record if no import-method and no data is supplied'))
@utils.arg('--uri', metavar='<IMAGE_URL>', default=None,
help=_('URI to download the external image.'))
@utils.on_data_require_fields(DATA_FIELDS)
def do_image_create_via_import(gc, args):
- """EXPERIMENTAL: Create a new image via image import."""
+ """EXPERIMENTAL: Create a new image via image import.
+
+ Use the interoperable image import workflow to create an image. This
+ command is designed to be backward compatible with the current image-create
+ command, so its behavior is as follows:
+
+ * If an import-method is specified (either on the command line or through
+ the OS_IMAGE_IMPORT_METHOD environment variable, then you must provide a
+ data source appropriate to that method (for example, --file for
+ glance-direct, or --uri for web-download).
+ * If no import-method is specified AND you provide either a --file or
+ data to stdin, the command will assume you are using the 'glance-direct'
+ import-method and will act accordingly.
+ * If no import-method is specified and no data is supplied via --file or
+ stdin, the command will simply create an image record in 'queued' status.
+ """
schema = gc.schemas.get("image")
_args = [(x[0].replace('-', '_'), x[1]) for x in vars(args).items()]
fields = dict(filter(lambda x: x[1] is not None and
@@ -123,29 +143,59 @@ def do_image_create_via_import(gc, args):
fields[key] = value
file_name = fields.pop('file', None)
- if file_name is not None and os.access(file_name, os.R_OK) is False:
- utils.exit("File %s does not exist or user does not have read "
- "privileges to it" % file_name)
+ using_stdin = not sys.stdin.isatty()
+
+ # special processing for backward compatibility with image-create
+ if args.import_method is None and (file_name or using_stdin):
+ args.import_method = 'glance-direct'
+
+ # determine whether the requested import method is valid
import_methods = gc.images.get_import_info().get('import-methods')
- if file_name and (not import_methods or
- 'glance-direct' not in import_methods.get('value')):
- utils.exit("No suitable import method available for direct upload, "
- "please use image-create instead.")
- if args.import_method == 'web-download' and not args.uri:
+ if args.import_method and args.import_method not in import_methods.get(
+ 'value'):
+ utils.exit("Import method '%s' is not valid for this cloud. "
+ "Valid values can be retrieved with import-info command." %
+ args.import_method)
+
+ # make sure we have all and only correct inputs for the requested method
+ if args.import_method is None:
+ if args.uri:
+ utils.exit("You cannot use --uri without specifying an import "
+ "method.")
+ if args.import_method == 'glance-direct':
+ if args.uri:
+ utils.exit("You cannot specify a --uri with the glance-direct "
+ "import method.")
+ if file_name is not None and os.access(file_name, os.R_OK) is False:
+ utils.exit("File %s does not exist or user does not have read "
+ "privileges to it." % file_name)
+ if file_name is not None and using_stdin:
+ utils.exit("You cannot use both --file and stdin with the "
+ "glance-direct import method.")
+ if not file_name and not using_stdin:
+ utils.exit("You must specify a --file or provide data via stdin "
+ "for the glance-direct import method.")
+ if args.import_method == 'web-download':
+ if not args.uri:
utils.exit("URI is required for web-download import method. "
"Please use '--uri <uri>'.")
- if args.uri and args.import_method != 'web-download':
- utils.exit("Import method should be 'web-download' if URI is "
- "provided.")
-
+ if file_name:
+ utils.exit("You cannot specify a --file with the web-download "
+ "import method.")
+ if using_stdin:
+ utils.exit("You cannot pass data via stdin with the web-download "
+ "import method.")
+
+ # process
image = gc.images.create(**fields)
try:
args.id = image['id']
- if utils.get_data_file(args) is not None:
- args.size = None
- do_image_stage(gc, args)
- args.from_create = True
- do_image_import(gc, args)
+ if args.import_method:
+ if utils.get_data_file(args) is not None:
+ args.size = None
+ do_image_stage(gc, args)
+ args.from_create = True
+ do_image_import(gc, args)
image = gc.images.get(args.id)
finally:
utils.print_image(image)