Merge "Add REST api to manage bare-metal nodes"
This commit is contained in:
commit
e6907236a4
@ -88,6 +88,14 @@
|
|||||||
"namespace": "http://docs.openstack.org/compute/ext/availabilityzone/api/v1.1",
|
"namespace": "http://docs.openstack.org/compute/ext/availabilityzone/api/v1.1",
|
||||||
"updated": "2012-08-09T00:00:00+00:00"
|
"updated": "2012-08-09T00:00:00+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"alias": "os-baremetal-nodes",
|
||||||
|
"description": "Admin-only bare-metal node administration.",
|
||||||
|
"links": [],
|
||||||
|
"name": "BareMetalNodes",
|
||||||
|
"namespace": "http://docs.openstack.org/compute/ext/baremetal_nodes/api/v2",
|
||||||
|
"updated": "2013-01-04T00:00:00+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"alias": "os-cells",
|
"alias": "os-cells",
|
||||||
"description": "Enables cells-related functionality such as adding neighbor cells,\n listing neighbor cells, and getting the capabilities of the local cell.\n ",
|
"description": "Enables cells-related functionality such as adding neighbor cells,\n listing neighbor cells, and getting the capabilities of the local cell.\n ",
|
||||||
|
@ -37,6 +37,9 @@
|
|||||||
<extension alias="os-availability-zone" updated="2012-08-09T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/availabilityzone/api/v1.1" name="AvailabilityZone">
|
<extension alias="os-availability-zone" updated="2012-08-09T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/availabilityzone/api/v1.1" name="AvailabilityZone">
|
||||||
<description>Add availability_zone to the Create Server v1.1 API.</description>
|
<description>Add availability_zone to the Create Server v1.1 API.</description>
|
||||||
</extension>
|
</extension>
|
||||||
|
<extension alias="os-baremetal-nodes" updated="2013-01-04T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/baremetal_nodes/api/v2" name="BareMetalNodes">
|
||||||
|
<description>Admin-only bare-metal node administration.</description>
|
||||||
|
</extension>
|
||||||
<extension alias="os-cells" updated="2011-09-21T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/cells/api/v1.1" name="Cells">
|
<extension alias="os-cells" updated="2011-09-21T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/cells/api/v1.1" name="Cells">
|
||||||
<description>Enables cells-related functionality such as adding child cells,
|
<description>Enables cells-related functionality such as adding child cells,
|
||||||
listing child cells, getting the capabilities of the local cell,
|
listing child cells, getting the capabilities of the local cell,
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"add_interface": {
|
||||||
|
"address": "aa:aa:aa:aa:aa:aa"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<add_interface
|
||||||
|
address="aa:aa:aa:aa:aa:aa"
|
||||||
|
/>
|
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"interface": {
|
||||||
|
"address": "aa:aa:aa:aa:aa:aa",
|
||||||
|
"datapath_id": null,
|
||||||
|
"id": 1,
|
||||||
|
"port_no": null
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,2 @@
|
|||||||
|
<?xml version='1.0' encoding='UTF-8'?>
|
||||||
|
<interface datapath_id="None" id="1" port_no="None" address="aa:aa:aa:aa:aa:aa"/>
|
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"node": {
|
||||||
|
"service_host": "host",
|
||||||
|
"cpus": 8,
|
||||||
|
"memory_mb": 8192,
|
||||||
|
"local_gb": 128,
|
||||||
|
"pm_address": "10.1.2.3",
|
||||||
|
"pm_user": "pm_user",
|
||||||
|
"pm_password": "pm_pass",
|
||||||
|
"prov_mac_address": "12:34:56:78:90:ab",
|
||||||
|
"prov_vlan_id": 1234,
|
||||||
|
"terminal_port": 8000
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<node
|
||||||
|
service_host="host"
|
||||||
|
cpus="8"
|
||||||
|
memory_mb="8192"
|
||||||
|
local_gb="128"
|
||||||
|
pm_address="10.1.2.3"
|
||||||
|
pm_user="pm_user"
|
||||||
|
prov_mac_address="12:34:56:78:90:ab"
|
||||||
|
prov_vlan_id="1234"
|
||||||
|
terminal_port="8000"
|
||||||
|
/>
|
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"node": {
|
||||||
|
"cpus": 8,
|
||||||
|
"id": 1,
|
||||||
|
"instance_uuid": null,
|
||||||
|
"interfaces": [],
|
||||||
|
"local_gb": 128,
|
||||||
|
"memory_mb": 8192,
|
||||||
|
"pm_address": "10.1.2.3",
|
||||||
|
"pm_user": "pm_user",
|
||||||
|
"prov_mac_address": "12:34:56:78:90:ab",
|
||||||
|
"prov_vlan_id": 1234,
|
||||||
|
"service_host": "host",
|
||||||
|
"terminal_port": 8000
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
<?xml version='1.0' encoding='UTF-8'?>
|
||||||
|
<node
|
||||||
|
instance_uuid="None"
|
||||||
|
pm_address="10.1.2.3"
|
||||||
|
cpus="8"
|
||||||
|
prov_vlan_id="1234"
|
||||||
|
memory_mb="8192"
|
||||||
|
prov_mac_address="12:34:56:78:90:ab"
|
||||||
|
service_host="host"
|
||||||
|
local_gb="128"
|
||||||
|
id="1"
|
||||||
|
pm_user="pm_user"
|
||||||
|
terminal_port="8000">
|
||||||
|
<interfaces/>
|
||||||
|
</node>
|
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"cpus": 8,
|
||||||
|
"id": 1,
|
||||||
|
"instance_uuid": null,
|
||||||
|
"interfaces": [
|
||||||
|
{
|
||||||
|
"address": "aa:aa:aa:aa:aa:aa",
|
||||||
|
"datapath_id": null,
|
||||||
|
"id": 1,
|
||||||
|
"port_no": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"local_gb": 128,
|
||||||
|
"memory_mb": 8192,
|
||||||
|
"pm_address": "10.1.2.3",
|
||||||
|
"pm_user": "pm_user",
|
||||||
|
"prov_mac_address": "12:34:56:78:90:ab",
|
||||||
|
"prov_vlan_id": 1234,
|
||||||
|
"service_host": "host",
|
||||||
|
"terminal_port": 8000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
<?xml version='1.0' encoding='UTF-8'?>
|
||||||
|
<nodes>
|
||||||
|
<node
|
||||||
|
instance_uuid="None"
|
||||||
|
pm_address="10.1.2.3"
|
||||||
|
cpus="8"
|
||||||
|
prov_vlan_id="1234"
|
||||||
|
memory_mb="8192"
|
||||||
|
prov_mac_address="12:34:56:78:90:ab"
|
||||||
|
service_host="host"
|
||||||
|
local_gb="128"
|
||||||
|
id="1"
|
||||||
|
pm_user="pm_user"
|
||||||
|
terminal_port="8000">
|
||||||
|
<interfaces>
|
||||||
|
<interface
|
||||||
|
datapath_id="None"
|
||||||
|
id="1"
|
||||||
|
port_no="None"
|
||||||
|
address="aa:aa:aa:aa:aa:aa"/>
|
||||||
|
</interfaces>
|
||||||
|
</node>
|
||||||
|
</nodes>
|
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"remove_interface": {
|
||||||
|
"address": "aa:aa:aa:aa:aa:aa"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<remove_interface
|
||||||
|
address="aa:aa:aa:aa:aa:aa"
|
||||||
|
/>
|
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"node": {
|
||||||
|
"cpus": 8,
|
||||||
|
"id": 1,
|
||||||
|
"instance_uuid": null,
|
||||||
|
"interfaces": [
|
||||||
|
{
|
||||||
|
"address": "aa:aa:aa:aa:aa:aa",
|
||||||
|
"datapath_id": null,
|
||||||
|
"id": 1,
|
||||||
|
"port_no": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"local_gb": 128,
|
||||||
|
"memory_mb": 8192,
|
||||||
|
"pm_address": "10.1.2.3",
|
||||||
|
"pm_user": "pm_user",
|
||||||
|
"prov_mac_address": "12:34:56:78:90:ab",
|
||||||
|
"prov_vlan_id": 1234,
|
||||||
|
"service_host": "host",
|
||||||
|
"terminal_port": 8000
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
<?xml version='1.0' encoding='UTF-8'?>
|
||||||
|
<node
|
||||||
|
instance_uuid="None"
|
||||||
|
pm_address="10.1.2.3"
|
||||||
|
cpus="8"
|
||||||
|
prov_vlan_id="1234"
|
||||||
|
memory_mb="8192"
|
||||||
|
prov_mac_address="12:34:56:78:90:ab"
|
||||||
|
service_host="host"
|
||||||
|
local_gb="128"
|
||||||
|
id="1"
|
||||||
|
pm_user="pm_user"
|
||||||
|
terminal_port="8000">
|
||||||
|
<interfaces>
|
||||||
|
<interface
|
||||||
|
datapath_id="None"
|
||||||
|
id="1"
|
||||||
|
port_no="None"
|
||||||
|
address="aa:aa:aa:aa:aa:aa"/>
|
||||||
|
</interfaces>
|
||||||
|
</node>
|
@ -29,6 +29,7 @@
|
|||||||
"compute_extension:admin_actions:migrate": "rule:admin_api",
|
"compute_extension:admin_actions:migrate": "rule:admin_api",
|
||||||
"compute_extension:aggregates": "rule:admin_api",
|
"compute_extension:aggregates": "rule:admin_api",
|
||||||
"compute_extension:agents": "rule:admin_api",
|
"compute_extension:agents": "rule:admin_api",
|
||||||
|
"compute_extension:baremetal_nodes": "rule:admin_api",
|
||||||
"compute_extension:cells": "rule:admin_api",
|
"compute_extension:cells": "rule:admin_api",
|
||||||
"compute_extension:certificates": "",
|
"compute_extension:certificates": "",
|
||||||
"compute_extension:cloudpipe": "rule:admin_api",
|
"compute_extension:cloudpipe": "rule:admin_api",
|
||||||
|
210
nova/api/openstack/compute/contrib/baremetal_nodes.py
Normal file
210
nova/api/openstack/compute/contrib/baremetal_nodes.py
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
# Copyright (c) 2013 NTT DOCOMO, INC.
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""The bare-metal admin extension."""
|
||||||
|
|
||||||
|
import webob
|
||||||
|
|
||||||
|
from nova.api.openstack import extensions
|
||||||
|
from nova.api.openstack import wsgi
|
||||||
|
from nova.api.openstack import xmlutil
|
||||||
|
from nova import exception
|
||||||
|
from nova.openstack.common import log as logging
|
||||||
|
from nova.virt.baremetal import db
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
authorize = extensions.extension_authorizer('compute', 'baremetal_nodes')
|
||||||
|
|
||||||
|
node_fields = ['id', 'cpus', 'local_gb', 'memory_mb', 'pm_address',
|
||||||
|
'pm_user', 'prov_mac_address', 'prov_vlan_id',
|
||||||
|
'service_host', 'terminal_port', 'instance_uuid',
|
||||||
|
]
|
||||||
|
|
||||||
|
interface_fields = ['id', 'address', 'datapath_id', 'port_no']
|
||||||
|
|
||||||
|
|
||||||
|
def _node_dict(node_ref):
|
||||||
|
d = {}
|
||||||
|
for f in node_fields:
|
||||||
|
d[f] = node_ref.get(f)
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
def _interface_dict(interface_ref):
|
||||||
|
d = {}
|
||||||
|
for f in interface_fields:
|
||||||
|
d[f] = interface_ref.get(f)
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
def _make_node_elem(elem):
|
||||||
|
for f in node_fields:
|
||||||
|
elem.set(f)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_interface_elem(elem):
|
||||||
|
for f in interface_fields:
|
||||||
|
elem.set(f)
|
||||||
|
|
||||||
|
|
||||||
|
class NodeTemplate(xmlutil.TemplateBuilder):
|
||||||
|
def construct(self):
|
||||||
|
node_elem = xmlutil.TemplateElement('node', selector='node')
|
||||||
|
_make_node_elem(node_elem)
|
||||||
|
ifs_elem = xmlutil.TemplateElement('interfaces')
|
||||||
|
if_elem = xmlutil.SubTemplateElement(ifs_elem, 'interface',
|
||||||
|
selector='interfaces')
|
||||||
|
_make_interface_elem(if_elem)
|
||||||
|
node_elem.append(ifs_elem)
|
||||||
|
return xmlutil.MasterTemplate(node_elem, 1)
|
||||||
|
|
||||||
|
|
||||||
|
class NodesTemplate(xmlutil.TemplateBuilder):
|
||||||
|
def construct(self):
|
||||||
|
root = xmlutil.TemplateElement('nodes')
|
||||||
|
node_elem = xmlutil.SubTemplateElement(root, 'node', selector='nodes')
|
||||||
|
_make_node_elem(node_elem)
|
||||||
|
ifs_elem = xmlutil.TemplateElement('interfaces')
|
||||||
|
if_elem = xmlutil.SubTemplateElement(ifs_elem, 'interface',
|
||||||
|
selector='interfaces')
|
||||||
|
_make_interface_elem(if_elem)
|
||||||
|
node_elem.append(ifs_elem)
|
||||||
|
return xmlutil.MasterTemplate(root, 1)
|
||||||
|
|
||||||
|
|
||||||
|
class InterfaceTemplate(xmlutil.TemplateBuilder):
|
||||||
|
def construct(self):
|
||||||
|
root = xmlutil.TemplateElement('interface', selector='interface')
|
||||||
|
_make_interface_elem(root)
|
||||||
|
return xmlutil.MasterTemplate(root, 1)
|
||||||
|
|
||||||
|
|
||||||
|
class BareMetalNodeController(wsgi.Controller):
|
||||||
|
"""The Bare-Metal Node API controller for the OpenStack API."""
|
||||||
|
|
||||||
|
@wsgi.serializers(xml=NodesTemplate)
|
||||||
|
def index(self, req):
|
||||||
|
context = req.environ['nova.context']
|
||||||
|
authorize(context)
|
||||||
|
nodes_from_db = db.bm_node_get_all(context)
|
||||||
|
nodes = []
|
||||||
|
for node_from_db in nodes_from_db:
|
||||||
|
try:
|
||||||
|
ifs = db.bm_interface_get_all_by_bm_node_id(
|
||||||
|
context, node_from_db['id'])
|
||||||
|
except exception.InstanceNotFound:
|
||||||
|
ifs = []
|
||||||
|
node = _node_dict(node_from_db)
|
||||||
|
node['interfaces'] = [_interface_dict(i) for i in ifs]
|
||||||
|
nodes.append(node)
|
||||||
|
return {'nodes': nodes}
|
||||||
|
|
||||||
|
@wsgi.serializers(xml=NodeTemplate)
|
||||||
|
def show(self, req, id):
|
||||||
|
context = req.environ['nova.context']
|
||||||
|
authorize(context)
|
||||||
|
try:
|
||||||
|
node = db.bm_node_get(context, id)
|
||||||
|
except exception.InstanceNotFound:
|
||||||
|
raise webob.exc.HTTPNotFound
|
||||||
|
try:
|
||||||
|
ifs = db.bm_interface_get_all_by_bm_node_id(context, id)
|
||||||
|
except exception.InstanceNotFound:
|
||||||
|
ifs = []
|
||||||
|
node = _node_dict(node)
|
||||||
|
node['interfaces'] = [_interface_dict(i) for i in ifs]
|
||||||
|
return {'node': node}
|
||||||
|
|
||||||
|
@wsgi.serializers(xml=NodeTemplate)
|
||||||
|
def create(self, req, body):
|
||||||
|
context = req.environ['nova.context']
|
||||||
|
authorize(context)
|
||||||
|
node = db.bm_node_create(context, body['node'])
|
||||||
|
node = _node_dict(node)
|
||||||
|
node['interfaces'] = []
|
||||||
|
return {'node': node}
|
||||||
|
|
||||||
|
def delete(self, req, id):
|
||||||
|
context = req.environ['nova.context']
|
||||||
|
authorize(context)
|
||||||
|
try:
|
||||||
|
db.bm_node_destroy(context, id)
|
||||||
|
except exception.InstanceNotFound:
|
||||||
|
raise webob.exc.HTTPNotFound
|
||||||
|
return webob.Response(status_int=202)
|
||||||
|
|
||||||
|
def _check_node_exists(self, context, node_id):
|
||||||
|
try:
|
||||||
|
db.bm_node_get(context, node_id)
|
||||||
|
except exception.InstanceNotFound:
|
||||||
|
raise webob.exc.HTTPNotFound
|
||||||
|
|
||||||
|
@wsgi.serializers(xml=InterfaceTemplate)
|
||||||
|
@wsgi.action('add_interface')
|
||||||
|
def _add_interface(self, req, id, body):
|
||||||
|
context = req.environ['nova.context']
|
||||||
|
authorize(context)
|
||||||
|
self._check_node_exists(context, id)
|
||||||
|
body = body['add_interface']
|
||||||
|
address = body['address']
|
||||||
|
datapath_id = body.get('datapath_id')
|
||||||
|
port_no = body.get('port_no')
|
||||||
|
if_id = db.bm_interface_create(context,
|
||||||
|
bm_node_id=id,
|
||||||
|
address=address,
|
||||||
|
datapath_id=datapath_id,
|
||||||
|
port_no=port_no)
|
||||||
|
if_ref = db.bm_interface_get(context, if_id)
|
||||||
|
return {'interface': _interface_dict(if_ref)}
|
||||||
|
|
||||||
|
@wsgi.response(202)
|
||||||
|
@wsgi.action('remove_interface')
|
||||||
|
def _remove_interface(self, req, id, body):
|
||||||
|
context = req.environ['nova.context']
|
||||||
|
authorize(context)
|
||||||
|
self._check_node_exists(context, id)
|
||||||
|
body = body['remove_interface']
|
||||||
|
print "body(%s)" % body
|
||||||
|
if_id = body.get('id')
|
||||||
|
address = body.get('address')
|
||||||
|
if not if_id and not address:
|
||||||
|
raise webob.exc.HTTPBadRequest(
|
||||||
|
explanation=_("Must specify id or address"))
|
||||||
|
ifs = db.bm_interface_get_all_by_bm_node_id(context, id)
|
||||||
|
for i in ifs:
|
||||||
|
if if_id and if_id != i['id']:
|
||||||
|
continue
|
||||||
|
if address and address != i['address']:
|
||||||
|
continue
|
||||||
|
db.bm_interface_destroy(context, i['id'])
|
||||||
|
return webob.Response(status_int=202)
|
||||||
|
raise webob.exc.HTTPNotFound
|
||||||
|
|
||||||
|
|
||||||
|
class Baremetal_nodes(extensions.ExtensionDescriptor):
|
||||||
|
"""Admin-only bare-metal node administration."""
|
||||||
|
|
||||||
|
name = "BareMetalNodes"
|
||||||
|
alias = "os-baremetal-nodes"
|
||||||
|
namespace = "http://docs.openstack.org/compute/ext/baremetal_nodes/api/v2"
|
||||||
|
updated = "2013-01-04T00:00:00+00:00"
|
||||||
|
|
||||||
|
def get_resources(self):
|
||||||
|
resources = []
|
||||||
|
res = extensions.ResourceExtension('os-baremetal-nodes',
|
||||||
|
BareMetalNodeController(),
|
||||||
|
member_actions={"action": "POST", })
|
||||||
|
resources.append(res)
|
||||||
|
return resources
|
197
nova/tests/api/openstack/compute/contrib/test_baremetal_nodes.py
Normal file
197
nova/tests/api/openstack/compute/contrib/test_baremetal_nodes.py
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
# Copyright (c) 2013 NTT DOCOMO, INC.
|
||||||
|
# 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 webob import exc
|
||||||
|
|
||||||
|
from nova.api.openstack.compute.contrib import baremetal_nodes
|
||||||
|
from nova import context
|
||||||
|
from nova import exception
|
||||||
|
from nova import test
|
||||||
|
from nova.virt.baremetal import db
|
||||||
|
|
||||||
|
|
||||||
|
class FakeRequest(object):
|
||||||
|
|
||||||
|
def __init__(self, context):
|
||||||
|
self.environ = {"nova.context": context}
|
||||||
|
|
||||||
|
|
||||||
|
class BareMetalNodesTest(test.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(BareMetalNodesTest, self).setUp()
|
||||||
|
|
||||||
|
self.context = context.get_admin_context()
|
||||||
|
self.controller = baremetal_nodes.BareMetalNodeController()
|
||||||
|
self.request = FakeRequest(self.context)
|
||||||
|
|
||||||
|
def test_create(self):
|
||||||
|
node = {
|
||||||
|
'service_host': "host",
|
||||||
|
'cpus': 8,
|
||||||
|
'memory_mb': 8192,
|
||||||
|
'local_gb': 128,
|
||||||
|
'pm_address': "10.1.2.3",
|
||||||
|
'pm_user': "pm_user",
|
||||||
|
'pm_password': "pm_pass",
|
||||||
|
'prov_mac_address': "12:34:56:78:90:ab",
|
||||||
|
'prov_vlan_id': 1234,
|
||||||
|
'terminal_port': 8000,
|
||||||
|
'interfaces': [],
|
||||||
|
}
|
||||||
|
response = node.copy()
|
||||||
|
response['id'] = 100
|
||||||
|
del response['pm_password']
|
||||||
|
response['instance_uuid'] = None
|
||||||
|
self.mox.StubOutWithMock(db, 'bm_node_create')
|
||||||
|
db.bm_node_create(self.context, node).AndReturn(response)
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
res_dict = self.controller.create(self.request, {'node': node})
|
||||||
|
self.assertEqual({'node': response}, res_dict)
|
||||||
|
|
||||||
|
def test_delete(self):
|
||||||
|
self.mox.StubOutWithMock(db, 'bm_node_destroy')
|
||||||
|
db.bm_node_destroy(self.context, 1)
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
self.controller.delete(self.request, 1)
|
||||||
|
|
||||||
|
def test_index(self):
|
||||||
|
nodes = [{'id': 1},
|
||||||
|
{'id': 2},
|
||||||
|
]
|
||||||
|
interfaces = [{'id': 1, 'address': '11:11:11:11:11:11'},
|
||||||
|
{'id': 2, 'address': '22:22:22:22:22:22'},
|
||||||
|
]
|
||||||
|
self.mox.StubOutWithMock(db, 'bm_node_get_all')
|
||||||
|
self.mox.StubOutWithMock(db, 'bm_interface_get_all_by_bm_node_id')
|
||||||
|
db.bm_node_get_all(self.context).AndReturn(nodes)
|
||||||
|
db.bm_interface_get_all_by_bm_node_id(self.context, 1).\
|
||||||
|
AndRaise(exception.InstanceNotFound(instance_id=1))
|
||||||
|
db.bm_interface_get_all_by_bm_node_id(self.context, 2).\
|
||||||
|
AndReturn(interfaces)
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
res_dict = self.controller.index(self.request)
|
||||||
|
self.assertEqual(2, len(res_dict['nodes']))
|
||||||
|
self.assertEqual([], res_dict['nodes'][0]['interfaces'])
|
||||||
|
self.assertEqual(2, len(res_dict['nodes'][1]['interfaces']))
|
||||||
|
|
||||||
|
def test_show(self):
|
||||||
|
node_id = 1
|
||||||
|
node = {'id': node_id}
|
||||||
|
interfaces = [{'id': 1, 'address': '11:11:11:11:11:11'},
|
||||||
|
{'id': 2, 'address': '22:22:22:22:22:22'},
|
||||||
|
]
|
||||||
|
self.mox.StubOutWithMock(db, 'bm_node_get')
|
||||||
|
self.mox.StubOutWithMock(db, 'bm_interface_get_all_by_bm_node_id')
|
||||||
|
db.bm_node_get(self.context, node_id).AndReturn(node)
|
||||||
|
db.bm_interface_get_all_by_bm_node_id(self.context, node_id).\
|
||||||
|
AndReturn(interfaces)
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
res_dict = self.controller.show(self.request, node_id)
|
||||||
|
self.assertEqual(node_id, res_dict['node']['id'])
|
||||||
|
self.assertEqual(2, len(res_dict['node']['interfaces']))
|
||||||
|
|
||||||
|
def test_add_interface(self):
|
||||||
|
node_id = 1
|
||||||
|
address = '11:22:33:44:55:66'
|
||||||
|
body = {'add_interface': {'address': address}}
|
||||||
|
self.mox.StubOutWithMock(db, 'bm_node_get')
|
||||||
|
self.mox.StubOutWithMock(db, 'bm_interface_create')
|
||||||
|
self.mox.StubOutWithMock(db, 'bm_interface_get')
|
||||||
|
db.bm_node_get(self.context, node_id)
|
||||||
|
db.bm_interface_create(self.context,
|
||||||
|
bm_node_id=node_id,
|
||||||
|
address=address,
|
||||||
|
datapath_id=None,
|
||||||
|
port_no=None).\
|
||||||
|
AndReturn(12345)
|
||||||
|
db.bm_interface_get(self.context, 12345).\
|
||||||
|
AndReturn({'id': 12345, 'address': address})
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
res_dict = self.controller._add_interface(self.request, node_id, body)
|
||||||
|
self.assertEqual(12345, res_dict['interface']['id'])
|
||||||
|
self.assertEqual(address, res_dict['interface']['address'])
|
||||||
|
|
||||||
|
def test_remove_interface(self):
|
||||||
|
node_id = 1
|
||||||
|
interfaces = [{'id': 1},
|
||||||
|
{'id': 2},
|
||||||
|
{'id': 3},
|
||||||
|
]
|
||||||
|
body = {'remove_interface': {'id': 2}}
|
||||||
|
self.mox.StubOutWithMock(db, 'bm_node_get')
|
||||||
|
self.mox.StubOutWithMock(db, 'bm_interface_get_all_by_bm_node_id')
|
||||||
|
self.mox.StubOutWithMock(db, 'bm_interface_destroy')
|
||||||
|
db.bm_node_get(self.context, node_id)
|
||||||
|
db.bm_interface_get_all_by_bm_node_id(self.context, node_id).\
|
||||||
|
AndReturn(interfaces)
|
||||||
|
db.bm_interface_destroy(self.context, 2)
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
self.controller._remove_interface(self.request, node_id, body)
|
||||||
|
|
||||||
|
def test_remove_interface_by_address(self):
|
||||||
|
node_id = 1
|
||||||
|
interfaces = [{'id': 1, 'address': '11:11:11:11:11:11'},
|
||||||
|
{'id': 2, 'address': '22:22:22:22:22:22'},
|
||||||
|
{'id': 3, 'address': '33:33:33:33:33:33'},
|
||||||
|
]
|
||||||
|
self.mox.StubOutWithMock(db, 'bm_node_get')
|
||||||
|
self.mox.StubOutWithMock(db, 'bm_interface_get_all_by_bm_node_id')
|
||||||
|
self.mox.StubOutWithMock(db, 'bm_interface_destroy')
|
||||||
|
db.bm_node_get(self.context, node_id)
|
||||||
|
db.bm_interface_get_all_by_bm_node_id(self.context, node_id).\
|
||||||
|
AndReturn(interfaces)
|
||||||
|
db.bm_interface_destroy(self.context, 2)
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
body = {'remove_interface': {'address': '22:22:22:22:22:22'}}
|
||||||
|
self.controller._remove_interface(self.request, node_id, body)
|
||||||
|
|
||||||
|
def test_remove_interface_no_id_no_address(self):
|
||||||
|
node_id = 1
|
||||||
|
self.mox.StubOutWithMock(db, 'bm_node_get')
|
||||||
|
db.bm_node_get(self.context, node_id)
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
body = {'remove_interface': {}}
|
||||||
|
self.assertRaises(exc.HTTPBadRequest,
|
||||||
|
self.controller._remove_interface,
|
||||||
|
self.request,
|
||||||
|
node_id,
|
||||||
|
body)
|
||||||
|
|
||||||
|
def test_add_interface_node_not_found(self):
|
||||||
|
node_id = 1
|
||||||
|
self.mox.StubOutWithMock(db, 'bm_node_get')
|
||||||
|
db.bm_node_get(self.context, node_id).\
|
||||||
|
AndRaise(exception.InstanceNotFound(instance_id=node_id))
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
body = {'add_interface': {'address': '11:11:11:11:11:11'}}
|
||||||
|
self.assertRaises(exc.HTTPNotFound,
|
||||||
|
self.controller._add_interface,
|
||||||
|
self.request,
|
||||||
|
node_id,
|
||||||
|
body)
|
||||||
|
|
||||||
|
def test_remove_interface_node_not_found(self):
|
||||||
|
node_id = 1
|
||||||
|
self.mox.StubOutWithMock(db, 'bm_node_get')
|
||||||
|
db.bm_node_get(self.context, node_id).\
|
||||||
|
AndRaise(exception.InstanceNotFound(instance_id=node_id))
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
body = {'remove_interface': {'address': '11:11:11:11:11:11'}}
|
||||||
|
self.assertRaises(exc.HTTPNotFound,
|
||||||
|
self.controller._remove_interface,
|
||||||
|
self.request,
|
||||||
|
node_id,
|
||||||
|
body)
|
@ -105,6 +105,7 @@ policy_data = """
|
|||||||
"compute_extension:admin_actions:migrate": "",
|
"compute_extension:admin_actions:migrate": "",
|
||||||
"compute_extension:aggregates": "",
|
"compute_extension:aggregates": "",
|
||||||
"compute_extension:agents": "",
|
"compute_extension:agents": "",
|
||||||
|
"compute_extension:baremetal_nodes": "",
|
||||||
"compute_extension:cells": "",
|
"compute_extension:cells": "",
|
||||||
"compute_extension:certificates": "",
|
"compute_extension:certificates": "",
|
||||||
"compute_extension:cloudpipe": "",
|
"compute_extension:cloudpipe": "",
|
||||||
|
@ -88,6 +88,14 @@
|
|||||||
"namespace": "http://docs.openstack.org/compute/ext/availabilityzone/api/v1.1",
|
"namespace": "http://docs.openstack.org/compute/ext/availabilityzone/api/v1.1",
|
||||||
"updated": "%(timestamp)s"
|
"updated": "%(timestamp)s"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"alias": "os-baremetal-nodes",
|
||||||
|
"description": "%(text)s",
|
||||||
|
"links": [],
|
||||||
|
"name": "BareMetalNodes",
|
||||||
|
"namespace": "http://docs.openstack.org/compute/ext/baremetal_nodes/api/v2",
|
||||||
|
"updated": "%(timestamp)s"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"alias": "os-cells",
|
"alias": "os-cells",
|
||||||
"description": "%(text)s",
|
"description": "%(text)s",
|
||||||
|
@ -33,6 +33,9 @@
|
|||||||
<extension alias="os-agents" name="Agents" namespace="http://docs.openstack.org/compute/ext/agents/api/v2" updated="%(timestamp)s">
|
<extension alias="os-agents" name="Agents" namespace="http://docs.openstack.org/compute/ext/agents/api/v2" updated="%(timestamp)s">
|
||||||
<description>%(text)s</description>
|
<description>%(text)s</description>
|
||||||
</extension>
|
</extension>
|
||||||
|
<extension alias="os-baremetal-nodes" name="BareMetalNodes" namespace="http://docs.openstack.org/compute/ext/baremetal_nodes/api/v2" updated="%(timestamp)s">
|
||||||
|
<description>%(text)s</description>
|
||||||
|
</extension>
|
||||||
<extension alias="os-cells" updated="%(timestamp)s" namespace="http://docs.openstack.org/compute/ext/cells/api/v1.1" name="Cells">
|
<extension alias="os-cells" updated="%(timestamp)s" namespace="http://docs.openstack.org/compute/ext/cells/api/v1.1" name="Cells">
|
||||||
<description>%(text)s</description>
|
<description>%(text)s</description>
|
||||||
</extension>
|
</extension>
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"add_interface": {
|
||||||
|
"address": "%(address)s"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<add_interface
|
||||||
|
address="%(address)s"
|
||||||
|
/>
|
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"interface": {
|
||||||
|
"id": %(interface_id)s,
|
||||||
|
"address": "aa:aa:aa:aa:aa:aa",
|
||||||
|
"datapath_id": null,
|
||||||
|
"port_no": null
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<interface
|
||||||
|
id="%(interface_id)s"
|
||||||
|
address="aa:aa:aa:aa:aa:aa"
|
||||||
|
datapath_id="None"
|
||||||
|
port_no="None"
|
||||||
|
/>
|
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"node": {
|
||||||
|
"service_host": "host",
|
||||||
|
"cpus": 8,
|
||||||
|
"memory_mb": 8192,
|
||||||
|
"local_gb": 128,
|
||||||
|
"pm_address": "10.1.2.3",
|
||||||
|
"pm_user": "pm_user",
|
||||||
|
"pm_password": "pm_pass",
|
||||||
|
"prov_mac_address": "12:34:56:78:90:ab",
|
||||||
|
"prov_vlan_id": 1234,
|
||||||
|
"terminal_port": 8000
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<node
|
||||||
|
service_host="host"
|
||||||
|
cpus="8"
|
||||||
|
memory_mb="8192"
|
||||||
|
local_gb="128"
|
||||||
|
pm_address="10.1.2.3"
|
||||||
|
pm_user="pm_user"
|
||||||
|
prov_mac_address="12:34:56:78:90:ab"
|
||||||
|
prov_vlan_id="1234"
|
||||||
|
terminal_port="8000"
|
||||||
|
/>
|
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"node": {
|
||||||
|
"service_host": "host",
|
||||||
|
"cpus": 8,
|
||||||
|
"memory_mb": 8192,
|
||||||
|
"local_gb": 128,
|
||||||
|
"pm_address": "10.1.2.3",
|
||||||
|
"pm_user": "pm_user",
|
||||||
|
"prov_mac_address": "12:34:56:78:90:ab",
|
||||||
|
"prov_vlan_id": 1234,
|
||||||
|
"terminal_port": 8000,
|
||||||
|
"instance_uuid": null,
|
||||||
|
"id": %(node_id)s,
|
||||||
|
"interfaces": []
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<node
|
||||||
|
service_host="host"
|
||||||
|
cpus="8"
|
||||||
|
memory_mb="8192"
|
||||||
|
local_gb="128"
|
||||||
|
pm_address="10.1.2.3"
|
||||||
|
pm_user="pm_user"
|
||||||
|
prov_mac_address="12:34:56:78:90:ab"
|
||||||
|
prov_vlan_id="1234"
|
||||||
|
terminal_port="8000"
|
||||||
|
instance_uuid="None"
|
||||||
|
id="%(node_id)s">
|
||||||
|
<interfaces/>
|
||||||
|
</node>
|
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"nodes": [{
|
||||||
|
"service_host": "host",
|
||||||
|
"cpus": 8,
|
||||||
|
"memory_mb": 8192,
|
||||||
|
"local_gb": 128,
|
||||||
|
"pm_address": "10.1.2.3",
|
||||||
|
"pm_user": "pm_user",
|
||||||
|
"prov_mac_address": "12:34:56:78:90:ab",
|
||||||
|
"prov_vlan_id": 1234,
|
||||||
|
"terminal_port": 8000,
|
||||||
|
"instance_uuid": null,
|
||||||
|
"id": %(node_id)s,
|
||||||
|
"interfaces": [{
|
||||||
|
"id": %(interface_id)s,
|
||||||
|
"address": "%(address)s",
|
||||||
|
"datapath_id": null,
|
||||||
|
"port_no": null
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<nodes>
|
||||||
|
<node
|
||||||
|
service_host="host"
|
||||||
|
cpus="8"
|
||||||
|
memory_mb="8192"
|
||||||
|
local_gb="128"
|
||||||
|
pm_address="10.1.2.3"
|
||||||
|
pm_user="pm_user"
|
||||||
|
prov_mac_address="12:34:56:78:90:ab"
|
||||||
|
prov_vlan_id="1234"
|
||||||
|
terminal_port="8000"
|
||||||
|
instance_uuid="None"
|
||||||
|
id="%(node_id)s">
|
||||||
|
<interfaces>
|
||||||
|
<interface id="%(interface_id)s" address="%(address)s" datapath_id="None" port_no="None"/>
|
||||||
|
</interfaces>
|
||||||
|
</node>
|
||||||
|
</nodes>
|
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"remove_interface": {
|
||||||
|
"address": "%(address)s"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<remove_interface
|
||||||
|
address="%(address)s"
|
||||||
|
/>
|
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"node": {
|
||||||
|
"service_host": "host",
|
||||||
|
"cpus": 8,
|
||||||
|
"memory_mb": 8192,
|
||||||
|
"local_gb": 128,
|
||||||
|
"pm_address": "10.1.2.3",
|
||||||
|
"pm_user": "pm_user",
|
||||||
|
"prov_mac_address": "12:34:56:78:90:ab",
|
||||||
|
"prov_vlan_id": 1234,
|
||||||
|
"terminal_port": 8000,
|
||||||
|
"instance_uuid": null,
|
||||||
|
"id": %(node_id)s,
|
||||||
|
"interfaces": [{
|
||||||
|
"id": %(interface_id)s,
|
||||||
|
"address": "%(address)s",
|
||||||
|
"datapath_id": null,
|
||||||
|
"port_no": null
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<node
|
||||||
|
service_host="host"
|
||||||
|
cpus="8"
|
||||||
|
memory_mb="8192"
|
||||||
|
local_gb="128"
|
||||||
|
pm_address="10.1.2.3"
|
||||||
|
pm_user="pm_user"
|
||||||
|
prov_mac_address="12:34:56:78:90:ab"
|
||||||
|
prov_vlan_id="1234"
|
||||||
|
terminal_port="8000"
|
||||||
|
instance_uuid="None"
|
||||||
|
id="%(node_id)s">
|
||||||
|
<interfaces>
|
||||||
|
<interface id="%(interface_id)s" address="%(address)s" datapath_id="None" port_no="None"/>
|
||||||
|
</interfaces>
|
||||||
|
</node>
|
@ -43,6 +43,7 @@ from nova.openstack.common import timeutils
|
|||||||
import nova.quota
|
import nova.quota
|
||||||
from nova.scheduler import driver
|
from nova.scheduler import driver
|
||||||
from nova import test
|
from nova import test
|
||||||
|
from nova.tests.baremetal.db import base as bm_db_base
|
||||||
from nova.tests import fake_network
|
from nova.tests import fake_network
|
||||||
from nova.tests.image import fake
|
from nova.tests.image import fake
|
||||||
from nova.tests.integrated import integrated_helpers
|
from nova.tests.integrated import integrated_helpers
|
||||||
@ -2589,3 +2590,75 @@ class CellsSampleJsonTest(ApiSampleTestBase):
|
|||||||
|
|
||||||
class CellsSampleXmlTest(CellsSampleJsonTest):
|
class CellsSampleXmlTest(CellsSampleJsonTest):
|
||||||
ctype = 'xml'
|
ctype = 'xml'
|
||||||
|
|
||||||
|
|
||||||
|
class BareMetalNodesJsonTest(ApiSampleTestBase, bm_db_base.BMDBTestCase):
|
||||||
|
extension_name = ('nova.api.openstack.compute.contrib.baremetal_nodes.'
|
||||||
|
'Baremetal_nodes')
|
||||||
|
|
||||||
|
def _create_node(self):
|
||||||
|
response = self._do_post("os-baremetal-nodes",
|
||||||
|
"baremetal-node-create-req",
|
||||||
|
{})
|
||||||
|
self.assertEqual(response.status, 200)
|
||||||
|
subs = {'node_id': '(?P<id>\d+)'}
|
||||||
|
return self._verify_response("baremetal-node-create-resp",
|
||||||
|
subs, response)
|
||||||
|
|
||||||
|
def test_create_node(self):
|
||||||
|
self._create_node()
|
||||||
|
|
||||||
|
def test_list_nodes(self):
|
||||||
|
node_id = self._create_node()
|
||||||
|
interface_id = self._add_interface(node_id)
|
||||||
|
response = self._do_get('os-baremetal-nodes')
|
||||||
|
self.assertEqual(response.status, 200)
|
||||||
|
subs = {'node_id': node_id,
|
||||||
|
'interface_id': interface_id,
|
||||||
|
'address': 'aa:aa:aa:aa:aa:aa',
|
||||||
|
}
|
||||||
|
return self._verify_response('baremetal-node-list-resp',
|
||||||
|
subs, response)
|
||||||
|
|
||||||
|
def test_show_node(self):
|
||||||
|
node_id = self._create_node()
|
||||||
|
interface_id = self._add_interface(node_id)
|
||||||
|
response = self._do_get('os-baremetal-nodes/%s' % node_id)
|
||||||
|
self.assertEqual(response.status, 200)
|
||||||
|
subs = {'node_id': node_id,
|
||||||
|
'interface_id': interface_id,
|
||||||
|
'address': 'aa:aa:aa:aa:aa:aa',
|
||||||
|
}
|
||||||
|
return self._verify_response('baremetal-node-show-resp',
|
||||||
|
subs, response)
|
||||||
|
|
||||||
|
def test_delete_node(self):
|
||||||
|
node_id = self._create_node()
|
||||||
|
response = self._do_delete("os-baremetal-nodes/%s" % node_id)
|
||||||
|
self.assertEqual(response.status, 202)
|
||||||
|
|
||||||
|
def _add_interface(self, node_id):
|
||||||
|
response = self._do_post("os-baremetal-nodes/%s/action" % node_id,
|
||||||
|
"baremetal-node-add-interface-req",
|
||||||
|
{'address': 'aa:aa:aa:aa:aa:aa'})
|
||||||
|
self.assertEqual(response.status, 200)
|
||||||
|
subs = {'interface_id': r'(?P<id>\d+)'}
|
||||||
|
return self._verify_response("baremetal-node-add-interface-resp",
|
||||||
|
subs, response)
|
||||||
|
|
||||||
|
def test_add_interface(self):
|
||||||
|
node_id = self._create_node()
|
||||||
|
self._add_interface(node_id)
|
||||||
|
|
||||||
|
def test_remove_interface(self):
|
||||||
|
node_id = self._create_node()
|
||||||
|
self._add_interface(node_id)
|
||||||
|
response = self._do_post("os-baremetal-nodes/%s/action" % node_id,
|
||||||
|
"baremetal-node-remove-interface-req",
|
||||||
|
{'address': 'aa:aa:aa:aa:aa:aa'})
|
||||||
|
self.assertEqual(response.status, 202)
|
||||||
|
self.assertEqual(response.read(), "")
|
||||||
|
|
||||||
|
|
||||||
|
class BareMetalNodesXmlTest(BareMetalNodesJsonTest):
|
||||||
|
ctype = 'xml'
|
||||||
|
Loading…
Reference in New Issue
Block a user