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:
parent
fefc3ba723
commit
038cfdd5b3
@ -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")
|
||||
|
@ -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):
|
||||
|
@ -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']
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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': {
|
||||
|
@ -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')
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
|
@ -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))
|
||||
|
@ -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')
|
||||
|
@ -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')
|
||||
|
@ -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)
|
||||
|
@ -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>',
|
||||
|
31
releasenotes/notes/microversion-v2_57-acae2ee11ddae4fb.yaml
Normal file
31
releasenotes/notes/microversion-v2_57-acae2ee11ddae4fb.yaml
Normal 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.
|
Loading…
Reference in New Issue
Block a user