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
This commit is contained in:
Matt Riedemann 2017-12-14 18:20:45 -05:00 committed by Yikun Jiang
parent fefc3ba723
commit 038cfdd5b3
16 changed files with 575 additions and 112 deletions

View File

@ -25,4 +25,4 @@ API_MIN_VERSION = api_versions.APIVersion("2.1")
# when client supported the max version, and bumped sequentially, otherwise # when client supported the max version, and bumped sequentially, otherwise
# the client may break due to server side new version may include some # the client may break due to server side new version may include some
# backward incompatible change. # backward incompatible change.
API_MAX_VERSION = api_versions.APIVersion("2.56") API_MAX_VERSION = api_versions.APIVersion("2.57")

View File

@ -48,6 +48,7 @@ class UnsupportedAttribute(AttributeError):
self.message = ( self.message = (
"'%(name)s' argument is only allowed since microversion " "'%(name)s' argument is only allowed since microversion "
"%(start)s." % {"name": argument_name, "start": start_version}) "%(start)s." % {"name": argument_name, "start": start_version})
super(UnsupportedAttribute, self).__init__(self.message)
class CommandError(Exception): class CommandError(Exception):

View File

@ -52,7 +52,7 @@ class TestQuotasNovaClient2_35(test_quotas.TestQuotasNovaClient):
class TestQuotasNovaClient2_36(TestQuotasNovaClient2_35): class TestQuotasNovaClient2_36(TestQuotasNovaClient2_35):
"""Nova quotas functional tests.""" """Nova quotas functional tests."""
COMPUTE_API_VERSION = "2.latest" COMPUTE_API_VERSION = "2.36"
# The 2.36 microversion stops proxying network quota resources like # The 2.36 microversion stops proxying network quota resources like
# floating/fixed IPs and security groups/rules. # floating/fixed IPs and security groups/rules.
@ -61,3 +61,14 @@ class TestQuotasNovaClient2_36(TestQuotasNovaClient2_35):
'injected_file_content_bytes', 'injected_file_content_bytes',
'injected_file_path_bytes', 'key_pairs', 'injected_file_path_bytes', 'key_pairs',
'server_groups', 'server_group_members'] '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']

View File

@ -16,6 +16,13 @@ from novaclient.tests.unit.fixture_data import base
class Fixture(base.Fixture): class Fixture(base.Fixture):
base_url = 'limits' base_url = 'limits'
absolute = {
"maxTotalRAMSize": 51200,
"maxServerMeta": 5,
"maxImageMeta": 5,
"maxPersonality": 5,
"maxPersonalitySize": 10240
}
def setUp(self): def setUp(self):
super(Fixture, self).setUp() super(Fixture, self).setUp()
@ -64,13 +71,7 @@ class Fixture(base.Fixture):
] ]
} }
], ],
"absolute": { "absolute": self.absolute,
"maxTotalRAMSize": 51200,
"maxServerMeta": 5,
"maxImageMeta": 5,
"maxPersonality": 5,
"maxPersonalitySize": 10240
},
}, },
} }
@ -78,3 +79,13 @@ class Fixture(base.Fixture):
self.requests_mock.get(self.url(), self.requests_mock.get(self.url(),
json=get_limits, json=get_limits,
headers=headers) headers=headers)
class Fixture2_57(Fixture):
"""Fixture data for the 2.57 microversion where personality files are
deprecated.
"""
absolute = {
"maxTotalRAMSize": 51200,
"maxServerMeta": 5
}

View File

@ -57,6 +57,7 @@ class V1(base.Fixture):
'injected_file_content_bytes': 1, 'injected_file_content_bytes': 1,
'injected_file_path_bytes': 1, 'injected_file_path_bytes': 1,
'ram': 1, 'ram': 1,
'fixed_ips': -1,
'floating_ips': 1, 'floating_ips': 1,
'instances': 1, 'instances': 1,
'injected_files': 1, 'injected_files': 1,
@ -67,3 +68,20 @@ class V1(base.Fixture):
'server_groups': 1, 'server_groups': 1,
'server_group_members': 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
}

View File

@ -331,6 +331,14 @@ class FakeSessionClient(base_client.SessionClient):
# #
def get_limits(self, **kw): 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": { return (200, {}, {"limits": {
"rate": [ "rate": [
{ {
@ -374,13 +382,7 @@ class FakeSessionClient(base_client.SessionClient):
] ]
} }
], ],
"absolute": { "absolute": absolute,
"maxTotalRAMSize": 51200,
"maxServerMeta": 5,
"maxImageMeta": 5,
"maxPersonality": 5,
"maxPersonalitySize": 10240
},
}}) }})
# #
@ -1297,6 +1299,19 @@ class FakeSessionClient(base_client.SessionClient):
# #
def get_os_quota_class_sets_test(self, **kw): 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'): if self.api_version >= api_versions.APIVersion('2.50'):
return (200, FAKE_RESPONSE_HEADERS, { return (200, FAKE_RESPONSE_HEADERS, {
'quota_class_set': { 'quota_class_set': {
@ -1329,6 +1344,18 @@ class FakeSessionClient(base_client.SessionClient):
def put_os_quota_class_sets_test(self, body, **kw): def put_os_quota_class_sets_test(self, body, **kw):
assert list(body) == ['quota_class_set'] 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'): if self.api_version >= api_versions.APIVersion('2.50'):
return (200, {}, { return (200, {}, {
'quota_class_set': { 'quota_class_set': {

View File

@ -11,6 +11,7 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from novaclient import api_versions
from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import client
from novaclient.tests.unit.fixture_data import limits as data from novaclient.tests.unit.fixture_data import limits as data
from novaclient.tests.unit import utils from novaclient.tests.unit import utils
@ -22,6 +23,8 @@ class LimitsTest(utils.FixturedTestCase):
client_fixture_class = client.V1 client_fixture_class = client.V1
data_fixture_class = data.Fixture 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): def test_get_limits(self):
obj = self.cs.limits.get() obj = self.cs.limits.get()
@ -39,13 +42,16 @@ class LimitsTest(utils.FixturedTestCase):
obj = self.cs.limits.get(reserved=True) obj = self.cs.limits.get(reserved=True)
self.assert_request_id(obj, fakes.FAKE_REQUEST_ID_LIST) self.assert_request_id(obj, fakes.FAKE_REQUEST_ID_LIST)
expected = ( expected = [
limits.AbsoluteLimit("maxTotalRAMSize", 51200), limits.AbsoluteLimit("maxTotalRAMSize", 51200),
limits.AbsoluteLimit("maxServerMeta", 5), limits.AbsoluteLimit("maxServerMeta", 5)
limits.AbsoluteLimit("maxImageMeta", 5), ]
limits.AbsoluteLimit("maxPersonality", 5), if self.supports_image_meta:
limits.AbsoluteLimit("maxPersonalitySize", 10240), 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') self.assert_called('GET', '/limits?reserved=1')
abs_limits = list(obj.absolute) abs_limits = list(obj.absolute)
@ -75,16 +81,29 @@ class LimitsTest(utils.FixturedTestCase):
for limit in rate_limits: for limit in rate_limits:
self.assertIn(limit, expected) self.assertIn(limit, expected)
expected = ( expected = [
limits.AbsoluteLimit("maxTotalRAMSize", 51200), limits.AbsoluteLimit("maxTotalRAMSize", 51200),
limits.AbsoluteLimit("maxServerMeta", 5), limits.AbsoluteLimit("maxServerMeta", 5)
limits.AbsoluteLimit("maxImageMeta", 5), ]
limits.AbsoluteLimit("maxPersonality", 5), if self.supports_image_meta:
limits.AbsoluteLimit("maxPersonalitySize", 10240), 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) abs_limits = list(obj.absolute)
self.assertEqual(len(abs_limits), len(expected)) self.assertEqual(len(abs_limits), len(expected))
for limit in abs_limits: for limit in abs_limits:
self.assertIn(limit, expected) 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')

View File

@ -49,17 +49,20 @@ class QuotaClassSetsTest(utils.TestCase):
class QuotaClassSetsTest2_50(QuotaClassSetsTest): class QuotaClassSetsTest2_50(QuotaClassSetsTest):
"""Tests the quota classes API binding using the 2.50 microversion.""" """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): def setUp(self):
super(QuotaClassSetsTest2_50, self).setUp() 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): def test_class_quotas_get(self):
"""Tests that network-related resources aren't in a 2.50 response """Tests that network-related resources aren't in a 2.50 response
and server group related resources are in the response. and server group related resources are in the response.
""" """
q = super(QuotaClassSetsTest2_50, self).test_class_quotas_get() q = super(QuotaClassSetsTest2_50, self).test_class_quotas_get()
for invalid_resource in ('floating_ips', 'fixed_ips', 'networks', for invalid_resource in self.invalid_resources:
'security_groups', 'security_group_rules'):
self.assertFalse(hasattr(q, invalid_resource), self.assertFalse(hasattr(q, invalid_resource),
'%s should not be in %s' % (invalid_resource, q)) '%s should not be in %s' % (invalid_resource, q))
# Also make sure server_groups and server_group_members are in the # 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. and server group related resources are in the response.
""" """
q = super(QuotaClassSetsTest2_50, self).test_update_quota() q = super(QuotaClassSetsTest2_50, self).test_update_quota()
for invalid_resource in ('floating_ips', 'fixed_ips', 'networks', for invalid_resource in self.invalid_resources:
'security_groups', 'security_group_rules'):
self.assertFalse(hasattr(q, invalid_resource), self.assertFalse(hasattr(q, invalid_resource),
'%s should not be in %s' % (invalid_resource, q)) '%s should not be in %s' % (invalid_resource, q))
# Also make sure server_groups and server_group_members are in the # 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_groups=1)
self.assertRaises(TypeError, q.update, security_group_rules=1) self.assertRaises(TypeError, q.update, security_group_rules=1)
self.assertRaises(TypeError, q.update, networks=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)

View File

@ -13,6 +13,7 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from novaclient import api_versions
from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import client
from novaclient.tests.unit.fixture_data import quotas as data from novaclient.tests.unit.fixture_data import quotas as data
from novaclient.tests.unit import utils from novaclient.tests.unit import utils
@ -29,6 +30,7 @@ class QuotaSetsTest(utils.FixturedTestCase):
q = self.cs.quotas.get(tenant_id) q = self.cs.quotas.get(tenant_id)
self.assert_request_id(q, fakes.FAKE_REQUEST_ID_LIST) self.assert_request_id(q, fakes.FAKE_REQUEST_ID_LIST)
self.assert_called('GET', '/os-quota-sets/%s' % tenant_id) self.assert_called('GET', '/os-quota-sets/%s' % tenant_id)
return q
def test_user_quotas_get(self): def test_user_quotas_get(self):
tenant_id = 'test' tenant_id = 'test'
@ -65,6 +67,7 @@ class QuotaSetsTest(utils.FixturedTestCase):
self.assert_called( self.assert_called(
'PUT', '/os-quota-sets/97f4c221bff44578b0300df4ef119353', 'PUT', '/os-quota-sets/97f4c221bff44578b0300df4ef119353',
{'quota_set': {'force': True, 'cores': 2}}) {'quota_set': {'force': True, 'cores': 2}})
return q
def test_quotas_delete(self): def test_quotas_delete(self):
tenant_id = 'test' tenant_id = 'test'
@ -79,3 +82,40 @@ class QuotaSetsTest(utils.FixturedTestCase):
self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST)
url = '/os-quota-sets/%s?user_id=%s' % (tenant_id, user_id) url = '/os-quota-sets/%s?user_id=%s' % (tenant_id, user_id)
self.assert_called('DELETE', url) 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)

View File

@ -34,6 +34,7 @@ class ServersTest(utils.FixturedTestCase):
client_fixture_class = client.V1 client_fixture_class = client.V1
data_fixture_class = data.V1 data_fixture_class = data.V1
api_version = None api_version = None
supports_files = True
def setUp(self): def setUp(self):
super(ServersTest, self).setUp() super(ServersTest, self).setUp()
@ -126,6 +127,12 @@ class ServersTest(utils.FixturedTestCase):
self.assertEqual(s1._info, s2._info) self.assertEqual(s1._info, s2._info)
def test_create_server(self): 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( s = self.cs.servers.create(
name="My server", name="My server",
image=1, image=1,
@ -133,11 +140,8 @@ class ServersTest(utils.FixturedTestCase):
meta={'foo': 'bar'}, meta={'foo': 'bar'},
userdata="hello moto", userdata="hello moto",
key_name="fakekey", key_name="fakekey",
files={ nics=self._get_server_create_default_nics(),
'/etc/passwd': 'some data', # a file **kwargs
'/tmp/foo.txt': six.StringIO('data'), # a stream
},
nics=self._get_server_create_default_nics()
) )
self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST)
self.assert_called('POST', '/servers') self.assert_called('POST', '/servers')
@ -253,23 +257,32 @@ class ServersTest(utils.FixturedTestCase):
self.assertIsInstance(s, servers.Server) self.assertIsInstance(s, servers.Server)
def test_create_server_userdata_file_object(self): 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( s = self.cs.servers.create(
name="My server", name="My server",
image=1, image=1,
flavor=1, flavor=1,
meta={'foo': 'bar'}, meta={'foo': 'bar'},
userdata=six.StringIO('hello moto'), 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(), nics=self._get_server_create_default_nics(),
**kwargs
) )
self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST)
self.assert_called('POST', '/servers') self.assert_called('POST', '/servers')
self.assertIsInstance(s, servers.Server) self.assertIsInstance(s, servers.Server)
def test_create_server_userdata_unicode(self): 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( s = self.cs.servers.create(
name="My server", name="My server",
image=1, image=1,
@ -277,17 +290,20 @@ class ServersTest(utils.FixturedTestCase):
meta={'foo': 'bar'}, meta={'foo': 'bar'},
userdata=six.u('こんにちは'), userdata=six.u('こんにちは'),
key_name="fakekey", 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_request_id(s, fakes.FAKE_REQUEST_ID_LIST)
self.assert_called('POST', '/servers') self.assert_called('POST', '/servers')
self.assertIsInstance(s, servers.Server) self.assertIsInstance(s, servers.Server)
def test_create_server_userdata_utf8(self): 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( s = self.cs.servers.create(
name="My server", name="My server",
image=1, image=1,
@ -295,11 +311,8 @@ class ServersTest(utils.FixturedTestCase):
meta={'foo': 'bar'}, meta={'foo': 'bar'},
userdata='こんにちは', userdata='こんにちは',
key_name="fakekey", 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_request_id(s, fakes.FAKE_REQUEST_ID_LIST)
self.assert_called('POST', '/servers') self.assert_called('POST', '/servers')
@ -323,6 +336,12 @@ class ServersTest(utils.FixturedTestCase):
self.assertEqual(test_password, body['server']['adminPass']) self.assertEqual(test_password, body['server']['adminPass'])
def test_create_server_userdata_bin(self): 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: with tempfile.TemporaryFile(mode='wb+') as bin_file:
original_data = os.urandom(1024) original_data = os.urandom(1024)
bin_file.write(original_data) bin_file.write(original_data)
@ -335,11 +354,8 @@ class ServersTest(utils.FixturedTestCase):
meta={'foo': 'bar'}, meta={'foo': 'bar'},
userdata=bin_file, userdata=bin_file,
key_name="fakekey", 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_request_id(s, fakes.FAKE_REQUEST_ID_LIST)
self.assert_called('POST', '/servers') self.assert_called('POST', '/servers')
@ -1500,3 +1516,29 @@ class ServersV256Test(ServersV254Test):
ex = self.assertRaises(TypeError, ex = self.assertRaises(TypeError,
s.migrate, host='target-host') s.migrate, host='target-host')
self.assertIn('host', six.text_type(ex)) 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))

View File

@ -36,6 +36,7 @@ from novaclient import exceptions
import novaclient.shell import novaclient.shell
from novaclient.tests.unit import utils from novaclient.tests.unit import utils
from novaclient.tests.unit.v2 import fakes from novaclient.tests.unit.v2 import fakes
from novaclient.v2 import servers
import novaclient.v2.shell import novaclient.v2.shell
FAKE_UUID_1 = fakes.FAKE_IMAGE_UUID_1 FAKE_UUID_1 = fakes.FAKE_IMAGE_UUID_1
@ -971,6 +972,16 @@ class ShellTest(utils.TestCase):
' --file /foo=%s' % (FAKE_UUID_1, invalid_file)) ' --file /foo=%s' % (FAKE_UUID_1, invalid_file))
self.assertRaises(exceptions.CommandError, self.run_command, cmd) 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): def test_boot_max_min_count(self):
self.run_command('boot --image %s --flavor 1 --min-count 1' self.run_command('boot --image %s --flavor 1 --min-count 1'
' --max-count 3 server' % FAKE_UUID_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'" expected = "'['foo']' is not in the format of 'key=value'"
self.assertEqual(expected, result.args[0]) 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): def test_start(self):
self.run_command('start sample-server') self.run_command('start sample-server')
self.assert_called('POST', '/servers/1234/action', {'os-start': None}) self.assert_called('POST', '/servers/1234/action', {'os-start': None})
@ -2643,6 +2710,17 @@ class ShellTest(utils.TestCase):
'PUT', '/os-quota-sets/97f4c221bff44578b0300df4ef119353', 'PUT', '/os-quota-sets/97f4c221bff44578b0300df4ef119353',
{'quota_set': {'fixed_ips': 5}}) {'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): def test_quota_delete(self):
self.run_command('quota-delete --tenant ' self.run_command('quota-delete --tenant '
'97f4c221bff44578b0300df4ef119353') '97f4c221bff44578b0300df4ef119353')
@ -2680,6 +2758,16 @@ class ShellTest(utils.TestCase):
'PUT', '/os-quota-class-sets/97f4c221bff44578b0300df4ef119353', 'PUT', '/os-quota-class-sets/97f4c221bff44578b0300df4ef119353',
body) 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): def test_backup(self):
out, err = self.run_command('backup sample-server back1 daily 1') out, err = self.run_command('backup sample-server back1 daily 1')
# With microversion < 2.45 there is no output from this command. # With microversion < 2.45 there is no output from this command.
@ -2712,8 +2800,9 @@ class ShellTest(utils.TestCase):
'rotation': '1'}}) 'rotation': '1'}})
def test_limits(self): def test_limits(self):
self.run_command('limits') out = self.run_command('limits')[0]
self.assert_called('GET', '/limits') self.assert_called('GET', '/limits')
self.assertIn('Personality', out)
self.run_command('limits --reserved') self.run_command('limits --reserved')
self.assert_called('GET', '/limits?reserved=1') self.assert_called('GET', '/limits?reserved=1')
@ -2725,6 +2814,14 @@ class ShellTest(utils.TestCase):
self.assertIn('Verb', stdout) self.assertIn('Verb', stdout)
self.assertIn('Name', 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): def test_evacuate(self):
self.run_command('evacuate sample-server new_host') self.run_command('evacuate sample-server new_host')
self.assert_called('POST', '/servers/1234/action', 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. 51, # There are no version-wrapped shell method changes for this.
52, # 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. 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, versions_supported = set(range(0,
novaclient.API_MAX_VERSION.ver_minor + 1)) novaclient.API_MAX_VERSION.ver_minor + 1))

View File

@ -50,7 +50,7 @@ class QuotaClassSetManager(base.Manager):
# NOTE(mriedem): 2.50 does strict validation of the resources you can # NOTE(mriedem): 2.50 does strict validation of the resources you can
# specify since the network-related resources are blocked in 2.50. # 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, def update(self, class_name, instances=None, cores=None, ram=None,
metadata_items=None, injected_files=None, metadata_items=None, injected_files=None,
injected_file_content_bytes=None, injected_file_path_bytes=None, injected_file_content_bytes=None, injected_file_path_bytes=None,
@ -81,3 +81,30 @@ class QuotaClassSetManager(base.Manager):
body = {'quota_class_set': resources} body = {'quota_class_set': resources}
return self._update('/os-quota-class-sets/%s' % class_name, body, return self._update('/os-quota-class-sets/%s' % class_name, body,
'quota_class_set') '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')

View File

@ -13,6 +13,7 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from novaclient import api_versions
from novaclient import base from novaclient import base
@ -38,6 +39,10 @@ class QuotaSetManager(base.Manager):
return self._get(url % params, "quota_set") 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): def update(self, tenant_id, **kwargs):
user_id = kwargs.pop('user_id', None) user_id = kwargs.pop('user_id', None)
@ -53,6 +58,40 @@ class QuotaSetManager(base.Manager):
url = '/os-quota-sets/%s' % tenant_id url = '/os-quota-sets/%s' % tenant_id
return self._update(url, body, 'quota_set') 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): def defaults(self, tenant_id):
return self._get('/os-quota-sets/%s/defaults' % tenant_id, return self._get('/os-quota-sets/%s/defaults' % tenant_id,
'quota_set') 'quota_set')

View File

@ -621,6 +621,27 @@ class SecurityGroup(base.Resource):
class ServerManager(base.BootingManagerWithFind): class ServerManager(base.BootingManagerWithFind):
resource_class = Server 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, def _boot(self, response_key, name, image, flavor,
meta=None, files=None, userdata=None, meta=None, files=None, userdata=None,
reservation_id=False, return_raw=False, min_count=None, reservation_id=False, return_raw=False, min_count=None,
@ -639,25 +660,7 @@ class ServerManager(base.BootingManagerWithFind):
"flavorRef": str(base.getid(flavor)), "flavorRef": str(base.getid(flavor)),
}} }}
if userdata: if userdata:
if hasattr(userdata, 'read'): body["server"]["user_data"] = self.transform_userdata(userdata)
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
if meta: if meta:
body["server"]["metadata"] = meta body["server"]["metadata"] = meta
if reservation_id: if reservation_id:
@ -1204,6 +1207,7 @@ class ServerManager(base.BootingManagerWithFind):
are the file contents (either as a string or as a are the file contents (either as a string or as a
file-like object). A maximum of five entries is allowed, file-like object). A maximum of five entries is allowed,
and each file must be 10k or less. 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 :param reservation_id: return a reservation_id for the set of
servers being requested, boolean. servers being requested, boolean.
:param min_count: (optional extension) The minimum number of :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: if "tags" in kwargs and self.api_version < boot_tags_microversion:
raise exceptions.UnsupportedAttribute("tags", "2.52") 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( boot_kwargs = dict(
meta=meta, files=files, userdata=userdata, meta=meta, files=files, userdata=userdata,
reservation_id=reservation_id, min_count=min_count, 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 are the file contents (either as a string or as a
file-like object). A maximum of five entries is allowed, file-like object). A maximum of five entries is allowed,
and each file must be 10k or less. and each file must be 10k or less.
(deprecated starting with microversion 2.57)
:param description: optional description of the server (allowed since :param description: optional description of the server (allowed since
microversion 2.19) microversion 2.19)
:param key_name: optional key pair name for rebuild operation; passing :param key_name: optional key pair name for rebuild operation; passing
None will unset the key for the server instance None will unset the key for the server instance
(starting from microversion 2.54) (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` :returns: :class:`Server`
""" """
descr_microversion = api_versions.APIVersion("2.19") descr_microversion = api_versions.APIVersion("2.19")
@ -1414,6 +1428,14 @@ class ServerManager(base.BootingManagerWithFind):
self.api_version < api_versions.APIVersion('2.54')): self.api_version < api_versions.APIVersion('2.54')):
raise exceptions.UnsupportedAttribute('key_name', '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)} body = {'imageRef': base.getid(image)}
if password is not None: if password is not None:
body['adminPass'] = password body['adminPass'] = password
@ -1443,6 +1465,12 @@ class ServerManager(base.BootingManagerWithFind):
'path': filepath, 'path': filepath,
'contents': cont, '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, resp, body = self._action_return_resp_and_body('rebuild', server,
body, **kwargs) body, **kwargs)

View File

@ -391,19 +391,22 @@ def _boot(cs, args):
meta = _meta_parsing(args.meta) meta = _meta_parsing(args.meta)
files = {} include_files = cs.api_version < api_versions.APIVersion('2.57')
for f in args.files: if include_files:
try: files = {}
dst, src = f.split('=', 1) for f in args.files:
files[dst] = open(src) try:
except IOError as e: dst, src = f.split('=', 1)
raise exceptions.CommandError(_("Can't open '%(src)s': %(exc)s") % files[dst] = open(src)
{'src': src, 'exc': e}) except IOError as e:
except ValueError: raise exceptions.CommandError(
raise exceptions.CommandError(_("Invalid file argument '%s'. " _("Can't open '%(src)s': %(exc)s") %
"File arguments must be of the " {'src': src, 'exc': e})
"form '--file " except ValueError:
"<dst-path=src-path>'") % f) 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 # use the os-keypair extension
key_name = None key_name = None
@ -481,7 +484,6 @@ def _boot(cs, args):
boot_kwargs = dict( boot_kwargs = dict(
meta=meta, meta=meta,
files=files,
key_name=key_name, key_name=key_name,
min_count=min_count, min_count=min_count,
max_count=max_count, max_count=max_count,
@ -504,6 +506,9 @@ def _boot(cs, args):
if 'tags' in args and args.tags: if 'tags' in args and args.tags:
boot_kwargs["tags"] = args.tags.split(',') boot_kwargs["tags"] = args.tags.split(',')
if include_files:
boot_kwargs['files'] = files
return boot_args, boot_kwargs return boot_args, boot_kwargs
@ -563,7 +568,8 @@ def _boot(cs, args):
"on the new server. More files can be injected using multiple " "on the new server. More files can be injected using multiple "
"'--file' options. Limited by the 'injected_files' quota value. " "'--file' options. Limited by the 'injected_files' quota value. "
"The default value is 5. You can get the current quota value by " "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( @utils.arg(
'--key-name', '--key-name',
default=os.environ.get('NOVACLIENT_DEFAULT_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 " "on the new server. More files can be injected using multiple "
"'--file' options. You may store up to 5 files by default. " "'--file' options. You may store up to 5 files by default. "
"The maximum number of files is specified by the 'Personality' " "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( @utils.arg(
'--key-name', '--key-name',
metavar='<key-name>', metavar='<key-name>',
@ -1785,6 +1792,19 @@ def do_reboot(cs, args):
help=_("Unset keypair in the server. " help=_("Unset keypair in the server. "
"Cannot be specified with the '--key-name' option."), "Cannot be specified with the '--key-name' option."),
start_version='2.54') 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): def do_rebuild(cs, args):
"""Shutdown, re-image, and re-boot a server.""" """Shutdown, re-image, and re-boot a server."""
server = _find_server(cs, args.server) server = _find_server(cs, args.server)
@ -1803,21 +1823,34 @@ def do_rebuild(cs, args):
meta = _meta_parsing(args.meta) meta = _meta_parsing(args.meta)
kwargs['meta'] = meta kwargs['meta'] = meta
files = {} # 2.57 deprecates the --file option and adds the --user-data and
for f in args.files: # --user-data-unset options.
try: if cs.api_version < api_versions.APIVersion('2.57'):
dst, src = f.split('=', 1) files = {}
with open(src, 'r') as s: for f in args.files:
files[dst] = s.read() try:
except IOError as e: dst, src = f.split('=', 1)
raise exceptions.CommandError(_("Can't open '%(src)s': %(exc)s") % with open(src, 'r') as s:
{'src': src, 'exc': e}) files[dst] = s.read()
except ValueError: except IOError as e:
raise exceptions.CommandError(_("Invalid file argument '%s'. " raise exceptions.CommandError(
"File arguments must be of the " _("Can't open '%(src)s': %(exc)s") %
"form '--file " {'src': src, 'exc': e})
"<dst-path=src-path>'") % f) except ValueError:
kwargs['files'] = files 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 cs.api_version >= api_versions.APIVersion('2.54'):
if args.key_unset: if args.key_unset:
@ -3739,6 +3772,10 @@ def do_ssh(cs, args):
# return floating_ips, fixed_ips, security_groups or security_group_members # return floating_ips, fixed_ips, security_groups or security_group_members
# as those are deprecated as networking service proxies and/or because # as those are deprecated as networking service proxies and/or because
# nova-network is deprecated. Similar to the 2.36 microversion. # 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', _quota_resources = ['instances', 'cores', 'ram',
'floating_ips', 'fixed_ips', 'metadata_items', 'floating_ips', 'fixed_ips', 'metadata_items',
'injected_files', 'injected_file_content_bytes', '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 # 2.36 does not support updating quota for floating IPs, fixed IPs, security
# groups or security group rules. # groups or security group rules.
# 2.57 does not support updating injected_file* quotas.
@api_versions.wraps("2.36") @api_versions.wraps("2.36")
@utils.arg( @utils.arg(
'tenant', 'tenant',
@ -3978,19 +4016,22 @@ def do_quota_update(cs, args):
metavar='<injected-files>', metavar='<injected-files>',
type=int, type=int,
default=None, 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( @utils.arg(
'--injected-file-content-bytes', '--injected-file-content-bytes',
metavar='<injected-file-content-bytes>', metavar='<injected-file-content-bytes>',
type=int, type=int,
default=None, 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( @utils.arg(
'--injected-file-path-bytes', '--injected-file-path-bytes',
metavar='<injected-file-path-bytes>', metavar='<injected-file-path-bytes>',
type=int, type=int,
default=None, 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( @utils.arg(
'--key-pairs', '--key-pairs',
metavar='<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, # 2.50 does not support updating quota class values for floating IPs,
# fixed IPs, security groups or security group rules. # fixed IPs, security groups or security group rules.
# 2.57 does not support updating injected_file* quotas.
@api_versions.wraps("2.50") @api_versions.wraps("2.50")
@utils.arg( @utils.arg(
'class_name', 'class_name',
@ -4178,19 +4220,22 @@ def do_quota_class_update(cs, args):
metavar='<injected-files>', metavar='<injected-files>',
type=int, type=int,
default=None, 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( @utils.arg(
'--injected-file-content-bytes', '--injected-file-content-bytes',
metavar='<injected-file-content-bytes>', metavar='<injected-file-content-bytes>',
type=int, type=int,
default=None, 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( @utils.arg(
'--injected-file-path-bytes', '--injected-file-path-bytes',
metavar='<injected-file-path-bytes>', metavar='<injected-file-path-bytes>',
type=int, type=int,
default=None, 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( @utils.arg(
'--key-pairs', '--key-pairs',
metavar='<key-pairs>', metavar='<key-pairs>',

View File

@ -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.