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
# 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")

View File

@ -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):

View File

@ -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']

View File

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

View File

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

View File

@ -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': {

View File

@ -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')

View File

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

View File

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

View File

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

View File

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

View File

@ -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')

View File

@ -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')

View File

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

View File

@ -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>',

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.