Phil Sphicas a75704b8cb fix: prevent multiple IP assignments
A recent change[0] to address PEP8 issues resulted in an unintended
behavior modification, in some cases resulting in MAAS allocation of
multiple IP addresses to the same NIC.

This reverts to the original code logic.

[0] 1755930331

Change-Id: I6dccd1b60c414e3aa966085e81dc0b61244e9814
2020-05-30 20:26:40 +00:00

481 lines
17 KiB
Python

# Copyright 2017 AT&T Intellectual Property. All other rights reserved.
#
# 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.
"""API model for MaaS network interface resource."""
import logging
import drydock_provisioner.drivers.node.maasdriver.models.base as model_base
import drydock_provisioner.drivers.node.maasdriver.models.fabric as maas_fabric
import drydock_provisioner.drivers.node.maasdriver.models.subnet as maas_subnet
import drydock_provisioner.drivers.node.maasdriver.models.vlan as maas_vlan
import drydock_provisioner.error as errors
class Interface(model_base.ResourceBase):
resource_url = 'nodes/{system_id}/interfaces/{resource_id}/'
fields = [
'resource_id',
'system_id',
'name',
'type',
'mac_address',
'vlan',
'links',
'effective_mtu',
'fabric_id',
'mtu',
'parents',
]
json_fields = [
'name',
'type',
'mac_address',
'vlan',
'mtu',
]
def __init__(self, api_client, **kwargs):
super(Interface, self).__init__(api_client, **kwargs)
self.logger = logging.getLogger('drydock.nodedriver.maasdriver')
def attach_fabric(self, fabric_id=None, fabric_name=None):
"""Attach this interface to a MaaS fabric.
Only one of fabric_id or fabric_name should be specified. If both
are, fabric_id rules
:param fabric_id: The MaaS resource ID of a network Fabric to connect to
:param fabric_name: The name of a MaaS fabric to connect to
"""
fabric = None
fabrics = maas_fabric.Fabrics(self.api_client)
fabrics.refresh()
if fabric_id is not None:
fabric = fabrics.select(fabric_id)
elif fabric_name is not None:
fabric = fabrics.singleton({'name': fabric_name})
else:
self.logger.warning("Must specify fabric_id or fabric_name")
raise ValueError("Must specify fabric_id or fabric_name")
if fabric is None:
self.logger.warning(
"Fabric not found in MaaS for fabric_id %s, fabric_name %s" %
(fabric_id, fabric_name))
raise errors.DriverError(
"Fabric not found in MaaS for fabric_id %s, fabric_name %s" %
(fabric_id, fabric_name))
# Locate the untagged VLAN for this fabric.
fabric_vlan = fabric.vlans.singleton({'vid': 0})
if fabric_vlan is None:
self.logger.warning(
"Cannot locate untagged VLAN on fabric %s" % (fabric_id))
raise errors.DriverError(
"Cannot locate untagged VLAN on fabric %s" % (fabric_id))
self.vlan = fabric_vlan.resource_id
self.logger.info(
"Attaching interface %s on system %s to VLAN %s on fabric %s" %
(self.resource_id, self.system_id, fabric_vlan.resource_id,
fabric.resource_id))
self.update()
def is_linked(self, subnet_id):
"""Check if this interface is linked to the given subnet.
:param subnet_id: MaaS resource id of the subnet
"""
for link in self.links:
if link.get('subnet_id', None) == subnet_id:
return True
return False
def disconnect(self):
"""Disconnect this interface from subnets and VLANs."""
url = self.interpolate_url()
self.logger.debug(
"Disconnecting interface %s from networks." % (self.name))
resp = self.api_client.post(url, op='disconnect')
if not resp.ok:
self.logger.warning(
"Could not disconnect interface, MaaS error: %s - %s" %
(resp.status_code, resp.text))
raise errors.DriverError(
"Could not disconnect interface, MaaS error: %s - %s" %
(resp.status_code, resp.text))
def unlink_subnet(self, subnet_id):
for link in self.links:
if link.get('subnet_id', None) == subnet_id:
url = self.interpolate_url()
resp = self.api_client.post(
url,
op='unlink_subnet',
files={'id': link.get('resource_id')})
if not resp.ok:
raise errors.DriverError("Error unlinking subnet")
else:
return
raise errors.DriverError(
"Error unlinking interface, Link to subnet_id %s not found." %
subnet_id)
def link_subnet(self,
subnet_id=None,
subnet_cidr=None,
ip_address=None,
primary=False):
"""Link this interface to a MaaS subnet.
One of subnet_id or subnet_cidr should be specified. If both are, subnet_id rules.
:param subnet_id: The MaaS resource ID of a network subnet to connect to
:param subnet_cidr: The CIDR of a MaaS subnet to connect to
:param ip_address: The IP address to assign this interface. Should be a string with
a static IP or None. If None, DHCP will be used.
:param primary: Boolean of whether this interface is the primary interface of the node. This
sets the node default gateway to the gateway of the subnet
"""
subnet = None
subnets = maas_subnet.Subnets(self.api_client)
subnets.refresh()
if subnet_id is not None:
subnet = subnets.select(subnet_id)
elif subnet_cidr is not None:
subnet = subnets.singleton({'cidr': subnet_cidr})
else:
self.logger.warning("Must specify subnet_id or subnet_cidr")
raise ValueError("Must specify subnet_id or subnet_cidr")
if subnet is None:
self.logger.warning(
"Subnet not found in MaaS for subnet_id %s, subnet_cidr %s" %
(subnet_id, subnet_cidr))
raise errors.DriverError(
"Subnet not found in MaaS for subnet_id %s, subnet_cidr %s" %
(subnet_id, subnet_cidr))
url = self.interpolate_url()
if self.is_linked(subnet.resource_id):
self.logger.info(
"Interface %s already linked to subnet %s, unlinking." %
(self.resource_id, subnet.resource_id))
self.unlink_subnet(subnet.resource_id)
# TODO(sh8121att) Probably need to enumerate link mode
options = {
'subnet': subnet.resource_id,
'default_gateway': primary,
}
if ip_address == 'dhcp':
options['mode'] = 'dhcp'
elif ip_address is not None:
options['ip_address'] = ip_address
options['mode'] = 'static'
else:
options['mode'] = 'link_up'
self.logger.debug(
"Linking interface %s to subnet: subnet=%s, mode=%s, address=%s, primary=%s"
% (self.resource_id, subnet.resource_id, options['mode'],
ip_address, primary))
resp = self.api_client.post(url, op='link_subnet', files=options)
if not resp.ok:
self.logger.error(
"Error linking interface %s to subnet %s - MaaS response %s: %s"
% (self.resouce_id, subnet.resource_id, resp.status_code,
resp.text))
raise errors.DriverError(
"Error linking interface %s to subnet %s - MaaS response %s" %
(self.resouce_id, subnet.resource_id, resp.status_code))
self.refresh()
return
def responds_to_ip(self, ip_address):
"""Check if this interface will respond to connections for an IP.
:param ip_address: string of the IP address we are checking
:return: true if this interface should respond to the IP, false otherwise
"""
for link in getattr(self, 'links', []):
if link.get('ip_address', None) == ip_address:
return True
return False
def responds_to_mac(self, mac_address):
"""Check if this interface will respond to a MAC address.
:param str mac_address: the MAC address to check
:return: true if this interface will respond to this MAC
"""
if mac_address.replace(':', '').upper() == self.mac_address.replace(':', '').upper():
return True
return False
def set_mtu(self, new_mtu):
"""Set interface MTU.
:param new_mtu: integer of the new MTU size for this inteface
"""
self.mtu = new_mtu
self.update()
@classmethod
def from_dict(cls, api_client, obj_dict):
"""Instantiate this model from a dictionary.
Because MaaS decides to replace the resource ids with the
representation of the resource, we must reverse it for a true
representation of the Interface
"""
refined_dict = {k: obj_dict.get(k, None) for k in cls.fields}
if 'id' in obj_dict.keys():
refined_dict['resource_id'] = obj_dict.get('id')
if isinstance(refined_dict.get('vlan', None), dict):
refined_dict['fabric_id'] = refined_dict['vlan']['fabric_id']
refined_dict['vlan'] = refined_dict['vlan']['id']
link_list = []
if isinstance(refined_dict.get('links', None), list):
for li in refined_dict['links']:
if isinstance(li, dict):
link = {'resource_id': li['id'], 'mode': li['mode']}
if li.get('subnet', None) is not None:
link['subnet_id'] = li['subnet']['id']
link['ip_address'] = li.get('ip_address', None)
link_list.append(link)
refined_dict['links'] = link_list
i = cls(api_client, **refined_dict)
return i
class Interfaces(model_base.ResourceCollectionBase):
collection_url = 'nodes/{system_id}/interfaces/'
collection_resource = Interface
def __init__(self, api_client, **kwargs):
super(Interfaces, self).__init__(api_client)
self.system_id = kwargs.get('system_id', None)
def create_vlan(self, vlan_tag, parent_name, mtu=None, tags=[]):
"""Create a new VLAN interface on this node.
:param vlan_tag: The VLAN ID (not MaaS resource id of a VLAN) to create interface for
:param parent_name: The name of a MaaS interface to build the VLAN interface on top of
:param mtu: Optional configuration of the interface MTU
:param tags: Optional list of string tags to apply to the VLAN interface
"""
self.refresh()
parent_iface = self.singleton({'name': parent_name})
if parent_iface is None:
self.logger.error(
"Cannot locate parent interface %s" % (parent_name))
raise errors.DriverError(
"Cannot locate parent interface %s" % (parent_name))
if parent_iface.vlan is None:
self.logger.error(
"Cannot create VLAN interface on disconnected parent %s" %
(parent_iface.resource_id))
raise errors.DriverError(
"Cannot create VLAN interface on disconnected parent %s" %
(parent_iface.resource_id))
vlans = maas_vlan.Vlans(
self.api_client, fabric_id=parent_iface.fabric_id)
vlans.refresh()
vlan = vlans.singleton({'vid': vlan_tag})
if vlan is None:
msg = ("Cannot locate VLAN %s on fabric %s to attach interface" %
(vlan_tag, parent_iface.fabric_id))
self.logger.warning(msg)
raise errors.DriverError(msg)
exists = self.singleton({'vlan': vlan.resource_id})
if exists is not None:
self.logger.info(
"Interface for VLAN %s already exists on node %s, skipping" %
(vlan_tag, self.system_id))
return exists
url = self.interpolate_url()
options = {
'tags': ','.join(tags),
'vlan': vlan.resource_id,
'parent': parent_iface.resource_id,
}
if mtu is not None:
options['mtu'] = mtu
resp = self.api_client.post(url, op='create_vlan', files=options)
if resp.status_code == 200:
resp_json = resp.json()
vlan_iface = Interface.from_dict(self.api_client, resp_json)
self.logger.debug(
"Created VLAN interface %s for parent %s attached to VLAN %s" %
(vlan_iface.resource_id, parent_iface.resource_id,
vlan.resource_id))
return vlan_iface
else:
self.logger.error(
"Error creating VLAN interface to VLAN %s on system %s - MaaS response %s: %s"
% (vlan.resource_id, self.system_id, resp.status_code,
resp.text))
raise errors.DriverError(
"Error creating VLAN interface to VLAN %s on system %s - MaaS response %s"
% (vlan.resource_id, self.system_id, resp.status_code))
self.refresh()
return
def create_bond(self,
device_name=None,
parent_names=[],
mtu=None,
mac_address=None,
tags=[],
fabric=None,
mode=None,
monitor_interval=None,
downdelay=None,
updelay=None,
lacp_rate=None,
hash_policy=None):
"""Create a new bonded interface on this node.
Slaves will be disconnected from networks.
:param device_name: What the bond interface should be named
:param parent_names: The names of interfaces to use as slaves
:param mtu: Optional configuration of the interface MTU
:param mac_address: String of valid 48-bit mac address, colon separated
:param tags: Optional list of string tags to apply to the bonded interface
:param fabric: Fabric (MaaS resource id) to attach the new bond to.
:param mode: The bonding mode
:param monitor_interval: The frequency of checking slave status in milliseconds
:param downdelay: The delay in disabling a down slave in milliseconds
:param updelay: The delay in enabling a recovered slave in milliseconds
:param lacp_rate: Rate LACP control units are emitted - 'fast' or 'slow'
:param hash_policy: Link selection hash policy
"""
self.refresh()
parent_ifaces = []
for n in parent_names:
parent_iface = self.singleton({'name': n})
if parent_iface is not None:
parent_ifaces.append(parent_iface)
else:
self.logger.error("Cannot locate slave interface %s" % (n))
if len(parent_ifaces) != len(parent_names):
self.logger.error("Missing slave interfaces.")
raise errors.DriverError("Missing slave interfaces.")
for i in parent_ifaces:
if mtu:
i.set_mtu(mtu)
i.disconnect()
i.attach_fabric(fabric_id=fabric)
url = self.interpolate_url()
options = {
'name': device_name,
'tags': tags,
'parents': [x.resource_id for x in parent_ifaces],
}
if mtu is not None:
options['mtu'] = mtu
if mac_address is not None:
options['mac_address'] = mac_address
if mode is not None:
options['bond_mode'] = mode
if monitor_interval is not None:
options['bond_miimon'] = monitor_interval
if downdelay is not None:
options['bond_downdelay'] = downdelay
if updelay is not None:
options['bond_updelay'] = updelay
if lacp_rate is not None:
options['bond_lacp_rate'] = lacp_rate
if hash_policy is not None:
options['bond_xmit_hash_policy'] = hash_policy
resp = self.api_client.post(url, op='create_bond', files=options)
if resp.status_code == 200:
resp_json = resp.json()
bond_iface = Interface.from_dict(self.api_client, resp_json)
self.logger.debug("Created bond interface %s with slaves %s" %
(bond_iface.resource_id, ','.join(parent_names)))
bond_iface.attach_fabric(fabric_id=fabric)
self.refresh()
return bond_iface
else:
self.logger.error(
"Error creating bond interface on system %s - MaaS response %s: %s"
% (self.system_id, resp.status_code, resp.text))
raise errors.DriverError(
"Error creating bond interface on system %s - MaaS response %s"
% (self.system_id, resp.status_code))