Implement 'personality' plugin for V2.1
User can specify 'personality' attribute while server creation & rebuild to inject files into server. This patch implement this as separate plugin for V2.1 API. Also define the schema and unit tests for the same. Partially implements blueprint v2-on-v3-api Change-Id: Ia0c527539af7fe33eba4999822476653e1b96bc6
This commit is contained in:
parent
b40ea7c0ec
commit
d99b308e6d
@ -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=="
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
16
doc/v3/api_samples/os-personality/server-post-req.json
Normal file
16
doc/v3/api_samples/os-personality/server-post-req.json
Normal file
@ -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=="
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
16
doc/v3/api_samples/os-personality/server-post-resp.json
Normal file
16
doc/v3/api_samples/os-personality/server-post-resp.json
Normal file
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@ -230,6 +230,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",
|
||||
|
65
nova/api/openstack/compute/plugins/v3/personality.py
Normal file
65
nova/api/openstack/compute/plugins/v3/personality.py
Normal file
@ -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
|
30
nova/api/openstack/compute/schemas/v3/personality.py
Normal file
30
nova/api/openstack/compute/schemas/v3/personality.py
Normal file
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
@ -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=="
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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=="
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
45
nova/tests/functional/v3/test_personality.py
Normal file
45
nova/tests/functional/v3/test_personality.py
Normal file
@ -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)
|
@ -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,6 +1977,12 @@ class ServersControllerCreateTest(test.TestCase):
|
||||
'hello': 'world',
|
||||
'open': 'stack',
|
||||
},
|
||||
'personality': [
|
||||
{
|
||||
"path": "/etc/banner.txt",
|
||||
"contents": "MQ==",
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
self.bdm = [{'delete_on_termination': 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'
|
||||
|
@ -111,6 +111,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
|
||||
@ -143,6 +144,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
|
||||
@ -150,6 +152,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
|
||||
|
Loading…
Reference in New Issue
Block a user