summaryrefslogtreecommitdiff
path: root/ironic
diff options
context:
space:
mode:
authorZuul <zuul@review.opendev.org>2020-11-18 16:19:04 +0000
committerGerrit Code Review <review@openstack.org>2020-11-18 16:19:04 +0000
commitb95478937dcf9ceba6136289ca0381a3c39d8b3f (patch)
tree744ce718af5d4739bbc7ac359e4796c9a46625f8 /ironic
parent0c610c03e865352ae984306f20181835cfb02c1d (diff)
parentfc4793960b88f1d73ecb299fd511b867e77968f7 (diff)
downloadironic-b95478937dcf9ceba6136289ca0381a3c39d8b3f.tar.gz
Merge "Convert nodes endpoint to plain JSON"
Diffstat (limited to 'ironic')
-rw-r--r--ironic/api/controllers/v1/node.py1422
-rw-r--r--ironic/api/controllers/v1/ramdisk.py13
-rw-r--r--ironic/api/controllers/v1/types.py30
-rw-r--r--ironic/api/controllers/v1/utils.py21
-rw-r--r--ironic/tests/unit/api/controllers/v1/test_node.py73
-rw-r--r--ironic/tests/unit/api/controllers/v1/test_types.py25
-rw-r--r--ironic/tests/unit/api/controllers/v1/test_utils.py52
-rw-r--r--ironic/tests/unit/api/utils.py11
8 files changed, 639 insertions, 1008 deletions
diff --git a/ironic/api/controllers/v1/node.py b/ironic/api/controllers/v1/node.py
index 46c02521d..96a5642e0 100644
--- a/ironic/api/controllers/v1/node.py
+++ b/ironic/api/controllers/v1/node.py
@@ -13,6 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.
+import copy
import datetime
from http import client as http_client
import json
@@ -28,7 +29,6 @@ import pecan
from pecan import rest
from ironic import api
-from ironic.api.controllers import base
from ironic.api.controllers import link
from ironic.api.controllers.v1 import allocation
from ironic.api.controllers.v1 import bios
@@ -36,12 +36,11 @@ from ironic.api.controllers.v1 import collection
from ironic.api.controllers.v1 import notification_utils as notify
from ironic.api.controllers.v1 import port
from ironic.api.controllers.v1 import portgroup
-from ironic.api.controllers.v1 import types
from ironic.api.controllers.v1 import utils as api_utils
from ironic.api.controllers.v1 import versions
from ironic.api.controllers.v1 import volume
-from ironic.api import expose
-from ironic.api import types as atypes
+from ironic.api import method
+from ironic.common import args
from ironic.common import exception
from ironic.common.i18n import _
from ironic.common import policy
@@ -97,8 +96,8 @@ METRICS = metrics_utils.get_metrics_logger(__name__)
# versions, the API service should be restarted.
_VENDOR_METHODS = {}
-_DEFAULT_RETURN_FIELDS = ('instance_uuid', 'maintenance', 'power_state',
- 'provision_state', 'uuid', 'name')
+_DEFAULT_RETURN_FIELDS = ['instance_uuid', 'maintenance', 'power_state',
+ 'provision_state', 'uuid', 'name']
# States where calling do_provisioning_action makes sense
PROVISION_ACTION_STATES = (ir_states.VERBS['manage'],
@@ -116,9 +115,141 @@ ALLOWED_TARGET_POWER_STATES = (ir_states.POWER_ON,
_NODE_DESCRIPTION_MAX_LENGTH = 4096
+with open(os.path.join(os.path.dirname(__file__),
+ 'network-data-schema.json'), 'rb') as fl:
+ NETWORK_DATA_SCHEMA = json.load(fl)
+
+NODE_SCHEMA = {
+ 'type': 'object',
+ 'properties': {
+ 'automated_clean': {'type': ['string', 'boolean', 'null']},
+ 'bios_interface': {'type': ['string', 'null']},
+ 'boot_interface': {'type': ['string', 'null']},
+ 'chassis_uuid': {'type': ['string', 'null']},
+ 'conductor_group': {'type': ['string', 'null']},
+ 'console_enabled': {'type': ['string', 'boolean', 'null']},
+ 'console_interface': {'type': ['string', 'null']},
+ 'deploy_interface': {'type': ['string', 'null']},
+ 'description': {'type': ['string', 'null'],
+ 'maxLength': _NODE_DESCRIPTION_MAX_LENGTH},
+ 'driver': {'type': 'string'},
+ 'driver_info': {'type': ['object', 'null']},
+ 'extra': {'type': ['object', 'null']},
+ 'inspect_interface': {'type': ['string', 'null']},
+ 'instance_info': {'type': ['object', 'null']},
+ 'instance_uuid': {'type': ['string', 'null']},
+ 'lessee': {'type': ['string', 'null']},
+ 'management_interface': {'type': ['string', 'null']},
+ 'maintenance': {'type': ['string', 'boolean', 'null']},
+ 'name': {'type': ['string', 'null']},
+ 'network_data': {'anyOf': [
+ {'type': 'null'},
+ {'type': 'object', 'additionalProperties': False},
+ NETWORK_DATA_SCHEMA
+ ]},
+ 'network_interface': {'type': ['string', 'null']},
+ 'owner': {'type': ['string', 'null']},
+ 'power_interface': {'type': ['string', 'null']},
+ 'properties': {'type': ['object', 'null']},
+ 'raid_interface': {'type': ['string', 'null']},
+ 'rescue_interface': {'type': ['string', 'null']},
+ 'resource_class': {'type': ['string', 'null'], 'maxLength': 80},
+ 'retired': {'type': ['string', 'boolean', 'null']},
+ 'retired_reason': {'type': ['string', 'null']},
+ 'storage_interface': {'type': ['string', 'null']},
+ 'uuid': {'type': ['string', 'null']},
+ 'vendor_interface': {'type': ['string', 'null']},
+ },
+ 'required': ['driver'],
+ 'additionalProperties': False,
+ 'definitions': NETWORK_DATA_SCHEMA.get('definitions')
+}
+
+NODE_PATCH_SCHEMA = copy.deepcopy(NODE_SCHEMA)
+# add schema for patchable fields
+NODE_PATCH_SCHEMA['properties']['protected'] = {
+ 'type': ['string', 'boolean', 'null']}
+NODE_PATCH_SCHEMA['properties']['protected_reason'] = {
+ 'type': ['string', 'null']}
+
+NODE_VALIDATE_EXTRA = args.dict_valid(
+ automated_clean=args.boolean,
+ chassis_uuid=args.uuid,
+ console_enabled=args.boolean,
+ instance_uuid=args.uuid,
+ protected=args.boolean,
+ maintenance=args.boolean,
+ retired=args.boolean,
+ uuid=args.uuid,
+)
+
+NODE_VALIDATOR = args.and_valid(
+ args.schema(NODE_SCHEMA),
+ NODE_VALIDATE_EXTRA
+)
+
+NODE_PATCH_VALIDATOR = args.and_valid(
+ args.schema(NODE_PATCH_SCHEMA),
+ NODE_VALIDATE_EXTRA
+)
+
+PATCH_ALLOWED_FIELDS = [
+ 'automated_clean',
+ 'bios_interface',
+ 'boot_interface',
+ 'chassis_uuid',
+ 'conductor_group',
+ 'console_interface',
+ 'deploy_interface',
+ 'description',
+ 'driver',
+ 'driver_info',
+ 'extra',
+ 'inspect_interface',
+ 'instance_info',
+ 'instance_uuid',
+ 'lessee',
+ 'maintenance',
+ 'management_interface',
+ 'name',
+ 'network_data',
+ 'network_interface',
+ 'owner',
+ 'power_interface',
+ 'properties',
+ 'protected',
+ 'protected_reason',
+ 'raid_interface',
+ 'rescue_interface',
+ 'resource_class',
+ 'retired',
+ 'retired_reason',
+ 'storage_interface',
+ 'vendor_interface'
+]
+
+TRAITS_SCHEMA = {
+ 'type': 'object',
+ 'properties': {
+ 'traits': {
+ 'type': 'array',
+ 'items': api_utils.TRAITS_SCHEMA
+ },
+ },
+ 'additionalProperties': False,
+}
-NETWORK_DATA_SCHEMA = os.path.join(
- os.path.dirname(__file__), 'network-data-schema.json')
+VIF_VALIDATOR = args.and_valid(
+ args.schema({
+ 'type': 'object',
+ 'properties': {
+ 'id': {'type': 'string'},
+ },
+ 'required': ['id'],
+ 'additionalProperties': True,
+ }),
+ args.dict_valid(id=args.uuid_or_name)
+)
def get_nodes_controller_reserved_names():
@@ -137,7 +268,7 @@ def hide_fields_in_newer_versions(obj):
matches or exceeds the versions when these fields were introduced.
"""
for field in api_utils.disallowed_fields():
- setattr(obj, field, atypes.Unset)
+ obj.pop(field, None)
def reject_fields_in_newer_versions(obj):
@@ -147,14 +278,14 @@ def reject_fields_in_newer_versions(obj):
# NOTE(jroll) this is special-cased to "" and not Unset,
# because it is used in hash ring calculations
empty_value = ''
- elif field == 'name' and obj.name is None:
+ elif field == 'name' and obj.get('name') is None:
# NOTE(dtantsur): for some reason we allow specifying name=None
# explicitly even in old API versions..
continue
else:
- empty_value = atypes.Unset
+ empty_value = None
- if getattr(obj, field, empty_value) != empty_value:
+ if obj.get(field, empty_value) != empty_value:
LOG.debug('Field %(field)s is not acceptable in version %(ver)s',
{'field': field, 'ver': api.request.version})
raise exception.NotAcceptable()
@@ -172,17 +303,17 @@ def reject_patch_in_newer_versions(patch):
def update_state_in_older_versions(obj):
"""Change provision state names for API backwards compatibility.
- :param obj: The object being returned to the API client that is
+ :param obj: The dict being returned to the API client that is
to be updated by this method.
"""
# if requested version is < 1.2, convert AVAILABLE to the old NOSTATE
if (api.request.version.minor < versions.MINOR_2_AVAILABLE_STATE
- and obj.provision_state == ir_states.AVAILABLE):
- obj.provision_state = ir_states.NOSTATE
+ and obj.get('provision_state') == ir_states.AVAILABLE):
+ obj['provision_state'] = ir_states.NOSTATE
# if requested version < 1.39, convert INSPECTWAIT to INSPECTING
if (not api_utils.allow_inspect_wait_state()
- and obj.provision_state == ir_states.INSPECTWAIT):
- obj.provision_state = ir_states.INSPECTING
+ and obj.get('provision_state') == ir_states.INSPECTWAIT):
+ obj['provision_state'] = ir_states.INSPECTING
def validate_network_data(network_data):
@@ -194,11 +325,8 @@ def validate_network_data(network_data):
:param network_data: a network_data field to validate
:raises: Invalid if network data is not schema-compliant
"""
- with open(NETWORK_DATA_SCHEMA, 'rb') as fl:
- network_data_schema = json.load(fl)
-
try:
- jsonschema.validate(network_data, network_data_schema)
+ jsonschema.validate(network_data, NETWORK_DATA_SCHEMA)
except json_schema_exc.ValidationError as e:
# NOTE: Even though e.message is deprecated in general, it is
@@ -233,8 +361,9 @@ class BootDeviceController(rest.RestController):
rpc_node.uuid, topic)
@METRICS.timer('BootDeviceController.put')
- @expose.expose(None, types.uuid_or_name, str, types.boolean,
- status_code=http_client.NO_CONTENT)
+ @method.expose(status_code=http_client.NO_CONTENT)
+ @args.validate(node_ident=args.uuid_or_name, boot_device=args.string,
+ persistent=args.boolean)
def put(self, node_ident, boot_device, persistent=False):
"""Set the boot device for a node.
@@ -259,7 +388,8 @@ class BootDeviceController(rest.RestController):
topic=topic)
@METRICS.timer('BootDeviceController.get')
- @expose.expose(str, types.uuid_or_name)
+ @method.expose()
+ @args.validate(node_ident=args.uuid_or_name)
def get(self, node_ident):
"""Get the current boot device for a node.
@@ -278,7 +408,8 @@ class BootDeviceController(rest.RestController):
return self._get_boot_device(rpc_node)
@METRICS.timer('BootDeviceController.supported')
- @expose.expose(str, types.uuid_or_name)
+ @method.expose()
+ @args.validate(node_ident=args.uuid_or_name)
def supported(self, node_ident):
"""Get a list of the supported boot devices.
@@ -323,87 +454,48 @@ class IndicatorAtComponent(object):
_('Missing indicator name "%s"'))
-class IndicatorState(base.APIBase):
- """API representation of indicator state."""
-
- state = atypes.wsattr(str)
-
- def __init__(self, **kwargs):
- self.state = kwargs.get('state')
-
-
-class Indicator(base.APIBase):
- """API representation of an indicator."""
-
- name = atypes.wsattr(str)
-
- component = atypes.wsattr(str)
-
- readonly = types.BooleanType()
-
- states = atypes.ArrayType(str)
-
- links = None
-
- def __init__(self, **kwargs):
- self.name = kwargs.get('name')
- self.component = kwargs.get('component')
- self.readonly = kwargs.get('readonly', True)
- self.states = kwargs.get('states', [])
-
- @staticmethod
- def _convert_with_links(node_uuid, indicator, url):
- """Add links to the indicator."""
- indicator.links = [
+def indicator_convert_with_links(node_uuid, rpc_component, rpc_name,
+ **rpc_fields):
+ """Add links to the indicator."""
+ url = api.request.public_url
+ return {
+ 'name': rpc_name,
+ 'component': rpc_component,
+ 'readonly': rpc_fields.get('readonly', True),
+ 'states': rpc_fields.get('states', []),
+ 'links': [
link.make_link(
'self', url, 'nodes',
'%s/management/indicators/%s' % (
- node_uuid, indicator.name)),
+ node_uuid, rpc_name)),
link.make_link(
'bookmark', url, 'nodes',
'%s/management/indicators/%s' % (
- node_uuid, indicator.name),
- bookmark=True)]
- return indicator
-
- @classmethod
- def convert_with_links(cls, node_uuid, rpc_component, rpc_name,
- **rpc_fields):
- """Add links to the indicator."""
- indicator = Indicator(
- component=rpc_component, name=rpc_name, **rpc_fields)
- return cls._convert_with_links(
- node_uuid, indicator, pecan.request.host_url)
-
-
-class IndicatorsCollection(atypes.Base):
- """API representation of the indicators for a node."""
-
- indicators = [Indicator]
- """Node indicators list"""
-
- @staticmethod
- def collection_from_dict(node_ident, indicators):
- col = IndicatorsCollection()
-
- indicator_list = []
- for component, names in indicators.items():
- for name, fields in names.items():
- indicator_at_component = IndicatorAtComponent(
- component=component, name=name)
- indicator = Indicator.convert_with_links(
- node_ident, component, indicator_at_component.unique_name,
- **fields)
- indicator_list.append(indicator)
- col.indicators = indicator_list
- return col
+ node_uuid, rpc_name),
+ bookmark=True)
+ ]
+ }
+
+
+def indicator_list_from_dict(node_ident, indicators):
+ indicator_list = []
+ for component, names in indicators.items():
+ for name, fields in names.items():
+ indicator_at_component = IndicatorAtComponent(
+ component=component, name=name)
+ indicator = indicator_convert_with_links(
+ node_ident, component, indicator_at_component.unique_name,
+ **fields)
+ indicator_list.append(indicator)
+ return {'indicators': indicator_list}
class IndicatorController(rest.RestController):
@METRICS.timer('IndicatorController.put')
- @expose.expose(None, types.uuid_or_name, str, str,
- status_code=http_client.NO_CONTENT)
+ @method.expose(status_code=http_client.NO_CONTENT)
+ @args.validate(node_ident=args.uuid_or_name, indicator=args.string,
+ state=args.string)
def put(self, node_ident, indicator, state):
"""Set node hardware component indicator to the desired state.
@@ -426,7 +518,8 @@ class IndicatorController(rest.RestController):
state, topic=topic)
@METRICS.timer('IndicatorController.get_one')
- @expose.expose(IndicatorState, types.uuid_or_name, str)
+ @method.expose()
+ @args.validate(node_ident=args.uuid_or_name, indicator=args.string)
def get_one(self, node_ident, indicator):
"""Get node hardware component indicator and its state.
@@ -446,12 +539,12 @@ class IndicatorController(rest.RestController):
pecan.request.context, rpc_node.uuid,
indicator_at_component.component, indicator_at_component.name,
topic=topic)
- return IndicatorState(state=state)
+ return {'state': state}
@METRICS.timer('IndicatorController.get_all')
- @expose.expose(IndicatorsCollection, types.uuid_or_name, str,
- ignore_extra_args=True)
- def get_all(self, node_ident):
+ @method.expose()
+ @args.validate(node_ident=args.uuid_or_name)
+ def get_all(self, node_ident, **kwargs):
"""Get node hardware components and their indicators.
:param node_ident: the UUID or logical name of a node.
@@ -468,15 +561,15 @@ class IndicatorController(rest.RestController):
indicators = pecan.request.rpcapi.get_supported_indicators(
pecan.request.context, rpc_node.uuid, topic=topic)
- return IndicatorsCollection.collection_from_dict(
+ return indicator_list_from_dict(
node_ident, indicators)
class InjectNmiController(rest.RestController):
@METRICS.timer('InjectNmiController.put')
- @expose.expose(None, types.uuid_or_name,
- status_code=http_client.NO_CONTENT)
+ @method.expose(status_code=http_client.NO_CONTENT)
+ @args.validate(node_ident=args.uuid_or_name)
def put(self, node_ident):
"""Inject NMI for a node.
@@ -518,26 +611,11 @@ class NodeManagementController(rest.RestController):
"""Expose indicators as a sub-element of management"""
-class ConsoleInfo(base.Base):
- """API representation of the console information for a node."""
-
- console_enabled = types.boolean
- """The console state: if the console is enabled or not."""
-
- console_info = {str: types.jsontype}
- """The console information. It typically includes the url to access the
- console and the type of the application that hosts the console."""
-
- @classmethod
- def sample(cls):
- console = {'type': 'shellinabox', 'url': 'http://<hostname>:4201'}
- return cls(console_enabled=True, console_info=console)
-
-
class NodeConsoleController(rest.RestController):
@METRICS.timer('NodeConsoleController.get')
- @expose.expose(ConsoleInfo, types.uuid_or_name)
+ @method.expose()
+ @args.validate(node_ident=args.uuid_or_name)
def get(self, node_ident):
"""Get connection information about the console.
@@ -555,11 +633,11 @@ class NodeConsoleController(rest.RestController):
console = None
console_state = False
- return ConsoleInfo(console_enabled=console_state, console_info=console)
+ return {'console_enabled': console_state, 'console_info': console}
@METRICS.timer('NodeConsoleController.put')
- @expose.expose(None, types.uuid_or_name, types.boolean,
- status_code=http_client.ACCEPTED)
+ @method.expose(status_code=http_client.ACCEPTED)
+ @args.validate(node_ident=args.uuid_or_name, enabled=args.boolean)
def put(self, node_ident, enabled):
"""Start and stop the node console.
@@ -578,65 +656,19 @@ class NodeConsoleController(rest.RestController):
api.response.location = link.build_url('nodes', url_args)
-class NodeStates(base.APIBase):
- """API representation of the states of a node."""
-
- console_enabled = types.boolean
- """Indicates whether the console access is enabled or disabled on
- the node."""
-
- power_state = str
- """Represent the current (not transition) power state of the node"""
-
- provision_state = str
- """Represent the current (not transition) provision state of the node"""
-
- provision_updated_at = datetime.datetime
- """The UTC date and time of the last provision state change"""
-
- target_power_state = str
- """The user modified desired power state of the node."""
-
- target_provision_state = str
- """The user modified desired provision state of the node."""
-
- last_error = str
- """Any error from the most recent (last) asynchronous transaction that
- started but failed to finish."""
-
- raid_config = atypes.wsattr({str: types.jsontype}, readonly=True)
- """Represents the RAID configuration that the node is configured with."""
-
- target_raid_config = atypes.wsattr({str: types.jsontype},
- readonly=True)
- """The desired RAID configuration, to be used the next time the node
- is configured."""
-
- @staticmethod
- def convert(rpc_node):
- attr_list = ['console_enabled', 'last_error', 'power_state',
- 'provision_state', 'target_power_state',
- 'target_provision_state', 'provision_updated_at']
- if api_utils.allow_raid_config():
- attr_list.extend(['raid_config', 'target_raid_config'])
- states = NodeStates()
- for attr in attr_list:
- setattr(states, attr, getattr(rpc_node, attr))
- update_state_in_older_versions(states)
- return states
-
- @classmethod
- def sample(cls):
- sample = cls(target_power_state=ir_states.POWER_ON,
- target_provision_state=ir_states.ACTIVE,
- last_error=None,
- console_enabled=False,
- provision_updated_at=None,
- power_state=ir_states.POWER_ON,
- provision_state=None,
- raid_config=None,
- target_raid_config=None)
- return sample
+def node_states_convert(rpc_node):
+ attr_list = ['console_enabled', 'last_error', 'power_state',
+ 'provision_state', 'target_power_state',
+ 'target_provision_state', 'provision_updated_at']
+ if api_utils.allow_raid_config():
+ attr_list.extend(['raid_config', 'target_raid_config'])
+ states = {}
+ for attr in attr_list:
+ states[attr] = getattr(rpc_node, attr)
+ if isinstance(states[attr], datetime.datetime):
+ states[attr] = states[attr].isoformat()
+ update_state_in_older_versions(states)
+ return states
class NodeStatesController(rest.RestController):
@@ -651,7 +683,8 @@ class NodeStatesController(rest.RestController):
"""Expose console as a sub-element of states"""
@METRICS.timer('NodeStatesController.get')
- @expose.expose(NodeStates, types.uuid_or_name)
+ @method.expose()
+ @args.validate(node_ident=args.uuid_or_name)
def get(self, node_ident):
"""List the states of the node.
@@ -663,10 +696,13 @@ class NodeStatesController(rest.RestController):
# NOTE(lucasagomes): All these state values come from the
# DB. Ironic counts with a periodic task that verify the current
# power states of the nodes and update the DB accordingly.
- return NodeStates.convert(rpc_node)
+ return node_states_convert(rpc_node)
@METRICS.timer('NodeStatesController.raid')
- @expose.expose(None, types.uuid_or_name, body=types.jsontype)
+ @method.expose(status_code=http_client.NO_CONTENT)
+ @method.body('target_raid_config')
+ @args.validate(node_ident=args.uuid_or_name,
+ target_raid_config=args.types(dict))
def raid(self, node_ident, target_raid_config):
"""Set the target raid config of the node.
@@ -697,9 +733,9 @@ class NodeStatesController(rest.RestController):
raise
@METRICS.timer('NodeStatesController.power')
- @expose.expose(None, types.uuid_or_name, str,
- atypes.IntegerType(minimum=1),
- status_code=http_client.ACCEPTED)
+ @method.expose(status_code=http_client.ACCEPTED)
+ @args.validate(node_ident=args.uuid_or_name, target=args.string,
+ timeout=args.integer)
def power(self, node_ident, target, timeout=None):
"""Set the power state of the node.
@@ -726,8 +762,6 @@ class NodeStatesController(rest.RestController):
if ((target in [ir_states.SOFT_REBOOT, ir_states.SOFT_POWER_OFF]
or timeout) and not api_utils.allow_soft_power_off()):
raise exception.NotAcceptable()
- # FIXME(naohirot): This check is workaround because
- # atypes.IntegerType(minimum=1) is not effective
if timeout is not None and timeout < 1:
raise exception.Invalid(
_("timeout has to be positive integer"))
@@ -801,9 +835,11 @@ class NodeStatesController(rest.RestController):
raise exception.InvalidStateRequested(message=msg)
@METRICS.timer('NodeStatesController.provision')
- @expose.expose(None, types.uuid_or_name, str,
- types.jsontype, types.jsontype, str,
- status_code=http_client.ACCEPTED)
+ @method.expose(status_code=http_client.ACCEPTED)
+ @args.validate(node_ident=args.uuid_or_name, target=args.string,
+ configdrive=args.types(type(None), dict, str),
+ clean_steps=args.types(type(None), list),
+ rescue_password=args.string)
def provision(self, node_ident, target, configdrive=None,
clean_steps=None, rescue_password=None):
"""Asynchronous trigger the provisioning of the node.
@@ -927,18 +963,6 @@ def _check_clean_steps(clean_steps):
exc)
-class Traits(base.APIBase):
- """API representation of the traits for a node."""
-
- traits = atypes.ArrayType(str)
- """node traits"""
-
- @classmethod
- def sample(cls):
- traits = ["CUSTOM_TRAIT1", "CUSTOM_TRAIT2"]
- return cls(traits=traits)
-
-
def _get_chassis_uuid(node):
"""Return the UUID of a node's chassis, or None.
@@ -952,6 +976,24 @@ def _get_chassis_uuid(node):
return chassis.uuid
+def _replace_chassis_uuid_with_id(node_dict):
+ chassis_uuid = node_dict.pop('chassis_uuid', None)
+ if not chassis_uuid:
+ node_dict['chassis_id'] = None
+ return
+
+ try:
+ chassis = objects.Chassis.get_by_uuid(api.request.context,
+ chassis_uuid)
+ node_dict['chassis_id'] = chassis.id
+ except exception.ChassisNotFound as e:
+ # Change error code because 404 (NotFound) is inappropriate
+ # response for requests acting on nodes
+ e.code = http_client.BAD_REQUEST # BadRequest
+ raise
+ return chassis
+
+
def _make_trait_list(context, node_id, traits):
"""Return a TraitList object for the specified node and traits.
@@ -974,19 +1016,21 @@ class NodeTraitsController(rest.RestController):
self.node_ident = node_ident
@METRICS.timer('NodeTraitsController.get_all')
- @expose.expose(Traits)
+ @method.expose()
def get_all(self):
"""List node traits."""
node = api_utils.check_node_policy_and_retrieve(
'baremetal:node:traits:list', self.node_ident)
traits = objects.TraitList.get_by_node_id(api.request.context,
node.id)
- return Traits(traits=traits.get_trait_names())
+ return {'traits': traits.get_trait_names()}
@METRICS.timer('NodeTraitsController.put')
- @expose.expose(None, str, atypes.ArrayType(str),
- status_code=http_client.NO_CONTENT)
- def put(self, trait=None, traits=None):
+ @method.expose(status_code=http_client.NO_CONTENT)
+ @method.body('body')
+ @args.validate(trait=args.schema(api_utils.TRAITS_SCHEMA),
+ body=args.schema(TRAITS_SCHEMA))
+ def put(self, trait=None, body=None):
"""Add a trait to a node.
:param trait: String value; trait to add to a node, or None. Mutually
@@ -999,6 +1043,10 @@ class NodeTraitsController(rest.RestController):
node = api_utils.check_node_policy_and_retrieve(
'baremetal:node:traits:set', self.node_ident)
+ traits = None
+ if body and 'traits' in body:
+ traits = body['traits']
+
if (trait and traits is not None) or not (trait or traits is not None):
msg = _("A single node trait may be added via PUT "
"/v1/nodes/<node identifier>/traits/<trait> with no body, "
@@ -1020,9 +1068,6 @@ class NodeTraitsController(rest.RestController):
replace = True
new_traits = set(traits)
- for trait in traits:
- api_utils.validate_trait(trait)
-
# Update the node's traits to reflect the desired state.
node.traits = _make_trait_list(context, node.id, sorted(new_traits))
node.obj_reset_changes()
@@ -1043,8 +1088,8 @@ class NodeTraitsController(rest.RestController):
api.response.location = link.build_url('nodes', url_args)
@METRICS.timer('NodeTraitsController.delete')
- @expose.expose(None, str,
- status_code=http_client.NO_CONTENT)
+ @method.expose(status_code=http_client.NO_CONTENT)
+ @args.validate(trait=args.string)
def delete(self, trait=None):
"""Remove one or all traits from a node.
@@ -1082,500 +1127,208 @@ class NodeTraitsController(rest.RestController):
chassis_uuid=chassis_uuid)
-class Node(base.APIBase):
- """API representation of a bare metal node.
-
- This class enforces type checking and value constraints, and converts
- between the internal object model and the API representation of a node.
- """
-
- _chassis_uuid = None
-
- def _get_chassis_uuid(self):
- return self._chassis_uuid
-
- def _set_chassis_uuid(self, value):
- if value in (atypes.Unset, None):
- self._chassis_uuid = value
- elif self._chassis_uuid != value:
- try:
- chassis = objects.Chassis.get(api.request.context, value)
- self._chassis_uuid = chassis.uuid
- # NOTE(lucasagomes): Create the chassis_id attribute on-the-fly
- # to satisfy the api -> rpc object
- # conversion.
- self.chassis_id = chassis.id
- except exception.ChassisNotFound as e:
- # Change error code because 404 (NotFound) is inappropriate
- # response for a POST request to create a Port
- e.code = http_client.BAD_REQUEST
- raise
-
- uuid = types.uuid
- """Unique UUID for this node"""
-
- instance_uuid = types.uuid
- """The UUID of the instance in nova-compute"""
-
- name = atypes.wsattr(str)
- """The logical name for this node"""
-
- power_state = atypes.wsattr(str, readonly=True)
- """Represent the current (not transition) power state of the node"""
-
- target_power_state = atypes.wsattr(str, readonly=True)
- """The user modified desired power state of the node."""
-
- last_error = atypes.wsattr(str, readonly=True)
- """Any error from the most recent (last) asynchronous transaction that
- started but failed to finish."""
-
- provision_state = atypes.wsattr(str, readonly=True)
- """Represent the current (not transition) provision state of the node"""
-
- reservation = atypes.wsattr(str, readonly=True)
- """The hostname of the conductor that holds an exclusive lock on
- the node."""
-
- provision_updated_at = datetime.datetime
- """The UTC date and time of the last provision state change"""
-
- inspection_finished_at = datetime.datetime
- """The UTC date and time when the last hardware inspection finished
- successfully."""
-
- inspection_started_at = datetime.datetime
- """The UTC date and time when the hardware inspection was started"""
-
- maintenance = types.boolean
- """Indicates whether the node is in maintenance mode."""
-
- maintenance_reason = atypes.wsattr(str, readonly=True)
- """Indicates reason for putting a node in maintenance mode."""
-
- fault = atypes.wsattr(str, readonly=True)
- """Indicates the active fault of a node."""
-
- target_provision_state = atypes.wsattr(str, readonly=True)
- """The user modified desired provision state of the node."""
-
- console_enabled = types.boolean
- """Indicates whether the console access is enabled or disabled on
- the node."""
-
- instance_info = {str: types.jsontype}
- """This node's instance info."""
-
- driver = atypes.wsattr(str, mandatory=True)
- """The driver responsible for controlling the node"""
-
- driver_info = {str: types.jsontype}
- """This node's driver configuration"""
-
- driver_internal_info = atypes.wsattr({str: types.jsontype},
- readonly=True)
- """This driver's internal configuration"""
-
- clean_step = atypes.wsattr({str: types.jsontype}, readonly=True)
- """The current clean step"""
-
- deploy_step = atypes.wsattr({str: types.jsontype}, readonly=True)
- """The current deploy step"""
-
- raid_config = atypes.wsattr({str: types.jsontype}, readonly=True)
- """Represents the current RAID configuration of the node """
-
- target_raid_config = atypes.wsattr({str: types.jsontype},
- readonly=True)
- """The user modified RAID configuration of the node """
-
- extra = {str: types.jsontype}
- """This node's meta data"""
-
- resource_class = atypes.wsattr(atypes.StringType(max_length=80))
- """The resource class for the node, useful for classifying or grouping
- nodes. Used, for example, to classify nodes in Nova's placement
- engine."""
-
- # NOTE: properties should use a class to enforce required properties
- # current list: arch, cpus, disk, ram, image
- properties = {str: types.jsontype}
- """The physical characteristics of this node"""
-
- chassis_uuid = atypes.wsproperty(types.uuid, _get_chassis_uuid,
- _set_chassis_uuid)
- """The UUID of the chassis this node belongs"""
-
- links = None
- """A list containing a self link and associated node links"""
-
- ports = None
- """Links to the collection of ports on this node"""
-
- portgroups = None
- """Links to the collection of portgroups on this node"""
-
- volume = None
- """Links to endpoint for retrieving volume resources on this node"""
-
- states = None
- """Links to endpoint for retrieving and setting node states"""
-
- boot_interface = atypes.wsattr(str)
- """The boot interface to be used for this node"""
-
- console_interface = atypes.wsattr(str)
- """The console interface to be used for this node"""
-
- deploy_interface = atypes.wsattr(str)
- """The deploy interface to be used for this node"""
-
- inspect_interface = atypes.wsattr(str)
- """The inspect interface to be used for this node"""
-
- management_interface = atypes.wsattr(str)
- """The management interface to be used for this node"""
-
- network_interface = atypes.wsattr(str)
- """The network interface to be used for this node"""
-
- power_interface = atypes.wsattr(str)
- """The power interface to be used for this node"""
-
- raid_interface = atypes.wsattr(str)
- """The raid interface to be used for this node"""
-
- rescue_interface = atypes.wsattr(str)
- """The rescue interface to be used for this node"""
-
- storage_interface = atypes.wsattr(str)
- """The storage interface to be used for this node"""
-
- vendor_interface = atypes.wsattr(str)
- """The vendor interface to be used for this node"""
-
- traits = atypes.ArrayType(str)
- """The traits associated with this node"""
-
- bios_interface = atypes.wsattr(str)
- """The bios interface to be used for this node"""
-
- conductor_group = atypes.wsattr(str)
- """The conductor group to manage this node"""
-
- automated_clean = types.boolean
- """Indicates whether the node will perform automated clean or not."""
-
- protected = types.boolean
- """Indicates whether the node is protected from undeploying/rebuilding."""
-
- protected_reason = atypes.wsattr(str)
- """Indicates reason for protecting the node."""
-
- conductor = atypes.wsattr(str, readonly=True)
- """Represent the conductor currently serving the node"""
-
- owner = atypes.wsattr(str)
- """Field for storage of physical node owner"""
-
- lessee = atypes.wsattr(str)
- """Field for storage of physical node lessee"""
-
- description = atypes.wsattr(str)
- """Field for node description"""
-
- allocation_uuid = atypes.wsattr(types.uuid, readonly=True)
- """The UUID of the allocation this node belongs"""
-
- retired = types.boolean
- """Indicates whether the node is marked for retirement."""
-
- retired_reason = atypes.wsattr(str)
- """Indicates the reason for a node's retirement."""
-
- network_data = atypes.wsattr({str: types.jsontype})
- """Static network configuration JSON ironic will hand over to the node."""
-
- # NOTE(tenbrae): "conductor_affinity" shouldn't be presented on the
- # API because it's an internal value. Don't add it here.
-
- def __init__(self, **kwargs):
- self.fields = []
- fields = list(objects.Node.fields)
- # NOTE(lucasagomes): chassis_uuid is not part of objects.Node.fields
- # because it's an API-only attribute.
- fields.append('chassis_uuid')
- # NOTE(kaifeng) conductor is not part of objects.Node.fields too.
- fields.append('conductor')
- for k in fields:
- # Add fields we expose.
- if hasattr(self, k):
- self.fields.append(k)
- # TODO(jroll) is there a less hacky way to do this?
- if k == 'traits' and kwargs.get('traits') is not None:
- value = [t['trait'] for t in kwargs['traits']['objects']]
- # NOTE(jroll) this is special-cased to "" and not Unset,
- # because it is used in hash ring calculations
- elif (k == 'conductor_group'
- and (k not in kwargs or kwargs[k] is atypes.Unset)):
- value = ''
- else:
- value = kwargs.get(k, atypes.Unset)
- setattr(self, k, value)
-
- # NOTE(lucasagomes): chassis_id is an attribute created on-the-fly
- # by _set_chassis_uuid(), it needs to be present in the fields so
- # that as_dict() will contain chassis_id field when converting it
- # before saving it in the database.
- self.fields.append('chassis_id')
- if 'chassis_uuid' not in kwargs:
- setattr(self, 'chassis_uuid', kwargs.get('chassis_id',
- atypes.Unset))
-
- @staticmethod
- def _convert_with_links(node, url, fields=None, show_states_links=True,
- show_portgroups=True, show_volume=True):
- if fields is None:
- node.ports = [link.make_link('self', url, 'nodes',
- node.uuid + "/ports"),
- link.make_link('bookmark', url, 'nodes',
- node.uuid + "/ports",
- bookmark=True)
- ]
- if show_states_links:
- node.states = [link.make_link('self', url, 'nodes',
- node.uuid + "/states"),
- link.make_link('bookmark', url, 'nodes',
- node.uuid + "/states",
- bookmark=True)]
- if show_portgroups:
- node.portgroups = [
- link.make_link('self', url, 'nodes',
- node.uuid + "/portgroups"),
- link.make_link('bookmark', url, 'nodes',
- node.uuid + "/portgroups",
- bookmark=True)]
-
- if show_volume:
- node.volume = [
- link.make_link('self', url, 'nodes',
- node.uuid + "/volume"),
- link.make_link('bookmark', url, 'nodes',
- node.uuid + "/volume",
- bookmark=True)]
-
- node.links = [link.make_link('self', url, 'nodes',
- node.uuid),
- link.make_link('bookmark', url, 'nodes',
- node.uuid, bookmark=True)
- ]
- return node
-
- @classmethod
- def convert_with_links(cls, rpc_node, fields=None, sanitize=True):
- node = Node(**rpc_node.as_dict())
-
- if (api_utils.allow_expose_conductors()
- and (fields is None or 'conductor' in fields)):
- # NOTE(kaifeng) It is possible a node gets orphaned in certain
- # circumstances, set conductor to None in such case.
+def node_convert_with_links(rpc_node, fields=None, sanitize=True):
+ node = api_utils.object_to_dict(
+ rpc_node,
+ link_resource='nodes',
+ fields=(
+ 'automated_clean',
+ 'bios_interface',
+ 'boot_interface',
+ 'clean_step',
+ 'conductor_group',
+ 'console_interface',
+ 'deploy_interface',
+ 'deploy_step',
+ 'description',
+ 'driver',
+ 'driver_info',
+ 'driver_internal_info',
+ 'extra',
+ 'fault',
+ 'inspect_interface',
+ 'instance_info',
+ 'instance_uuid',
+ 'last_error',
+ 'lessee',
+ 'maintenance_reason',
+ 'management_interface',
+ 'name',
+ 'network_data',
+ 'network_interface',
+ 'owner',
+ 'power_interface',
+ 'power_state',
+ 'properties',
+ 'protected_reason',
+ 'provision_state',
+ 'raid_config',
+ 'raid_interface',
+ 'rescue_interface',
+ 'reservation',
+ 'resource_class',
+ 'retired_reason',
+ 'storage_interface',
+ 'target_power_state',
+ 'target_provision_state',
+ 'target_raid_config',
+ 'vendor_interface'
+ ),
+ boolean_fields=('console_enabled', 'maintenance', 'protected',
+ 'retired'),
+ date_fields=('inspection_finished_at', 'inspection_started_at',
+ 'provision_updated_at'),
+ )
+ node['traits'] = rpc_node.traits.get_trait_names()
+
+ if (api_utils.allow_expose_conductors()
+ and (fields is None or 'conductor' in fields)):
+ # NOTE(kaifeng) It is possible a node gets orphaned in certain
+ # circumstances, set conductor to None in such case.
+ try:
+ host = api.request.rpcapi.get_conductor_for(rpc_node)
+ node['conductor'] = host
+ except (exception.NoValidHost, exception.TemporaryFailure):
+ LOG.debug('Currently there is no conductor servicing node '
+ '%(node)s.', {'node': rpc_node.uuid})
+ node['conductor'] = None
+
+ if (api_utils.allow_allocations()
+ and (fields is None or 'allocation_uuid' in fields)):
+ node['allocation_uuid'] = None
+ if rpc_node.allocation_id:
try:
- host = api.request.rpcapi.get_conductor_for(rpc_node)
- node.conductor = host
- except (exception.NoValidHost, exception.TemporaryFailure):
- LOG.debug('Currently there is no conductor servicing node '
- '%(node)s.', {'node': rpc_node.uuid})
- node.conductor = None
-
- if (api_utils.allow_allocations()
- and (fields is None or 'allocation_uuid' in fields)):
- node.allocation_uuid = None
- if rpc_node.allocation_id:
- try:
- allocation = objects.Allocation.get_by_id(
- api.request.context,
- rpc_node.allocation_id)
- node.allocation_uuid = allocation.uuid
- except exception.AllocationNotFound:
- pass
-
- if fields is not None:
- api_utils.check_for_invalid_fields(
- fields, set(node.as_dict()) | {'allocation_uuid'})
-
- show_states_links = (
- api_utils.allow_links_node_states_and_driver_properties())
- show_portgroups = api_utils.allow_portgroups_subcontrollers()
- show_volume = api_utils.allow_volume()
-
- node = cls._convert_with_links(node, api.request.public_url,
- fields=fields,
- show_states_links=show_states_links,
- show_portgroups=show_portgroups,
- show_volume=show_volume)
- if not sanitize:
- return node
-
- node.sanitize(fields)
+ allocation = objects.Allocation.get_by_id(
+ api.request.context,
+ rpc_node.allocation_id)
+ node['allocation_uuid'] = allocation.uuid
+ except exception.AllocationNotFound:
+ pass
+ if fields is None or 'chassis_uuid' in fields:
+ node['chassis_uuid'] = _get_chassis_uuid(rpc_node)
+
+ if fields is not None:
+ api_utils.check_for_invalid_fields(
+ fields, set(node))
+
+ show_states_links = (
+ api_utils.allow_links_node_states_and_driver_properties())
+ show_portgroups = api_utils.allow_portgroups_subcontrollers()
+ show_volume = api_utils.allow_volume()
+
+ url = api.request.public_url
+
+ if fields is None:
+ node['ports'] = [link.make_link('self', url, 'nodes',
+ node['uuid'] + "/ports"),
+ link.make_link('bookmark', url, 'nodes',
+ node['uuid'] + "/ports",
+ bookmark=True)]
+ if show_states_links:
+ node['states'] = [link.make_link('self', url, 'nodes',
+ node['uuid'] + "/states"),
+ link.make_link('bookmark', url, 'nodes',
+ node['uuid'] + "/states",
+ bookmark=True)]
+ if show_portgroups:
+ node['portgroups'] = [
+ link.make_link('self', url, 'nodes',
+ node['uuid'] + "/portgroups"),
+ link.make_link('bookmark', url, 'nodes',
+ node['uuid'] + "/portgroups",
+ bookmark=True)]
+
+ if show_volume:
+ node['volume'] = [
+ link.make_link('self', url, 'nodes',
+ node['uuid'] + "/volume"),
+ link.make_link('bookmark', url, 'nodes',
+ node['uuid'] + "/volume",
+ bookmark=True)]
+
+ if not sanitize:
return node
- def sanitize(self, fields):
- """Removes sensitive and unrequested data.
+ node_sanitize(node, fields)
- Will only keep the fields specified in the ``fields`` parameter.
+ return node
- :param fields:
- list of fields to preserve, or ``None`` to preserve them all
- :type fields: list of str
- """
- cdict = api.request.context.to_policy_values()
- # NOTE(tenbrae): the 'show_password' policy setting name exists for
- # legacy purposes and can not be changed. Changing it will
- # cause upgrade problems for any operators who have
- # customized the value of this field
- show_driver_secrets = policy.check("show_password", cdict, cdict)
- show_instance_secrets = policy.check("show_instance_secrets",
- cdict, cdict)
-
- if not show_driver_secrets and self.driver_info != atypes.Unset:
- self.driver_info = strutils.mask_dict_password(
- self.driver_info, "******")
-
- # NOTE(derekh): mask ssh keys for the ssh power driver.
- # As this driver is deprecated masking here (opposed to strutils)
- # is simpler, and easier to backport. This can be removed along
- # with support for the ssh power driver.
- if self.driver_info.get('ssh_key_contents'):
- self.driver_info['ssh_key_contents'] = "******"
-
- if not show_instance_secrets and self.instance_info != atypes.Unset:
- self.instance_info = strutils.mask_dict_password(
- self.instance_info, "******")
- # NOTE(tenbrae): agent driver may store a swift temp_url on the
- # instance_info, which shouldn't be exposed to non-admin users.
- # Now that ironic supports additional policies, we need to hide
- # it here, based on this policy.
- # Related to bug #1613903
- if self.instance_info.get('image_url'):
- self.instance_info['image_url'] = "******"
-
- if self.driver_internal_info.get('agent_secret_token'):
- self.driver_internal_info['agent_secret_token'] = "******"
-
- update_state_in_older_versions(self)
- hide_fields_in_newer_versions(self)
-
- if fields is not None:
- self.unset_fields_except(fields)
-
- # NOTE(lucasagomes): The numeric ID should not be exposed to
- # the user, it's internal only.
- self.chassis_id = atypes.Unset
-
- show_states_links = (
- api_utils.allow_links_node_states_and_driver_properties())
- show_portgroups = api_utils.allow_portgroups_subcontrollers()
- show_volume = api_utils.allow_volume()
-
- if not show_volume:
- self.volume = atypes.Unset
- if not show_portgroups:
- self.portgroups = atypes.Unset
- if not show_states_links:
- self.states = atypes.Unset
-
- @classmethod
- def sample(cls, expand=True):
- time = datetime.datetime(2000, 1, 1, 12, 0, 0)
- node_uuid = '1be26c0b-03f2-4d2e-ae87-c02d7f33c123'
- instance_uuid = 'dcf1fbc5-93fc-4596-9395-b80572f6267b'
- name = 'database16-dc02'
- sample = cls(uuid=node_uuid, instance_uuid=instance_uuid,
- name=name, power_state=ir_states.POWER_ON,
- target_power_state=ir_states.NOSTATE,
- last_error=None, provision_state=ir_states.ACTIVE,
- target_provision_state=ir_states.NOSTATE,
- reservation=None, driver='fake', driver_info={},
- driver_internal_info={}, extra={},
- properties={
- 'memory_mb': '1024', 'local_gb': '10', 'cpus': '1'},
- updated_at=time, created_at=time,
- provision_updated_at=time, instance_info={},
- maintenance=False, maintenance_reason=None, fault=None,
- inspection_finished_at=None, inspection_started_at=time,
- console_enabled=False, clean_step={}, deploy_step={},
- raid_config=None, target_raid_config=None,
- network_interface='flat', resource_class='baremetal-gold',
- boot_interface=None, console_interface=None,
- deploy_interface=None, inspect_interface=None,
- management_interface=None, power_interface=None,
- raid_interface=None, vendor_interface=None,
- storage_interface=None, traits=[], rescue_interface=None,
- bios_interface=None, conductor_group="",
- automated_clean=None, protected=False,
- protected_reason=None, owner=None,
- allocation_uuid='982ddb5b-bce5-4d23-8fb8-7f710f648cd5',
- retired=False, retired_reason=None, lessee=None,
- network_data={})
-
- # NOTE(matty_dubs): The chassis_uuid getter() is based on the
- # _chassis_uuid variable:
- sample._chassis_uuid = 'edcad704-b2da-41d5-96d9-afd580ecfa12'
- fields = None if expand else _DEFAULT_RETURN_FIELDS
- return cls._convert_with_links(sample, 'http://localhost:6385',
- fields=fields)
-
-
-class NodePatchType(types.JsonPatchType):
-
- _api_base = Node
-
- @staticmethod
- def internal_attrs():
- defaults = types.JsonPatchType.internal_attrs()
- # TODO(lucasagomes): Include maintenance once the endpoint
- # v1/nodes/<uuid>/maintenance do more things than updating the DB.
- return defaults + ['/console_enabled', '/last_error',
- '/power_state', '/provision_state', '/reservation',
- '/target_power_state', '/target_provision_state',
- '/provision_updated_at', '/maintenance_reason',
- '/driver_internal_info', '/inspection_finished_at',
- '/inspection_started_at', '/clean_step',
- '/deploy_step',
- '/raid_config', '/target_raid_config',
- '/fault', '/conductor', '/allocation_uuid']
-
-
-class NodeCollection(collection.Collection):
- """API representation of a collection of nodes."""
-
- nodes = [Node]
- """A list containing nodes objects"""
- def __init__(self, **kwargs):
- self._type = 'nodes'
+def node_sanitize(node, fields):
+ """Removes sensitive and unrequested data.
- @staticmethod
- def convert_with_links(nodes, limit, url=None, fields=None, **kwargs):
- collection = NodeCollection()
- collection.nodes = [Node.convert_with_links(n, fields=fields,
- sanitize=False)
- for n in nodes]
- collection.next = collection.get_next(limit, url=url, fields=fields,
- **kwargs)
+ Will only keep the fields specified in the ``fields`` parameter.
- for node in collection.nodes:
- node.sanitize(fields)
-
- return collection
-
- @classmethod
- def sample(cls):
- sample = cls()
- node = Node.sample(expand=False)
- sample.nodes = [node]
- return sample
+ :param fields:
+ list of fields to preserve, or ``None`` to preserve them all
+ :type fields: list of str
+ """
+ cdict = api.request.context.to_policy_values()
+ # NOTE(tenbrae): the 'show_password' policy setting name exists for
+ # legacy purposes and can not be changed. Changing it will
+ # cause upgrade problems for any operators who have
+ # customized the value of this field
+ show_driver_secrets = policy.check("show_password", cdict, cdict)
+ show_instance_secrets = policy.check("show_instance_secrets",
+ cdict, cdict)
+
+ if not show_driver_secrets and node.get('driver_info'):
+ node['driver_info'] = strutils.mask_dict_password(
+ node['driver_info'], "******")
+
+ # NOTE(derekh): mask ssh keys for the ssh power driver.
+ # As this driver is deprecated masking here (opposed to strutils)
+ # is simpler, and easier to backport. This can be removed along
+ # with support for the ssh power driver.
+ if node['driver_info'].get('ssh_key_contents'):
+ node['driver_info']['ssh_key_contents'] = "******"
+
+ if not show_instance_secrets and node.get('instance_info'):
+ node['instance_info'] = strutils.mask_dict_password(
+ node['instance_info'], "******")
+ # NOTE(tenbrae): agent driver may store a swift temp_url on the
+ # instance_info, which shouldn't be exposed to non-admin users.
+ # Now that ironic supports additional policies, we need to hide
+ # it here, based on this policy.
+ # Related to bug #1613903
+ if node['instance_info'].get('image_url'):
+ node['instance_info']['image_url'] = "******"
+
+ if node.get('driver_internal_info', {}).get('agent_secret_token'):
+ node['driver_internal_info']['agent_secret_token'] = "******"
+
+ update_state_in_older_versions(node)
+ hide_fields_in_newer_versions(node)
+
+ api_utils.sanitize_dict(node, fields)
+
+ show_states_links = (
+ api_utils.allow_links_node_states_and_driver_properties())
+ show_portgroups = api_utils.allow_portgroups_subcontrollers()
+ show_volume = api_utils.allow_volume()
+
+ if not show_volume:
+ node.pop('volume', None)
+ if not show_portgroups:
+ node.pop('portgroups', None)
+ if not show_states_links:
+ node.pop('states', None)
+
+
+def node_list_convert_with_links(nodes, limit, url=None, fields=None,
+ **kwargs):
+ return collection.list_convert_with_links(
+ items=[node_convert_with_links(n, fields=fields,
+ sanitize=False)
+ for n in nodes],
+ item_name='nodes',
+ limit=limit,
+ url=url,
+ fields=fields,
+ sanitize_func=node_sanitize,
+ **kwargs
+ )
class NodeVendorPassthruController(rest.RestController):
@@ -1591,7 +1344,8 @@ class NodeVendorPassthruController(rest.RestController):
}
@METRICS.timer('NodeVendorPassthruController.methods')
- @expose.expose(str, types.uuid_or_name)
+ @method.expose()
+ @args.validate(node_ident=args.uuid_or_name)
def methods(self, node_ident):
"""Retrieve information about vendor methods of the given node.
@@ -1613,8 +1367,9 @@ class NodeVendorPassthruController(rest.RestController):
return _VENDOR_METHODS[rpc_node.driver]
@METRICS.timer('NodeVendorPassthruController._default')
- @expose.expose(str, types.uuid_or_name, str,
- body=str)
+ @method.expose()
+ @method.body('data')
+ @args.validate(node_ident=args.uuid_or_name, method=args.string)
def _default(self, node_ident, method, data=None):
"""Call a vendor extension.
@@ -1627,8 +1382,10 @@ class NodeVendorPassthruController(rest.RestController):
# Raise an exception if node is not found
topic = api.request.rpcapi.get_topic_for(rpc_node)
- return api_utils.vendor_passthru(rpc_node.uuid, method, topic,
+ resp = api_utils.vendor_passthru(rpc_node.uuid, method, topic,
data=data)
+ api.response.status_code = resp.status_code
+ return resp.obj
class NodeMaintenanceController(rest.RestController):
@@ -1651,8 +1408,8 @@ class NodeMaintenanceController(rest.RestController):
notify.emit_end_notification(context, new_node, 'maintenance_set')
@METRICS.timer('NodeMaintenanceController.put')
- @expose.expose(None, types.uuid_or_name, str,
- status_code=http_client.ACCEPTED)
+ @method.expose(status_code=http_client.ACCEPTED)
+ @args.validate(node_ident=args.uuid_or_name, reason=args.string)
def put(self, node_ident, reason=None):
"""Put the node in maintenance mode.
@@ -1666,7 +1423,8 @@ class NodeMaintenanceController(rest.RestController):
self._set_maintenance(rpc_node, True, reason=reason)
@METRICS.timer('NodeMaintenanceController.delete')
- @expose.expose(None, types.uuid_or_name, status_code=http_client.ACCEPTED)
+ @method.expose(status_code=http_client.ACCEPTED)
+ @args.validate(node_ident=args.uuid_or_name)
def delete(self, node_ident):
"""Remove the node from maintenance mode.
@@ -1679,21 +1437,6 @@ class NodeMaintenanceController(rest.RestController):
self._set_maintenance(rpc_node, False)
-# NOTE(vsaienko) We don't support pagination with VIFs, so we don't use
-# collection.Collection here.
-class VifCollection(base.Base):
- """API representation of a collection of VIFs. """
-
- vifs = [types.viftype]
- """A list containing VIFs objects"""
-
- @staticmethod
- def collection_from_list(vifs):
- col = VifCollection()
- col.vifs = [types.VifType.frombasetype(vif) for vif in vifs]
- return col
-
-
class NodeVIFController(rest.RestController):
def __init__(self, node_ident):
@@ -1709,17 +1452,18 @@ class NodeVIFController(rest.RestController):
raise
@METRICS.timer('NodeVIFController.get_all')
- @expose.expose(VifCollection)
+ @method.expose()
def get_all(self):
"""Get a list of attached VIFs"""
rpc_node, topic = self._get_node_and_topic('baremetal:node:vif:list')
vifs = api.request.rpcapi.vif_list(api.request.context,
rpc_node.uuid, topic=topic)
- return VifCollection.collection_from_list(vifs)
+ return {'vifs': vifs}
@METRICS.timer('NodeVIFController.post')
- @expose.expose(None, body=types.viftype,
- status_code=http_client.NO_CONTENT)
+ @method.expose(status_code=http_client.NO_CONTENT)
+ @method.body('vif')
+ @args.validate(vif=VIF_VALIDATOR)
def post(self, vif):
"""Attach a VIF to this node
@@ -1736,8 +1480,8 @@ class NodeVIFController(rest.RestController):
vif_info=vif, topic=topic)
@METRICS.timer('NodeVIFController.delete')
- @expose.expose(None, types.uuid_or_name,
- status_code=http_client.NO_CONTENT)
+ @method.expose(status_code=http_client.NO_CONTENT)
+ @args.validate(vif_id=args.uuid_or_name)
def delete(self, vif_id):
"""Detach a VIF from this node
@@ -1797,9 +1541,13 @@ class NodesController(rest.RestController):
@pecan.expose()
def _lookup(self, ident, *remainder):
+
+ if ident in self._subcontroller_map:
+ pecan.abort(http_client.NOT_FOUND)
+
try:
- ident = types.uuid_or_name.validate(ident)
- except exception.InvalidUuidOrName as e:
+ ident = args.uuid_or_name('node', ident)
+ except exception.InvalidParameterValue as e:
pecan.abort(http_client.BAD_REQUEST, e.args[0])
if not remainder:
return
@@ -1914,10 +1662,10 @@ class NodesController(rest.RestController):
if detail is not None:
parameters['detail'] = detail
- return NodeCollection.convert_with_links(nodes, limit,
- url=resource_url,
- fields=fields,
- **parameters)
+ return node_list_convert_with_links(nodes, limit,
+ url=resource_url,
+ fields=fields,
+ **parameters)
def _get_nodes_by_instance(self, instance_uuid):
"""Retrieve a node by its instance uuid.
@@ -1963,36 +1711,31 @@ class NodesController(rest.RestController):
"""Update rpc_node based on changed fields in a node.
"""
+
+ original_chassis_id = rpc_node.chassis_id
+ chassis = _replace_chassis_uuid_with_id(node)
+
+ # conductor_group is case-insensitive, and we use it to
+ # calculate the conductor to send an update too. lowercase
+ # it here instead of just before saving so we calculate
+ # correctly.
+ node['conductor_group'] = node['conductor_group'].lower()
+
+ # Node object protected field is not nullable
+ if node.get('protected') is None:
+ node['protected'] = False
+
# NOTE(mgoddard): Traits cannot be updated via a node PATCH.
- fields = set(objects.Node.fields) - {'traits'}
- for field in fields:
- try:
- patch_val = getattr(node, field)
- except AttributeError:
- # Ignore fields that aren't exposed in the API, except
- # chassis_id. chassis_id would have been set (instead of
- # chassis_uuid) if the node belongs to a chassis. This
- # AttributeError is raised for chassis_id only if
- # 1. the node doesn't belong to a chassis or
- # 2. the node belonged to a chassis but is now being removed
- # from the chassis.
- if (field == "chassis_id" and rpc_node[field] is not None):
- if not api_utils.allow_remove_chassis_uuid():
- raise exception.NotAcceptable()
- rpc_node[field] = None
- continue
- if patch_val == atypes.Unset:
- patch_val = None
- # conductor_group is case-insensitive, and we use it to calculate
- # the conductor to send an update too. lowercase it here instead
- # of just before saving so we calculate correctly.
- if field == 'conductor_group':
- patch_val = patch_val.lower()
- # Node object protected field is not nullable
- if field == 'protected' and patch_val is None:
- patch_val = False
- if rpc_node[field] != patch_val:
- rpc_node[field] = patch_val
+ api_utils.patch_update_changed_fields(
+ node, rpc_node,
+ fields=set(objects.Node.fields) - {'traits'},
+ schema=NODE_PATCH_SCHEMA,
+ id_map={'chassis_id': chassis and chassis.id or None}
+ )
+
+ if original_chassis_id and not rpc_node.chassis_id:
+ if not api_utils.allow_remove_chassis_uuid():
+ raise exception.NotAcceptable()
def _check_driver_changed_and_console_enabled(self, rpc_node, node_ident):
"""Checks if the driver and the console is enabled in a node.
@@ -2012,11 +1755,17 @@ class NodesController(rest.RestController):
status_code=http_client.CONFLICT)
@METRICS.timer('NodesController.get_all')
- @expose.expose(NodeCollection, types.uuid, types.uuid, types.boolean,
- types.boolean, types.boolean, str, types.uuid, int, str,
- str, str, types.listtype, str,
- str, str, types.boolean, str,
- str, str, str, str)
+ @method.expose()
+ @args.validate(chassis_uuid=args.uuid, instance_uuid=args.uuid,
+ associated=args.boolean, maintenance=args.boolean,
+ retired=args.boolean, provision_state=args.string,
+ marker=args.uuid, limit=args.integer, sort_key=args.string,
+ sort_dir=args.string, driver=args.string,
+ fields=args.string_list, resource_class=args.string,
+ fault=args.string, conductor_group=args.string,
+ detail=args.boolean, conductor=args.string,
+ owner=args.string, description_contains=args.string,
+ lessee=args.string, project=args.string)
def get_all(self, chassis_uuid=None, instance_uuid=None, associated=None,
maintenance=None, retired=None, provision_state=None,
marker=None, limit=None, sort_key='id', sort_dir='asc',
@@ -2101,11 +1850,16 @@ class NodesController(rest.RestController):
**extra_args)
@METRICS.timer('NodesController.detail')
- @expose.expose(NodeCollection, types.uuid, types.uuid, types.boolean,
- types.boolean, types.boolean, str, types.uuid, int, str,
- str, str, str, str,
- str, str, str, str,
- str, str)
+ @method.expose()
+ @args.validate(chassis_uuid=args.uuid, instance_uuid=args.uuid,
+ associated=args.boolean, maintenance=args.boolean,
+ retired=args.boolean, provision_state=args.string,
+ marker=args.uuid, limit=args.integer, sort_key=args.string,
+ sort_dir=args.string, driver=args.string,
+ resource_class=args.string, fault=args.string,
+ conductor_group=args.string, conductor=args.string,
+ owner=args.string, description_contains=args.string,
+ lessee=args.string, project=args.string)
def detail(self, chassis_uuid=None, instance_uuid=None, associated=None,
maintenance=None, retired=None, provision_state=None,
marker=None, limit=None, sort_key='id', sort_dir='asc',
@@ -2186,7 +1940,8 @@ class NodesController(rest.RestController):
**extra_args)
@METRICS.timer('NodesController.validate')
- @expose.expose(str, types.uuid_or_name, types.uuid)
+ @method.expose()
+ @args.validate(node=args.uuid_or_name, node_uuid=args.uuid)
def validate(self, node=None, node_uuid=None):
"""Validate the driver interfaces, using the node's UUID or name.
@@ -2211,7 +1966,8 @@ class NodesController(rest.RestController):
api.request.context, rpc_node.uuid, topic)
@METRICS.timer('NodesController.get_one')
- @expose.expose(Node, types.uuid_or_name, types.listtype)
+ @method.expose()
+ @args.validate(node_ident=args.uuid_or_name, fields=args.string_list)
def get_one(self, node_ident, fields=None):
"""Retrieve information about the given node.
@@ -2228,10 +1984,12 @@ class NodesController(rest.RestController):
api_utils.check_allow_specify_fields(fields)
api_utils.check_allowed_fields(fields)
- return Node.convert_with_links(rpc_node, fields=fields)
+ return node_convert_with_links(rpc_node, fields=fields)
@METRICS.timer('NodesController.post')
- @expose.expose(Node, body=Node, status_code=http_client.CREATED)
+ @method.expose(status_code=http_client.CREATED)
+ @method.body('node')
+ @args.validate(node=NODE_VALIDATOR)
def post(self, node):
"""Create a new node.
@@ -2244,45 +2002,35 @@ class NodesController(rest.RestController):
cdict = context.to_policy_values()
policy.authorize('baremetal:node:create', cdict, cdict)
- if node.conductor is not atypes.Unset:
- msg = _("Cannot specify conductor on node creation.")
- raise exception.Invalid(msg)
-
reject_fields_in_newer_versions(node)
- if node.traits is not atypes.Unset:
- msg = _("Cannot specify node traits on node creation. Traits must "
- "be set via the node traits API.")
- raise exception.Invalid(msg)
+ # NOTE(tenbrae): get_topic_for checks if node.driver is in the hash
+ # ring and raises NoValidHost if it is not.
+ # We need to ensure that node has a UUID before it can
+ # be mapped onto the hash ring.
+ if not node.get('uuid'):
+ node['uuid'] = uuidutils.generate_uuid()
- if (node.protected is not atypes.Unset
- or node.protected_reason is not atypes.Unset):
- msg = _("Cannot specify protected or protected_reason on node "
- "creation. These fields can only be set for active nodes")
- raise exception.Invalid(msg)
+ # NOTE(jroll) this is special-cased to "" and not None,
+ # because it is used in hash ring calculations
+ if not node.get('conductor_group'):
+ node['conductor_group'] = ''
- if (node.description is not atypes.Unset
- and len(node.description) > _NODE_DESCRIPTION_MAX_LENGTH):
- msg = _("Cannot create node with description exceeding %s "
- "characters") % _NODE_DESCRIPTION_MAX_LENGTH
- raise exception.Invalid(msg)
+ if node.get('name') is not None:
+ error_msg = _("Cannot create node with invalid name '%(name)s'")
+ self._check_names_acceptable([node['name']], error_msg)
+ node['provision_state'] = api_utils.initial_node_provision_state()
- if node.allocation_uuid is not atypes.Unset:
- msg = _("Allocation UUID cannot be specified, use allocations API")
- raise exception.Invalid(msg)
+ if not node.get('resource_class'):
+ node['resource_class'] = CONF.default_resource_class
- if node.network_data is not atypes.Unset:
- validate_network_data(node.network_data)
+ chassis = _replace_chassis_uuid_with_id(node)
+ chassis_uuid = chassis and chassis.uuid or None
- # NOTE(tenbrae): get_topic_for checks if node.driver is in the hash
- # ring and raises NoValidHost if it is not.
- # We need to ensure that node has a UUID before it can
- # be mapped onto the hash ring.
- if not node.uuid:
- node.uuid = uuidutils.generate_uuid()
+ new_node = objects.Node(context, **node)
try:
- topic = api.request.rpcapi.get_topic_for(node)
+ topic = api.request.rpcapi.get_topic_for(new_node)
except exception.NoValidHost as e:
# NOTE(tenbrae): convert from 404 to 400 because client can see
# list of available drivers and shouldn't request
@@ -2290,32 +2038,26 @@ class NodesController(rest.RestController):
e.code = http_client.BAD_REQUEST
raise
- if node.name != atypes.Unset and node.name is not None:
- error_msg = _("Cannot create node with invalid name '%(name)s'")
- self._check_names_acceptable([node.name], error_msg)
- node.provision_state = api_utils.initial_node_provision_state()
-
- if not node.resource_class:
- node.resource_class = CONF.default_resource_class
-
- new_node = objects.Node(context, **node.as_dict())
notify.emit_start_notification(context, new_node, 'create',
- chassis_uuid=node.chassis_uuid)
+ chassis_uuid=chassis_uuid)
with notify.handle_error_notification(context, new_node, 'create',
- chassis_uuid=node.chassis_uuid):
+ chassis_uuid=chassis_uuid):
new_node = api.request.rpcapi.create_node(context,
new_node, topic)
# Set the HTTP Location Header
api.response.location = link.build_url('nodes', new_node.uuid)
- api_node = Node.convert_with_links(new_node)
+ api_node = node_convert_with_links(new_node)
+ chassis_uuid = api_node.get('chassis_uuid')
notify.emit_end_notification(context, new_node, 'create',
- chassis_uuid=api_node.chassis_uuid)
+ chassis_uuid=chassis_uuid)
return api_node
def _validate_patch(self, patch, reset_interfaces):
if self.from_chassis:
raise exception.OperationNotPermitted()
+ api_utils.patch_validate_allowed_fields(patch, PATCH_ALLOWED_FIELDS)
+
reject_patch_in_newer_versions(patch)
traits = api_utils.get_patch_values(patch, '/traits')
@@ -2362,9 +2104,10 @@ class NodesController(rest.RestController):
policy_checks, node_ident, with_suffix=True)
@METRICS.timer('NodesController.patch')
- @expose.validate(types.uuid, types.boolean, [NodePatchType])
- @expose.expose(Node, types.uuid_or_name, types.boolean,
- body=[NodePatchType])
+ @method.expose()
+ @method.body('patch')
+ @args.validate(node_ident=args.uuid_or_name, reset_interfaces=args.boolean,
+ patch=args.patch)
def patch(self, node_ident, reset_interfaces=None, patch=None):
"""Update an existing node.
@@ -2446,9 +2189,15 @@ class NodesController(rest.RestController):
# 1) Remove chassis_id because it's an internal value and
# not present in the API object
# 2) Add chassis_uuid
- node_dict['chassis_uuid'] = node_dict.pop('chassis_id', None)
- node = Node(**api_utils.apply_jsonpatch(node_dict, patch))
- self._update_changed_fields(node, rpc_node)
+ node_dict['chassis_uuid'] = _get_chassis_uuid(rpc_node)
+
+ node_dict = api_utils.apply_jsonpatch(node_dict, patch)
+
+ api_utils.patched_validate_with_schema(
+ node_dict, NODE_PATCH_SCHEMA, NODE_PATCH_VALIDATOR)
+
+ self._update_changed_fields(node_dict, rpc_node)
+
# NOTE(tenbrae): we calculate the rpc topic here in case node.driver
# has changed, so that update is sent to the
# new conductor, not the old one which may fail to
@@ -2463,28 +2212,35 @@ class NodesController(rest.RestController):
raise
self._check_driver_changed_and_console_enabled(rpc_node, node_ident)
+ chassis_uuid = _get_chassis_uuid(rpc_node)
notify.emit_start_notification(context, rpc_node, 'update',
- chassis_uuid=node.chassis_uuid)
+ chassis_uuid=chassis_uuid)
with notify.handle_error_notification(context, rpc_node, 'update',
- chassis_uuid=node.chassis_uuid):
+ chassis_uuid=chassis_uuid):
new_node = api.request.rpcapi.update_node(context,
rpc_node, topic,
reset_interfaces)
- api_node = Node.convert_with_links(new_node)
+ api_node = node_convert_with_links(new_node)
+ chassis_uuid = api_node.get('chassis_uuid')
notify.emit_end_notification(context, new_node, 'update',
- chassis_uuid=api_node.chassis_uuid)
+ chassis_uuid=chassis_uuid)
return api_node
@METRICS.timer('NodesController.delete')
- @expose.expose(None, types.uuid_or_name,
- status_code=http_client.NO_CONTENT)
- def delete(self, node_ident):
+ @method.expose(status_code=http_client.NO_CONTENT)
+ @args.validate(node_ident=args.uuid_or_name)
+ def delete(self, node_ident, *args):
"""Delete a node.
:param node_ident: UUID or logical name of a node.
"""
+
+ # occurs when deleting traits with an old API version
+ if args:
+ raise exception.NotFound()
+
if self.from_chassis:
raise exception.OperationNotPermitted()
diff --git a/ironic/api/controllers/v1/ramdisk.py b/ironic/api/controllers/v1/ramdisk.py
index 4fe7239ab..6cf234b70 100644
--- a/ironic/api/controllers/v1/ramdisk.py
+++ b/ironic/api/controllers/v1/ramdisk.py
@@ -35,8 +35,8 @@ from ironic import objects
CONF = cfg.CONF
LOG = log.getLogger(__name__)
-_LOOKUP_RETURN_FIELDS = ('uuid', 'properties', 'instance_info',
- 'driver_internal_info')
+_LOOKUP_RETURN_FIELDS = ['uuid', 'properties', 'instance_info',
+ 'driver_internal_info']
def config(token):
@@ -64,21 +64,16 @@ def config(token):
class LookupResult(base.APIBase):
"""API representation of the node lookup result."""
- node = node_ctl.Node
+ node = None
"""The short node representation."""
config = {str: types.jsontype}
"""The configuration to pass to the ramdisk."""
@classmethod
- def sample(cls):
- return cls(node=node_ctl.Node.sample(),
- config={'heartbeat_timeout': 600})
-
- @classmethod
def convert_with_links(cls, node):
token = node.driver_internal_info.get('agent_secret_token')
- node = node_ctl.Node.convert_with_links(node, _LOOKUP_RETURN_FIELDS)
+ node = node_ctl.node_convert_with_links(node, _LOOKUP_RETURN_FIELDS)
return cls(node=node, config=config(token))
diff --git a/ironic/api/controllers/v1/types.py b/ironic/api/controllers/v1/types.py
index 7891fae32..8abc4bc83 100644
--- a/ironic/api/controllers/v1/types.py
+++ b/ironic/api/controllers/v1/types.py
@@ -393,33 +393,3 @@ class LocalLinkConnectionType(atypes.UserType):
locallinkconnectiontype = LocalLinkConnectionType()
-
-
-class VifType(JsonType):
-
- basetype = str
- name = 'viftype'
-
- mandatory_fields = {'id'}
-
- @staticmethod
- def validate(value):
- super(VifType, VifType).validate(value)
- keys = set(value)
- # Check all mandatory fields are present
- missing = VifType.mandatory_fields - keys
- if missing:
- msg = _('Missing mandatory keys: %s') % ', '.join(list(missing))
- raise exception.Invalid(msg)
- UuidOrNameType.validate(value['id'])
-
- return value
-
- @staticmethod
- def frombasetype(value):
- if value is None:
- return None
- return VifType.validate(value)
-
-
-viftype = VifType()
diff --git a/ironic/api/controllers/v1/utils.py b/ironic/api/controllers/v1/utils.py
index 8c0530095..2d0f389d6 100644
--- a/ironic/api/controllers/v1/utils.py
+++ b/ironic/api/controllers/v1/utils.py
@@ -343,27 +343,6 @@ def validate_sort_dir(sort_dir):
return sort_dir
-def validate_trait(trait, error_prefix=_('Invalid trait')):
- # TODO(sbaker) remove when all trait validation is jsonschema based
- error = exception.ClientSideError(
- _('%(error_prefix)s. A valid trait must be no longer than 255 '
- 'characters. Standard traits are defined in the os_traits library. '
- 'A custom trait must start with the prefix CUSTOM_ and use '
- 'the following characters: A-Z, 0-9 and _') %
- {'error_prefix': error_prefix})
- if not isinstance(trait, str):
- raise error
-
- if len(trait) > 255 or len(trait) < 1:
- raise error
-
- if trait in STANDARD_TRAITS:
- return
-
- if CUSTOM_TRAIT_REGEX.match(trait) is None:
- raise error
-
-
def apply_jsonpatch(doc, patch):
"""Apply a JSON patch, one operation at a time.
diff --git a/ironic/tests/unit/api/controllers/v1/test_node.py b/ironic/tests/unit/api/controllers/v1/test_node.py
index 2d979fb42..68c0e73ad 100644
--- a/ironic/tests/unit/api/controllers/v1/test_node.py
+++ b/ironic/tests/unit/api/controllers/v1/test_node.py
@@ -32,7 +32,6 @@ from ironic.api.controllers.v1 import node as api_node
from ironic.api.controllers.v1 import notification_utils
from ironic.api.controllers.v1 import utils as api_utils
from ironic.api.controllers.v1 import versions
-from ironic.api import types as atypes
from ironic.common import boot_devices
from ironic.common import components
from ironic.common import driver_factory
@@ -57,15 +56,6 @@ with open(
NETWORK_DATA = json.load(fl)
-class TestNodeObject(base.TestCase):
-
- def test_node_init(self):
- node_dict = test_api_utils.node_post_data()
- del node_dict['instance_uuid']
- node = api_node.Node(**node_dict)
- self.assertEqual(atypes.Unset, node.instance_uuid)
-
-
class TestListNodes(test_api_base.BaseApiTest):
def setUp(self):
@@ -1359,11 +1349,11 @@ class TestListNodes(test_api_base.BaseApiTest):
self.assertEqual(http_client.NOT_FOUND, response.status_int)
def test_ports_subresource_invalid_ident(self):
- invalid_ident = '123~123'
+ invalid_ident = '123 123'
response = self.get_json('/nodes/%s/ports' % invalid_ident,
expect_errors=True)
self.assertEqual(http_client.BAD_REQUEST, response.status_int)
- self.assertIn('Expected a logical name or UUID',
+ self.assertIn('Expected UUID or name for node',
response.json['error_message'])
def test_ports_subresource_via_portgroups_subres_not_allowed(self):
@@ -2798,9 +2788,8 @@ class TestPatch(test_api_base.BaseApiTest):
node_dict = self.node.as_dict()
node_dict['conductor_group'] = 'NEW-GROUP'
- node_obj = api_node.Node(**node_dict)
- controller._update_changed_fields(node_obj, self.node)
+ controller._update_changed_fields(node_dict, self.node)
self.assertEqual('new-group', self.node.conductor_group)
@mock.patch("ironic.api.request")
@@ -2810,9 +2799,8 @@ class TestPatch(test_api_base.BaseApiTest):
node_dict = self.node.as_dict()
del node_dict['chassis_id']
- node_no_chassis = api_node.Node(**node_dict)
- controller._update_changed_fields(node_no_chassis, self.node)
+ controller._update_changed_fields(node_dict, self.node)
self.assertIsNone(self.node.chassis_id)
def test_add_chassis_id(self):
@@ -2876,7 +2864,7 @@ class TestPatch(test_api_base.BaseApiTest):
response = self.patch_json('/nodes/%s' % self.node.uuid,
[{'path': '/maintenance', 'op': 'replace',
- 'value': 'true'}])
+ 'value': True}])
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.OK, response.status_code)
@@ -2889,7 +2877,7 @@ class TestPatch(test_api_base.BaseApiTest):
response = self.patch_json(
'/nodes/%s' % self.node.name,
[{'path': '/maintenance', 'op': 'replace',
- 'value': 'true'}],
+ 'value': True}],
headers={api_base.Version.string: "1.5"})
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.OK, response.status_code)
@@ -3387,6 +3375,18 @@ class TestPatch(test_api_base.BaseApiTest):
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.OK, response.status_code)
+ def test_update_protected_remove(self):
+ node = obj_utils.create_test_node(self.context,
+ uuid=uuidutils.generate_uuid(),
+ provision_state='active')
+ self.mock_update_node.return_value = node
+ headers = {api_base.Version.string: '1.48'}
+ response = self.patch_json('/nodes/%s' % node.uuid,
+ [{"op": "remove", "path": "/protected"}],
+ headers=headers)
+ self.assertEqual('application/json', response.content_type)
+ self.assertEqual(http_client.OK, response.status_code)
+
def test_update_protected_with_reason(self):
node = obj_utils.create_test_node(self.context,
uuid=uuidutils.generate_uuid(),
@@ -3634,6 +3634,18 @@ class TestPatch(test_api_base.BaseApiTest):
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.OK, response.status_code)
+ def test_update_retired_remove(self):
+ node = obj_utils.create_test_node(self.context,
+ uuid=uuidutils.generate_uuid(),
+ provision_state='active')
+ self.mock_update_node.return_value = node
+ headers = {api_base.Version.string: '1.61'}
+ response = self.patch_json('/nodes/%s' % node.uuid,
+ [{"op": "remove", "path": "/retired"}],
+ headers=headers)
+ self.assertEqual('application/json', response.content_type)
+ self.assertEqual(http_client.OK, response.status_code)
+
def test_update_retired_with_reason(self):
node = obj_utils.create_test_node(self.context,
uuid=uuidutils.generate_uuid(),
@@ -4132,14 +4144,13 @@ class TestPost(test_api_base.BaseApiTest):
def test_create_node_valid_driver_info(self):
self._test_jsontype_attributes('driver_info')
- def test_create_node_valid_instance_info(self):
- self._test_jsontype_attributes('instance_info')
-
def _test_vendor_passthru_ok(self, mock_vendor, return_value=None,
is_async=True):
expected_status = http_client.ACCEPTED if is_async else http_client.OK
- expected_return_value = json.dumps(return_value)
- expected_return_value = expected_return_value.encode('utf-8')
+ if return_value is None:
+ expected_return_value = b''
+ else:
+ expected_return_value = json.dumps(return_value).encode('utf-8')
node = obj_utils.create_test_node(self.context)
info = {'foo': 'bar'}
@@ -4156,8 +4167,10 @@ class TestPost(test_api_base.BaseApiTest):
def _test_vendor_passthru_ok_by_name(self, mock_vendor, return_value=None,
is_async=True):
expected_status = http_client.ACCEPTED if is_async else http_client.OK
- expected_return_value = json.dumps(return_value)
- expected_return_value = expected_return_value.encode('utf-8')
+ if return_value is None:
+ expected_return_value = b''
+ else:
+ expected_return_value = json.dumps(return_value).encode('utf-8')
node = obj_utils.create_test_node(self.context, name='node-109')
info = {'foo': 'bar'}
@@ -4191,7 +4204,7 @@ class TestPost(test_api_base.BaseApiTest):
'/nodes/%s/vendor_passthru/do_test' % node.uuid,
{'test_key': 'test_value'})
self.assertEqual(http_client.ACCEPTED, response.status_int)
- self.assertEqual(return_value['return'], response.json)
+ self.assertEqual(b'', response.body)
@mock.patch.object(rpcapi.ConductorAPI, 'vendor_passthru')
def test_vendor_passthru_by_name(self, mock_vendor):
@@ -4214,7 +4227,7 @@ class TestPost(test_api_base.BaseApiTest):
response = self.delete(
'/nodes/%s/vendor_passthru/do_test' % node.uuid)
self.assertEqual(http_client.ACCEPTED, response.status_int)
- self.assertEqual(return_value['return'], response.json)
+ self.assertEqual(b'', response.body)
def test_vendor_passthru_no_such_method(self):
node = obj_utils.create_test_node(self.context)
@@ -4245,7 +4258,7 @@ class TestPost(test_api_base.BaseApiTest):
pdict['node_uuid'] = node.uuid
response = self.post_json('/nodes/ports', pdict,
expect_errors=True)
- self.assertEqual(http_client.BAD_REQUEST, response.status_int)
+ self.assertEqual(http_client.NOT_FOUND, response.status_int)
def test_post_ports_subresource(self):
node = obj_utils.create_test_node(self.context)
@@ -4475,7 +4488,8 @@ class TestPost(test_api_base.BaseApiTest):
def test_create_node_protected_not_allowed(self):
headers = {api_base.Version.string: '1.48'}
- ndict = test_api_utils.post_get_test_node(protected=True)
+ ndict = test_api_utils.post_get_test_node()
+ ndict['protected'] = True
response = self.post_json('/nodes', ndict, headers=headers,
expect_errors=True)
self.assertEqual('application/json', response.content_type)
@@ -6038,6 +6052,7 @@ class TestAttachDetachVif(test_api_base.BaseApiTest):
@mock.patch.object(rpcapi.ConductorAPI, 'vif_list')
def test_vif_list(self, mock_list, mock_get):
mock_get.return_value = self.node
+ mock_list.return_value = []
self.get_json('/nodes/%s/vifs' % self.node.uuid,
headers={api_base.Version.string:
self.vif_version})
diff --git a/ironic/tests/unit/api/controllers/v1/test_types.py b/ironic/tests/unit/api/controllers/v1/test_types.py
index 7d16e9834..088469e70 100644
--- a/ironic/tests/unit/api/controllers/v1/test_types.py
+++ b/ironic/tests/unit/api/controllers/v1/test_types.py
@@ -386,28 +386,3 @@ class TestLocalLinkConnectionType(base.TestCase):
v = types.locallinkconnectiontype
value = {'network_type': 'invalid'}
self.assertRaises(exception.Invalid, v.validate, value)
-
-
-@mock.patch("ironic.api.request", mock.Mock(version=mock.Mock(minor=10)))
-class TestVifType(base.TestCase):
-
- def test_vif_type(self):
- v = types.viftype
- value = {'id': 'foo'}
- self.assertCountEqual(value, v.validate(value))
-
- def test_vif_type_missing_mandatory_key(self):
- v = types.viftype
- value = {'foo': 'bar'}
- self.assertRaisesRegex(exception.Invalid, 'Missing mandatory',
- v.validate, value)
-
- def test_vif_type_optional_key(self):
- v = types.viftype
- value = {'id': 'foo', 'misc': 'something'}
- self.assertCountEqual(value, v.frombasetype(value))
-
- def test_vif_type_bad_id(self):
- v = types.viftype
- self.assertRaises(exception.InvalidUuidOrName,
- v.frombasetype, {'id': 5678})
diff --git a/ironic/tests/unit/api/controllers/v1/test_utils.py b/ironic/tests/unit/api/controllers/v1/test_utils.py
index dc51ef41f..debffc964 100644
--- a/ironic/tests/unit/api/controllers/v1/test_utils.py
+++ b/ironic/tests/unit/api/controllers/v1/test_utils.py
@@ -19,7 +19,6 @@ from http import client as http_client
import io
from unittest import mock
-import os_traits
from oslo_config import cfg
from oslo_utils import uuidutils
@@ -27,7 +26,6 @@ from ironic import api
from ironic.api.controllers.v1 import node as api_node
from ironic.api.controllers.v1 import utils
from ironic.api import types as atypes
-from ironic.common import args
from ironic.common import exception
from ironic.common import policy
from ironic.common import states
@@ -64,56 +62,6 @@ class TestApiUtils(base.TestCase):
utils.validate_sort_dir,
'fake-sort')
- def test_validate_trait(self):
- utils.validate_trait(os_traits.HW_CPU_X86_AVX2)
- utils.validate_trait("CUSTOM_1")
- utils.validate_trait("CUSTOM_TRAIT_GOLD")
- self.assertRaises(exception.ClientSideError,
- utils.validate_trait, "A" * 256)
- self.assertRaises(exception.ClientSideError,
- utils.validate_trait, "CuSTOM_1")
- self.assertRaises(exception.ClientSideError,
- utils.validate_trait, "")
- self.assertRaises(exception.ClientSideError,
- utils.validate_trait, "CUSTOM_bob")
- self.assertRaises(exception.ClientSideError,
- utils.validate_trait, "CUSTOM_1-BOB")
- self.assertRaises(exception.ClientSideError,
- utils.validate_trait, "aCUSTOM_1a")
- large = "CUSTOM_" + ("1" * 248)
- self.assertEqual(255, len(large))
- utils.validate_trait(large)
- self.assertRaises(exception.ClientSideError,
- utils.validate_trait, large + "1")
- # Check custom error prefix.
- self.assertRaisesRegex(exception.ClientSideError,
- "spongebob",
- utils.validate_trait, "invalid", "spongebob")
-
- def test_validate_trait_jsonschema(self):
-
- validate_trait = args.schema(utils.TRAITS_SCHEMA)
- validate_trait('foo', os_traits.HW_CPU_X86_AVX2)
- validate_trait('foo', "CUSTOM_1")
- validate_trait('foo', "CUSTOM_TRAIT_GOLD")
- self.assertRaises(exception.InvalidParameterValue,
- validate_trait, 'foo', "A" * 256)
- self.assertRaises(exception.InvalidParameterValue,
- validate_trait, 'foo', "CuSTOM_1")
- self.assertRaises(exception.InvalidParameterValue,
- validate_trait, 'foo', "")
- self.assertRaises(exception.InvalidParameterValue,
- validate_trait, 'foo', "CUSTOM_bob")
- self.assertRaises(exception.InvalidParameterValue,
- validate_trait, 'foo', "CUSTOM_1-BOB")
- self.assertRaises(exception.InvalidParameterValue,
- validate_trait, 'foo', "aCUSTOM_1a")
- large = "CUSTOM_" + ("1" * 248)
- self.assertEqual(255, len(large))
- validate_trait('foo', large)
- self.assertRaises(exception.InvalidParameterValue,
- validate_trait, 'foo', large + "1")
-
def test_apply_jsonpatch(self):
doc = {"foo": {"bar": "baz"}}
patch = [{"op": "add", "path": "/foo/answer", "value": 42}]
diff --git a/ironic/tests/unit/api/utils.py b/ironic/tests/unit/api/utils.py
index aa6510d18..309d560bf 100644
--- a/ironic/tests/unit/api/utils.py
+++ b/ironic/tests/unit/api/utils.py
@@ -100,13 +100,6 @@ def remove_other_fields(values, allowed_fields):
def node_post_data(**kw):
node = db_utils.get_test_node(**kw)
- # These values are not part of the API object
- node.pop('version')
- node.pop('conductor_affinity')
- node.pop('chassis_id')
- node.pop('tags')
- node.pop('traits')
- node.pop('allocation_id')
# NOTE(jroll): pop out fields that were introduced in later API versions,
# unless explicitly requested. Otherwise, these will cause tests using
@@ -115,8 +108,8 @@ def node_post_data(**kw):
if field not in kw:
node.pop(field, None)
- internal = node_controller.NodePatchType.internal_attrs()
- return remove_internal(node, internal)
+ return remove_other_fields(
+ node, node_controller.NODE_SCHEMA['properties'])
def port_post_data(**kw):