# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from oslo_log import log as logging
from oslo_utils import netutils
from ironic.common import exception
from ironic.common.i18n import _
from ironic.common import utils
from ironic.conf import CONF
from ironic import objects
LOG = logging.getLogger(__name__)
[docs]def get_pxe_mac(inventory):
"""Get MAC address of the PXE interface."""
pxe_mac = inventory.get('boot', {}).get('pxe_interface')
if pxe_mac and '-' in pxe_mac:
# pxelinux format: 01-aa-bb-cc-dd-ee-ff
pxe_mac = pxe_mac.split('-', 1)[1]
pxe_mac = pxe_mac.replace('-', ':').lower()
return pxe_mac
[docs]def get_interfaces(node, inventory):
"""Convert inventory to a dict with interfaces.
:return: dict interface name -> interface (for valid interfaces).
"""
result = {}
pxe_mac = get_pxe_mac(inventory)
for iface in inventory['interfaces']:
name = iface.get('name')
mac = iface.get('mac_address')
ipv4_address = iface.get('ipv4_address')
ipv6_address = iface.get('ipv6_address')
# NOTE(kaifeng) ipv6 address may in the form of fd00::1%enp2s0,
# which is not supported by netaddr, remove the suffix if exists.
if ipv6_address and '%' in ipv6_address:
ipv6_address = ipv6_address.split('%')[0]
ip = ipv4_address or ipv6_address
client_id = iface.get('client_id')
if not name:
LOG.error('Malformed interface record for node %(node)s: %(if)s',
{'node': node.uuid, 'if': iface})
continue
if not mac:
LOG.debug('Skipping interface %(if)s for node %(node)s without '
'link information',
{'node': node.uuid, 'if': iface})
continue
if not netutils.is_valid_mac(mac):
LOG.warning('MAC address of interface %(if)s of node %(node)s '
'is not valid, skipping',
{'node': node.uuid, 'if': iface})
continue
mac = mac.lower()
LOG.debug('Found interface %(name)s with MAC "%(mac)s", '
'IP address "%(ip)s" and client_id "%(client_id)s" '
'for node %(node)s',
{'name': name, 'mac': mac, 'ip': ip,
'client_id': client_id, 'node': node.uuid})
result[name] = dict(iface, pxe_enabled=(mac == pxe_mac),
# IPv6 address without scope.
ipv6_address=ipv6_address)
return result
[docs]def validate_interfaces(node, inventory, interfaces):
"""Validate interfaces on correctness and suitability.
:return: dict interface name -> interface.
"""
if not interfaces:
raise exception.InvalidNodeInventory(
node=node.uuid,
reason=_('no valid network interfaces'))
pxe_mac = get_pxe_mac(inventory)
if not pxe_mac and CONF.inspector.add_ports == 'pxe':
LOG.warning('No boot interface provided in the inventory for node '
'%s, will add all ports with IP addresses', node.uuid)
result = {}
for name, iface in interfaces.items():
ip = iface.get('ipv4_address') or iface.get('ipv6_address')
pxe = iface.get('pxe_enabled', True)
if name == 'lo' or (ip and utils.is_loopback(ip)):
LOG.debug('Skipping local interface %(iface)s for node %(node)s',
{'iface': name, 'node': node.uuid})
continue
if CONF.inspector.add_ports == 'pxe' and pxe_mac and not pxe:
LOG.debug('Skipping interface %(iface)s for node %(node)s as it '
'was not PXE booting and add_ports is set to "pxe"',
{'iface': name, 'node': node.uuid})
continue
if CONF.inspector.add_ports == 'active' and not ip:
LOG.debug('Skipping interface %(iface)s for node %(node)s as it '
'did not have an IP address assigned during the ramdisk '
'run and add_ports is set to "active"',
{'iface': name, 'node': node.uuid})
continue
result[name] = iface.copy()
if not result:
raise exception.InvalidNodeInventory(
node=node.uuid,
reason=_('no network interfaces match the configuration '
'(add_ports set to "%s")') % CONF.inspector.add_ports)
return result
[docs]def add_ports(task, interfaces):
"""Add ports for all previously validated interfaces."""
for iface in interfaces.values():
mac = iface['mac_address']
extra = {}
if iface.get('client_id'):
extra['client-id'] = iface['client_id']
port_dict = {'address': mac, 'node_id': task.node.id,
'pxe_enabled': iface['pxe_enabled'], 'extra': extra}
port = objects.Port(task.context, **port_dict)
try:
port.create()
LOG.info("Port created for MAC address %(address)s for "
"node %(node)s%(pxe)s",
{'address': mac, 'node': task.node.uuid,
'pxe': ' (PXE booting)' if iface['pxe_enabled'] else ''})
except exception.MACAlreadyExists:
LOG.info("Port already exists for MAC address %(address)s "
"for node %(node)s",
{'address': mac, 'node': task.node.uuid})
[docs]def update_ports(task, all_interfaces, valid_macs):
"""Update ports to match the valid MACs.
Depending on the value of ``[inspector]keep_ports``, some ports may be
removed.
"""
# TODO(dtantsur): no port update for active nodes (when supported)
if CONF.inspector.keep_ports == 'present':
expected_macs = {iface['mac_address']
for iface in all_interfaces.values()}
elif CONF.inspector.keep_ports == 'added':
expected_macs = set(valid_macs)
else:
expected_macs = None # unused
pxe_macs = {iface['mac_address'] for iface in all_interfaces.values()
if iface['pxe_enabled']}
for port in objects.Port.list_by_node_id(task.context, task.node.id):
if expected_macs and port.address not in expected_macs:
expected_str = ', '.join(sorted(expected_macs))
LOG.info("Deleting port %(port)s of node %(node)s as its MAC "
"%(mac)s is not in the expected MAC list [%(expected)s]",
{'port': port.uuid, 'mac': port.address,
'node': task.node.uuid, 'expected': expected_str})
port.destroy()
elif CONF.inspector.update_pxe_enabled:
pxe_enabled = port.address in pxe_macs
if pxe_enabled != port.pxe_enabled:
LOG.debug('Changing pxe_enabled=%(val)s on port %(port)s '
'of node %(node)s to match the inventory',
{'port': port.address, 'val': pxe_enabled,
'node': task.node.uuid})
port.pxe_enabled = pxe_enabled
port.save()
[docs]def process_interfaces(task, inventory, plugin_data):
"""Process network interfaces in the inventory."""
# TODO(dtantsur): this function will become two hooks in the future.
all_interfaces = get_interfaces(task.node, inventory)
interfaces = validate_interfaces(task.node, inventory, all_interfaces)
valid_macs = [iface['mac_address'] for iface in interfaces.values()]
plugin_data['all_interfaces'] = all_interfaces
plugin_data['valid_interfaces'] = interfaces
plugin_data['macs'] = valid_macs
if CONF.inspector.add_ports != 'disabled':
add_ports(task, interfaces)
update_ports(task, all_interfaces, valid_macs)
Except where otherwise noted, this document is licensed under Creative Commons Attribution 3.0 License. See all OpenStack Legal Documents.