DRYD-50 Drydock support of NIC bonding

Implement interface bonding via the MaaS
API

- Documentation on writing topology definition of networking and host
  network attachment
- Adjust topology YAML schema for interface definition
- Add MaaS API support for create_bond
- Fix some bugs from Gerrit #377818
- Update MaaS API client to support multi-select options

Change-Id: I1c42300ede3f67595ebc8029b0f375622b459254
This commit is contained in:
Scott Hussey 2017-09-12 16:35:02 -05:00
parent 864f0d35a5
commit 48df97e133
14 changed files with 686 additions and 202 deletions

View File

@ -12,17 +12,279 @@ metadata.
The best source for a sample of the YAML schema for a topology is the unit
test input source_ /tests/yaml_samples/fullsite.yaml in tests/yaml_samples/fullsite.yaml.
Defining Networking
===================
Network definitions in the topology are described by two document types: NetworkLink and
Network. NetworkLink describes a physical or logical link between a node and switch. It
is concerned with attributes that must be agreed upon by both endpoints: bonding, media
speed, trunking, etc. A Network describes the layer 2 and layer 3 networks accessible
over a link.
Network Links
-------------
The NetworkLink document defines layer 1 and layer 2 attributes that should be in-sync
between the node and the switch. Each link can support a single untagged VLAN and 0 or more
tagged VLANs.
Example YAML schema of the NetworkLink spec::
spec:
bonding:
mode: 802.3ad
hash: layer3+4
peer_rate: slow
mtu: 9000
linkspeed: auto
trunking:
mode: 802.1q
allowed_networks:
- public
- mgmt
``bonding`` describes combining multiple physical links into a single logical link (aka LAG
or link aggregation group).
* ``mode``: What bonding mode to configure
* ``disabled``: Do not configure a bond
* ``802.3ad``: Use 802.3ad dynamic aggregation (aka LACP)
* ``active-backup``: Use static active/standby bonding
* ``balanced-rr``: Use static round-robin bonding
For a ``mode`` of ``802.3ad`` the below attributes are available, but optional.
* ``hash``: The link selection hash. Supported values are ``layer3+4``, ``layer2+3``, ``layer2``. Default is ``layer3+4``
* ``peer_rate``: How frequently to send LACP control frames. Supported values are ``fast`` and ``slow``. Default is ``fast``
* ``mon_rate``: Interval between checking link state in milliseconds. Default is ``100``
* ``up_delay``: Delay in milliseconds between a link coming up and being marked up in the bond. Must be greater than ``mon_rate``. Default is ``200``
* ``down_delay``: Delay in milliseconds between a link going down and being marked down in the bond. Must be greater than ``mon_rate``. Default is ``200``
``mtu`` is the maximum transmission unit for the link. It must be equal or greater than the MTU of any VLAN interfaces
using the link. Default is ``1500``.
``linkspeed`` is the physical layer speed and duplex. Recommended to always be ``auto``
``trunking`` describes how multiple layer 2 networks will be multiplexed on the link.
* ``mode``: Can be ``disabled`` for no trunking or ``802.1q`` for standard VLAN tagging
* ``default_network``: For ``mode: disabled``, this is the single network on the link. For ``mode: 802.1q`` this is optionally the network accessed by untagged frames.
``allowed_networks`` is a sequence of network names listing all networks allowed on this link. Each Network can
be listed on one and only one NetworkLink.
Network
-------
The Network document defines the layer 2 and layer 3 networks nodes will access. Each Network is accessible over
exactly one NetworkLink. However that NetworkLink can be attached to different interfaces on different nodes
to support changing hardware configurations.
Example YAML schema of the Network spec::
spec:
vlan: '102'
mtu: 1500
cidr: 172.16.3.0/24
ranges:
- type: static
start: 172.16.3.15
end: 172.16.3.200
- type: dhcp
start: 172.16.3.201
end: 172.16.3.254
routes:
- subnet: 0.0.0.0/0
gateway: 172.16.3.1
metric: 10
dns:
domain: sitename.example.com
servers: 8.8.8.8
If a Network is accessible over a NetworkLink using 802.1q VLAN tagging, the ``vlan`` attribute
specified the VLAN tag for this Network. It should be omitted for non-tagged Networks.
``mtu`` is the maximum transmission unit for this Network. Must be equal or less than the ``mtu``
defined for the hosting NetworkLink. Can be omitted to default to the NetworkLink ``mtu``.
``cidr`` is the classless inter-domain routing address for the network.
``ranges`` defines a sequence of IP addresses within the defined ``cidr``. Ranges cannot overlap.
* ``type``: The type of address range.
* ``static``: A range used for static, explicit address assignments for nodes.
* ``dhcp``: A range used for assigning DHCP addresses. Note that a network being used for PXE booting must have a DHCP range defined.
* ``reserved``: A range of addresses that will not be used by MaaS.
* ``start``: The starting IP of the range, inclusive.
* ``end``: The last IP of the range, inclusive
*NOTE: Static routes is not currently implemented beyond specifying a route for 0.0.0.0/0 for default route*
``routes`` defines a list of static routes to be configured on nodes attached to this network.
* ``subnet``: Destination CIDR for the route
* ``gateway``: The gateway IP on this Network to use for accessing the destination
* ``metric``: The metric or weight for this route
``dns`` is used for specifying the list of DNS servers to use if this network
is the priamry network for the node.
* ``servers``: A comma-separated list of IP addresses to use for DNS resolution
* ``domain``: A domain that can be used for automated registeration of IP addresses assigned from this Network
DHCP Relay
~~~~~~~~~~
DHCP relaying is used when a DHCP server is not attached to the same layer 2 broadcast domain as nodes that
are being PXE booted. The DHCP requests from the node are consumed by the relay (generally configured on a
top-of-rack switch) which then enscapsulates the request in layer 3 routing and sends it to an upstream DHCP
server. The Network spec supports a ``dhcp_relay`` key for Networks that should relay DHCP requests.
* The Network must have a configured DHCP relay, this is *not* configured by Drydock or MaaS.
* The ``upstream_target`` IP address must be a host IP address for a MaaS rack controller
* The Network must have a defined DHCP address range.
* The upstream target network must have a defined DHCP address range.
The ``dhcp_relay`` stanza::
dhcp_relay:
upstream_target: 172.16.4.100
Defining Node Configuration
===========================
Node configuration is defined in three documents: HostProfile, HardwareProfile and BaremetalNode. HardwareProfile
defines attributes directly related to hardware configuration such as card-slot layout and firmware levels. HostProfile
is a generic definition for how a node should be configured such that many nodes can reference a single HostProfile
and each will be configured identically. A BaremetalNode is a concrete reference to particular physical node.
The BaremetalNode definition will reference a HostProfile and can then extend or override any of the configuration values.
Example HostProfile and BaremetalNode configuration::
---
apiVersion: 'drydock/v1'
kind: HostProfile
metadata:
name: defaults
region: sitename
date: 17-FEB-2017
author: sh8121@att.com
spec:
# configuration values
---
apiVersion: 'drydock/v1'
kind: HostProfile
metadata:
name: compute_node
region: sitename
date: 17-FEB-2017
author: sh8121@att.com
spec:
host_profile: defaults
# compute_node customizations to defaults
---
apiVersion: 'drydock/v1'
kind: BaremetalNode
metadata:
name: compute01
region: sitename
date: 17-FEB-2017
author: sh8121@att.com
spec:
host_profile: compute_node
# configuration customization specific to single node compute01
...
In the above example, the ``compute_node`` HostProfile adopts all values from the ``defaults``
HostProfile and can then override defined values or append additional values. BaremetalNode
``compute01`` then adopts all values from the ``compute_node`` HostProfile (which includes all
the configuration items it adopted from ``defaults``) and can then again override or append any
configuration that is specific to that node.
Defining Node Interfaces and Network Addressing
===============================================
Node network attachment can be described in a HostProfile or a BaremetalNode document. Node addressing
is allowed only in a BaremetalNode document. If a HostProfile or BaremetalNode needs to remove a defined
interface from an inherited configuration, it can set the mapping value for the interface name to ``null``.
Once the interface attachments to networks is defined, HostProfile and BaremetalNode specs must define a
``primary_network`` attribute to denote which network the node should use a the primary route. This designation
Interfaces
----------
Interfaces for a node can be described in either a HostProfile or BaremetalNode definition. This will attach
a defined NetworkLink to a host interface and define which Networks should be configured to use that interface.
Example interface definition YAML schema::
interfaces:
pxe:
device_link: pxe
labels:
pxe: true
slaves:
- prim_nic01
networks:
- pxe
bond0:
device_link: gp
slaves:
- prim_nic01
- prim_nic02
networks:
- mgmt
- private
Each key in the interfaces mapping is a defined interface. The key is the name that will be used
on the deployed node for the interface. The value must be a mapping defining the interface configuration
or ``null`` to denote removal of that interface for an inherited configuration.
* ``device_link``: The name of the defined NetworkLink that will be attached to this interface. The NetworkLink
definition includes part of the interface configuration such as bonding.
* ``labels``: Metadata for describing this interface.
* ``slaves``: The list of hardware interfaces used for creating this interface. This value can be a device alias
defined in the HardwareProfile or the kernel name of the hardware interface. For bonded interfaces, this would
list all the slaves. For non-bonded interfaces, this should list the single hardware interface used.
* ``networks``: This is the list of networks to enable on this interface. If multiple networks are listed, the
NetworkLink attached to this interface must have trunking enabled or the design validation will fail.
Addressing
----------
Addressing for a node can only be defined in a BaremetalNode definition. The ``addressing`` stanza simply
defines a static IP address or ``dhcp`` for each network a node should have a configured layer 3 interface on. It
is a valid design to omit networks from the ``addressing`` stanza, in that case the interface attached to the omitted
network will be configured as link up with no address.
Example ``addressing`` YAML schema::
addressing:
- network: pxe
address: dhcp
- network: mgmt
address: 172.16.1.21
- network: private
address: 172.16.2.21
- network: oob
address: 172.16.100.21
Defining Node Storage
=====================
Storage can be defined in the `storage` stanza of either a HostProfile or BaremetalNode
Storage can be defined in the ``storage`` stanza of either a HostProfile or BaremetalNode
document. The storage configuration can describe creation of partitions on physical disks,
the assignment of physical disks and/or partitions to volume groups, and the creation of
logical volumes. Drydock will make a best effort to parse out system-level storage such
as the root filesystem or boot filesystem and take appropriate steps to configure them in
the active node provisioning driver.
the active node provisioning driver. At a minimum the storage configuration *must* contain
a root filesystem partition.
Example YAML schema of the `storage` stanza::
Example YAML schema of the ``storage`` stanza::
storage:
physical_devices:
@ -58,23 +320,21 @@ Example YAML schema of the `storage` stanza::
Schema
------
The `storage` stanza can contain two top level keys: `physical_devices` and
`volume_groups`. The latter is optional.
The ``storage`` stanza can contain two top level keys: ``physical_devices`` and
``volume_groups``. The latter is optional.
Physical Devices and Partitions
-------------------------------
A physical device can either be carved up in partitions (including a single partition
consuming the entire device) or added to a volume group as a physical volume. Each
key in the `physical_devices` mapping represents a device on a node. The key should either
key in the ``physical_devices`` mapping represents a device on a node. The key should either
be a device alias defined in the HardwareProfile or the name of the device published
by the OS. The value of each key must be a mapping with the following keys
* `labels`: A mapping of key/value strings providing generic labels for the device
* `partitions`: A sequence of mappings listing the partitions to be created on the device.
The mapping is described below. Incompatible with the `volume_group` specification.
* `volume_group`: A volume group name to add the device to as a physical volume. Incompatible
with the `partitions` specification.
* ``labels``: A mapping of key/value strings providing generic labels for the device
* ``partitions``: A sequence of mappings listing the partitions to be created on the device. The mapping is described below. Incompatible with the ``volume_group`` specification.
* ``volume_group``: A volume group name to add the device to as a physical volume. Incompatible with the ``partitions`` specification.
Partition
~~~~~~~~~
@ -82,51 +342,51 @@ Partition
A partition mapping describes a GPT partition on a physical disk. It can left as a raw
block device or formatted and mounted as a filesystem
* `name`: Metadata describing the partition in the topology
* `size`: The size of the partition. See the *Size Format* section below
* `bootable`: Boolean whether this partition should be the bootable device
* `part_uuid`: A UUID4 formatted UUID to assign to the partition. If not specified one will be generated
* `filesystem`: A optional mapping describing how the partition should be formatted and mounted
* `mountpoint`: Where the filesystem should be mounted. If not specified the partition will be left as a raw deice
* `fstype`: The format of the filesyste. Defaults to ext4
* `mount_options`: fstab style mount options. Default is 'defaults'
* `fs_uuid`: A UUID4 formatted UUID to assign to the filesystem. If not specified one will be generated
* `fs_label`: A filesystem label to assign to the filesystem. Optional.
* ``name``: Metadata describing the partition in the topology
* ``size``: The size of the partition. See the *Size Format* section below
* ``bootable``: Boolean whether this partition should be the bootable device
* ``part_uuid``: A UUID4 formatted UUID to assign to the partition. If not specified one will be generated
* ``filesystem``: A optional mapping describing how the partition should be formatted and mounted
* ``mountpoint``: Where the filesystem should be mounted. If not specified the partition will be left as a raw deice
* ``fstype``: The format of the filesyste. Defaults to ext4
* ``mount_options``: fstab style mount options. Default is 'defaults'
* ``fs_uuid``: A UUID4 formatted UUID to assign to the filesystem. If not specified one will be generated
* ``fs_label``: A filesystem label to assign to the filesystem. Optional.
Size Format
~~~~~~~~~~~
The size specification for a partition or logical volume is formed from three parts
* The first character can optionally be `>` indicating that the size specified is a minimum and the
calculated size should be at least the minimum and should take the rest of the available space on
the physical device or volume group.
* The first character can optionally be ``>`` indicating that the size specified is a minimum and the calculated size should be at least the minimum and should take the rest of the available space on the physical device or volume group.
* The second part is the numeric portion and must be an integer
* The third part is a label
* `m`\|`M`\|`mb`\|`MB`: Megabytes or 10^6 * the numeric
* `g`\|`G`\|`gb`\|`GB`: Gigabytes or 10^9 * the numeric
* `t`\|`T`\|`tb`\|`TB`: Terabytes or 10^12 * the numeric
* `%`: The percentage of total device or volume group space
* ``m``\|``M``\|``mb``\|``MB``: Megabytes or 10^6 * the numeric
* ``g``\|``G``\|``gb``\|``GB``: Gigabytes or 10^9 * the numeric
* ``t``\|``T``\|``tb``\|``TB``: Terabytes or 10^12 * the numeric
* ``%``: The percentage of total device or volume group space
Volume Groups and Logical Volumes
---------------------------------
Logical volumes can be used to create RAID-0 volumes spanning multiple physical disks or partitions.
Each key in the `volume_groups` mapping is a name assigned to a volume group. This name must be specified
as the `volume_group` attribute on one or more physical devices or partitions, or the configuration is invalid.
Each key in the ``volume_groups`` mapping is a name assigned to a volume group. This name must be specified
as the ``volume_group`` attribute on one or more physical devices or partitions, or the configuration is invalid.
Each mapping value is another mapping describing the volume group.
* `vg_uuid`: A UUID4 format uuid applied to the volume group. If not specified, one is generated
* `logical_volumes`: A sequence of mappings listing the logical volumes to be created in the volume group
* ``vg_uuid``: A UUID4 format uuid applied to the volume group. If not specified, one is generated
* ``logical_volumes``: A sequence of mappings listing the logical volumes to be created in the volume group
Logical Volume
~~~~~~~~~~~~~~
A logical volume is a RAID-0 volume. Using logical volumes for `/` and `/boot` is supported
A logical volume is a RAID-0 volume. Using logical volumes for ``/`` and ``/boot`` is supported
* ``name``: Required field. Used as the logical volume name.
* ``size``: The logical volume size. See *Size Format* above for details.
* ``lv_uuid``: A UUID4 format uuid applied to the logical volume: If not specified, one is generated
* ``filesystem``: A mapping specifying how the logical volume should be formatted and mounted. See the *Partition* section above for filesystem details.
* `name`: Required field. Used as the logical volume name.
* `size`: The logical volume size. See *Size Format* above for details.
* `lv_uuid`: A UUID4 format uuid applied to the logical volume: If not specified, one is generated
* `filesystem`: A mapping specifying how the logical volume should be formatted and mounted. See the
*Partition* section above for filesystem details.

View File

@ -76,7 +76,7 @@ class MaasRequestFactory(object):
def test_connectivity(self):
try:
resp = self.get('version/')
except requests.Timeout as ex:
except requests.Timeout:
raise errors.TransientDriverError("Timeout connection to MaaS")
if resp.status_code in [500, 503]:
@ -122,24 +122,26 @@ class MaasRequestFactory(object):
if kwargs.get('files', None) is not None:
files = kwargs.pop('files')
files_tuples = {}
files_tuples = []
for (k, v) in files.items():
if v is None:
continue
files_tuples[k] = (
None,
base64.b64encode(str(v).encode('utf-8')).decode('utf-8'),
'text/plain; charset="utf-8"', {
'Content-Transfer-Encoding': 'base64'
})
# elif isinstance(v, str):
# files_tuples[k] = (None, base64.b64encode(v.encode('utf-8')).decode('utf-8'), 'text/plain; charset="utf-8"', {'Content-Transfer-Encoding': 'base64'})
# elif isinstance(v, int) or isinstance(v, bool):
# if isinstance(v, bool):
# v = int(v)
# files_tuples[k] = (None, base64.b64encode(v.to_bytes(2, byteorder='big')), 'application/octet-stream', {'Content-Transfer-Encoding': 'base64'})
elif isinstance(v, list):
for i in v:
files_tuples.append(
(k, (None, base64.b64encode(
str(i).encode('utf-8')).decode('utf-8'),
'text/plain; charset="utf-8"', {
'Content-Transfer-Encoding': 'base64'
})))
else:
files_tuples.append((k, (None, base64.b64encode(
str(v).encode('utf-8')).decode('utf-8'),
'text/plain; charset="utf-8"', {
'Content-Transfer-Encoding':
'base64'
})))
kwargs['files'] = files_tuples

View File

@ -1419,6 +1419,9 @@ class MaasTaskRunner(drivers.DriverTaskRunner):
"Located node %s in MaaS, starting interface configuration"
% (n))
machine.reset_network_config()
machine.refresh()
for i in node.interfaces:
nl = site_design.get_network_link(
i.network_link)
@ -1439,9 +1442,51 @@ class MaasTaskRunner(drivers.DriverTaskRunner):
failed = True
continue
# TODO(sh8121att): HardwareProfile device alias integration
iface = machine.get_network_interface(
i.device_name)
if nl.bonding_mode != hd_fields.NetworkLinkBondingMode.Disabled:
if len(i.get_hw_slaves()) > 1:
msg = "Building node %s interface %s as a bond." % (
n, i.device_name)
self.logger.debug(msg)
result_detail['detail'].append(msg)
hw_iface_list = i.get_hw_slaves()
iface = machine.interfaces.create_bond(
device_name=i.device_name,
parent_names=hw_iface_list,
mtu=nl.mtu,
fabric=fabric.resource_id,
mode=nl.bonding_mode,
monitor_interval=nl.
bonding_mon_rate,
downdelay=nl.bonding_down_delay,
updelay=nl.bonding_up_delay,
lacp_rate=nl.bonding_peer_rate,
hash_policy=nl.bonding_xmit_hash)
else:
msg = "Network link %s indicates bonding, interface %s has less than 2 slaves." % \
(nl.name, i.device_name)
self.logger.warning(msg)
result_detail['detail'].append(msg)
continue
else:
if len(i.get_hw_slaves()) > 1:
msg = "Network link %s disables bonding, interface %s has multiple slaves." % \
(nl.name, i.device_name)
self.logger.warning(msg)
result_detail['detail'].append(msg)
continue
elif len(i.get_hw_slaves()) == 0:
msg = "Interface %s has 0 slaves." % (
i.device_name)
self.logger.warning(msg)
result_detail['detail'].append(msg)
else:
msg = "Configuring interface %s on node %s" % (
i.device_name, n)
self.logger.debug(msg)
hw_iface = i.get_hw_slaves()[0]
# TODO(sh8121att): HardwareProfile device alias integration
iface = machine.get_network_interface(
hw_iface)
if iface is None:
self.logger.warning(
@ -1950,7 +1995,8 @@ class MaasTaskRunner(drivers.DriverTaskRunner):
maas_volgroup.refresh()
for lv in v.logical_volumes:
calc_size = MaasTaskRunner.calculate_bytes(size_str=lv.size, context=maas_volgroup)
calc_size = MaasTaskRunner.calculate_bytes(
size_str=lv.size, context=maas_volgroup)
bd_id = maas_volgroup.create_lv(
name=lv.name,
uuid_str=lv.lv_uuid,

View File

@ -56,7 +56,7 @@ class BlockDevice(model_base.ResourceBase):
def __init__(self, api_client, **kwargs):
super().__init__(api_client, **kwargs)
if getattr(self, 'resource_id', None) is not None:
if hasattr(self, 'resource_id') and hasattr(self, 'system_id'):
try:
self.partitions = maas_partition.Partitions(
api_client,
@ -116,7 +116,7 @@ class BlockDevice(model_base.ResourceBase):
(self.name))
return
if self.filesystem.get('mount_pount', None) is not None:
if self.filesystem.get('mount_point', None) is not None:
self.unmount()
url = self.interpolate_url()

View File

@ -37,13 +37,13 @@ class Interface(model_base.ResourceBase):
'effective_mtu',
'fabric_id',
'mtu',
'parents',
]
json_fields = [
'name',
'type',
'mac_address',
'vlan',
'links',
'mtu',
]
@ -98,12 +98,32 @@ class Interface(model_base.ResourceBase):
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 l in self.links:
if l.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 l in self.links:
if l.get('subnet_id', None) == subnet_id:
@ -216,7 +236,7 @@ class Interface(model_base.ResourceBase):
return False
def set_mtu(self, new_mtu):
"""Set interface MTU
"""Set interface MTU.
:param new_mtu: integer of the new MTU size for this inteface
"""
@ -284,14 +304,6 @@ class Interfaces(model_base.ResourceCollectionBase):
raise errors.DriverError("Cannot locate parent interface %s" %
(parent_name))
if parent_iface.type != 'physical':
self.logger.error(
"Cannot create VLAN interface on parent of type %s" %
(parent_iface.type))
raise errors.DriverError(
"Cannot create VLAN interface on parent of type %s" %
(parent_iface.type))
if parent_iface.vlan is None:
self.logger.error(
"Cannot create VLAN interface on disconnected parent %s" %
@ -352,3 +364,104 @@ class Interfaces(model_base.ResourceCollectionBase):
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))

View File

@ -48,7 +48,7 @@ class Machine(model_base.ResourceBase):
super(Machine, self).__init__(api_client, **kwargs)
# Replace generic dicts with interface collection model
if getattr(self, 'resource_id', None) is not None:
if hasattr(self, 'resource_id'):
self.interfaces = maas_interface.Interfaces(
api_client, system_id=self.resource_id)
self.interfaces.refresh()
@ -56,14 +56,14 @@ class Machine(model_base.ResourceBase):
self.block_devices = maas_blockdev.BlockDevices(
api_client, system_id=self.resource_id)
self.block_devices.refresh()
except Exception as ex:
except Exception:
self.logger.warning("Failed loading node %s block devices." %
(self.resource_id))
try:
self.volume_groups = maas_vg.VolumeGroups(
api_client, system_id=self.resource_id)
self.volume_groups.refresh()
except Exception as ex:
except Exception:
self.logger.warning("Failed load node %s volume groups." %
(self.resource_id))
else:
@ -84,6 +84,7 @@ class Machine(model_base.ResourceBase):
return None
def get_power_params(self):
"""Load power parameters for this node from MaaS."""
url = self.interpolate_url()
resp = self.api_client.get(url, op='power_parameters')
@ -91,6 +92,21 @@ class Machine(model_base.ResourceBase):
if resp.status_code == 200:
self.power_parameters = resp.json()
def reset_network_config(self):
"""Reset the node networking configuration."""
self.logger.info("Resetting networking configuration on node %s" %
(self.resource_id))
url = self.interpolate_url()
resp = self.api_client.post(url, op='restore_networking_configuration')
if not resp.ok:
msg = "Error resetting network on node %s: %s - %s" \
% (self.resource_id, resp.status_code, resp.text)
self.logger.error(msg)
raise errors.DriverError(msg)
def reset_storage_config(self):
"""Reset storage config on this machine.
@ -186,6 +202,10 @@ class Machine(model_base.ResourceBase):
raise errors.DriverError(msg)
def commission(self, debug=False):
"""Start the MaaS commissioning process.
:param debug: If true, enable ssh on the node and leave it power up after commission
"""
url = self.interpolate_url()
# If we want to debug this node commissioning, enable SSH
@ -206,6 +226,12 @@ class Machine(model_base.ResourceBase):
resp.status_code)
def deploy(self, user_data=None, platform=None, kernel=None):
"""Start the MaaS deployment process.
:param user_data: cloud-init user data
:param platform: Which image to install
:param kernel: Which kernel to enable
"""
deploy_options = {}
if user_data is not None:
@ -247,14 +273,14 @@ class Machine(model_base.ResourceBase):
return detail_config
def set_owner_data(self, key, value):
"""
Add/update/remove node owner data. If the machine is not currently allocated to a user
"""Add/update/remove node owner data.
If the machine is not currently allocated to a user
it cannot have owner data
:param key: Key of the owner data
:param value: Value of the owner data. If None, the key is removed
"""
url = self.interpolate_url()
resp = self.api_client.post(
@ -270,8 +296,9 @@ class Machine(model_base.ResourceBase):
resp.status_code)
def to_dict(self):
"""
Serialize this resource instance into a dict matching the
"""Serialize this resource instance into a dict.
The dict format matches the
MAAS representation of the resource
"""
data_dict = {}
@ -287,9 +314,9 @@ class Machine(model_base.ResourceBase):
@classmethod
def from_dict(cls, api_client, obj_dict):
"""
Create a instance of this resource class based on a dict
of MaaS type attributes
"""Create a instance of this resource class based on a dict.
Dict format matches MaaS type attributes
Customized for Machine due to use of system_id instead of id
as resource key
@ -297,7 +324,6 @@ class Machine(model_base.ResourceBase):
: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():
@ -327,12 +353,10 @@ class Machines(model_base.ResourceCollectionBase):
v.get_power_params()
def acquire_node(self, node_name):
"""
Acquire a commissioned node fro deployment
"""Acquire a commissioned node fro deployment.
:param node_name: The hostname of a node to acquire
"""
self.refresh()
node = self.singleton({'hostname': node_name})
@ -364,7 +388,8 @@ class Machines(model_base.ResourceCollectionBase):
return node
def identify_baremetal_node(self, node_model, update_name=True):
"""
"""Find MaaS node resource matching Drydock BaremetalNode.
Search all the defined MaaS Machines and attempt to match
one against the provided Drydock BaremetalNode model. Update
the MaaS instance with the correct hostname
@ -372,7 +397,6 @@ class Machines(model_base.ResourceCollectionBase):
:param node_model: Instance of objects.node.BaremetalNode to search MaaS for matching resource
:param update_name: Whether Drydock should update the MaaS resource name to match the Drydock design
"""
maas_node = None
if node_model.oob_type == 'ipmi':
@ -390,7 +414,7 @@ class Machines(model_base.ResourceCollectionBase):
'power_params.power_address':
node_oob_ip
})
except ValueError as ve:
except ValueError:
self.logger.warn(
"Error locating matching MaaS resource for OOB IP %s" %
(node_oob_ip))
@ -438,10 +462,11 @@ class Machines(model_base.ResourceCollectionBase):
return result
def add(self, res):
"""
Create a new resource in this collection in MaaS
"""Create a new resource in this collection in MaaS.
Customize as Machine resources use 'system_id' instead of 'id'
:param res: A instance of the Machine model
"""
data_dict = res.to_dict()
url = self.interpolate_url()

View File

@ -108,7 +108,7 @@ class Vlans(model_base.ResourceCollectionBase):
'name': res.name,
'description': getattr(res, 'description', None),
}
if getattr(res, 'vid', None) is None:
min_fields['vid'] = 0
else:
@ -124,7 +124,7 @@ class Vlans(model_base.ResourceCollectionBase):
# Submit PUT for additonal fields
res.update()
return res
raise errors.DriverError("Failed updating MAAS url %s - return code %s\n%s"
% (url, resp.status_code, resp.text))
"""

View File

@ -115,6 +115,17 @@ class VolumeGroup(model_base.ResourceBase):
self.logger.error(msg)
raise errors.DriverError(msg)
def delete(self):
"""Delete this volume group.
Override the default delete so that logical volumes can be
removed first.
"""
for lv in self.logical_volumes:
self.delete_lv(lv_name=lv)
super().delete()
@classmethod
def from_dict(cls, api_client, obj_dict):
"""Instantiate this model from a dictionary.

View File

@ -25,11 +25,7 @@ class DrydockSession(object):
:param string marker: (optional) external context marker
"""
def __init__(self,
host,
port=None,
scheme='http',
token=None,
def __init__(self, host, port=None, scheme='http', token=None,
marker=None):
self.__session = requests.Session()
self.__session.headers.update({

View File

@ -225,8 +225,6 @@ class YamlIngester(IngesterPlugin):
model.metalabels.append(l)
model.cidr = spec.get('cidr', None)
model.allocation_strategy = spec.get(
'allocation', 'static')
model.vlan_id = spec.get('vlan', None)
model.mtu = spec.get('mtu', None)
@ -414,25 +412,30 @@ class YamlIngester(IngesterPlugin):
vg.logical_volumes.append(lv)
interfaces = spec.get('interfaces', [])
interfaces = spec.get('interfaces', {})
model.interfaces = objects.HostInterfaceList()
for i in interfaces:
for k, v in interfaces.items():
int_model = objects.HostInterface()
int_model.device_name = i.get(
'device_name', None)
int_model.network_link = i.get(
# A null value indicates this interface should be removed
# from any parent profiles
if v is None:
int_model.device_name = '!' + k
continue
int_model.device_name = k
int_model.network_link = v.get(
'device_link', None)
int_model.hardware_slaves = []
slaves = i.get('slaves', [])
slaves = v.get('slaves', [])
for s in slaves:
int_model.hardware_slaves.append(s)
int_model.networks = []
networks = i.get('networks', [])
networks = v.get('networks', [])
for n in networks:
int_model.networks.append(n)

View File

@ -25,19 +25,27 @@ is compatible with the physical state of the site.
#### Validations ####
* Networking
** No static IP assignments are duplicated
** No static IP assignments are outside of the network they are targetted for
** All IP assignments are within declared ranges on the network
** Networks assigned to each node's interface are within the set of of the attached link's allowed\_networks
** No network is allowed on multiple network links
** Network MTU is equal or less than NetworkLink MTU
** MTU values are sane
* No static IP assignments are duplicated
* No static IP assignments are outside of the network they are targetted for
* All IP assignments are within declared ranges on the network
* No network is allowed on multiple network links
* Network MTU is equal or less than NetworkLink MTU
* MTU values are sane
* NetworkLink bond mode is compatible with other bond options
* NetworkLink with more than one allowed network supports trunking
* Storage
** Boot drive is above minimum size
** Root drive is above minimum size
** No physical device specifies a target VG and a partition list
** No partition specifies a target VG and a filesystem
* Boot drive is above minimum size
* Root drive is above minimum size
* No physical device specifies a target VG and a partition list
* No partition specifies a target VG and a filesystem
* All defined VGs have at least one defined PV (partition or physical device)
* Partition and LV sizing is sane
* Percentages don't sum to above 100%
* If percentages sum to 100%, no other partitions or LVs are defined
* Node
* Root filesystem is defined on a partition or LV
* Networks assigned to each node's interface are within the set of of the attached link's allowed\_networks
* Inter
### VerifySite ###
Verify site-wide resources are in a useful state

View File

@ -23,7 +23,6 @@ from drydock_provisioner.drivers.node.maasdriver.models.blockdev import BlockDev
from drydock_provisioner.drivers.node.maasdriver.models.volumegroup import VolumeGroup
class TestCalculateBytes():
def test_calculate_m_label(self):
'''Convert megabyte labels to x * 10^6 bytes.'''
@ -32,7 +31,8 @@ class TestCalculateBytes():
drive = BlockDevice(None, size=drive_size, available_size=drive_size)
calc_size = MaasTaskRunner.calculate_bytes(size_str=size_str, context=drive)
calc_size = MaasTaskRunner.calculate_bytes(
size_str=size_str, context=drive)
assert calc_size == 15 * 1000 * 1000
@ -42,7 +42,8 @@ class TestCalculateBytes():
drive_size = 20 * 1000 * 1000
drive = BlockDevice(None, size=drive_size, available_size=drive_size)
calc_size = MaasTaskRunner.calculate_bytes(size_str=size_str, context=drive)
calc_size = MaasTaskRunner.calculate_bytes(
size_str=size_str, context=drive)
assert calc_size == 15 * 1000 * 1000
@ -52,7 +53,8 @@ class TestCalculateBytes():
drive_size = 20 * 1000 * 1000
drive = BlockDevice(None, size=drive_size, available_size=drive_size)
calc_size = MaasTaskRunner.calculate_bytes(size_str=size_str, context=drive)
calc_size = MaasTaskRunner.calculate_bytes(
size_str=size_str, context=drive)
assert calc_size == 15 * 1000 * 1000
@ -62,7 +64,8 @@ class TestCalculateBytes():
drive_size = 20 * 1000 * 1000
drive = BlockDevice(None, size=drive_size, available_size=drive_size)
calc_size = MaasTaskRunner.calculate_bytes(size_str=size_str, context=drive)
calc_size = MaasTaskRunner.calculate_bytes(
size_str=size_str, context=drive)
assert calc_size == 15 * 1000 * 1000
@ -72,7 +75,8 @@ class TestCalculateBytes():
drive_size = 20 * 1000 * 1000 * 1000
drive = BlockDevice(None, size=drive_size, available_size=drive_size)
calc_size = MaasTaskRunner.calculate_bytes(size_str=size_str, context=drive)
calc_size = MaasTaskRunner.calculate_bytes(
size_str=size_str, context=drive)
assert calc_size == 15 * 1000 * 1000 * 1000
@ -82,7 +86,8 @@ class TestCalculateBytes():
drive_size = 20 * 1000 * 1000 * 1000
drive = BlockDevice(None, size=drive_size, available_size=drive_size)
calc_size = MaasTaskRunner.calculate_bytes(size_str=size_str, context=drive)
calc_size = MaasTaskRunner.calculate_bytes(
size_str=size_str, context=drive)
assert calc_size == 15 * 1000 * 1000 * 1000
@ -92,7 +97,8 @@ class TestCalculateBytes():
drive_size = 20 * 1000 * 1000 * 1000
drive = BlockDevice(None, size=drive_size, available_size=drive_size)
calc_size = MaasTaskRunner.calculate_bytes(size_str=size_str, context=drive)
calc_size = MaasTaskRunner.calculate_bytes(
size_str=size_str, context=drive)
assert calc_size == 15 * 1000 * 1000 * 1000
@ -102,7 +108,8 @@ class TestCalculateBytes():
drive_size = 20 * 1000 * 1000 * 1000
drive = BlockDevice(None, size=drive_size, available_size=drive_size)
calc_size = MaasTaskRunner.calculate_bytes(size_str=size_str, context=drive)
calc_size = MaasTaskRunner.calculate_bytes(
size_str=size_str, context=drive)
assert calc_size == 15 * 1000 * 1000 * 1000
@ -112,7 +119,8 @@ class TestCalculateBytes():
drive_size = 20 * 1000 * 1000 * 1000 * 1000
drive = BlockDevice(None, size=drive_size, available_size=drive_size)
calc_size = MaasTaskRunner.calculate_bytes(size_str=size_str, context=drive)
calc_size = MaasTaskRunner.calculate_bytes(
size_str=size_str, context=drive)
assert calc_size == 15 * 1000 * 1000 * 1000 * 1000
@ -122,7 +130,8 @@ class TestCalculateBytes():
drive_size = 20 * 1000 * 1000 * 1000 * 1000
drive = BlockDevice(None, size=drive_size, available_size=drive_size)
calc_size = MaasTaskRunner.calculate_bytes(size_str=size_str, context=drive)
calc_size = MaasTaskRunner.calculate_bytes(
size_str=size_str, context=drive)
assert calc_size == 15 * 1000 * 1000 * 1000 * 1000
@ -132,7 +141,8 @@ class TestCalculateBytes():
drive_size = 20 * 1000 * 1000 * 1000 * 1000
drive = BlockDevice(None, size=drive_size, available_size=drive_size)
calc_size = MaasTaskRunner.calculate_bytes(size_str=size_str, context=drive)
calc_size = MaasTaskRunner.calculate_bytes(
size_str=size_str, context=drive)
assert calc_size == 15 * 1000 * 1000 * 1000 * 1000
@ -142,55 +152,60 @@ class TestCalculateBytes():
drive_size = 20 * 1000 * 1000 * 1000 * 1000
drive = BlockDevice(None, size=drive_size, available_size=drive_size)
calc_size = MaasTaskRunner.calculate_bytes(size_str=size_str, context=drive)
calc_size = MaasTaskRunner.calculate_bytes(
size_str=size_str, context=drive)
assert calc_size == 15 * 1000 * 1000 * 1000 * 1000
def test_calculate_percent_blockdev(self):
'''Convert a percent of total blockdev space to explicit byte count.'''
drive_size = 20 * 1000 * 1000 # 20 mb drive
part_size = math.floor(.2 * drive_size) # calculate 20% of drive size
drive_size = 20 * 1000 * 1000 # 20 mb drive
part_size = math.floor(.2 * drive_size) # calculate 20% of drive size
size_str = '20%'
drive = BlockDevice(None, size=drive_size, available_size=drive_size)
calc_size = MaasTaskRunner.calculate_bytes(size_str=size_str, context=drive)
calc_size = MaasTaskRunner.calculate_bytes(
size_str=size_str, context=drive)
assert calc_size == part_size
def test_calculate_percent_vg(self):
'''Convert a percent of total blockdev space to explicit byte count.'''
vg_size = 20 * 1000 * 1000 # 20 mb drive
lv_size = math.floor(.2 * vg_size) # calculate 20% of drive size
vg_size = 20 * 1000 * 1000 # 20 mb drive
lv_size = math.floor(.2 * vg_size) # calculate 20% of drive size
size_str = '20%'
vg = VolumeGroup(None, size=vg_size, available_size=vg_size)
calc_size = MaasTaskRunner.calculate_bytes(size_str=size_str, context=vg)
calc_size = MaasTaskRunner.calculate_bytes(
size_str=size_str, context=vg)
assert calc_size == lv_size
def test_calculate_overprovision(self):
'''When calculated space is higher than available space, raise an exception.'''
vg_size = 20 * 1000 * 1000 # 20 mb drive
vg_size = 20 * 1000 * 1000 # 20 mb drive
vg_available = 10 # 10 bytes available
lv_size = math.floor(.8 * vg_size) # calculate 80% of drive size
lv_size = math.floor(.8 * vg_size) # calculate 80% of drive size
size_str = '80%'
vg = VolumeGroup(None, size=vg_size, available_size=vg_available)
with pytest.raises(error.NotEnoughStorage):
calc_size = MaasTaskRunner.calculate_bytes(size_str=size_str, context=vg)
calc_size = MaasTaskRunner.calculate_bytes(
size_str=size_str, context=vg)
def test_calculate_min_label(self):
'''Adding the min marker '>' should provision all available space.'''
vg_size = 20 * 1000 * 1000 # 20 mb drive
vg_size = 20 * 1000 * 1000 # 20 mb drive
vg_available = 15 * 1000 * 1000
lv_size = math.floor(.1 * vg_size) # calculate 20% of drive size
lv_size = math.floor(.1 * vg_size) # calculate 20% of drive size
size_str = '>10%'
vg = VolumeGroup(None, size=vg_size, available_size=vg_available)
calc_size = MaasTaskRunner.calculate_bytes(size_str=size_str, context=vg)
calc_size = MaasTaskRunner.calculate_bytes(
size_str=size_str, context=vg)
assert calc_size == vg_available

View File

@ -170,8 +170,6 @@ metadata:
author: sh8121@att.com
description: Describe layer 2/3 attributes. Primarily CIs used for configuring server interfaces
spec:
# Layer 2 VLAN segment id, could support other segmentations. Optional
vlan_id: '99'
# If this network utilizes a dhcp relay, where does it forward DHCPDISCOVER requests to?
dhcp_relay:
# Required if Drydock is configuring a switch with the relay
@ -203,7 +201,7 @@ metadata:
author: sh8121@att.com
description: Describe layer 2/3 attributes. Primarily CIs used for configuring server interfaces
spec:
vlan_id: '100'
vlan: '100'
# Allow MTU to be inherited from link the network rides on
mtu: 1500
# Network address
@ -236,7 +234,7 @@ metadata:
author: sh8121@att.com
description: Describe layer 2/3 attributes. Primarily CIs used for configuring server interfaces
spec:
vlan_id: '101'
vlan: '101'
mtu: 9000
cidr: 172.16.2.0/24
# Desribe IP address ranges
@ -259,7 +257,7 @@ metadata:
author: sh8121@att.com
description: Describe layer 2/3 attributes. Primarily CIs used for configuring server interfaces
spec:
vlan_id: '102'
vlan: '102'
# MTU size for the VLAN interface
mtu: 1500
cidr: 172.16.3.0/24
@ -369,18 +367,20 @@ spec:
primary_network: mgmt
interfaces:
# Keyed on device_name
# pxe is a special marker indicating which device should be used for pxe boot
- device_name: pxe
pxe:
# The network link attached to this
device_link: pxe
# Slaves will specify aliases from hwdefinition.yaml
labels:
# this interface will be used only for PXE booting during deploy
noconfig: true
# Slaves will specify aliases from hwdefinition.yaml or kernel device names
slaves:
- prim_nic01
# Which networks will be configured on this interface
networks:
- pxe
- device_name: bond0
network_link: gp
bond0:
device_link: gp
# If multiple slaves are specified, but no bonding config
# is applied to the link, design validation will fail
slaves:
@ -409,7 +409,7 @@ spec:
# the hostname for a server, could be used in multiple DNS domains to
# represent different interfaces
interfaces:
- device_name: bond0
bond0:
networks:
# '!' prefix for the value of the primary key indicates a record should be removed
- '!private'

View File

@ -61,33 +61,36 @@ spec:
credential: admin
# Specify storage layout of base OS. Ceph out of scope
storage:
# How storage should be carved up: lvm (logical volumes), flat
# (single partition)
layout: lvm
# Info specific to the boot and root disk/partitions
bootdisk:
# Device will specify an alias defined in hwdefinition.yaml
device: primary_boot
# For LVM, the size of the partition added to VG as a PV
# For flat, the size of the partition formatted as ext4
root_size: 50g
# The /boot partition. If not specified, /boot will in root
boot_size: 2g
# Info for additional partitions. Need to balance between
# flexibility and complexity
partitions:
- name: logs
device: primary_boot
# Partition uuid if needed
part_uuid: 84db9664-f45e-11e6-823d-080027ef795a
size: 10g
# Optional, can carve up unformatted block devices
mountpoint: /var/log
fstype: ext4
mount_options: defaults
# Filesystem UUID or label can be specified. UUID recommended
fs_uuid: cdb74f1c-9e50-4e51-be1d-068b0e9ff69e
fs_label: logs
physical_devices:
sda:
labels:
role: rootdisk
partitions:
- name: root
size: 20g
bootable: true
filesystem:
mountpoint: '/'
fstype: 'ext4'
mount_options: 'defaults'
- name: boot
size: 1g
bootable: false
filesystem:
mountpoint: '/boot'
fstype: 'ext4'
mount_options: 'defaults'
sdb:
volume_group: 'log_vg'
volume_groups:
log_vg:
logical_volumes:
- name: 'log_lv'
size: '500m'
filesystem:
mountpoint: '/var/log'
fstype: 'xfs'
mount_options: 'defaults'
# Platform (Operating System) settings
platform:
image: ubuntu_16.04
@ -128,28 +131,30 @@ spec:
primary_network: mgmt
interfaces:
# Keyed on device_name
# pxe is a special marker indicating which device should be used for pxe boot
- device_name: pxe
# The network link attached to this
device_link: pxe
# Slaves will specify aliases from hwdefinition.yaml
slaves:
- prim_nic01
# Which networks will be configured on this interface
networks:
- pxe
- device_name: bond0
network_link: gp
# If multiple slaves are specified, but no bonding config
# is applied to the link, design validation will fail
slaves:
- prim_nic01
- prim_nic02
# If multiple networks are specified, but no trunking
# config is applied to the link, design validation will fail
networks:
- mgmt
- private
pxe:
# The network link attached to this
device_link: pxe
labels:
# this interface will be used only for PXE booting during deploy
noconfig: true
# Slaves will specify aliases from hwdefinition.yaml or kernel device names
slaves:
- prim_nic01
# Which networks will be configured on this interface
networks:
- pxe
bond0:
device_link: gp
# If multiple slaves are specified, but no bonding config
# is applied to the link, design validation will fail
slaves:
- prim_nic01
- prim_nic02
# If multiple networks are specified, but no trunking
# config is applied to the link, design validation will fail
networks:
- mgmt
- private
metadata:
# Explicit tag assignment
tags: