summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMatt Riedemann <mriedem.os@gmail.com>2017-12-14 18:20:45 -0500
committerYikun Jiang <yikunkero@gmail.com>2018-01-15 14:42:53 +0800
commit038cfdd5b3395acbf483dc982cae3eba34ddf1c5 (patch)
treefc020977e18a1d40293957bc0e6e9a089361900b
parentfefc3ba723865307f4cc2ca287eb59faa4e8a5b1 (diff)
downloadpython-novaclient-038cfdd5b3395acbf483dc982cae3eba34ddf1c5.tar.gz
Add support for the 2.57 microversion
With the 2.57 microversion, we: * Deprecate the --file option from the nova boot and nova rebuild CLIs and API bindings. * Add --user-data and --user-data-unset to the nova rebuild CLI and API bindings. * Deprecate the maxPersonality and maxPersonalitySize fields from the nova limits and nova absolute-limits CLIs and API bindings. * Deprecate injected_files, injected_file_content_bytes, and injected_file_path_bytes from the nova quota-show, nova quota-update, nova quota-defaults, nova quota-class-show, and nova quota-class-update CLIs and API bindings. Part of blueprint deprecate-file-injection Change-Id: Id13e3eac3ef87d429454042ac7046e865774ff8e
-rw-r--r--novaclient/__init__.py2
-rw-r--r--novaclient/exceptions.py1
-rw-r--r--novaclient/tests/functional/v2/test_quotas.py13
-rw-r--r--novaclient/tests/unit/fixture_data/limits.py25
-rw-r--r--novaclient/tests/unit/fixture_data/quotas.py18
-rw-r--r--novaclient/tests/unit/v2/fakes.py41
-rw-r--r--novaclient/tests/unit/v2/test_limits.py43
-rw-r--r--novaclient/tests/unit/v2/test_quota_classes.py36
-rw-r--r--novaclient/tests/unit/v2/test_quotas.py40
-rw-r--r--novaclient/tests/unit/v2/test_servers.py84
-rw-r--r--novaclient/tests/unit/v2/test_shell.py100
-rw-r--r--novaclient/v2/quota_classes.py29
-rw-r--r--novaclient/v2/quotas.py39
-rw-r--r--novaclient/v2/servers.py66
-rw-r--r--novaclient/v2/shell.py119
-rw-r--r--releasenotes/notes/microversion-v2_57-acae2ee11ddae4fb.yaml31
16 files changed, 575 insertions, 112 deletions
diff --git a/novaclient/__init__.py b/novaclient/__init__.py
index 435ff9c9..2103d5ce 100644
--- a/novaclient/__init__.py
+++ b/novaclient/__init__.py
@@ -25,4 +25,4 @@ API_MIN_VERSION = api_versions.APIVersion("2.1")
# when client supported the max version, and bumped sequentially, otherwise
# the client may break due to server side new version may include some
# backward incompatible change.
-API_MAX_VERSION = api_versions.APIVersion("2.56")
+API_MAX_VERSION = api_versions.APIVersion("2.57")
diff --git a/novaclient/exceptions.py b/novaclient/exceptions.py
index 707aa889..a7046d33 100644
--- a/novaclient/exceptions.py
+++ b/novaclient/exceptions.py
@@ -48,6 +48,7 @@ class UnsupportedAttribute(AttributeError):
self.message = (
"'%(name)s' argument is only allowed since microversion "
"%(start)s." % {"name": argument_name, "start": start_version})
+ super(UnsupportedAttribute, self).__init__(self.message)
class CommandError(Exception):
diff --git a/novaclient/tests/functional/v2/test_quotas.py b/novaclient/tests/functional/v2/test_quotas.py
index 1d4e6af3..effddf8a 100644
--- a/novaclient/tests/functional/v2/test_quotas.py
+++ b/novaclient/tests/functional/v2/test_quotas.py
@@ -52,7 +52,7 @@ class TestQuotasNovaClient2_35(test_quotas.TestQuotasNovaClient):
class TestQuotasNovaClient2_36(TestQuotasNovaClient2_35):
"""Nova quotas functional tests."""
- COMPUTE_API_VERSION = "2.latest"
+ COMPUTE_API_VERSION = "2.36"
# The 2.36 microversion stops proxying network quota resources like
# floating/fixed IPs and security groups/rules.
@@ -61,3 +61,14 @@ class TestQuotasNovaClient2_36(TestQuotasNovaClient2_35):
'injected_file_content_bytes',
'injected_file_path_bytes', 'key_pairs',
'server_groups', 'server_group_members']
+
+
+class TestQuotasNovaClient2_57(TestQuotasNovaClient2_35):
+ """Nova quotas functional tests."""
+
+ COMPUTE_API_VERSION = "2.latest"
+
+ # The 2.57 microversion deprecates injected_file* quotas.
+ _quota_resources = ['instances', 'cores', 'ram',
+ 'metadata_items', 'key_pairs',
+ 'server_groups', 'server_group_members']
diff --git a/novaclient/tests/unit/fixture_data/limits.py b/novaclient/tests/unit/fixture_data/limits.py
index f9a86ff7..0e798658 100644
--- a/novaclient/tests/unit/fixture_data/limits.py
+++ b/novaclient/tests/unit/fixture_data/limits.py
@@ -16,6 +16,13 @@ from novaclient.tests.unit.fixture_data import base
class Fixture(base.Fixture):
base_url = 'limits'
+ absolute = {
+ "maxTotalRAMSize": 51200,
+ "maxServerMeta": 5,
+ "maxImageMeta": 5,
+ "maxPersonality": 5,
+ "maxPersonalitySize": 10240
+ }
def setUp(self):
super(Fixture, self).setUp()
@@ -64,13 +71,7 @@ class Fixture(base.Fixture):
]
}
],
- "absolute": {
- "maxTotalRAMSize": 51200,
- "maxServerMeta": 5,
- "maxImageMeta": 5,
- "maxPersonality": 5,
- "maxPersonalitySize": 10240
- },
+ "absolute": self.absolute,
},
}
@@ -78,3 +79,13 @@ class Fixture(base.Fixture):
self.requests_mock.get(self.url(),
json=get_limits,
headers=headers)
+
+
+class Fixture2_57(Fixture):
+ """Fixture data for the 2.57 microversion where personality files are
+ deprecated.
+ """
+ absolute = {
+ "maxTotalRAMSize": 51200,
+ "maxServerMeta": 5
+ }
diff --git a/novaclient/tests/unit/fixture_data/quotas.py b/novaclient/tests/unit/fixture_data/quotas.py
index e3d179e2..1ffa8c26 100644
--- a/novaclient/tests/unit/fixture_data/quotas.py
+++ b/novaclient/tests/unit/fixture_data/quotas.py
@@ -57,6 +57,7 @@ class V1(base.Fixture):
'injected_file_content_bytes': 1,
'injected_file_path_bytes': 1,
'ram': 1,
+ 'fixed_ips': -1,
'floating_ips': 1,
'instances': 1,
'injected_files': 1,
@@ -67,3 +68,20 @@ class V1(base.Fixture):
'server_groups': 1,
'server_group_members': 1
}
+
+
+class V2_57(V1):
+ """2.57 fixture data where there are no injected file or network resources
+ """
+
+ def test_quota(self, tenant_id='test'):
+ return {
+ 'id': tenant_id,
+ 'metadata_items': 1,
+ 'ram': 1,
+ 'instances': 1,
+ 'cores': 1,
+ 'key_pairs': 1,
+ 'server_groups': 1,
+ 'server_group_members': 1
+ }
diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py
index 9b059a38..11789ec1 100644
--- a/novaclient/tests/unit/v2/fakes.py
+++ b/novaclient/tests/unit/v2/fakes.py
@@ -331,6 +331,14 @@ class FakeSessionClient(base_client.SessionClient):
#
def get_limits(self, **kw):
+ absolute = {
+ "maxTotalRAMSize": 51200,
+ "maxServerMeta": 5,
+ "maxImageMeta": 5
+ }
+ # 2.57 removes injected_file* entries from the response.
+ if self.api_version < api_versions.APIVersion('2.57'):
+ absolute.update({"maxPersonality": 5, "maxPersonalitySize": 10240})
return (200, {}, {"limits": {
"rate": [
{
@@ -374,13 +382,7 @@ class FakeSessionClient(base_client.SessionClient):
]
}
],
- "absolute": {
- "maxTotalRAMSize": 51200,
- "maxServerMeta": 5,
- "maxImageMeta": 5,
- "maxPersonality": 5,
- "maxPersonalitySize": 10240
- },
+ "absolute": absolute,
}})
#
@@ -1297,6 +1299,19 @@ class FakeSessionClient(base_client.SessionClient):
#
def get_os_quota_class_sets_test(self, **kw):
+ # 2.57 removes injected_file* entries from the response.
+ if self.api_version >= api_versions.APIVersion('2.57'):
+ return (200, FAKE_RESPONSE_HEADERS, {
+ 'quota_class_set': {
+ 'id': 'test',
+ 'metadata_items': 1,
+ 'ram': 1,
+ 'instances': 1,
+ 'cores': 1,
+ 'key_pairs': 1,
+ 'server_groups': 1,
+ 'server_group_members': 1}})
+
if self.api_version >= api_versions.APIVersion('2.50'):
return (200, FAKE_RESPONSE_HEADERS, {
'quota_class_set': {
@@ -1329,6 +1344,18 @@ class FakeSessionClient(base_client.SessionClient):
def put_os_quota_class_sets_test(self, body, **kw):
assert list(body) == ['quota_class_set']
+ # 2.57 removes injected_file* entries from the response.
+ if self.api_version >= api_versions.APIVersion('2.57'):
+ return (200, {}, {
+ 'quota_class_set': {
+ 'metadata_items': 1,
+ 'ram': 1,
+ 'instances': 1,
+ 'cores': 1,
+ 'key_pairs': 1,
+ 'server_groups': 1,
+ 'server_group_members': 1}})
+
if self.api_version >= api_versions.APIVersion('2.50'):
return (200, {}, {
'quota_class_set': {
diff --git a/novaclient/tests/unit/v2/test_limits.py b/novaclient/tests/unit/v2/test_limits.py
index a0b8bcf2..1abcd1a5 100644
--- a/novaclient/tests/unit/v2/test_limits.py
+++ b/novaclient/tests/unit/v2/test_limits.py
@@ -11,6 +11,7 @@
# License for the specific language governing permissions and limitations
# under the License.
+from novaclient import api_versions
from novaclient.tests.unit.fixture_data import client
from novaclient.tests.unit.fixture_data import limits as data
from novaclient.tests.unit import utils
@@ -22,6 +23,8 @@ class LimitsTest(utils.FixturedTestCase):
client_fixture_class = client.V1
data_fixture_class = data.Fixture
+ supports_image_meta = True # 2.39 deprecates maxImageMeta
+ supports_personality = True # 2.57 deprecates maxPersonality*
def test_get_limits(self):
obj = self.cs.limits.get()
@@ -39,13 +42,16 @@ class LimitsTest(utils.FixturedTestCase):
obj = self.cs.limits.get(reserved=True)
self.assert_request_id(obj, fakes.FAKE_REQUEST_ID_LIST)
- expected = (
+ expected = [
limits.AbsoluteLimit("maxTotalRAMSize", 51200),
- limits.AbsoluteLimit("maxServerMeta", 5),
- limits.AbsoluteLimit("maxImageMeta", 5),
- limits.AbsoluteLimit("maxPersonality", 5),
- limits.AbsoluteLimit("maxPersonalitySize", 10240),
- )
+ limits.AbsoluteLimit("maxServerMeta", 5)
+ ]
+ if self.supports_image_meta:
+ expected.append(limits.AbsoluteLimit("maxImageMeta", 5))
+ if self.supports_personality:
+ expected.extend([
+ limits.AbsoluteLimit("maxPersonality", 5),
+ limits.AbsoluteLimit("maxPersonalitySize", 10240)])
self.assert_called('GET', '/limits?reserved=1')
abs_limits = list(obj.absolute)
@@ -75,16 +81,29 @@ class LimitsTest(utils.FixturedTestCase):
for limit in rate_limits:
self.assertIn(limit, expected)
- expected = (
+ expected = [
limits.AbsoluteLimit("maxTotalRAMSize", 51200),
- limits.AbsoluteLimit("maxServerMeta", 5),
- limits.AbsoluteLimit("maxImageMeta", 5),
- limits.AbsoluteLimit("maxPersonality", 5),
- limits.AbsoluteLimit("maxPersonalitySize", 10240),
- )
+ limits.AbsoluteLimit("maxServerMeta", 5)
+ ]
+ if self.supports_image_meta:
+ expected.append(limits.AbsoluteLimit("maxImageMeta", 5))
+ if self.supports_personality:
+ expected.extend([
+ limits.AbsoluteLimit("maxPersonality", 5),
+ limits.AbsoluteLimit("maxPersonalitySize", 10240)])
abs_limits = list(obj.absolute)
self.assertEqual(len(abs_limits), len(expected))
for limit in abs_limits:
self.assertIn(limit, expected)
+
+
+class LimitsTest2_57(LimitsTest):
+ data_fixture_class = data.Fixture2_57
+ supports_image_meta = False
+ supports_personality = False
+
+ def setUp(self):
+ super(LimitsTest2_57, self).setUp()
+ self.cs.api_version = api_versions.APIVersion('2.57')
diff --git a/novaclient/tests/unit/v2/test_quota_classes.py b/novaclient/tests/unit/v2/test_quota_classes.py
index e9d9ad75..3becb632 100644
--- a/novaclient/tests/unit/v2/test_quota_classes.py
+++ b/novaclient/tests/unit/v2/test_quota_classes.py
@@ -49,17 +49,20 @@ class QuotaClassSetsTest(utils.TestCase):
class QuotaClassSetsTest2_50(QuotaClassSetsTest):
"""Tests the quota classes API binding using the 2.50 microversion."""
+ api_version = '2.50'
+ invalid_resources = ['floating_ips', 'fixed_ips', 'networks',
+ 'security_groups', 'security_group_rules']
+
def setUp(self):
super(QuotaClassSetsTest2_50, self).setUp()
- self.cs = fakes.FakeClient(api_versions.APIVersion("2.50"))
+ self.cs = fakes.FakeClient(api_versions.APIVersion(self.api_version))
def test_class_quotas_get(self):
"""Tests that network-related resources aren't in a 2.50 response
and server group related resources are in the response.
"""
q = super(QuotaClassSetsTest2_50, self).test_class_quotas_get()
- for invalid_resource in ('floating_ips', 'fixed_ips', 'networks',
- 'security_groups', 'security_group_rules'):
+ for invalid_resource in self.invalid_resources:
self.assertFalse(hasattr(q, invalid_resource),
'%s should not be in %s' % (invalid_resource, q))
# Also make sure server_groups and server_group_members are in the
@@ -73,8 +76,7 @@ class QuotaClassSetsTest2_50(QuotaClassSetsTest):
and server group related resources are in the response.
"""
q = super(QuotaClassSetsTest2_50, self).test_update_quota()
- for invalid_resource in ('floating_ips', 'fixed_ips', 'networks',
- 'security_groups', 'security_group_rules'):
+ for invalid_resource in self.invalid_resources:
self.assertFalse(hasattr(q, invalid_resource),
'%s should not be in %s' % (invalid_resource, q))
# Also make sure server_groups and server_group_members are in the
@@ -95,3 +97,27 @@ class QuotaClassSetsTest2_50(QuotaClassSetsTest):
self.assertRaises(TypeError, q.update, security_groups=1)
self.assertRaises(TypeError, q.update, security_group_rules=1)
self.assertRaises(TypeError, q.update, networks=1)
+ return q
+
+
+class QuotaClassSetsTest2_57(QuotaClassSetsTest2_50):
+ """Tests the quota classes API binding using the 2.57 microversion."""
+ api_version = '2.57'
+
+ def setUp(self):
+ super(QuotaClassSetsTest2_57, self).setUp()
+ self.invalid_resources.extend(['injected_files',
+ 'injected_file_content_bytes',
+ 'injected_file_path_bytes'])
+
+ def test_update_quota_invalid_resources(self):
+ """Tests trying to update quota class values for invalid resources.
+
+ This will fail with TypeError because the file-related resource
+ kwargs aren't defined.
+ """
+ q = super(
+ QuotaClassSetsTest2_57, self).test_update_quota_invalid_resources()
+ self.assertRaises(TypeError, q.update, injected_files=1)
+ self.assertRaises(TypeError, q.update, injected_file_content_bytes=1)
+ self.assertRaises(TypeError, q.update, injected_file_path_bytes=1)
diff --git a/novaclient/tests/unit/v2/test_quotas.py b/novaclient/tests/unit/v2/test_quotas.py
index 0ed766a0..67a0bc3d 100644
--- a/novaclient/tests/unit/v2/test_quotas.py
+++ b/novaclient/tests/unit/v2/test_quotas.py
@@ -13,6 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.
+from novaclient import api_versions
from novaclient.tests.unit.fixture_data import client
from novaclient.tests.unit.fixture_data import quotas as data
from novaclient.tests.unit import utils
@@ -29,6 +30,7 @@ class QuotaSetsTest(utils.FixturedTestCase):
q = self.cs.quotas.get(tenant_id)
self.assert_request_id(q, fakes.FAKE_REQUEST_ID_LIST)
self.assert_called('GET', '/os-quota-sets/%s' % tenant_id)
+ return q
def test_user_quotas_get(self):
tenant_id = 'test'
@@ -65,6 +67,7 @@ class QuotaSetsTest(utils.FixturedTestCase):
self.assert_called(
'PUT', '/os-quota-sets/97f4c221bff44578b0300df4ef119353',
{'quota_set': {'force': True, 'cores': 2}})
+ return q
def test_quotas_delete(self):
tenant_id = 'test'
@@ -79,3 +82,40 @@ class QuotaSetsTest(utils.FixturedTestCase):
self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST)
url = '/os-quota-sets/%s?user_id=%s' % (tenant_id, user_id)
self.assert_called('DELETE', url)
+
+
+class QuotaSetsTest2_57(QuotaSetsTest):
+ """Tests the quotas API binding using the 2.57 microversion."""
+ data_fixture_class = data.V2_57
+ invalid_resources = ['floating_ips', 'fixed_ips', 'networks',
+ 'security_groups', 'security_group_rules',
+ 'injected_files', 'injected_file_content_bytes',
+ 'injected_file_path_bytes']
+
+ def setUp(self):
+ super(QuotaSetsTest2_57, self).setUp()
+ self.cs.api_version = api_versions.APIVersion('2.57')
+
+ def test_tenant_quotas_get(self):
+ q = super(QuotaSetsTest2_57, self).test_tenant_quotas_get()
+ for invalid_resource in self.invalid_resources:
+ self.assertFalse(hasattr(q, invalid_resource),
+ '%s should not be in %s' % (invalid_resource, q))
+
+ def test_force_update_quota(self):
+ q = super(QuotaSetsTest2_57, self).test_force_update_quota()
+ for invalid_resource in self.invalid_resources:
+ self.assertFalse(hasattr(q, invalid_resource),
+ '%s should not be in %s' % (invalid_resource, q))
+
+ def test_update_quota_invalid_resources(self):
+ """Tests trying to update quota values for invalid resources."""
+ q = self.cs.quotas.get('test')
+ self.assertRaises(TypeError, q.update, floating_ips=1)
+ self.assertRaises(TypeError, q.update, fixed_ips=1)
+ self.assertRaises(TypeError, q.update, security_groups=1)
+ self.assertRaises(TypeError, q.update, security_group_rules=1)
+ self.assertRaises(TypeError, q.update, networks=1)
+ self.assertRaises(TypeError, q.update, injected_files=1)
+ self.assertRaises(TypeError, q.update, injected_file_content_bytes=1)
+ self.assertRaises(TypeError, q.update, injected_file_path_bytes=1)
diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py
index b09bf6cd..07ad1497 100644
--- a/novaclient/tests/unit/v2/test_servers.py
+++ b/novaclient/tests/unit/v2/test_servers.py
@@ -34,6 +34,7 @@ class ServersTest(utils.FixturedTestCase):
client_fixture_class = client.V1
data_fixture_class = data.V1
api_version = None
+ supports_files = True
def setUp(self):
super(ServersTest, self).setUp()
@@ -126,6 +127,12 @@ class ServersTest(utils.FixturedTestCase):
self.assertEqual(s1._info, s2._info)
def test_create_server(self):
+ kwargs = {}
+ if self.supports_files:
+ kwargs['files'] = {
+ '/etc/passwd': 'some data', # a file
+ '/tmp/foo.txt': six.StringIO('data'), # a stream
+ }
s = self.cs.servers.create(
name="My server",
image=1,
@@ -133,11 +140,8 @@ class ServersTest(utils.FixturedTestCase):
meta={'foo': 'bar'},
userdata="hello moto",
key_name="fakekey",
- files={
- '/etc/passwd': 'some data', # a file
- '/tmp/foo.txt': six.StringIO('data'), # a stream
- },
- nics=self._get_server_create_default_nics()
+ nics=self._get_server_create_default_nics(),
+ **kwargs
)
self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST)
self.assert_called('POST', '/servers')
@@ -253,23 +257,32 @@ class ServersTest(utils.FixturedTestCase):
self.assertIsInstance(s, servers.Server)
def test_create_server_userdata_file_object(self):
+ kwargs = {}
+ if self.supports_files:
+ kwargs['files'] = {
+ '/etc/passwd': 'some data', # a file
+ '/tmp/foo.txt': six.StringIO('data'), # a stream
+ }
s = self.cs.servers.create(
name="My server",
image=1,
flavor=1,
meta={'foo': 'bar'},
userdata=six.StringIO('hello moto'),
- files={
- '/etc/passwd': 'some data', # a file
- '/tmp/foo.txt': six.StringIO('data'), # a stream
- },
nics=self._get_server_create_default_nics(),
+ **kwargs
)
self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST)
self.assert_called('POST', '/servers')
self.assertIsInstance(s, servers.Server)
def test_create_server_userdata_unicode(self):
+ kwargs = {}
+ if self.supports_files:
+ kwargs['files'] = {
+ '/etc/passwd': 'some data', # a file
+ '/tmp/foo.txt': six.StringIO('data'), # a stream
+ }
s = self.cs.servers.create(
name="My server",
image=1,
@@ -277,17 +290,20 @@ class ServersTest(utils.FixturedTestCase):
meta={'foo': 'bar'},
userdata=six.u('こんにちは'),
key_name="fakekey",
- files={
- '/etc/passwd': 'some data', # a file
- '/tmp/foo.txt': six.StringIO('data'), # a stream
- },
nics=self._get_server_create_default_nics(),
+ **kwargs
)
self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST)
self.assert_called('POST', '/servers')
self.assertIsInstance(s, servers.Server)
def test_create_server_userdata_utf8(self):
+ kwargs = {}
+ if self.supports_files:
+ kwargs['files'] = {
+ '/etc/passwd': 'some data', # a file
+ '/tmp/foo.txt': six.StringIO('data'), # a stream
+ }
s = self.cs.servers.create(
name="My server",
image=1,
@@ -295,11 +311,8 @@ class ServersTest(utils.FixturedTestCase):
meta={'foo': 'bar'},
userdata='こんにちは',
key_name="fakekey",
- files={
- '/etc/passwd': 'some data', # a file
- '/tmp/foo.txt': six.StringIO('data'), # a stream
- },
nics=self._get_server_create_default_nics(),
+ **kwargs
)
self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST)
self.assert_called('POST', '/servers')
@@ -323,6 +336,12 @@ class ServersTest(utils.FixturedTestCase):
self.assertEqual(test_password, body['server']['adminPass'])
def test_create_server_userdata_bin(self):
+ kwargs = {}
+ if self.supports_files:
+ kwargs['files'] = {
+ '/etc/passwd': 'some data', # a file
+ '/tmp/foo.txt': six.StringIO('data'), # a stream
+ }
with tempfile.TemporaryFile(mode='wb+') as bin_file:
original_data = os.urandom(1024)
bin_file.write(original_data)
@@ -335,11 +354,8 @@ class ServersTest(utils.FixturedTestCase):
meta={'foo': 'bar'},
userdata=bin_file,
key_name="fakekey",
- files={
- '/etc/passwd': 'some data', # a file
- '/tmp/foo.txt': six.StringIO('data'), # a stream
- },
nics=self._get_server_create_default_nics(),
+ **kwargs
)
self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST)
self.assert_called('POST', '/servers')
@@ -1500,3 +1516,29 @@ class ServersV256Test(ServersV254Test):
ex = self.assertRaises(TypeError,
s.migrate, host='target-host')
self.assertIn('host', six.text_type(ex))
+
+
+class ServersV257Test(ServersV256Test):
+ """Tests the servers python API bindings with microversion 2.57 where
+ personality files are deprecated.
+ """
+ api_version = "2.57"
+ supports_files = False
+
+ def test_create_server_with_files_fails(self):
+ ex = self.assertRaises(
+ exceptions.UnsupportedAttribute, self.cs.servers.create,
+ name="My server", image=1, flavor=1,
+ files={
+ '/etc/passwd': 'some data', # a file
+ '/tmp/foo.txt': six.StringIO('data'), # a stream
+ }, nics='auto')
+ self.assertIn('files', six.text_type(ex))
+
+ def test_rebuild_server_name_meta_files(self):
+ files = {'/etc/passwd': 'some data'}
+ s = self.cs.servers.get(1234)
+ ex = self.assertRaises(
+ exceptions.UnsupportedAttribute, s.rebuild, image=1, name='new',
+ meta={'foo': 'bar'}, files=files)
+ self.assertIn('files', six.text_type(ex))
diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py
index 5163959a..fb21eb5e 100644
--- a/novaclient/tests/unit/v2/test_shell.py
+++ b/novaclient/tests/unit/v2/test_shell.py
@@ -36,6 +36,7 @@ from novaclient import exceptions
import novaclient.shell
from novaclient.tests.unit import utils
from novaclient.tests.unit.v2 import fakes
+from novaclient.v2 import servers
import novaclient.v2.shell
FAKE_UUID_1 = fakes.FAKE_IMAGE_UUID_1
@@ -971,6 +972,16 @@ class ShellTest(utils.TestCase):
' --file /foo=%s' % (FAKE_UUID_1, invalid_file))
self.assertRaises(exceptions.CommandError, self.run_command, cmd)
+ def test_boot_files_2_57(self):
+ """Tests that trying to run the boot command with the --file option
+ after microversion 2.56 fails.
+ """
+ testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt')
+ cmd = ('boot some-server --flavor 1 --image %s'
+ ' --file /tmp/foo=%s')
+ self.assertRaises(SystemExit, self.run_command,
+ cmd % (FAKE_UUID_1, testfile), api_version='2.57')
+
def test_boot_max_min_count(self):
self.run_command('boot --image %s --flavor 1 --min-count 1'
' --max-count 3 server' % FAKE_UUID_1)
@@ -1570,6 +1581,62 @@ class ShellTest(utils.TestCase):
expected = "'['foo']' is not in the format of 'key=value'"
self.assertEqual(expected, result.args[0])
+ def test_rebuild_user_data_2_56(self):
+ """Tests that trying to run the rebuild command with the --user-data*
+ options before microversion 2.57 fails.
+ """
+ cmd = 'rebuild sample-server %s --user-data test' % FAKE_UUID_1
+ self.assertRaises(SystemExit, self.run_command, cmd,
+ api_version='2.56')
+ cmd = 'rebuild sample-server %s --user-data-unset' % FAKE_UUID_1
+ self.assertRaises(SystemExit, self.run_command, cmd,
+ api_version='2.56')
+
+ def test_rebuild_files_2_57(self):
+ """Tests that trying to run the rebuild command with the --file option
+ after microversion 2.56 fails.
+ """
+ testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt')
+ cmd = 'rebuild sample-server %s --file /tmp/foo=%s'
+ self.assertRaises(SystemExit, self.run_command,
+ cmd % (FAKE_UUID_1, testfile), api_version='2.57')
+
+ def test_rebuild_change_user_data(self):
+ self.run_command('rebuild sample-server %s --user-data test' %
+ FAKE_UUID_1, api_version='2.57')
+ user_data = servers.ServerManager.transform_userdata('test')
+ self.assert_called('GET', '/servers?name=sample-server', pos=0)
+ self.assert_called('GET', '/servers/1234', pos=1)
+ self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2)
+ self.assert_called('POST', '/servers/1234/action',
+ {'rebuild': {'imageRef': FAKE_UUID_1,
+ 'user_data': user_data,
+ 'description': None}}, pos=3)
+ self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=4)
+
+ def test_rebuild_unset_user_data(self):
+ self.run_command('rebuild sample-server %s --user-data-unset' %
+ FAKE_UUID_1, api_version='2.57')
+ self.assert_called('GET', '/servers?name=sample-server', pos=0)
+ self.assert_called('GET', '/servers/1234', pos=1)
+ self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2)
+ self.assert_called('POST', '/servers/1234/action',
+ {'rebuild': {'imageRef': FAKE_UUID_1,
+ 'user_data': None,
+ 'description': None}}, pos=3)
+ self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=4)
+
+ def test_rebuild_user_data_and_unset_user_data(self):
+ """Tests that trying to set --user-data and --unset-user-data in the
+ same rebuild call fails.
+ """
+ cmd = ('rebuild sample-server %s --user-data x --user-data-unset' %
+ FAKE_UUID_1)
+ ex = self.assertRaises(exceptions.CommandError, self.run_command, cmd,
+ api_version='2.57')
+ self.assertIn("Cannot specify '--user-data-unset' with "
+ "'--user-data'.", six.text_type(ex))
+
def test_start(self):
self.run_command('start sample-server')
self.assert_called('POST', '/servers/1234/action', {'os-start': None})
@@ -2643,6 +2710,17 @@ class ShellTest(utils.TestCase):
'PUT', '/os-quota-sets/97f4c221bff44578b0300df4ef119353',
{'quota_set': {'fixed_ips': 5}})
+ def test_quota_update_injected_file_2_57(self):
+ """Tests that trying to update injected_file* quota with microversion
+ 2.57 fails.
+ """
+ for quota in ('--injected-files', '--injected-file-content-bytes',
+ '--injected-file-path-bytes'):
+ cmd = ('quota-update 97f4c221bff44578b0300df4ef119353 %s=5' %
+ quota)
+ self.assertRaises(SystemExit, self.run_command, cmd,
+ api_version='2.57')
+
def test_quota_delete(self):
self.run_command('quota-delete --tenant '
'97f4c221bff44578b0300df4ef119353')
@@ -2680,6 +2758,16 @@ class ShellTest(utils.TestCase):
'PUT', '/os-quota-class-sets/97f4c221bff44578b0300df4ef119353',
body)
+ def test_quota_class_update_injected_file_2_57(self):
+ """Tests that trying to update injected_file* quota with microversion
+ 2.57 fails.
+ """
+ for quota in ('--injected-files', '--injected-file-content-bytes',
+ '--injected-file-path-bytes'):
+ cmd = 'quota-class-update default %s=5' % quota
+ self.assertRaises(SystemExit, self.run_command, cmd,
+ api_version='2.57')
+
def test_backup(self):
out, err = self.run_command('backup sample-server back1 daily 1')
# With microversion < 2.45 there is no output from this command.
@@ -2712,8 +2800,9 @@ class ShellTest(utils.TestCase):
'rotation': '1'}})
def test_limits(self):
- self.run_command('limits')
+ out = self.run_command('limits')[0]
self.assert_called('GET', '/limits')
+ self.assertIn('Personality', out)
self.run_command('limits --reserved')
self.assert_called('GET', '/limits?reserved=1')
@@ -2725,6 +2814,14 @@ class ShellTest(utils.TestCase):
self.assertIn('Verb', stdout)
self.assertIn('Name', stdout)
+ def test_limits_2_57(self):
+ """Tests the limits command at microversion 2.57 where personality
+ size limits should not be shown.
+ """
+ out = self.run_command('limits', api_version='2.57')[0]
+ self.assert_called('GET', '/limits')
+ self.assertNotIn('Personality', out)
+
def test_evacuate(self):
self.run_command('evacuate sample-server new_host')
self.assert_called('POST', '/servers/1234/action',
@@ -3128,6 +3225,7 @@ class ShellTest(utils.TestCase):
51, # There are no version-wrapped shell method changes for this.
52, # There are no version-wrapped shell method changes for this.
54, # There are no version-wrapped shell method changes for this.
+ 57, # There are no version-wrapped shell method changes for this.
])
versions_supported = set(range(0,
novaclient.API_MAX_VERSION.ver_minor + 1))
diff --git a/novaclient/v2/quota_classes.py b/novaclient/v2/quota_classes.py
index eae5bfde..917cc9c4 100644
--- a/novaclient/v2/quota_classes.py
+++ b/novaclient/v2/quota_classes.py
@@ -50,7 +50,7 @@ class QuotaClassSetManager(base.Manager):
# NOTE(mriedem): 2.50 does strict validation of the resources you can
# specify since the network-related resources are blocked in 2.50.
- @api_versions.wraps("2.50")
+ @api_versions.wraps("2.50", "2.56")
def update(self, class_name, instances=None, cores=None, ram=None,
metadata_items=None, injected_files=None,
injected_file_content_bytes=None, injected_file_path_bytes=None,
@@ -81,3 +81,30 @@ class QuotaClassSetManager(base.Manager):
body = {'quota_class_set': resources}
return self._update('/os-quota-class-sets/%s' % class_name, body,
'quota_class_set')
+
+ # NOTE(mriedem): 2.57 deprecates the usage of injected_files,
+ # injected_file_content_bytes and injected_file_path_bytes so those
+ # kwargs are removed.
+ @api_versions.wraps("2.57")
+ def update(self, class_name, instances=None, cores=None, ram=None,
+ metadata_items=None, key_pairs=None, server_groups=None,
+ server_group_members=None):
+ resources = {}
+ if instances is not None:
+ resources['instances'] = instances
+ if cores is not None:
+ resources['cores'] = cores
+ if ram is not None:
+ resources['ram'] = ram
+ if metadata_items is not None:
+ resources['metadata_items'] = metadata_items
+ if key_pairs is not None:
+ resources['key_pairs'] = key_pairs
+ if server_groups is not None:
+ resources['server_groups'] = server_groups
+ if server_group_members is not None:
+ resources['server_group_members'] = server_group_members
+
+ body = {'quota_class_set': resources}
+ return self._update('/os-quota-class-sets/%s' % class_name, body,
+ 'quota_class_set')
diff --git a/novaclient/v2/quotas.py b/novaclient/v2/quotas.py
index 0e421169..82249f25 100644
--- a/novaclient/v2/quotas.py
+++ b/novaclient/v2/quotas.py
@@ -13,6 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.
+from novaclient import api_versions
from novaclient import base
@@ -38,6 +39,10 @@ class QuotaSetManager(base.Manager):
return self._get(url % params, "quota_set")
+ # NOTE(mriedem): Before 2.57 the resources you could update was just a
+ # kwargs dict and not validated on the client-side, only on the API server
+ # side.
+ @api_versions.wraps("2.0", "2.56")
def update(self, tenant_id, **kwargs):
user_id = kwargs.pop('user_id', None)
@@ -53,6 +58,40 @@ class QuotaSetManager(base.Manager):
url = '/os-quota-sets/%s' % tenant_id
return self._update(url, body, 'quota_set')
+ # NOTE(mriedem): 2.57 does strict validation of the resources you can
+ # specify. 2.36 blocks network-related resources and 2.57 blocks
+ # injected files related quotas.
+ @api_versions.wraps("2.57")
+ def update(self, tenant_id, user_id=None, force=False,
+ instances=None, cores=None, ram=None,
+ metadata_items=None, key_pairs=None, server_groups=None,
+ server_group_members=None):
+
+ resources = {}
+ if force:
+ resources['force'] = force
+ if instances is not None:
+ resources['instances'] = instances
+ if cores is not None:
+ resources['cores'] = cores
+ if ram is not None:
+ resources['ram'] = ram
+ if metadata_items is not None:
+ resources['metadata_items'] = metadata_items
+ if key_pairs is not None:
+ resources['key_pairs'] = key_pairs
+ if server_groups is not None:
+ resources['server_groups'] = server_groups
+ if server_group_members is not None:
+ resources['server_group_members'] = server_group_members
+ body = {'quota_set': resources}
+
+ if user_id:
+ url = '/os-quota-sets/%s?user_id=%s' % (tenant_id, user_id)
+ else:
+ url = '/os-quota-sets/%s' % tenant_id
+ return self._update(url, body, 'quota_set')
+
def defaults(self, tenant_id):
return self._get('/os-quota-sets/%s/defaults' % tenant_id,
'quota_set')
diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py
index e305253e..79ab0cdc 100644
--- a/novaclient/v2/servers.py
+++ b/novaclient/v2/servers.py
@@ -621,6 +621,27 @@ class SecurityGroup(base.Resource):
class ServerManager(base.BootingManagerWithFind):
resource_class = Server
+ @staticmethod
+ def transform_userdata(userdata):
+ if hasattr(userdata, 'read'):
+ userdata = userdata.read()
+
+ # NOTE(melwitt): Text file data is converted to bytes prior to
+ # base64 encoding. The utf-8 encoding will fail for binary files.
+ if six.PY3:
+ try:
+ userdata = userdata.encode("utf-8")
+ except AttributeError:
+ # In python 3, 'bytes' object has no attribute 'encode'
+ pass
+ else:
+ try:
+ userdata = encodeutils.safe_encode(userdata)
+ except UnicodeDecodeError:
+ pass
+
+ return base64.b64encode(userdata).decode('utf-8')
+
def _boot(self, response_key, name, image, flavor,
meta=None, files=None, userdata=None,
reservation_id=False, return_raw=False, min_count=None,
@@ -639,25 +660,7 @@ class ServerManager(base.BootingManagerWithFind):
"flavorRef": str(base.getid(flavor)),
}}
if userdata:
- if hasattr(userdata, 'read'):
- userdata = userdata.read()
-
- # NOTE(melwitt): Text file data is converted to bytes prior to
- # base64 encoding. The utf-8 encoding will fail for binary files.
- if six.PY3:
- try:
- userdata = userdata.encode("utf-8")
- except AttributeError:
- # In python 3, 'bytes' object has no attribute 'encode'
- pass
- else:
- try:
- userdata = encodeutils.safe_encode(userdata)
- except UnicodeDecodeError:
- pass
-
- userdata_b64 = base64.b64encode(userdata).decode('utf-8')
- body["server"]["user_data"] = userdata_b64
+ body["server"]["user_data"] = self.transform_userdata(userdata)
if meta:
body["server"]["metadata"] = meta
if reservation_id:
@@ -1204,6 +1207,7 @@ class ServerManager(base.BootingManagerWithFind):
are the file contents (either as a string or as a
file-like object). A maximum of five entries is allowed,
and each file must be 10k or less.
+ (deprecated starting with microversion 2.57)
:param reservation_id: return a reservation_id for the set of
servers being requested, boolean.
:param min_count: (optional extension) The minimum number of
@@ -1284,6 +1288,10 @@ class ServerManager(base.BootingManagerWithFind):
if "tags" in kwargs and self.api_version < boot_tags_microversion:
raise exceptions.UnsupportedAttribute("tags", "2.52")
+ personality_files_deprecation = api_versions.APIVersion('2.57')
+ if files and self.api_version >= personality_files_deprecation:
+ raise exceptions.UnsupportedAttribute('files', '2.0', '2.56')
+
boot_kwargs = dict(
meta=meta, files=files, userdata=userdata,
reservation_id=reservation_id, min_count=min_count,
@@ -1397,11 +1405,17 @@ class ServerManager(base.BootingManagerWithFind):
are the file contents (either as a string or as a
file-like object). A maximum of five entries is allowed,
and each file must be 10k or less.
+ (deprecated starting with microversion 2.57)
:param description: optional description of the server (allowed since
microversion 2.19)
:param key_name: optional key pair name for rebuild operation; passing
None will unset the key for the server instance
(starting from microversion 2.54)
+ :param userdata: optional user data to pass to be exposed by the
+ metadata server; this can be a file type object as
+ well or a string. If None is specified, the existing
+ user_data is unset.
+ (starting from microversion 2.57)
:returns: :class:`Server`
"""
descr_microversion = api_versions.APIVersion("2.19")
@@ -1414,6 +1428,14 @@ class ServerManager(base.BootingManagerWithFind):
self.api_version < api_versions.APIVersion('2.54')):
raise exceptions.UnsupportedAttribute('key_name', '2.54')
+ # Microversion 2.57 deprecates personality files and adds support
+ # for user_data.
+ files_and_userdata = api_versions.APIVersion('2.57')
+ if files and self.api_version >= files_and_userdata:
+ raise exceptions.UnsupportedAttribute('files', '2.0', '2.56')
+ if 'userdata' in kwargs and self.api_version < files_and_userdata:
+ raise exceptions.UnsupportedAttribute('userdata', '2.57')
+
body = {'imageRef': base.getid(image)}
if password is not None:
body['adminPass'] = password
@@ -1443,6 +1465,12 @@ class ServerManager(base.BootingManagerWithFind):
'path': filepath,
'contents': cont,
})
+ if 'userdata' in kwargs:
+ # If userdata is specified but None, it means unset the existing
+ # user_data on the instance.
+ userdata = kwargs['userdata']
+ body['user_data'] = (userdata if userdata is None else
+ self.transform_userdata(userdata))
resp, body = self._action_return_resp_and_body('rebuild', server,
body, **kwargs)
diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py
index 8ad018c4..ecd72a26 100644
--- a/novaclient/v2/shell.py
+++ b/novaclient/v2/shell.py
@@ -391,19 +391,22 @@ def _boot(cs, args):
meta = _meta_parsing(args.meta)
- files = {}
- for f in args.files:
- try:
- dst, src = f.split('=', 1)
- files[dst] = open(src)
- except IOError as e:
- raise exceptions.CommandError(_("Can't open '%(src)s': %(exc)s") %
- {'src': src, 'exc': e})
- except ValueError:
- raise exceptions.CommandError(_("Invalid file argument '%s'. "
- "File arguments must be of the "
- "form '--file "
- "<dst-path=src-path>'") % f)
+ include_files = cs.api_version < api_versions.APIVersion('2.57')
+ if include_files:
+ files = {}
+ for f in args.files:
+ try:
+ dst, src = f.split('=', 1)
+ files[dst] = open(src)
+ except IOError as e:
+ raise exceptions.CommandError(
+ _("Can't open '%(src)s': %(exc)s") %
+ {'src': src, 'exc': e})
+ except ValueError:
+ raise exceptions.CommandError(
+ _("Invalid file argument '%s'. "
+ "File arguments must be of the "
+ "form '--file <dst-path=src-path>'") % f)
# use the os-keypair extension
key_name = None
@@ -481,7 +484,6 @@ def _boot(cs, args):
boot_kwargs = dict(
meta=meta,
- files=files,
key_name=key_name,
min_count=min_count,
max_count=max_count,
@@ -504,6 +506,9 @@ def _boot(cs, args):
if 'tags' in args and args.tags:
boot_kwargs["tags"] = args.tags.split(',')
+ if include_files:
+ boot_kwargs['files'] = files
+
return boot_args, boot_kwargs
@@ -563,7 +568,8 @@ def _boot(cs, args):
"on the new server. More files can be injected using multiple "
"'--file' options. Limited by the 'injected_files' quota value. "
"The default value is 5. You can get the current quota value by "
- "'Personality' limit from 'nova limits' command."))
+ "'Personality' limit from 'nova limits' command."),
+ start_version='2.0', end_version='2.56')
@utils.arg(
'--key-name',
default=os.environ.get('NOVACLIENT_DEFAULT_KEY_NAME'),
@@ -1770,7 +1776,8 @@ def do_reboot(cs, args):
"on the new server. More files can be injected using multiple "
"'--file' options. You may store up to 5 files by default. "
"The maximum number of files is specified by the 'Personality' "
- "limit reported by the 'nova limits' command."))
+ "limit reported by the 'nova limits' command."),
+ start_version='2.0', end_version='2.56')
@utils.arg(
'--key-name',
metavar='<key-name>',
@@ -1785,6 +1792,19 @@ def do_reboot(cs, args):
help=_("Unset keypair in the server. "
"Cannot be specified with the '--key-name' option."),
start_version='2.54')
+@utils.arg(
+ '--user-data',
+ default=None,
+ metavar='<user-data>',
+ help=_("User data file to pass to be exposed by the metadata server."),
+ start_version='2.57')
+@utils.arg(
+ '--user-data-unset',
+ action='store_true',
+ default=False,
+ help=_("Unset user_data in the server. Cannot be specified with the "
+ "'--user-data' option."),
+ start_version='2.57')
def do_rebuild(cs, args):
"""Shutdown, re-image, and re-boot a server."""
server = _find_server(cs, args.server)
@@ -1803,21 +1823,34 @@ def do_rebuild(cs, args):
meta = _meta_parsing(args.meta)
kwargs['meta'] = meta
- files = {}
- for f in args.files:
- try:
- dst, src = f.split('=', 1)
- with open(src, 'r') as s:
- files[dst] = s.read()
- except IOError as e:
- raise exceptions.CommandError(_("Can't open '%(src)s': %(exc)s") %
- {'src': src, 'exc': e})
- except ValueError:
- raise exceptions.CommandError(_("Invalid file argument '%s'. "
- "File arguments must be of the "
- "form '--file "
- "<dst-path=src-path>'") % f)
- kwargs['files'] = files
+ # 2.57 deprecates the --file option and adds the --user-data and
+ # --user-data-unset options.
+ if cs.api_version < api_versions.APIVersion('2.57'):
+ files = {}
+ for f in args.files:
+ try:
+ dst, src = f.split('=', 1)
+ with open(src, 'r') as s:
+ files[dst] = s.read()
+ except IOError as e:
+ raise exceptions.CommandError(
+ _("Can't open '%(src)s': %(exc)s") %
+ {'src': src, 'exc': e})
+ except ValueError:
+ raise exceptions.CommandError(
+ _("Invalid file argument '%s'. "
+ "File arguments must be of the "
+ "form '--file <dst-path=src-path>'") % f)
+ kwargs['files'] = files
+ else:
+ if args.user_data_unset:
+ kwargs['userdata'] = None
+ if args.user_data:
+ raise exceptions.CommandError(
+ _("Cannot specify '--user-data-unset' with "
+ "'--user-data'."))
+ elif args.user_data:
+ kwargs['userdata'] = args.user_data
if cs.api_version >= api_versions.APIVersion('2.54'):
if args.key_unset:
@@ -3739,6 +3772,10 @@ def do_ssh(cs, args):
# return floating_ips, fixed_ips, security_groups or security_group_members
# as those are deprecated as networking service proxies and/or because
# nova-network is deprecated. Similar to the 2.36 microversion.
+# NOTE(mriedem): In the 2.57 microversion, the os-quota-sets and
+# os-quota-class-sets APIs will no longer return injected_files,
+# injected_file_content_bytes or injected_file_content_bytes since personality
+# files (file injection) is deprecated starting with v2.57.
_quota_resources = ['instances', 'cores', 'ram',
'floating_ips', 'fixed_ips', 'metadata_items',
'injected_files', 'injected_file_content_bytes',
@@ -3942,6 +3979,7 @@ def do_quota_update(cs, args):
# 2.36 does not support updating quota for floating IPs, fixed IPs, security
# groups or security group rules.
+# 2.57 does not support updating injected_file* quotas.
@api_versions.wraps("2.36")
@utils.arg(
'tenant',
@@ -3978,19 +4016,22 @@ def do_quota_update(cs, args):
metavar='<injected-files>',
type=int,
default=None,
- help=_('New value for the "injected-files" quota.'))
+ help=_('New value for the "injected-files" quota.'),
+ start_version='2.36', end_version='2.56')
@utils.arg(
'--injected-file-content-bytes',
metavar='<injected-file-content-bytes>',
type=int,
default=None,
- help=_('New value for the "injected-file-content-bytes" quota.'))
+ help=_('New value for the "injected-file-content-bytes" quota.'),
+ start_version='2.36', end_version='2.56')
@utils.arg(
'--injected-file-path-bytes',
metavar='<injected-file-path-bytes>',
type=int,
default=None,
- help=_('New value for the "injected-file-path-bytes" quota.'))
+ help=_('New value for the "injected-file-path-bytes" quota.'),
+ start_version='2.36', end_version='2.56')
@utils.arg(
'--key-pairs',
metavar='<key-pairs>',
@@ -4147,6 +4188,7 @@ def do_quota_class_update(cs, args):
# 2.50 does not support updating quota class values for floating IPs,
# fixed IPs, security groups or security group rules.
+# 2.57 does not support updating injected_file* quotas.
@api_versions.wraps("2.50")
@utils.arg(
'class_name',
@@ -4178,19 +4220,22 @@ def do_quota_class_update(cs, args):
metavar='<injected-files>',
type=int,
default=None,
- help=_('New value for the "injected-files" quota.'))
+ help=_('New value for the "injected-files" quota.'),
+ start_version='2.50', end_version='2.56')
@utils.arg(
'--injected-file-content-bytes',
metavar='<injected-file-content-bytes>',
type=int,
default=None,
- help=_('New value for the "injected-file-content-bytes" quota.'))
+ help=_('New value for the "injected-file-content-bytes" quota.'),
+ start_version='2.50', end_version='2.56')
@utils.arg(
'--injected-file-path-bytes',
metavar='<injected-file-path-bytes>',
type=int,
default=None,
- help=_('New value for the "injected-file-path-bytes" quota.'))
+ help=_('New value for the "injected-file-path-bytes" quota.'),
+ start_version='2.50', end_version='2.56')
@utils.arg(
'--key-pairs',
metavar='<key-pairs>',
diff --git a/releasenotes/notes/microversion-v2_57-acae2ee11ddae4fb.yaml b/releasenotes/notes/microversion-v2_57-acae2ee11ddae4fb.yaml
new file mode 100644
index 00000000..4625d506
--- /dev/null
+++ b/releasenotes/notes/microversion-v2_57-acae2ee11ddae4fb.yaml
@@ -0,0 +1,31 @@
+---
+features:
+ - |
+ Support is added for the 2.57 microversion:
+
+ * A ``userdata`` keyword argument can be passed to the ``Server.rebuild``
+ python API binding. If set to None, it will unset any existing userdata
+ on the server.
+ * The ``--user-data`` and ``--user-data-unset`` options are added to the
+ ``nova rebuild`` CLI. The options are mutually exclusive. Specifying
+ ``--user-data`` will overwrite the existing userdata in the server, and
+ ``--user-data-unset`` will unset any existing userdata on the server.
+upgrade:
+ - |
+ Support is added for the 2.57 microversion:
+
+ * The ``--file`` option for the ``nova boot`` and ``nova rebuild`` CLIs is
+ capped at the 2.56 microversion. Similarly, the ``file`` parameter to
+ the ``Server.create`` and ``Server.rebuild`` python API binding methods
+ is capped at 2.56. Users are recommended to use the ``--user-data``
+ option instead.
+ * The ``--injected-files``, ``--injected-file-content-bytes`` and
+ ``--injected-file-path-bytes`` options are capped at the 2.56
+ microversion in the ``nova quota-update`` and ``nova quota-class-update``
+ commands.
+ * The ``maxPersonality`` and ``maxPersonalitySize`` fields are capped at
+ the 2.56 microversion in the ``nova limits`` command and API binding.
+ * The ``injected_files``, ``injected_file_content_bytes`` and
+ ``injected_file_path_bytes`` entries are capped at version 2.56 from
+ the output of the ``nova quota-show`` and ``nova quota-class-show``
+ commands and related python API bindings.