summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ironicclient/shell.py61
-rw-r--r--ironicclient/tests/functional/base.py4
-rw-r--r--ironicclient/tests/functional/test_help_msg.py8
-rw-r--r--ironicclient/tests/functional/test_json_response.py12
-rw-r--r--ironicclient/tests/functional/test_node.py15
-rw-r--r--ironicclient/tests/unit/test_shell.py119
-rw-r--r--releasenotes/notes/ironic-cli-version-a5cdec73d585444d.yaml25
7 files changed, 183 insertions, 61 deletions
diff --git a/ironicclient/shell.py b/ironicclient/shell.py
index 25676f1..b1a08cb 100644
--- a/ironicclient/shell.py
+++ b/ironicclient/shell.py
@@ -38,14 +38,8 @@ from ironicclient.common import utils
from ironicclient import exc
-LATEST_API_VERSION = ('1', 'latest')
-MISSING_VERSION_WARNING = (
- "You are using the default API version of the 'ironic' command. "
- "This is currently API version %s. In the future, the default will be "
- "the latest API version understood by both API and CLI. You can preserve "
- "the current behavior by passing the --ironic-api-version argument with "
- "the desired version or using the IRONIC_API_VERSION environment variable."
-)
+LAST_KNOWN_API_VERSION = 34
+LATEST_VERSION = '1.{}'.format(LAST_KNOWN_API_VERSION)
class IronicShell(object):
@@ -161,12 +155,10 @@ class IronicShell(object):
parser.add_argument('--ironic-api-version',
default=cliutils.env('IRONIC_API_VERSION',
- default=None),
- help=_('Accepts 1.x (where "x" is microversion) '
- 'or "latest". Defaults to '
- 'env[IRONIC_API_VERSION] or %s. Starting '
- 'with the Queens release this will '
- 'default to "latest".') % http.DEFAULT_VER)
+ default="latest"),
+ help=_('Accepts 1.x (where "x" is microversion), '
+ '1 or "latest". Defaults to '
+ 'env[IRONIC_API_VERSION] or "latest".'))
parser.add_argument('--ironic_api_version',
help=argparse.SUPPRESS)
@@ -300,35 +292,31 @@ class IronicShell(object):
print(' '.join(commands | options))
def _check_version(self, api_version):
- if api_version == 'latest':
- return LATEST_API_VERSION
- else:
- if api_version is None:
- print(MISSING_VERSION_WARNING % http.DEFAULT_VER,
- file=sys.stderr)
- api_version = '1'
+ """Validate the supplied API (micro)version.
+ :param api_version: API version as a string ("1", "1.x" or "latest")
+ :returns: tuple (major version, version string)
+ """
+ if api_version in ('1', 'latest'):
+ return (1, LATEST_VERSION)
+ else:
try:
versions = tuple(int(i) for i in api_version.split('.'))
except ValueError:
versions = ()
- if len(versions) == 1:
- # Default value of ironic_api_version is '1'.
- # If user not specify the value of api version, not passing
- # headers at all.
- os_ironic_api_version = None
- elif len(versions) == 2:
- os_ironic_api_version = api_version
- # In the case of '1.0'
- if versions[1] == 0:
- os_ironic_api_version = None
- else:
+
+ if not versions or len(versions) > 2:
msg = _("The requested API version %(ver)s is an unexpected "
"format. Acceptable formats are 'X', 'X.Y', or the "
- "literal string '%(latest)s'."
- ) % {'ver': api_version, 'latest': 'latest'}
+ "literal string 'latest'."
+ ) % {'ver': api_version}
raise exc.CommandError(msg)
+ if versions == (1, 0):
+ os_ironic_api_version = None
+ else:
+ os_ironic_api_version = api_version
+
api_major_version = versions[0]
return (api_major_version, os_ironic_api_version)
@@ -422,6 +410,11 @@ class IronicShell(object):
kwargs[key] = getattr(args, key)
kwargs['os_ironic_api_version'] = os_ironic_api_version
client = ironicclient.client.get_client(api_major_version, **kwargs)
+ if options.ironic_api_version in ('1', 'latest'):
+ # Allow negotiating a lower version, if the latest version
+ # supported by the client is higher than the latest version
+ # supported by the server.
+ client.http_client.api_version_select_state = 'default'
try:
args.func(client, args)
diff --git a/ironicclient/tests/functional/base.py b/ironicclient/tests/functional/base.py
index ab0aefa..7d0a693 100644
--- a/ironicclient/tests/functional/base.py
+++ b/ironicclient/tests/functional/base.py
@@ -216,7 +216,9 @@ class FunctionalTestBase(base.ClientTestBase):
if utils.get_object(node_list, node_id):
node_show = self.show_node(node_id)
- if node_show['provision_state'] != 'available':
+ if node_show['provision_state'] not in ('available',
+ 'manageable',
+ 'enroll'):
self.ironic('node-set-provision-state',
params='{0} deleted'.format(node_id))
if node_show['power_state'] not in ('None', 'off'):
diff --git a/ironicclient/tests/functional/test_help_msg.py b/ironicclient/tests/functional/test_help_msg.py
index 07b094b..39bde41 100644
--- a/ironicclient/tests/functional/test_help_msg.py
+++ b/ironicclient/tests/functional/test_help_msg.py
@@ -67,11 +67,3 @@ class IronicClientHelp(base.FunctionalTestBase):
self.assertIn(caption, output)
for string in subcommands:
self.assertIn(string, output)
-
- def test_warning_on_api_version(self):
- result = self._ironic('help', merge_stderr=True)
- self.assertIn('You are using the default API version', result)
-
- result = self._ironic('help', flags='--ironic-api-version 1.9',
- merge_stderr=True)
- self.assertNotIn('You are using the default API version', result)
diff --git a/ironicclient/tests/functional/test_json_response.py b/ironicclient/tests/functional/test_json_response.py
index ad1240f..dc3fda8 100644
--- a/ironicclient/tests/functional/test_json_response.py
+++ b/ironicclient/tests/functional/test_json_response.py
@@ -48,10 +48,10 @@ class TestNodeJsonResponse(base.FunctionalTestBase):
"uuid": {"type": "string"},
"console_enabled": {"type": "boolean"},
"target_provision_state": {"type": ["string", "null"]},
- "raid_config": {"type": "string"},
+ "raid_config": {"type": "object"},
"provision_updated_at": {"type": ["string", "null"]},
"maintenance": {"type": "boolean"},
- "target_raid_config": {"type": "string"},
+ "target_raid_config": {"type": "object"},
"inspection_started_at": {"type": ["string", "null"]},
"inspection_finished_at": {"type": ["string", "null"]},
"power_state": {"type": ["string", "null"]},
@@ -65,8 +65,12 @@ class TestNodeJsonResponse(base.FunctionalTestBase):
"driver_internal_info": {"type": "object"},
"chassis_uuid": {"type": ["string", "null"]},
"instance_info": {"type": "object"}
- }
- }
+ },
+ "patternProperties": {
+ ".*_interface$": {"type": ["string", "null"]}
+ },
+ "additionalProperties": True
+ }
def setUp(self):
super(TestNodeJsonResponse, self).setUp()
diff --git a/ironicclient/tests/functional/test_node.py b/ironicclient/tests/functional/test_node.py
index a512b7f..5466225 100644
--- a/ironicclient/tests/functional/test_node.py
+++ b/ironicclient/tests/functional/test_node.py
@@ -161,18 +161,21 @@ class NodeSanityTestIronicClient(base.FunctionalTestBase):
"""Test steps:
1) create node
- 2) check that provision state is 'available'
+ 2) check that provision state is 'enroll'
3) set new provision state to the node
4) check that provision state has been updated successfully
"""
node_show = self.show_node(self.node['uuid'])
- self.assertEqual('available', node_show['provision_state'])
+ self.assertEqual('enroll', node_show['provision_state'])
- self.set_node_provision_state(self.node['uuid'], 'active')
- node_show = self.show_node(self.node['uuid'])
-
- self.assertEqual('active', node_show['provision_state'])
+ for verb, target in [('manage', 'manageable'),
+ ('provide', 'available'),
+ ('active', 'active'),
+ ('deleted', 'available')]:
+ self.set_node_provision_state(self.node['uuid'], verb)
+ node_show = self.show_node(self.node['uuid'])
+ self.assertEqual(target, node_show['provision_state'])
def test_node_validate(self):
"""Test steps:
diff --git a/ironicclient/tests/unit/test_shell.py b/ironicclient/tests/unit/test_shell.py
index 29865a7..38d0de9 100644
--- a/ironicclient/tests/unit/test_shell.py
+++ b/ironicclient/tests/unit/test_shell.py
@@ -174,7 +174,8 @@ class ShellTest(utils.BaseTestCase):
'os_cert': None, 'os_key': None,
'max_retries': http.DEFAULT_MAX_RETRIES,
'retry_interval': http.DEFAULT_RETRY_INTERVAL,
- 'os_ironic_api_version': None, 'timeout': 600, 'insecure': False
+ 'os_ironic_api_version': ironic_shell.LATEST_VERSION,
+ 'timeout': 600, 'insecure': False
}
mock_client.assert_called_once_with(1, **expected_kwargs)
# Make sure we are actually prompted.
@@ -203,7 +204,8 @@ class ShellTest(utils.BaseTestCase):
'os_endpoint_type': '', 'os_cacert': None, 'os_cert': None,
'os_key': None, 'max_retries': http.DEFAULT_MAX_RETRIES,
'retry_interval': http.DEFAULT_RETRY_INTERVAL,
- 'os_ironic_api_version': None, 'timeout': 600, 'insecure': False
+ 'os_ironic_api_version': ironic_shell.LATEST_VERSION,
+ 'timeout': 600, 'insecure': False
}
mock_client.assert_called_once_with(1, **expected_kwargs)
self.assertFalse(mock_getpass.called)
@@ -254,17 +256,118 @@ class ShellTest(utils.BaseTestCase):
err = self.shell('--ironic-api-version latest help')[1]
self.assertIn('The "ironic" CLI is deprecated', err)
- self.assertRaises(exc.CommandError,
- self.shell, '--ironic-api-version 1.2.1 help')
+ err = self.shell('--ironic-api-version 1 help')[1]
+ self.assertIn('The "ironic" CLI is deprecated', err)
def test_invalid_ironic_api_version(self):
self.assertRaises(exceptions.UnsupportedVersion,
self.shell, '--ironic-api-version 0.8 help')
+ self.assertRaises(exc.CommandError,
+ self.shell, '--ironic-api-version 1.2.1 help')
- def test_warning_on_no_version(self):
- err = self.shell('help')[1]
- self.assertIn('You are using the default API version', err)
- self.assertIn('The "ironic" CLI is deprecated', err)
+ @mock.patch.object(client, 'get_client', autospec=True,
+ side_effect=keystone_exc.ConnectFailure)
+ def test_api_version_in_env(self, mock_client):
+ env = dict(IRONIC_API_VERSION='1.10', **FAKE_ENV)
+ self.make_env(environ_dict=env)
+ # We will get a ConnectFailure because there is no keystone.
+ self.assertRaises(keystone_exc.ConnectFailure,
+ self.shell, 'node-list')
+ expected_kwargs = {
+ 'ironic_url': '', 'os_auth_url': FAKE_ENV['OS_AUTH_URL'],
+ 'os_tenant_id': '', 'os_tenant_name': '',
+ 'os_username': FAKE_ENV['OS_USERNAME'], 'os_user_domain_id': '',
+ 'os_user_domain_name': '', 'os_password': FAKE_ENV['OS_PASSWORD'],
+ 'os_auth_token': '', 'os_project_id': '',
+ 'os_project_name': FAKE_ENV['OS_PROJECT_NAME'],
+ 'os_project_domain_id': '',
+ 'os_project_domain_name': '', 'os_region_name': '',
+ 'os_service_type': '', 'os_endpoint_type': '', 'os_cacert': None,
+ 'os_cert': None, 'os_key': None,
+ 'max_retries': http.DEFAULT_MAX_RETRIES,
+ 'retry_interval': http.DEFAULT_RETRY_INTERVAL,
+ 'os_ironic_api_version': '1.10',
+ 'timeout': 600, 'insecure': False
+ }
+ mock_client.assert_called_once_with(1, **expected_kwargs)
+
+ @mock.patch.object(client, 'get_client', autospec=True,
+ side_effect=keystone_exc.ConnectFailure)
+ def test_api_version_v1_in_env(self, mock_client):
+ env = dict(IRONIC_API_VERSION='1', **FAKE_ENV)
+ self.make_env(environ_dict=env)
+ # We will get a ConnectFailure because there is no keystone.
+ self.assertRaises(keystone_exc.ConnectFailure,
+ self.shell, 'node-list')
+ expected_kwargs = {
+ 'ironic_url': '', 'os_auth_url': FAKE_ENV['OS_AUTH_URL'],
+ 'os_tenant_id': '', 'os_tenant_name': '',
+ 'os_username': FAKE_ENV['OS_USERNAME'], 'os_user_domain_id': '',
+ 'os_user_domain_name': '', 'os_password': FAKE_ENV['OS_PASSWORD'],
+ 'os_auth_token': '', 'os_project_id': '',
+ 'os_project_name': FAKE_ENV['OS_PROJECT_NAME'],
+ 'os_project_domain_id': '',
+ 'os_project_domain_name': '', 'os_region_name': '',
+ 'os_service_type': '', 'os_endpoint_type': '', 'os_cacert': None,
+ 'os_cert': None, 'os_key': None,
+ 'max_retries': http.DEFAULT_MAX_RETRIES,
+ 'retry_interval': http.DEFAULT_RETRY_INTERVAL,
+ 'os_ironic_api_version': ironic_shell.LATEST_VERSION,
+ 'timeout': 600, 'insecure': False
+ }
+ mock_client.assert_called_once_with(1, **expected_kwargs)
+
+ @mock.patch.object(client, 'get_client', autospec=True,
+ side_effect=keystone_exc.ConnectFailure)
+ def test_api_version_in_args(self, mock_client):
+ env = dict(IRONIC_API_VERSION='1.10', **FAKE_ENV)
+ self.make_env(environ_dict=env)
+ # We will get a ConnectFailure because there is no keystone.
+ self.assertRaises(keystone_exc.ConnectFailure,
+ self.shell, '--ironic-api-version 1.11 node-list')
+ expected_kwargs = {
+ 'ironic_url': '', 'os_auth_url': FAKE_ENV['OS_AUTH_URL'],
+ 'os_tenant_id': '', 'os_tenant_name': '',
+ 'os_username': FAKE_ENV['OS_USERNAME'], 'os_user_domain_id': '',
+ 'os_user_domain_name': '', 'os_password': FAKE_ENV['OS_PASSWORD'],
+ 'os_auth_token': '', 'os_project_id': '',
+ 'os_project_name': FAKE_ENV['OS_PROJECT_NAME'],
+ 'os_project_domain_id': '',
+ 'os_project_domain_name': '', 'os_region_name': '',
+ 'os_service_type': '', 'os_endpoint_type': '', 'os_cacert': None,
+ 'os_cert': None, 'os_key': None,
+ 'max_retries': http.DEFAULT_MAX_RETRIES,
+ 'retry_interval': http.DEFAULT_RETRY_INTERVAL,
+ 'os_ironic_api_version': '1.11',
+ 'timeout': 600, 'insecure': False
+ }
+ mock_client.assert_called_once_with(1, **expected_kwargs)
+
+ @mock.patch.object(client, 'get_client', autospec=True,
+ side_effect=keystone_exc.ConnectFailure)
+ def test_api_version_v1_in_args(self, mock_client):
+ env = dict(IRONIC_API_VERSION='1.10', **FAKE_ENV)
+ self.make_env(environ_dict=env)
+ # We will get a ConnectFailure because there is no keystone.
+ self.assertRaises(keystone_exc.ConnectFailure,
+ self.shell, '--ironic-api-version 1 node-list')
+ expected_kwargs = {
+ 'ironic_url': '', 'os_auth_url': FAKE_ENV['OS_AUTH_URL'],
+ 'os_tenant_id': '', 'os_tenant_name': '',
+ 'os_username': FAKE_ENV['OS_USERNAME'], 'os_user_domain_id': '',
+ 'os_user_domain_name': '', 'os_password': FAKE_ENV['OS_PASSWORD'],
+ 'os_auth_token': '', 'os_project_id': '',
+ 'os_project_name': FAKE_ENV['OS_PROJECT_NAME'],
+ 'os_project_domain_id': '',
+ 'os_project_domain_name': '', 'os_region_name': '',
+ 'os_service_type': '', 'os_endpoint_type': '', 'os_cacert': None,
+ 'os_cert': None, 'os_key': None,
+ 'max_retries': http.DEFAULT_MAX_RETRIES,
+ 'retry_interval': http.DEFAULT_RETRY_INTERVAL,
+ 'os_ironic_api_version': ironic_shell.LATEST_VERSION,
+ 'timeout': 600, 'insecure': False
+ }
+ mock_client.assert_called_once_with(1, **expected_kwargs)
class TestCase(testtools.TestCase):
diff --git a/releasenotes/notes/ironic-cli-version-a5cdec73d585444d.yaml b/releasenotes/notes/ironic-cli-version-a5cdec73d585444d.yaml
new file mode 100644
index 0000000..22edecb
--- /dev/null
+++ b/releasenotes/notes/ironic-cli-version-a5cdec73d585444d.yaml
@@ -0,0 +1,25 @@
+---
+upgrade:
+ - |
+ The default API version for the ``ironic`` command is now "latest", which
+ is the maximum version understood by both the client and the server.
+ This change makes the CLI automatically pull in new features and changes
+ (including potentially breaking), when talking to new servers.
+
+ Scripts that rely on some specific API behavior should set the
+ ``IRONIC_API_VERSION`` environment variable or use the
+ ``--ironic-api-version`` CLI argument.
+
+ .. note:: This change does not affect the Python API.
+features:
+ - |
+ The ``ironic`` command now supports the specification of API version ``1``.
+ The actual version used will be the maximum 1.x version understood by both
+ the client and the server. Thus, it is currently identical to the
+ ``latest`` value.
+fixes:
+ - |
+ Users of the ``ironic`` command no longer have to specify an explicit
+ API version to use the latest features. The default API version is now
+ "latest", which is the maximum version understood by both the client
+ and the server.