
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
481 lines
17 KiB
Python
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))
|