diff --git a/doc/v3/api_samples/os-personality/server-action-rebuild-req.json b/doc/v3/api_samples/os-personality/server-action-rebuild-req.json new file mode 100644 index 0000000000..2d82ec7835 --- /dev/null +++ b/doc/v3/api_samples/os-personality/server-action-rebuild-req.json @@ -0,0 +1,15 @@ +{ + "rebuild": { + "imageRef": "http://glance.openstack.example.com/images/70a599e0-31e7-49b7-b260-868f441e862b", + "name": "new-server-test", + "metadata": { + "meta_var": "meta_val" + }, + "personality": [ + { + "path": "/etc/banner.txt", + "contents": "ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBp dCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5k IGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVs c2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4g QnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRo ZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlv dSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vy c2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6 b25zLiINCg0KLVJpY2hhcmQgQmFjaA==" + } + ] + } +} diff --git a/doc/v3/api_samples/os-personality/server-action-rebuild-resp.json b/doc/v3/api_samples/os-personality/server-action-rebuild-resp.json new file mode 100644 index 0000000000..a78ede2669 --- /dev/null +++ b/doc/v3/api_samples/os-personality/server-action-rebuild-resp.json @@ -0,0 +1,55 @@ +{ + "server": { + "addresses": { + "private": [ + { + "addr": "192.168.0.3", + "OS-EXT-IPS-MAC:mac_addr": "aa:bb:cc:dd:ee:ff", + "OS-EXT-IPS:type": "fixed", + "version": 4 + } + ] + }, + "adminPass": "99WHAxN8gpvg", + "created": "2013-11-06T07:51:09Z", + "flavor": { + "id": "1", + "links": [ + { + "href": "http://openstack.example.com/flavors/1", + "rel": "bookmark" + } + ] + }, + "hostId": "5c8072dbcda8ce3f26deb6662bd7718e1a6d349bdf2296911d1be4ac", + "id": "53a63a19-c145-47f8-9ae5-b39d6bff33ec", + "image": { + "id": "70a599e0-31e7-49b7-b260-868f441e862b", + "links": [ + { + "href": "http://openstack.example.com/images/70a599e0-31e7-49b7-b260-868f441e862b", + "rel": "bookmark" + } + ] + }, + "links": [ + { + "href": "http://openstack.example.com/v3/servers/53a63a19-c145-47f8-9ae5-b39d6bff33ec", + "rel": "self" + }, + { + "href": "http://openstack.example.com/servers/53a63a19-c145-47f8-9ae5-b39d6bff33ec", + "rel": "bookmark" + } + ], + "metadata": { + "meta_var": "meta_val" + }, + "name": "new-server-test", + "progress": 0, + "status": "ACTIVE", + "tenant_id": "openstack", + "updated": "2013-11-06T07:51:11Z", + "user_id": "fake" + } +} diff --git a/doc/v3/api_samples/os-personality/server-post-req.json b/doc/v3/api_samples/os-personality/server-post-req.json new file mode 100644 index 0000000000..b5718995ee --- /dev/null +++ b/doc/v3/api_samples/os-personality/server-post-req.json @@ -0,0 +1,16 @@ +{ + "server": { + "name": "new-server-test", + "imageRef": "http://openstack.example.com/openstack/images/70a599e0-31e7-49b7-b260-868f441e862b", + "flavorRef": "http://openstack.example.com/openstack/flavors/1", + "metadata": { + "My Server Name": "Apache1" + }, + "personality": [ + { + "path": "/etc/banner.txt", + "contents": "ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBpdCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5kIGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVsc2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4gQnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRoZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlvdSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vyc2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6b25zLiINCg0KLVJpY2hhcmQgQmFjaA==" + } + ] + } +} \ No newline at end of file diff --git a/doc/v3/api_samples/os-personality/server-post-resp.json b/doc/v3/api_samples/os-personality/server-post-resp.json new file mode 100644 index 0000000000..07cd1854fe --- /dev/null +++ b/doc/v3/api_samples/os-personality/server-post-resp.json @@ -0,0 +1,16 @@ +{ + "server": { + "adminPass": "n7JGBda664QG", + "id": "934760e1-2b0b-4f9e-a916-eac1e69839dc", + "links": [ + { + "href": "http://openstack.example.com/v3/servers/934760e1-2b0b-4f9e-a916-eac1e69839dc", + "rel": "self" + }, + { + "href": "http://openstack.example.com/servers/934760e1-2b0b-4f9e-a916-eac1e69839dc", + "rel": "bookmark" + } + ] + } +} \ No newline at end of file diff --git a/etc/nova/policy.json b/etc/nova/policy.json index 70ef01c273..d40dd6f6b2 100644 --- a/etc/nova/policy.json +++ b/etc/nova/policy.json @@ -232,6 +232,7 @@ "compute_extension:v3:os-pci:index": "rule:admin_api", "compute_extension:v3:os-pci:detail": "rule:admin_api", "compute_extension:v3:os-pci:show": "rule:admin_api", + "compute_extension:v3:os-personality:discoverable": "", "compute_extension:quotas:show": "", "compute_extension:quotas:update": "rule:admin_api", "compute_extension:quotas:delete": "rule:admin_api", diff --git a/nova/api/openstack/compute/plugins/v3/personality.py b/nova/api/openstack/compute/plugins/v3/personality.py new file mode 100644 index 0000000000..7bcf35bec5 --- /dev/null +++ b/nova/api/openstack/compute/plugins/v3/personality.py @@ -0,0 +1,65 @@ +# Copyright 2014 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from nova.api.openstack.compute.schemas.v3 import personality +from nova.api.openstack import extensions + +ALIAS = "os-personality" + + +class Personality(extensions.V3APIExtensionBase): + """Personality support.""" + + name = "Personality" + alias = ALIAS + version = 1 + + def get_controller_extensions(self): + return [] + + def get_resources(self): + return [] + + def _get_injected_files(self, personality): + """Create a list of injected files from the personality attribute. + + At this time, injected_files must be formatted as a list of + (file_path, file_content) pairs for compatibility with the + underlying compute service. + """ + injected_files = [] + for item in personality: + injected_files.append((item['path'], item['contents'])) + return injected_files + + # NOTE(gmann): This function is not supposed to use 'body_deprecated_param' + # parameter as this is placed to handle scheduler_hint extension for V2.1. + # making 'body_deprecated_param' as optional to avoid changes for + # server_update & server_rebuild + def server_create(self, server_dict, create_kwargs, + body_deprecated_param=None): + if 'personality' in server_dict: + create_kwargs['injected_files'] = self._get_injected_files( + server_dict['personality']) + + def server_rebuild(self, server_dict, create_kwargs, + body_deprecated_param=None): + if 'personality' in server_dict: + create_kwargs['files_to_inject'] = self._get_injected_files( + server_dict['personality']) + + def get_server_create_schema(self): + return personality.server_create + + get_server_rebuild_schema = get_server_create_schema diff --git a/nova/api/openstack/compute/schemas/v3/personality.py b/nova/api/openstack/compute/schemas/v3/personality.py new file mode 100644 index 0000000000..0c7d5eaef2 --- /dev/null +++ b/nova/api/openstack/compute/schemas/v3/personality.py @@ -0,0 +1,30 @@ +# Copyright 2014 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +server_create = { + 'personality': { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'path': {'type': 'string'}, + 'contents': { + 'type': 'string', + 'format': 'base64' + } + }, + 'additionalProperties': False, + } + } +} diff --git a/nova/tests/functional/v3/api_samples/os-personality/server-action-rebuild-req.json.tpl b/nova/tests/functional/v3/api_samples/os-personality/server-action-rebuild-req.json.tpl new file mode 100644 index 0000000000..fc4e1ee994 --- /dev/null +++ b/nova/tests/functional/v3/api_samples/os-personality/server-action-rebuild-req.json.tpl @@ -0,0 +1,15 @@ +{ + "rebuild": { + "imageRef": "%(glance_host)s/images/%(image_id)s", + "name": "new-server-test", + "metadata": { + "meta_var": "meta_val" + }, + "personality": [ + { + "path": "/etc/banner.txt", + "contents": "ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBp dCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5k IGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVs c2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4g QnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRo ZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlv dSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vy c2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6 b25zLiINCg0KLVJpY2hhcmQgQmFjaA==" + } + ] + } +} diff --git a/nova/tests/functional/v3/api_samples/os-personality/server-action-rebuild-resp.json.tpl b/nova/tests/functional/v3/api_samples/os-personality/server-action-rebuild-resp.json.tpl new file mode 100644 index 0000000000..d3d788e444 --- /dev/null +++ b/nova/tests/functional/v3/api_samples/os-personality/server-action-rebuild-resp.json.tpl @@ -0,0 +1,55 @@ +{ + "server": { + "addresses": { + "private": [ + { + "addr": "%(ip)s", + "version": 4, + "OS-EXT-IPS-MAC:mac_addr": "aa:bb:cc:dd:ee:ff", + "OS-EXT-IPS:type": "fixed" + } + ] + }, + "adminPass": "%(password)s", + "created": "%(isotime)s", + "flavor": { + "id": "1", + "links": [ + { + "href": "%(host)s/flavors/1", + "rel": "bookmark" + } + ] + }, + "hostId": "%(hostid)s", + "id": "%(uuid)s", + "image": { + "id": "%(image_id)s", + "links": [ + { + "href": "%(host)s/images/%(image_id)s", + "rel": "bookmark" + } + ] + }, + "links": [ + { + "href": "%(host)s/v3/servers/%(uuid)s", + "rel": "self" + }, + { + "href": "%(host)s/servers/%(uuid)s", + "rel": "bookmark" + } + ], + "metadata": { + "meta_var": "meta_val" + }, + "name": "new-server-test", + "progress": 0, + "status": "ACTIVE", + "tenant_id": "openstack", + "updated": "%(isotime)s", + "user_id": "fake" + } +} diff --git a/nova/tests/functional/v3/api_samples/os-personality/server-post-req.json.tpl b/nova/tests/functional/v3/api_samples/os-personality/server-post-req.json.tpl new file mode 100644 index 0000000000..3271a58a7d --- /dev/null +++ b/nova/tests/functional/v3/api_samples/os-personality/server-post-req.json.tpl @@ -0,0 +1,16 @@ +{ + "server": { + "name": "new-server-test", + "imageRef": "%(host)s/openstack/images/%(image_id)s", + "flavorRef": "%(host)s/openstack/flavors/1", + "metadata": { + "My Server Name": "Apache1" + }, + "personality": [ + { + "path": "/etc/banner.txt", + "contents": "ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBpdCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5kIGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVsc2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4gQnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRoZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlvdSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vyc2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6b25zLiINCg0KLVJpY2hhcmQgQmFjaA==" + } + ] + } +} diff --git a/nova/tests/functional/v3/api_samples/os-personality/server-post-resp.json.tpl b/nova/tests/functional/v3/api_samples/os-personality/server-post-resp.json.tpl new file mode 100644 index 0000000000..71654b4b8a --- /dev/null +++ b/nova/tests/functional/v3/api_samples/os-personality/server-post-resp.json.tpl @@ -0,0 +1,16 @@ +{ + "server": { + "adminPass": "%(password)s", + "id": "%(id)s", + "links": [ + { + "href": "http://openstack.example.com/v3/servers/%(uuid)s", + "rel": "self" + }, + { + "href": "http://openstack.example.com/servers/%(uuid)s", + "rel": "bookmark" + } + ] + } +} diff --git a/nova/tests/functional/v3/test_personality.py b/nova/tests/functional/v3/test_personality.py new file mode 100644 index 0000000000..69d9c1cb6a --- /dev/null +++ b/nova/tests/functional/v3/test_personality.py @@ -0,0 +1,45 @@ +# Copyright 2014 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from nova.tests.functional.v3 import api_sample_base +from nova.tests.unit.image import fake + + +class PersonalitySampleJsonTest(api_sample_base.ApiSampleTestBaseV3): + extension_name = 'os-personality' + + def _servers_post(self, subs): + response = self._do_post('servers', 'server-post-req', subs) + subs.update(self._get_regexes()) + return self._verify_response('server-post-resp', subs, response, 202) + + def test_servers_post(self): + subs = { + 'image_id': fake.get_valid_image_id(), + 'host': self._get_host() + } + self._servers_post(subs) + + def test_servers_rebuild(self): + subs = { + 'image_id': fake.get_valid_image_id(), + 'host': self._get_host() + } + uuid = self._servers_post(subs) + response = self._do_post('servers/%s/action' % uuid, + 'server-action-rebuild-req', subs) + subs['hostid'] = '[a-f0-9]+' + subs['id'] = uuid + self._verify_response('server-action-rebuild-resp', + subs, response, 202) diff --git a/nova/tests/unit/api/openstack/compute/plugins/v3/test_servers.py b/nova/tests/unit/api/openstack/compute/plugins/v3/test_servers.py index 30388300c2..5beca5d37d 100644 --- a/nova/tests/unit/api/openstack/compute/plugins/v3/test_servers.py +++ b/nova/tests/unit/api/openstack/compute/plugins/v3/test_servers.py @@ -1530,6 +1530,37 @@ class ServersControllerRebuildInstanceTest(ControllerTest): self.controller._action_rebuild, self.req, FAKE_UUID, body=self.body) + def test_rebuild_bad_personality(self): + body = { + "rebuild": { + "imageRef": self.image_href, + "personality": [{ + "path": "/path/to/file", + "contents": "INVALID b64", + }] + }, + } + + self.assertRaises(exception.ValidationError, + self.controller._action_rebuild, + self.req, FAKE_UUID, body=body) + + def test_rebuild_personality(self): + body = { + "rebuild": { + "imageRef": self.image_href, + "personality": [{ + "path": "/path/to/file", + "contents": base64.b64encode("Test String"), + }] + }, + } + + body = self.controller._action_rebuild(self.req, FAKE_UUID, + body=body).obj + + self.assertNotIn('personality', body['server']) + def test_start(self): self.mox.StubOutWithMock(compute_api.API, 'start') compute_api.API.start(mox.IgnoreArg(), mox.IgnoreArg()) @@ -1946,8 +1977,14 @@ class ServersControllerCreateTest(test.TestCase): 'hello': 'world', 'open': 'stack', }, - }, - } + 'personality': [ + { + "path": "/etc/banner.txt", + "contents": "MQ==", + }, + ], + }, + } self.bdm = [{'delete_on_termination': 1, 'device_name': 123, 'volume_size': 1, @@ -2646,6 +2683,40 @@ class ServersControllerCreateTest(test.TestCase): self.controller.create, self.req, body=self.body) + @mock.patch.object(compute_api.API, 'create') + def test_create_instance_invalid_personality(self, mock_create): + codec = 'utf8' + content = 'b25zLiINCg0KLVJpY2hhcmQgQ$$%QQmFjaA==' + start_position = 19 + end_position = 20 + msg = 'invalid start byte' + mock_create.side_effect = UnicodeDecodeError(codec, content, + start_position, + end_position, msg) + + self.body['server']['personality'] = [ + { + "path": "/etc/banner.txt", + "contents": "b25zLiINCg0KLVJpY2hhcmQgQ$$%QQmFjaA==", + }, + ] + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, self.req, body=self.body) + + def test_create_instance_with_extra_personality_arg(self): + self.body['server']['personality'] = [ + { + "path": "/etc/banner.txt", + "contents": "b25zLiINCg0KLVJpY2hhcmQgQ$$%QQmFjaA==", + "extra_arg": "extra value" + }, + ] + + self.assertRaises(exception.ValidationError, + self.controller.create, + self.req, body=self.body) + class ServersControllerCreateTestWithMock(test.TestCase): image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' diff --git a/setup.cfg b/setup.cfg index 2f4ab774bc..11d3359abc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -112,6 +112,7 @@ nova.api.v3.extensions = networks_associate = nova.api.openstack.compute.plugins.v3.networks_associate:NetworksAssociate pause_server = nova.api.openstack.compute.plugins.v3.pause_server:PauseServer pci = nova.api.openstack.compute.plugins.v3.pci:Pci + personality = nova.api.openstack.compute.plugins.v3.personality:Personality quota_classes = nova.api.openstack.compute.plugins.v3.quota_classes:QuotaClasses quota_sets = nova.api.openstack.compute.plugins.v3.quota_sets:QuotaSets remote_consoles = nova.api.openstack.compute.plugins.v3.remote_consoles:RemoteConsoles @@ -144,6 +145,7 @@ nova.api.v3.extensions.server.create = disk_config = nova.api.openstack.compute.plugins.v3.disk_config:DiskConfig keypairs_create = nova.api.openstack.compute.plugins.v3.keypairs:Keypairs multiple_create = nova.api.openstack.compute.plugins.v3.multiple_create:MultipleCreate + personality = nova.api.openstack.compute.plugins.v3.personality:Personality scheduler_hints = nova.api.openstack.compute.plugins.v3.scheduler_hints:SchedulerHints security_groups = nova.api.openstack.compute.plugins.v3.security_groups:SecurityGroups user_data = nova.api.openstack.compute.plugins.v3.user_data:UserData @@ -151,6 +153,7 @@ nova.api.v3.extensions.server.create = nova.api.v3.extensions.server.rebuild = access_ips = nova.api.openstack.compute.plugins.v3.access_ips:AccessIPs disk_config = nova.api.openstack.compute.plugins.v3.disk_config:DiskConfig + personality = nova.api.openstack.compute.plugins.v3.personality:Personality nova.api.v3.extensions.server.update = access_ips = nova.api.openstack.compute.plugins.v3.access_ips:AccessIPs