67fac2f0aa
In Python 2.7, functools.wraps() does not provide the '__wrapped__' attribute. This attribute is used by oslo_utils.reflection.get_signature() when getting the signature of a function. If a function is decorated without the '__wrapped__' attribute then the signature will be of the decorator rather than the underlying function. From the six documentation for six.wraps(): This is exactly the functools.wraps() decorator, but it sets the __wrapped__ attribute on what it decorates as functools.wraps() does on Python versions after 3.2. Change-Id: I6be901f8e01e0ff1b1e2b8c5197b320b5f8026fb
343 lines
13 KiB
Python
343 lines
13 KiB
Python
# 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.
|
|
|
|
import json
|
|
|
|
import jsonschema
|
|
import six
|
|
import yaml
|
|
|
|
from ironicclient import exc
|
|
|
|
_CREATE_SCHEMA = {
|
|
"$schema": "http://json-schema.org/draft-04/schema#",
|
|
"description": "Schema for ironic resources file",
|
|
"type": "object",
|
|
"properties": {
|
|
"chassis": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object"
|
|
}
|
|
},
|
|
"nodes": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object"
|
|
}
|
|
}
|
|
},
|
|
"additionalProperties": False
|
|
}
|
|
|
|
|
|
def create_resources(client, filenames):
|
|
"""Create resources using their JSON or YAML descriptions.
|
|
|
|
:param client: an instance of ironic client;
|
|
:param filenames: a list of filenames containing JSON or YAML resources
|
|
definitions.
|
|
:raises: ClientException if any operation during files processing/resource
|
|
creation fails.
|
|
"""
|
|
errors = []
|
|
resources = []
|
|
for resource_file in filenames:
|
|
try:
|
|
resource = load_from_file(resource_file)
|
|
jsonschema.validate(resource, _CREATE_SCHEMA)
|
|
resources.append(resource)
|
|
except (exc.ClientException, jsonschema.ValidationError) as e:
|
|
errors.append(e)
|
|
if errors:
|
|
raise exc.ClientException('While validating the resources file(s), the'
|
|
' following error(s) were encountered:\n%s' %
|
|
'\n'.join(six.text_type(e) for e in errors))
|
|
for r in resources:
|
|
errors.extend(create_chassis(client, r.get('chassis', [])))
|
|
errors.extend(create_nodes(client, r.get('nodes', [])))
|
|
if errors:
|
|
raise exc.ClientException('During resources creation, the following '
|
|
'error(s) were encountered:\n%s' %
|
|
'\n'.join(six.text_type(e) for e in errors))
|
|
|
|
|
|
def load_from_file(filename):
|
|
"""Deserialize JSON or YAML from file.
|
|
|
|
:param filename: name of the file containing JSON or YAML.
|
|
:returns: a dictionary deserialized from JSON or YAML.
|
|
:raises: ClientException if the file can not be loaded or if its contents
|
|
is not a valid JSON or YAML, or if the file extension is not supported.
|
|
"""
|
|
try:
|
|
with open(filename) as f:
|
|
if filename.endswith('.yaml'):
|
|
return yaml.safe_load(f)
|
|
elif filename.endswith('.json'):
|
|
return json.load(f)
|
|
else:
|
|
# The file is neither .json, nor .yaml, raise an exception
|
|
raise exc.ClientException(
|
|
'Cannot process file "%(file)s" - it must have .json or '
|
|
'.yaml extension.' % {'file': filename})
|
|
except IOError as e:
|
|
raise exc.ClientException('Cannot read file "%(file)s" due to '
|
|
'error: %(err)s' %
|
|
{'err': e, 'file': filename})
|
|
except (ValueError, yaml.YAMLError) as e:
|
|
# json.load raises only ValueError
|
|
raise exc.ClientException('File "%(file)s" is invalid due to error: '
|
|
'%(err)s' % {'err': e, 'file': filename})
|
|
|
|
|
|
def create_single_handler(resource_type):
|
|
"""Catch errors of the creation of a single resource.
|
|
|
|
This decorator appends an error (which is an instance of some client
|
|
exception class) to the return value of the create_method, changing the
|
|
return value from just UUID to (UUID, error), and does some exception
|
|
handling.
|
|
|
|
:param resource_type: string value, the type of the resource being created,
|
|
e.g. 'node', used purely for exception messages.
|
|
"""
|
|
|
|
def outer_wrapper(create_method):
|
|
@six.wraps(create_method)
|
|
def wrapper(client, **params):
|
|
uuid = None
|
|
error = None
|
|
try:
|
|
uuid = create_method(client, **params)
|
|
except exc.InvalidAttribute as e:
|
|
error = exc.InvalidAttribute(
|
|
'Cannot create the %(resource)s with attributes '
|
|
'%(params)s. One or more attributes are invalid: %(err)s' %
|
|
{'params': params, 'resource': resource_type, 'err': e}
|
|
)
|
|
except Exception as e:
|
|
error = exc.ClientException(
|
|
'Unable to create the %(resource)s with the specified '
|
|
'attributes: %(params)s. The error is: %(error)s' %
|
|
{'error': e, 'resource': resource_type, 'params': params})
|
|
return uuid, error
|
|
return wrapper
|
|
return outer_wrapper
|
|
|
|
|
|
@create_single_handler('node')
|
|
def create_single_node(client, **params):
|
|
"""Call the client to create a node.
|
|
|
|
:param client: ironic client instance.
|
|
:param params: dictionary to be POSTed to /nodes endpoint, excluding
|
|
"ports" and "portgroups" keys.
|
|
:returns: UUID of the created node or None in case of exception, and an
|
|
exception, if it appears.
|
|
:raises: InvalidAttribute, if some parameters passed to client's
|
|
create_method are invalid.
|
|
:raises: ClientException, if the creation of the node fails.
|
|
"""
|
|
params.pop('ports', None)
|
|
params.pop('portgroups', None)
|
|
ret = client.node.create(**params)
|
|
return ret.uuid
|
|
|
|
|
|
@create_single_handler('port')
|
|
def create_single_port(client, **params):
|
|
"""Call the client to create a port.
|
|
|
|
:param client: ironic client instance.
|
|
:param params: dictionary to be POSTed to /ports endpoint.
|
|
:returns: UUID of the created port or None in case of exception, and an
|
|
exception, if it appears.
|
|
:raises: InvalidAttribute, if some parameters passed to client's
|
|
create_method are invalid.
|
|
:raises: ClientException, if the creation of the port fails.
|
|
"""
|
|
ret = client.port.create(**params)
|
|
return ret.uuid
|
|
|
|
|
|
@create_single_handler('port group')
|
|
def create_single_portgroup(client, **params):
|
|
"""Call the client to create a port group.
|
|
|
|
:param client: ironic client instance.
|
|
:param params: dictionary to be POSTed to /portgroups endpoint, excluding
|
|
"ports" key.
|
|
:returns: UUID of the created port group or None in case of exception, and
|
|
an exception, if it appears.
|
|
:raises: InvalidAttribute, if some parameters passed to client's
|
|
create_method are invalid.
|
|
:raises: ClientException, if the creation of the portgroup fails.
|
|
"""
|
|
params.pop('ports', None)
|
|
ret = client.portgroup.create(**params)
|
|
return ret.uuid
|
|
|
|
|
|
@create_single_handler('chassis')
|
|
def create_single_chassis(client, **params):
|
|
"""Call the client to create a chassis.
|
|
|
|
:param client: ironic client instance.
|
|
:param params: dictionary to be POSTed to /chassis endpoint, excluding
|
|
"nodes" key.
|
|
:returns: UUID of the created chassis or None in case of exception, and an
|
|
exception, if it appears.
|
|
:raises: InvalidAttribute, if some parameters passed to client's
|
|
create_method are invalid.
|
|
:raises: ClientException, if the creation of the chassis fails.
|
|
"""
|
|
params.pop('nodes', None)
|
|
ret = client.chassis.create(**params)
|
|
return ret.uuid
|
|
|
|
|
|
def create_ports(client, port_list, node_uuid, portgroup_uuid=None):
|
|
"""Create ports from dictionaries.
|
|
|
|
:param client: ironic client instance.
|
|
:param port_list: list of dictionaries to be POSTed to /ports
|
|
endpoint.
|
|
:param node_uuid: UUID of a node the ports should be associated with.
|
|
:param portgroup_uuid: UUID of a port group the ports should be associated
|
|
with, if they are its members.
|
|
:returns: array of exceptions encountered during creation.
|
|
"""
|
|
errors = []
|
|
for port in port_list:
|
|
port_node_uuid = port.get('node_uuid')
|
|
if port_node_uuid and port_node_uuid != node_uuid:
|
|
errors.append(exc.ClientException(
|
|
'Cannot create a port as part of node %(node_uuid)s '
|
|
'because the port %(port)s has a different node UUID '
|
|
'specified.',
|
|
{'node_uuid': node_uuid,
|
|
'port': port}))
|
|
continue
|
|
port['node_uuid'] = node_uuid
|
|
if portgroup_uuid:
|
|
port_portgroup_uuid = port.get('portgroup_uuid')
|
|
if port_portgroup_uuid and port_portgroup_uuid != portgroup_uuid:
|
|
errors.append(exc.ClientException(
|
|
'Cannot create a port as part of port group '
|
|
'%(portgroup_uuid)s because the port %(port)s has a '
|
|
'different port group UUID specified.',
|
|
{'portgroup_uuid': portgroup_uuid,
|
|
'port': port}))
|
|
continue
|
|
port['portgroup_uuid'] = portgroup_uuid
|
|
port_uuid, error = create_single_port(client, **port)
|
|
if error:
|
|
errors.append(error)
|
|
return errors
|
|
|
|
|
|
def create_portgroups(client, portgroup_list, node_uuid):
|
|
"""Create port groups from dictionaries.
|
|
|
|
:param client: ironic client instance.
|
|
:param portgroup_list: list of dictionaries to be POSTed to /portgroups
|
|
endpoint, if some of them contain "ports" key, its content is POSTed
|
|
separately to /ports endpoint.
|
|
:param node_uuid: UUID of a node the port groups should be associated with.
|
|
:returns: array of exceptions encountered during creation.
|
|
"""
|
|
errors = []
|
|
for portgroup in portgroup_list:
|
|
portgroup_node_uuid = portgroup.get('node_uuid')
|
|
if portgroup_node_uuid and portgroup_node_uuid != node_uuid:
|
|
errors.append(exc.ClientException(
|
|
'Cannot create a port group as part of node %(node_uuid)s '
|
|
'because the port group %(portgroup)s has a different node '
|
|
'UUID specified.',
|
|
{'node_uuid': node_uuid,
|
|
'portgroup': portgroup}))
|
|
continue
|
|
portgroup['node_uuid'] = node_uuid
|
|
portgroup_uuid, error = create_single_portgroup(client, **portgroup)
|
|
if error:
|
|
errors.append(error)
|
|
ports = portgroup.get('ports')
|
|
# Port group UUID == None means that port group creation failed, don't
|
|
# create the ports inside it
|
|
if ports is not None and portgroup_uuid is not None:
|
|
errors.extend(create_ports(client, ports, node_uuid,
|
|
portgroup_uuid=portgroup_uuid))
|
|
return errors
|
|
|
|
|
|
def create_nodes(client, node_list, chassis_uuid=None):
|
|
"""Create nodes from dictionaries.
|
|
|
|
:param client: ironic client instance.
|
|
:param node_list: list of dictionaries to be POSTed to /nodes
|
|
endpoint, if some of them contain "ports" key, its content is POSTed
|
|
separately to /ports endpoint.
|
|
:param chassis_uuid: UUID of a chassis the nodes should be associated with.
|
|
:returns: array of exceptions encountered during creation.
|
|
"""
|
|
errors = []
|
|
for node in node_list:
|
|
if chassis_uuid is not None:
|
|
node_chassis_uuid = node.get('chassis_uuid')
|
|
if node_chassis_uuid and node_chassis_uuid != chassis_uuid:
|
|
errors.append(exc.ClientException(
|
|
'Cannot create a node as part of chassis %(chassis_uuid)s '
|
|
'because the node %(node)s has a different chassis UUID '
|
|
'specified.' %
|
|
{'chassis_uuid': chassis_uuid,
|
|
'node': node}))
|
|
continue
|
|
node['chassis_uuid'] = chassis_uuid
|
|
node_uuid, error = create_single_node(client, **node)
|
|
if error:
|
|
errors.append(error)
|
|
ports = node.get('ports')
|
|
portgroups = node.get('portgroups')
|
|
# Node UUID == None means that node creation failed, don't
|
|
# create the port(group)s inside it
|
|
if node_uuid is not None:
|
|
if portgroups is not None:
|
|
errors.extend(
|
|
create_portgroups(client, portgroups, node_uuid))
|
|
if ports is not None:
|
|
errors.extend(create_ports(client, ports, node_uuid))
|
|
return errors
|
|
|
|
|
|
def create_chassis(client, chassis_list):
|
|
"""Create chassis from dictionaries.
|
|
|
|
:param client: ironic client instance.
|
|
:param chassis_list: list of dictionaries to be POSTed to /chassis
|
|
endpoint, if some of them contain "nodes" key, its content is POSTed
|
|
separately to /nodes endpoint.
|
|
:returns: array of exceptions encountered during creation.
|
|
"""
|
|
errors = []
|
|
for chassis in chassis_list:
|
|
chassis_uuid, error = create_single_chassis(client, **chassis)
|
|
if error:
|
|
errors.append(error)
|
|
nodes = chassis.get('nodes')
|
|
# Chassis UUID == None means that chassis creation failed, don't
|
|
# create the nodes inside it
|
|
if nodes is not None and chassis_uuid is not None:
|
|
errors.extend(create_nodes(client, nodes,
|
|
chassis_uuid=chassis_uuid))
|
|
return errors
|