Drydock support for multirack and MTU

- Add steps in CreateNetworkTemplate task for dhcp relay
- Add steps in CreateNetworkTemplate task for MTU settings
- Add steps in ApplyNodeNetworking to set interface MTU

Change-Id: Ia575c2b2686d8ad98f5878c4b755e58607470b38
This commit is contained in:
Scott Hussey 2017-08-28 11:51:42 -05:00
parent 32be590a53
commit cc528a070a
5 changed files with 134 additions and 139 deletions

View File

@ -1055,7 +1055,16 @@ class MaasTaskRunner(drivers.DriverTaskRunner):
if link_fabric is None:
link_fabric = maas_fabric.Fabric(
self.maas_client, name=l.name)
fabrics.add(link_fabric)
link_fabric = fabrics.add(link_fabric)
# Ensure that the MTU of the untagged VLAN on the fabric
# matches that on the NetworkLink config
vlan_list = maas_vlan.Vlans(self.maas_client, fabric_id=link_fabric.resource_id)
vlan_list.refresh()
vlan = vlan_list.singleton({'vid': 0})
vlan.mtu = l.mtu
vlan.update()
# Now that we have the fabrics sorted out, check
# that VLAN tags and subnet attributes are correct
@ -1187,36 +1196,59 @@ class MaasTaskRunner(drivers.DriverTaskRunner):
vlan = vlan_list.select(subnet.vlan)
if dhcp_on and not vlan.dhcp_on:
# check if design requires a dhcp relay and if the MaaS vlan already uses a dhcp_relay
self.logger.info(
"DHCP enabled for subnet %s, activating in MaaS"
% (subnet.name))
# TODO(sh8121att): Ugly hack assuming a single rack controller
# for now until we implement multirack
resp = self.maas_client.get("rackcontrollers/")
rack_ctlrs = maas_rack.RackControllers(self.maas_client)
rack_ctlrs.refresh()
if resp.ok:
resp_json = resp.json()
dhcp_config_set=False
if not isinstance(resp_json, list):
self.logger.warning(
"Unexpected response when querying list of rack controllers"
)
self.logger.debug("%s" % resp.text)
else:
if len(resp_json) > 1:
self.logger.warning(
"Received more than one rack controller, defaulting to first"
for r in rack_ctlrs:
if n.dhcp_relay_upstream_target is not None:
if r.interface_for_ip(n.dhcp_relay_upstream_target):
iface = r.interface_for_ip(n.dhcp_relay_upstream_target)
vlan.relay_vlan = iface.vlan
self.logger.debug(
"Relaying DHCP on vlan %s to vlan %s" % (vlan.resource_id, vlan.relay_vlan)
)
result_detail['detail'].append(
"Relaying DHCP on vlan %s to vlan %s" % (vlan.resource_id, vlan.relay_vlan))
vlan.update()
dhcp_config_set=True
break
else:
for i in r.interfaces:
if i.vlan == vlan.resource_id:
self.logger.debug(
"Rack controller %s has interface on vlan %s" %
(r.resource_id, vlan.resource_id))
rackctl_id = r.resource_id
rackctl_id = resp_json[0]['system_id']
vlan.dhcp_on = True
vlan.primary_rack = rackctl_id
self.logger.debug(
"Enabling DHCP on VLAN %s managed by rack ctlr %s"
% (vlan.resource_id, rackctl_id))
result_detail['detail'].append(
"Enabling DHCP on VLAN %s managed by rack ctlr %s"
% (vlan.resource_id, rackctl_id))
vlan.update()
dhcp_config_set=True
break
if dhcp_config_set:
break
if not dhcp_config_set:
self.logger.error(
"Network %s requires DHCP, but could not locate a rack controller to serve it." %
(n.name))
result_detail['detail'].append(
"Network %s requires DHCP, but could not locate a rack controller to serve it." %
(n.name))
vlan.dhcp_on = True
vlan.primary_rack = rackctl_id
vlan.update()
self.logger.debug(
"Enabling DHCP on VLAN %s managed by rack ctlr %s"
% (vlan.resource_id, rackctl_id))
elif dhcp_on and vlan.dhcp_on:
self.logger.info(
"DHCP already enabled for subnet %s" %
@ -1576,6 +1608,12 @@ class MaasTaskRunner(drivers.DriverTaskRunner):
iface.attach_fabric(
fabric_id=fabric.resource_id)
if iface.effective_mtu != nl.mtu:
self.logger.debug(
"Updating interface %s MTU to %s"
% (i.device_name, nl.mtu))
iface.set_mtu(nl.mtu)
for iface_net in getattr(i, 'networks', []):
dd_net = site_design.get_network(iface_net)

View File

@ -11,6 +11,8 @@
# 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
@ -26,10 +28,10 @@ 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'
'links', 'effective_mtu', 'fabric_id', 'mtu',
]
json_fields = [
'name', 'type', 'mac_address', 'vlan', 'links', 'effective_mtu'
'name', 'type', 'mac_address', 'vlan', 'links', 'mtu',
]
def __init__(self, api_client, **kwargs):
@ -37,14 +39,14 @@ class Interface(model_base.ResourceBase):
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
"""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)
@ -113,9 +115,9 @@ class Interface(model_base.ResourceBase):
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.
"""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
@ -124,7 +126,6 @@ class Interface(model_base.ResourceBase):
: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)
@ -154,7 +155,7 @@ class Interface(model_base.ResourceBase):
(self.resource_id, subnet.resource_id))
self.unlink_subnet(subnet.resource_id)
# TODO Probably need to enumerate link mode
# TODO(sh8121att) Probably need to enumerate link mode
options = {
'subnet': subnet.resource_id,
'default_gateway': primary,
@ -188,9 +189,31 @@ class Interface(model_base.ResourceBase):
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 l in getattr(self, 'links', []):
if l.get('ip_address', None) == ip_address:
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
@ -231,15 +254,13 @@ class Interfaces(model_base.ResourceCollectionBase):
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
"""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})

View File

@ -41,6 +41,18 @@ class Machine(model_base.ResourceBase):
else:
self.interfaces = None
def interface_for_ip(self, ip_address):
"""Find the machine interface that will respond to ip_address.
:param ip_address: The IP address to check interfaces
:return: The interface that responds to this IP or None
"""
for i in self.interfaces:
if i.responds_to_ip(ip_address):
return i
return None
def get_power_params(self):
url = self.interpolate_url()
@ -283,9 +295,7 @@ class Machines(model_base.ResourceCollectionBase):
return maas_node
def query(self, query):
"""
Custom query method to deal with complex fields
"""
"""Custom query method to deal with complex fields."""
result = list(self.resources.values())
for (k, v) in query.items():
if k.startswith('power_params.'):

View File

@ -17,9 +17,10 @@ import bson
import drydock_provisioner.drivers.node.maasdriver.models.base as model_base
import drydock_provisioner.drivers.node.maasdriver.models.interface as maas_interface
import drydock_provisioner.drivers.node.maasdriver.models.machine as maas_machine
class RackController(model_base.ResourceBase):
class RackController(maas_machine.Machine):
"""Model for a rack controller singleton."""
# These are the services that must be 'running'
@ -47,29 +48,6 @@ class RackController(model_base.ResourceBase):
def __init__(self, api_client, **kwargs):
super().__init__(api_client, **kwargs)
# Replace generic dicts with interface collection model
if getattr(self, 'resource_id', None) is not None:
self.interfaces = maas_interface.Interfaces(
api_client, system_id=self.resource_id)
self.interfaces.refresh()
else:
self.interfaces = None
def get_power_params(self):
"""Get parameters for managing server power."""
url = self.interpolate_url()
resp = self.api_client.get(url, op='power_parameters')
if resp.status_code == 200:
self.power_parameters = resp.json()
def get_network_interface(self, iface_name):
"""Retrieve network interface on this machine."""
if self.interfaces is not None:
iface = self.interfaces.singleton({'name': iface_name})
return iface
def get_services(self):
"""Get status of required services on this rack controller."""
self.refresh()
@ -89,55 +67,6 @@ class RackController(model_base.ResourceBase):
return svc_status
def get_details(self):
url = self.interpolate_url()
resp = self.api_client.get(url, op='details')
if resp.status_code == 200:
detail_config = bson.loads(resp.text)
return detail_config
def to_dict(self):
"""Serialize this resource instance.
Serialize into a dict matching the MAAS representation of the resource
"""
data_dict = {}
for f in self.json_fields:
if getattr(self, f, None) is not None:
if f == 'resource_id':
data_dict['system_id'] = getattr(self, f)
else:
data_dict[f] = getattr(self, f)
return data_dict
@classmethod
def from_dict(cls, api_client, obj_dict):
"""Create a instance of this resource class based on a dict of MaaS type attributes.
Customized for Machine due to use of system_id instead of id
as resource key
:param api_client: Instance of api_client.MaasRequestFactory for accessing MaaS API
:param obj_dict: Python dict as parsed from MaaS API JSON representing this resource type
"""
refined_dict = {k: obj_dict.get(k, None) for k in cls.fields}
if 'system_id' in obj_dict.keys():
refined_dict['resource_id'] = obj_dict.get('system_id')
# Capture the boot interface MAC to allow for node id of VMs
if 'boot_interface' in obj_dict.keys():
if isinstance(obj_dict['boot_interface'], dict):
refined_dict['boot_mac'] = obj_dict['boot_interface'][
'mac_address']
i = cls(api_client, **refined_dict)
return i
class RackControllers(model_base.ResourceCollectionBase):
"""Model for a collection of rack controllers."""
@ -147,27 +76,3 @@ class RackControllers(model_base.ResourceCollectionBase):
def __init__(self, api_client, **kwargs):
super().__init__(api_client)
# Add the OOB power parameters to each machine instance
def collect_power_params(self):
for k, v in self.resources.items():
v.get_power_params()
def query(self, query):
"""Custom query method to deal with complex fields."""
result = list(self.resources.values())
for (k, v) in query.items():
if k.startswith('power_params.'):
field = k[13:]
result = [
i for i in result
if str(
getattr(i, 'power_parameters', {}).get(field, None)) ==
str(v)
]
else:
result = [
i for i in result if str(getattr(i, k, None)) == str(v)
]
return result

View File

@ -22,11 +22,11 @@ class Vlan(model_base.ResourceBase):
resource_url = 'fabrics/{fabric_id}/vlans/{api_id}/'
fields = [
'resource_id', 'name', 'description', 'vid', 'fabric_id', 'dhcp_on',
'mtu', 'primary_rack', 'secondary_rack'
'mtu', 'primary_rack', 'secondary_rack', 'relay_vlan',
]
json_fields = [
'name', 'description', 'vid', 'dhcp_on', 'mtu', 'primary_rack',
'secondary_rack'
'secondary_rack', 'relay_vlan',
]
def __init__(self, api_client, **kwargs):
@ -52,6 +52,27 @@ class Vlan(model_base.ResourceBase):
else:
self.vid = int(new_vid)
def set_dhcp_relay(self, relay_vlan_id):
self.relay_vlan = relay_vlan_id
self.update()
@classmethod
def from_dict(cls, api_client, obj_dict):
"""
Because MaaS decides to replace the relay_vlan id with the
representation of the VLAN, we must reverse it for a true
representation of the resource
"""
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('relay_vlan', None), dict):
refined_dict['relay_vlan'] = refined_dict['relay_vlan']['id']
i = cls(api_client, **refined_dict)
return i
class Vlans(model_base.ResourceCollectionBase):