Merge "Add network_data field to ironic node object"

This commit is contained in:
Zuul 2020-05-19 19:10:30 +00:00 committed by Gerrit Code Review
commit ff8a0c71b0
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

@ -491,6 +491,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)
@ -3655,6 +3680,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',