Add `network_data` field to ironic node object

A new dictionary field `network_data` is added to the node object.
This new field can be populated by the operator with node static
network configuration.

Ironic API now performs formal JSON document validation of node
`network_data` field content against user-supplied JSON schema at
driver validation step.

As of this commit, the new `network_data` field is not actually
used by ironic, otherwise it should be perfectly functional. In
the following commits, network static configuration will be taken
from this field and handed over to ironic ramdisk bootstrapping
utilities.

Change-Id: I868b3b56a17f59e5aa1494b2e0ebc9c4e34ef173
Story: 2006691
Task: 36991
This commit is contained in:
Ilya Etingof 2020-03-23 13:47:08 +01:00 committed by Iury Gregory Melo Ferreira
parent 82c2663564
commit 653d4e4ef5
21 changed files with 888 additions and 10 deletions

View File

@ -442,6 +442,7 @@ Response
- allocation_uuid: allocation_uuid
- retired: retired
- retired_reason: retired_reason
- network_data: network_data
**Example detailed list of Nodes:**
@ -491,6 +492,9 @@ only the specified set.
.. versionadded:: 1.65
Introduced the ``lessee`` field.
.. versionadded:: 1.66
Introduced the ``network_data`` field.
Normal response codes: 200
Error codes: 400,403,404,406

View File

@ -1012,6 +1012,13 @@ name:
in: body
required: true
type: string
network_data:
description: |
Static network configuration for the node to eventually pass to node's
operating system.
in: body
required: false
type: JSON
network_interface:
description: |
Which Network Interface provider to use when plumbing the network

View File

@ -38,6 +38,7 @@
"maintenance_reason": null,
"management_interface": null,
"name": "test_node_classic",
"network_data": {},
"network_interface": "flat",
"owner": null,
"portgroups": [

View File

@ -41,6 +41,7 @@
"maintenance_reason": null,
"management_interface": null,
"name": "test_node_classic",
"network_data": {},
"network_interface": "flat",
"owner": null,
"portgroups": [

View File

@ -42,6 +42,7 @@
"maintenance_reason": "Replacing the hard drive",
"management_interface": null,
"name": "test_node_classic",
"network_data": {},
"network_interface": "flat",
"owner": null,
"portgroups": [

View File

@ -43,6 +43,7 @@
"maintenance_reason": null,
"management_interface": null,
"name": "test_node_classic",
"network_data": {},
"network_interface": "flat",
"owner": "john doe",
"portgroups": [
@ -148,6 +149,7 @@
"maintenance_reason": null,
"management_interface": "ipmitool",
"name": "test_node_dynamic",
"network_data": {},
"network_interface": "flat",
"owner": "43e61ec9-8e42-4dcb-bc45-30d66aa93e5b",
"portgroups": [

View File

@ -2,6 +2,13 @@
REST API Version History
========================
1.66 (Victoria, master)
-----------------------
Add ``network_data`` field to the node object, that will be used by
stand-alone ironic to pass L3 network configuration information to
ramdisk.
1.65 (Ussuri, 15.0)
---------------------

View File

@ -0,0 +1,580 @@
{
"$schema": "http://openstack.org/nova/network_data.json#",
"id": "http://openstack.org/nova/network_data.json",
"type": "object",
"title": "OpenStack Nova network metadata schema",
"description": "Schema of Nova instance network configuration information",
"required": [
"links",
"networks",
"services"
],
"properties": {
"links": {
"$id": "#/properties/links",
"type": "array",
"title": "L2 interfaces settings",
"items": {
"$id": "#/properties/links/items",
"oneOf": [
{
"$ref": "#/definitions/l2_link"
},
{
"$ref": "#/definitions/l2_bond"
},
{
"$ref": "#/definitions/l2_vlan"
}
]
}
},
"networks": {
"$id": "#/properties/networks",
"type": "array",
"title": "L3 networks",
"items": {
"$id": "#/properties/networks/items",
"oneOf": [
{
"$ref": "#/definitions/l3_ipv4_network"
},
{
"$ref": "#/definitions/l3_ipv6_network"
}
]
}
},
"services": {
"$ref": "#/definitions/services"
}
},
"definitions": {
"l2_address": {
"$id": "#/definitions/l2_address",
"type": "string",
"pattern": "(?i)^([0-9A-F]{2}[:-]){5}([0-9A-F]{2})$",
"title": "L2 interface address",
"examples": [
"fa:16:3e:9c:bf:3d"
]
},
"l2_id": {
"$id": "#/definitions/l2_id",
"type": "string",
"title": "L2 interface ID",
"examples": [
"eth0"
]
},
"l2_mtu": {
"$id": "#/definitions/l2_mtu",
"title": "L2 interface MTU",
"anyOf": [
{
"type": "number",
"minimum": 1,
"maximum": 65535
},
{
"type": "null"
}
],
"examples": [
1500
]
},
"l2_vif_id": {
"$id": "#/definitions/l2_vif_id",
"type": "string",
"title": "Virtual interface ID",
"examples": [
"cd9f6d46-4a3a-43ab-a466-994af9db96fc"
]
},
"l2_link": {
"$id": "#/definitions/l2_link",
"type": "object",
"title": "L2 interface configuration settings",
"required": [
"ethernet_mac_address",
"id",
"type"
],
"properties": {
"id": {
"$ref": "#/definitions/l2_id"
},
"ethernet_mac_address": {
"$ref": "#/definitions/l2_address"
},
"mtu": {
"$ref": "#/definitions/l2_mtu"
},
"type": {
"$id": "#/definitions/l2_link/properties/type",
"type": "string",
"enum": [
"bridge",
"dvs",
"hw_veb",
"hyperv",
"ovs",
"tap",
"vhostuser",
"vif",
"phy"
],
"title": "Interface type",
"examples": [
"bridge"
]
},
"vif_id": {
"$ref": "#/definitions/l2_vif_id"
}
}
},
"l2_bond": {
"$id": "#/definitions/l2_bond",
"type": "object",
"title": "L2 bonding interface configuration settings",
"required": [
"ethernet_mac_address",
"id",
"type",
"bond_mode",
"bond_links"
],
"properties": {
"id": {
"$ref": "#/definitions/l2_id"
},
"ethernet_mac_address": {
"$ref": "#/definitions/l2_address"
},
"mtu": {
"$ref": "#/definitions/l2_mtu"
},
"type": {
"$id": "#/definitions/l2_bond/properties/type",
"type": "string",
"enum": [
"bond"
],
"title": "Interface type",
"examples": [
"bond"
]
},
"vif_id": {
"$ref": "#/definitions/l2_vif_id"
},
"bond_mode": {
"$id": "#/definitions/bond/properties/bond_mode",
"type": "string",
"title": "Port bonding type",
"enum": [
"802.1ad",
"balance-rr",
"active-backup",
"balance-xor",
"broadcast",
"balance-tlb",
"balance-alb"
],
"examples": [
"802.1ad"
]
},
"bond_links": {
"$id": "#/definitions/bond/properties/bond_links",
"type": "array",
"title": "Port bonding links",
"items": {
"$id": "#/definitions/bond/properties/bond_links/items",
"type": "string"
}
}
}
},
"l2_vlan": {
"$id": "#/definitions/l2_vlan",
"type": "object",
"title": "L2 VLAN interface configuration settings",
"required": [
"vlan_mac_address",
"id",
"type",
"vlan_link",
"vlan_id"
],
"properties": {
"id": {
"$ref": "#/definitions/l2_id"
},
"vlan_mac_address": {
"$ref": "#/definitions/l2_address"
},
"mtu": {
"$ref": "#/definitions/l2_mtu"
},
"type": {
"$id": "#/definitions/l2_vlan/properties/type",
"type": "string",
"enum": [
"vlan"
],
"title": "VLAN interface type",
"examples": [
"vlan"
]
},
"vif_id": {
"$ref": "#/definitions/l2_vif_id"
},
"vlan_id": {
"$id": "#/definitions/l2_vlan/properties/vlan_id",
"type": "integer",
"title": "VLAN ID"
},
"vlan_link": {
"$id": "#/definitions/l2_vlan/properties/vlan_link",
"type": "string",
"title": "VLAN link name"
}
}
},
"l3_id": {
"$id": "#/definitions/l3_id",
"type": "string",
"title": "Network name",
"examples": [
"network0"
]
},
"l3_link": {
"$id": "#/definitions/l3_link",
"type": "string",
"title": "L2 network link to use for L3 interface",
"examples": [
"99e88329-f20d-4741-9593-25bf07847b16"
]
},
"l3_network_id": {
"$id": "#/definitions/l3_network_id",
"type": "string",
"title": "Network ID",
"examples": [
"99e88329-f20d-4741-9593-25bf07847b16"
]
},
"l3_ipv4_type": {
"$id": "#/definitions/l3_ipv4_type",
"type": "string",
"enum": [
"ipv4",
"ipv4_dhcp"
],
"title": "L3 IPv4 network type",
"examples": [
"ipv4_dhcp"
]
},
"l3_ipv6_type": {
"$id": "#/definitions/l3_ipv6_type",
"type": "string",
"enum": [
"ipv6",
"ipv6_dhcp",
"ipv6_slaac"
],
"title": "L3 IPv6 network type",
"examples": [
"ipv6_dhcp"
]
},
"l3_ipv4_host": {
"$id": "#/definitions/l3_ipv4_host",
"type": "string",
"pattern": "^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$",
"title": "L3 IPv4 host address",
"examples": [
"192.168.81.99"
]
},
"l3_ipv6_host": {
"$id": "#/definitions/l3_ipv6_host",
"type": "string",
"pattern": "^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))(/[0-9]{1,2})?$",
"title": "L3 IPv6 host address",
"examples": [
"2001:db8:3:4::192.168.81.99"
]
},
"l3_ipv4_netmask": {
"$id": "#/definitions/l3_ipv4_netmask",
"type": "string",
"pattern": "^(254|252|248|240|224|192|128|0)\\.0\\.0\\.0|255\\.(254|252|248|240|224|192|128|0)\\.0\\.0|255\\.255\\.(254|252|248|240|224|192|128|0)\\.0|255\\.255\\.255\\.(254|252|248|240|224|192|128|0)$",
"title": "L3 IPv4 network mask",
"examples": [
"255.255.252.0"
]
},
"l3_ipv6_netmask": {
"$id": "#/definitions/l3_ipv6_netmask",
"type": "string",
"pattern": "^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7})|(::))$",
"title": "L3 IPv6 network mask",
"examples": [
"ffff:ffff:ffff:ffff::"
]
},
"l3_ipv4_nw": {
"$id": "#/definitions/l3_ipv4_nw",
"type": "string",
"pattern": "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$",
"title": "L3 IPv4 network address",
"examples": [
"0.0.0.0"
]
},
"l3_ipv6_nw": {
"$id": "#/definitions/l3_ipv6_nw",
"type": "string",
"pattern": "^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7})|(::))$",
"title": "L3 IPv6 network address",
"examples": [
"8000::"
]
},
"l3_ipv4_gateway": {
"$id": "#/definitions/l3_ipv4_gateway",
"type": "string",
"pattern": "^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$",
"title": "L3 IPv4 gateway address",
"examples": [
"192.168.200.1"
]
},
"l3_ipv6_gateway": {
"$id": "#/definitions/l3_ipv6_gateway",
"type": "string",
"pattern": "^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$",
"title": "L3 IPv6 gateway address",
"examples": [
"2001:db8:3:4::192.168.81.99"
]
},
"l3_ipv4_network_route": {
"$id": "#/definitions/l3_ipv4_network_route",
"type": "object",
"title": "L3 IPv4 routing configuration item",
"required": [
"gateway",
"netmask",
"network"
],
"properties": {
"network": {
"$ref": "#/definitions/l3_ipv4_nw"
},
"netmask": {
"$ref": "#/definitions/l3_ipv4_netmask"
},
"gateway": {
"$ref": "#/definitions/l3_ipv4_gateway"
},
"services": {
"$ref": "#/definitions/ipv4_services"
}
}
},
"l3_ipv6_network_route": {
"$id": "#/definitions/l3_ipv6_network_route",
"type": "object",
"title": "L3 IPv6 routing configuration item",
"required": [
"gateway",
"netmask",
"network"
],
"properties": {
"network": {
"$ref": "#/definitions/l3_ipv6_nw"
},
"netmask": {
"$ref": "#/definitions/l3_ipv6_netmask"
},
"gateway": {
"$ref": "#/definitions/l3_ipv6_gateway"
},
"services": {
"$ref": "#/definitions/ipv6_services"
}
}
},
"l3_ipv4_network": {
"$id": "#/definitions/l3_ipv4_network",
"type": "object",
"title": "L3 IPv4 network configuration",
"required": [
"id",
"link",
"network_id",
"type"
],
"properties": {
"id": {
"$ref": "#/definitions/l3_id"
},
"link": {
"$ref": "#/definitions/l3_link"
},
"network_id": {
"$ref": "#/definitions/l3_network_id"
},
"type": {
"$ref": "#/definitions/l3_ipv4_type"
},
"ip_address": {
"$ref": "#/definitions/l3_ipv4_host"
},
"netmask": {
"$ref": "#/definitions/l3_ipv4_netmask"
},
"routes": {
"$id": "#/definitions/l3_ipv4_network/routes",
"type": "array",
"title": "L3 IPv4 network routes",
"items": {
"$ref": "#/definitions/l3_ipv4_network_route"
}
}
}
},
"l3_ipv6_network": {
"$id": "#/definitions/l3_ipv6_network",
"type": "object",
"title": "L3 IPv6 network configuration",
"required": [
"id",
"link",
"network_id",
"type"
],
"properties": {
"id": {
"$ref": "#/definitions/l3_id"
},
"link": {
"$ref": "#/definitions/l3_link"
},
"network_id": {
"$ref": "#/definitions/l3_network_id"
},
"type": {
"$ref": "#/definitions/l3_ipv6_type"
},
"ip_address": {
"$ref": "#/definitions/l3_ipv6_host"
},
"netmask": {
"$ref": "#/definitions/l3_ipv6_netmask"
},
"routes": {
"$id": "#/definitions/properties/l3_ipv6_network/routes",
"type": "array",
"title": "L3 IPv6 network routes",
"items": {
"$ref": "#/definitions/l3_ipv6_network_route"
}
}
}
},
"ipv4_service": {
"$id": "#/definitions/ipv4_service",
"type": "object",
"title": "Service on a IPv4 network",
"required": [
"address",
"type"
],
"properties": {
"address": {
"$ref": "#/definitions/l3_ipv4_host"
},
"type": {
"$id": "#/definitions/ipv4_service/properties/type",
"type": "string",
"enum": [
"dns"
],
"title": "Service type",
"examples": [
"dns"
]
}
}
},
"ipv6_service": {
"$id": "#/definitions/ipv6_service",
"type": "object",
"title": "Service on a IPv6 network",
"required": [
"address",
"type"
],
"properties": {
"address": {
"$ref": "#/definitions/l3_ipv6_host"
},
"type": {
"$id": "#/definitions/ipv4_service/properties/type",
"type": "string",
"enum": [
"dns"
],
"title": "Service type",
"examples": [
"dns"
]
}
}
},
"ipv4_services": {
"$id": "#/definitions/ipv4_services",
"type": "array",
"title": "Network services on IPv4 network",
"items": {
"$id": "#/definitions/ipv4_services/items",
"$ref": "#/definitions/ipv4_service"
}
},
"ipv6_services": {
"$id": "#/definitions/ipv6_services",
"type": "array",
"title": "Network services on IPv6 network",
"items": {
"$id": "#/definitions/ipv6_services/items",
"$ref": "#/definitions/ipv6_service"
}
},
"services": {
"$id": "#/definitions/services",
"type": "array",
"title": "Network services",
"items": {
"$id": "#/definitions/services/items",
"anyOf": [
{
"$ref": "#/definitions/ipv4_service"
},
{
"$ref": "#/definitions/ipv6_service"
}
]
}
}
}
}

View File

@ -15,9 +15,12 @@
import datetime
from http import client as http_client
import json
import os
from ironic_lib import metrics_utils
import jsonschema
from jsonschema import exceptions as json_schema_exc
from oslo_log import log
from oslo_utils import strutils
from oslo_utils import uuidutils
@ -115,6 +118,10 @@ ALLOWED_TARGET_POWER_STATES = (ir_states.POWER_ON,
_NODE_DESCRIPTION_MAX_LENGTH = 4096
NETWORK_DATA_SCHEMA = os.path.join(
os.path.dirname(__file__), 'network-data-schema.json')
def get_nodes_controller_reserved_names():
global _NODES_CONTROLLER_RESERVED_WORDS
if _NODES_CONTROLLER_RESERVED_WORDS is None:
@ -179,6 +186,28 @@ def update_state_in_older_versions(obj):
obj.provision_state = ir_states.INSPECTING
def validate_network_data(network_data):
"""Validates node network_data field.
This method validates network data configuration against JSON
schema.
: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)
except json_schema_exc.ValidationError as e:
# NOTE: Even though e.message is deprecated in general, it is
# said in jsonschema documentation to use this still.
msg = _("Invalid network_data: %s ") % e.message
raise exception.Invalid(msg)
class BootDeviceController(rest.RestController):
_custom_actions = {
@ -1265,6 +1294,9 @@ class Node(base.APIBase):
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.
@ -1485,7 +1517,9 @@ class Node(base.APIBase):
automated_clean=None, protected=False,
protected_reason=None, owner=None,
allocation_uuid='982ddb5b-bce5-4d23-8fb8-7f710f648cd5',
retired=False, retired_reason=None, lessee=None)
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'
@ -1746,7 +1780,7 @@ class NodesController(rest.RestController):
'instance_info', 'driver_internal_info',
'clean_step', 'deploy_step',
'raid_config', 'target_raid_config',
'traits']
'traits', 'network_data']
_subcontroller_map = {
'ports': port.PortsController,
@ -2231,6 +2265,9 @@ class NodesController(rest.RestController):
msg = _("Allocation UUID cannot be specified, use allocations API")
raise exception.Invalid(msg)
if node.network_data is not atypes.Unset:
validate_network_data(node.network_data)
# 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
@ -2293,6 +2330,12 @@ class NodesController(rest.RestController):
"characters") % _NODE_DESCRIPTION_MAX_LENGTH
raise exception.Invalid(msg)
network_data_fields = api_utils.get_patch_values(
patch, '/network_data')
for network_data in network_data_fields:
validate_network_data(network_data)
def _authorize_patch_and_get_node(self, node_ident, patch):
# deal with attribute-specific policy rules
policy_checks = []

View File

@ -492,6 +492,7 @@ VERSIONED_FIELDS = {
'retired': versions.MINOR_61_NODE_RETIRED,
'retired_reason': versions.MINOR_61_NODE_RETIRED,
'lessee': versions.MINOR_65_NODE_LESSEE,
'network_data': versions.MINOR_66_NODE_NETWORK_DATA,
}
for field in V31_FIELDS:

View File

@ -103,6 +103,7 @@ BASE_VERSION = 1
# v1.63: Add support for indicators
# v1.64: Add network_type to port.local_link_connection
# v1.65: Add lessee to the node object.
# v1.66: Add support for node network_data field.
MINOR_0_JUNO = 0
MINOR_1_INITIAL_VERSION = 1
@ -170,6 +171,7 @@ MINOR_62_AGENT_TOKEN = 62
MINOR_63_INDICATORS = 63
MINOR_64_LOCAL_LINK_CONNECTION_NETWORK_TYPE = 64
MINOR_65_NODE_LESSEE = 65
MINOR_66_NODE_NETWORK_DATA = 66
# When adding another version, update:
# - MINOR_MAX_VERSION
@ -177,7 +179,7 @@ MINOR_65_NODE_LESSEE = 65
# explanation of what changed in the new version
# - common/release_mappings.py, RELEASE_MAPPING['master']['api']
MINOR_MAX_VERSION = MINOR_65_NODE_LESSEE
MINOR_MAX_VERSION = MINOR_66_NODE_NETWORK_DATA
# String representations of the minor and maximum versions
_MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)

View File

@ -231,11 +231,11 @@ RELEASE_MAPPING = {
}
},
'master': {
'api': '1.65',
'api': '1.66',
'rpc': '1.50',
'objects': {
'Allocation': ['1.1'],
'Node': ['1.34'],
'Node': ['1.35', '1.34'],
'Conductor': ['1.3'],
'Chassis': ['1.3'],
'DeployTemplate': ['1.1'],

View File

@ -0,0 +1,30 @@
# 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.
"""Add nodes.network_data field
Revision ID: cf1a80fdb352
Revises: b2ad35726bb0
Create Date: 2020-03-20 22:41:14.163881
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'cf1a80fdb352'
down_revision = 'b2ad35726bb0'
def upgrade():
op.add_column('nodes', sa.Column('network_data', sa.Text(),
nullable=True))

View File

@ -197,6 +197,7 @@ class Node(Base):
retired = Column(Boolean, nullable=True, default=False,
server_default=false())
retired_reason = Column(Text, nullable=True)
network_data = Column(db_types.JsonEncodedDict)
storage_interface = Column(String(255), nullable=True)
power_interface = Column(String(255), nullable=True)
vendor_interface = Column(String(255), nullable=True)

View File

@ -67,7 +67,6 @@ RESCUE_LIKE_STATES = (states.RESCUING, states.RESCUEWAIT, states.RESCUEFAIL,
DISK_LAYOUT_PARAMS = ('root_gb', 'swap_mb', 'ephemeral_gb')
# All functions are called from deploy() directly or indirectly.
# They are split for stub-out.

View File

@ -75,7 +75,8 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
# Version 1.32: Add description field
# Version 1.33: Add retired and retired_reason fields
# Version 1.34: Add lessee field
VERSION = '1.34'
# Version 1.35: Add network_data field
VERSION = '1.35'
dbapi = db_api.get_instance()
@ -164,6 +165,7 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
'description': object_fields.StringField(nullable=True),
'retired': objects.fields.BooleanField(nullable=True),
'retired_reason': object_fields.StringField(nullable=True),
'network_data': object_fields.FlexibleDictField(nullable=True),
}
def as_dict(self, secure=False):
@ -549,6 +551,21 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
elif self.conductor_group:
self.conductor_group = ''
def _convert_network_data_field(self, target_version,
remove_unavailable_fields=True):
# NOTE(etingof): The default value for `network_data` is an empty
# dict. Therefore we can't use generic version adjustment
# routine.
field_is_set = self.obj_attr_is_set('network_data')
if target_version >= (1, 35):
if not field_is_set:
self.network_data = {}
elif field_is_set:
if remove_unavailable_fields:
delattr(self, 'network_data')
elif self.network_data:
self.network_data = {}
# NOTE (yolanda): new method created to avoid repeating code in
# _convert_to_version, and to avoid pep8 too complex error
def _adjust_field_to_version(self, field_name, field_default_value,
@ -606,6 +623,8 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
should be set to False (or removed).
Version 1.34: lessee was added. For versions prior to this, it should
be set to None or removed.
Version 1.35: network_data was added. For versions prior to this, it
should be set to empty dict (or removed).
:param target_version: the desired version of the object
:param remove_unavailable_fields: True to remove fields that are
@ -621,6 +640,7 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
('automated_clean', 28), ('protected_reason', 29),
('owner', 30), ('allocation_id', 31), ('description', 32),
('retired_reason', 33), ('lessee', 34)]
for name, minor in fields:
self._adjust_field_to_version(name, None, target_version,
1, minor, remove_unavailable_fields)
@ -637,14 +657,17 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
self._adjust_field_to_version('retired', False, target_version,
1, 33, remove_unavailable_fields)
self._convert_network_data_field(target_version,
remove_unavailable_fields)
@base.IronicObjectRegistry.register
class NodePayload(notification.NotificationPayloadBase):
"""Base class used for all notification payloads about a Node object."""
# NOTE: This payload does not include the Node fields "chassis_id",
# "driver_info", "driver_internal_info", "instance_info", "raid_config",
# "reservation", or "target_raid_config". These were excluded for reasons
# including:
# "network_data", "reservation", or "target_raid_config". These were
# excluded for reasons including:
# - increased complexity needed for creating the payload
# - sensitive information in the fields that shouldn't be exposed to
# external services

View File

@ -0,0 +1,113 @@
{
"links": [
{
"id": "interface2",
"type": "vif",
"ethernet_mac_address": "a0:36:9f:2c:e8:70",
"vif_id": "e1c90e9f-eafc-4e2d-8ec9-58b91cebb53d",
"mtu": 1500
},
{
"id": "interface0",
"type": "phy",
"ethernet_mac_address": "a0:36:9f:2c:e8:80",
"mtu": 9000
},
{
"id": "interface1",
"type": "phy",
"ethernet_mac_address": "a0:36:9f:2c:e8:81",
"mtu": 9000
},
{
"id": "bond0",
"type": "bond",
"bond_links": [
"interface0",
"interface1"
],
"ethernet_mac_address": "a0:36:9f:2c:e8:82",
"bond_mode": "802.1ad",
"bond_xmit_hash_policy": "layer3+4",
"bond_miimon": 100
},
{
"id": "vlan0",
"type": "vlan",
"vlan_link": "bond0",
"vlan_id": 101,
"vlan_mac_address": "a0:36:9f:2c:e8:80",
"vif_id": "e1c90e9f-eafc-4e2d-8ec9-58b91cebb53f"
}
],
"networks": [
{
"id": "private-ipv4",
"type": "ipv4",
"link": "interface0",
"ip_address": "10.184.0.244",
"netmask": "255.255.240.0",
"routes": [
{
"network": "10.0.0.0",
"netmask": "255.0.0.0",
"gateway": "11.0.0.1"
},
{
"network": "0.0.0.0",
"netmask": "0.0.0.0",
"gateway": "23.253.157.1"
}
],
"network_id": "da5bb487-5193-4a65-a3df-4a0055a8c0d7"
},
{
"id": "private-ipv4",
"type": "ipv6",
"link": "interface0",
"ip_address": "2001:cdba::3257:9652/24",
"routes": [
{
"network": "::",
"netmask": "::",
"gateway": "fd00::1"
},
{
"network": "::",
"netmask": "ffff:ffff:ffff::",
"gateway": "fd00::1:1"
}
],
"network_id": "da5bb487-5193-4a65-a3df-4a0055a8c0d8"
},
{
"id": "publicnet-ipv4",
"type": "ipv4",
"link": "vlan0",
"ip_address": "23.253.157.244",
"netmask": "255.255.255.0",
"dns_nameservers": [
"69.20.0.164",
"69.20.0.196"
],
"routes": [
{
"network": "0.0.0.0",
"netmask": "0.0.0.0",
"gateway": "23.253.157.1"
}
],
"network_id": "62611d6f-66cb-4270-8b1f-503ef0dd4736"
}
],
"services": [
{
"type": "dns",
"address": "8.8.8.8"
},
{
"type": "dns",
"address": "8.8.4.4"
}
]
}

View File

@ -16,6 +16,7 @@ Tests for the API /nodes/ methods.
import datetime
from http import client as http_client
import json
import os
from urllib import parse as urlparse
import fixtures
@ -42,12 +43,20 @@ from ironic.common import states
from ironic.conductor import rpcapi
from ironic import objects
from ironic.objects import fields as obj_fields
from ironic import tests as tests_root
from ironic.tests import base
from ironic.tests.unit.api import base as test_api_base
from ironic.tests.unit.api import utils as test_api_utils
from ironic.tests.unit.objects import utils as obj_utils
with open(
os.path.join(
os.path.dirname(tests_root.__file__),
'json_samples', 'network_data.json')) as fl:
NETWORK_DATA = json.load(fl)
class TestNodeObject(base.TestCase):
def test_node_init(self):
@ -138,6 +147,7 @@ class TestListNodes(test_api_base.BaseApiTest):
self.assertNotIn('retired', data['nodes'][0])
self.assertNotIn('retired_reason', data['nodes'][0])
self.assertNotIn('lessee', data['nodes'][0])
self.assertNotIn('network_data', data['nodes'][0])
def test_get_one(self):
node = obj_utils.create_test_node(self.context,
@ -403,6 +413,19 @@ class TestListNodes(test_api_base.BaseApiTest):
headers={api_base.Version.string: '1.65'})
self.assertEqual(data['lessee'], "some-lucky-project")
def test_node_network_data_hidden_in_lower_version(self):
self._test_node_field_hidden_in_lower_version('network_data',
'1.65', '1.66')
def test_node_network_data(self):
node = obj_utils.create_test_node(
self.context, network_data=NETWORK_DATA,
provision_state='active',
uuid=uuidutils.generate_uuid())
data = self.get_json('/nodes/%s' % node.uuid,
headers={api_base.Version.string: '1.66'})
self.assertEqual(data['network_data'], NETWORK_DATA)
def test_get_one_custom_fields(self):
node = obj_utils.create_test_node(self.context,
chassis_id=self.chassis.id)
@ -684,6 +707,7 @@ class TestListNodes(test_api_base.BaseApiTest):
self.assertIn('allocation_uuid', data['nodes'][0])
self.assertIn('retired', data['nodes'][0])
self.assertIn('retired_reason', data['nodes'][0])
self.assertIn('network_data', data['nodes'][0])
def test_detail_using_query(self):
node = obj_utils.create_test_node(self.context,
@ -722,6 +746,7 @@ class TestListNodes(test_api_base.BaseApiTest):
self.assertNotIn('chassis_id', data['nodes'][0])
self.assertIn('retired', data['nodes'][0])
self.assertIn('retired_reason', data['nodes'][0])
self.assertIn('network_data', data['nodes'][0])
def test_detail_query_false(self):
obj_utils.create_test_node(self.context)
@ -3654,6 +3679,36 @@ class TestPatch(test_api_base.BaseApiTest):
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)
def test_update_network_data(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.66'}
response = self.patch_json('/nodes/%s' % node.uuid,
[{'path': '/network_data',
'value': NETWORK_DATA,
'op': 'replace'}],
headers=headers)
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.OK, response.status_code)
def test_update_network_data_old_api(self):
node = obj_utils.create_test_node(self.context,
uuid=uuidutils.generate_uuid())
self.mock_update_node.return_value = node
headers = {api_base.Version.string: '1.62'}
response = self.patch_json('/nodes/%s' % node.uuid,
[{'path': '/network_data',
'value': NETWORK_DATA,
'op': 'replace'}],
headers=headers,
expect_errors=True)
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)
@mock.patch.object(api_utils, 'check_multiple_node_policies_and_retrieve',
autospec=True)
def test_patch_policy_update(self, mock_cmnpar):

View File

@ -969,6 +969,13 @@ class MigrationCheckersMixin(object):
col_names = [column.name for column in allocations.c]
self.assertIn('owner', col_names)
def _check_cf1a80fdb352(self, engine, data):
nodes = db_utils.get_table(engine, 'nodes')
col_names = [column.name for column in nodes.c]
self.assertIn('network_data', col_names)
self.assertIsInstance(
nodes.c.network_data.type, sqlalchemy.types.String)
def _pre_upgrade_cd2c80feb331(self, engine):
data = {
'node_uuid': uuidutils.generate_uuid(),

View File

@ -228,6 +228,7 @@ def get_test_node(**kw):
'retired': kw.get('retired', False),
'retired_reason': kw.get('retired_reason', None),
'lessee': kw.get('lessee', None),
'network_data': kw.get('network_data'),
}
for iface in drivers_base.ALL_INTERFACES:

View File

@ -676,7 +676,7 @@ class TestObject(_LocalTest, _TestObject):
# version bump. It is an MD5 hash of the object fields and remotable methods.
# The fingerprint values should only be changed if there is a version bump.
expected_object_fingerprints = {
'Node': '1.34-ae873e627cf30bf28fe9f98a807b6200',
'Node': '1.35-aee8ecf5c4d0ed590eb484762aee7fca',
'MyObj': '1.5-9459d30d6954bffc7a9afd347a807ca6',
'Chassis': '1.3-d656e039fd8ae9f34efc232ab3980905',
'Port': '1.9-0cb9202a4ec442e8c0d87a324155eaaf',