Adding files to ansible core modules.
This commit is contained in:
parent
d3b826dda2
commit
d9a071089b
10 changed files with 3013 additions and 0 deletions
468
lib/ansible/modules/network/cumulus/cl_bond.py
Executable file
468
lib/ansible/modules/network/cumulus/cl_bond.py
Executable file
|
@ -0,0 +1,468 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# (c) 2016, Cumulus Networks <ce-ceng@cumulusnetworks.com>
|
||||||
|
#
|
||||||
|
# This file is part of Ansible
|
||||||
|
# Ansible is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
# Ansible is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: cl_bond
|
||||||
|
version_added: "2.1"
|
||||||
|
author: "Cumulus Networks (@CumulusNetworks)"
|
||||||
|
short_description: Configures a bond port on Cumulus Linux
|
||||||
|
description:
|
||||||
|
- Configures a bond interface on Cumulus Linux To configure a bridge port
|
||||||
|
use the cl_bridge module. To configure any other type of interface use the
|
||||||
|
cl_interface module. Follow the guidelines for bonding found in the
|
||||||
|
Cumulus User Guide at http://docs.cumulusnetworks.com
|
||||||
|
options:
|
||||||
|
name:
|
||||||
|
description:
|
||||||
|
- name of the interface
|
||||||
|
required: true
|
||||||
|
alias_name:
|
||||||
|
description:
|
||||||
|
- add a port description
|
||||||
|
ipv4:
|
||||||
|
description:
|
||||||
|
- list of IPv4 addresses to configure on the interface.
|
||||||
|
use X.X.X.X/YY syntax.
|
||||||
|
ipv6:
|
||||||
|
description:
|
||||||
|
- list of IPv6 addresses to configure on the interface.
|
||||||
|
use X:X:X::X/YYY syntax
|
||||||
|
addr_method:
|
||||||
|
description:
|
||||||
|
- configures the port to use DHCP.
|
||||||
|
To enable this feature use the option 'dhcp'
|
||||||
|
choices: ['dhcp']
|
||||||
|
mtu:
|
||||||
|
description:
|
||||||
|
- set MTU. Configure Jumbo Frame by setting MTU to 9000.
|
||||||
|
virtual_ip:
|
||||||
|
description:
|
||||||
|
- define IPv4 virtual IP used by the Cumulus Linux VRR feature
|
||||||
|
virtual_mac:
|
||||||
|
description:
|
||||||
|
- define Ethernet mac associated with Cumulus Linux VRR feature
|
||||||
|
vids:
|
||||||
|
description:
|
||||||
|
- in vlan aware mode, lists vlans defined under the interface
|
||||||
|
mstpctl_bpduguard:
|
||||||
|
description:
|
||||||
|
- Enables BPDU Guard on a port in vlan-aware mode
|
||||||
|
mstpctl_portnetwork:
|
||||||
|
description:
|
||||||
|
- Enables bridge assurance in vlan-aware mode
|
||||||
|
mstpctl_portadminedge:
|
||||||
|
description:
|
||||||
|
- Enables admin edge port
|
||||||
|
clag_id:
|
||||||
|
description:
|
||||||
|
- specify a unique clag_id for every dual connected bond on each
|
||||||
|
peer switch. The value must be between 1 and 65535 and must be the
|
||||||
|
same on both peer switches in order for the bond to be considered
|
||||||
|
dual-connected
|
||||||
|
pvid:
|
||||||
|
description:
|
||||||
|
- in vlan aware mode, defines vlan that is the untagged vlan
|
||||||
|
miimon:
|
||||||
|
description:
|
||||||
|
- mii link monitoring interval
|
||||||
|
default: 100
|
||||||
|
mode:
|
||||||
|
description:
|
||||||
|
- bond mode. as of Cumulus Linux 2.5 only LACP bond mode is
|
||||||
|
supported
|
||||||
|
default: '802.3ad'
|
||||||
|
min_links:
|
||||||
|
description:
|
||||||
|
- minimum number of links
|
||||||
|
default: 1
|
||||||
|
lacp_bypass_allow:
|
||||||
|
description:
|
||||||
|
- Enable LACP bypass.
|
||||||
|
lacp_bypass_period:
|
||||||
|
description:
|
||||||
|
- Period for enabling LACP bypass. Max value is 900.
|
||||||
|
lacp_bypass_priority:
|
||||||
|
description:
|
||||||
|
- List of ports and priorities. Example "swp1=10, swp2=20"
|
||||||
|
lacp_bypass_all_active:
|
||||||
|
description:
|
||||||
|
- Activate all interfaces for bypass.
|
||||||
|
It is recommended to configure all_active instead
|
||||||
|
of using bypass_priority.
|
||||||
|
lacp_rate:
|
||||||
|
description:
|
||||||
|
- lacp rate
|
||||||
|
default: 1
|
||||||
|
slaves:
|
||||||
|
description:
|
||||||
|
- bond members
|
||||||
|
required: True
|
||||||
|
xmit_hash_policy:
|
||||||
|
description:
|
||||||
|
- transmit load balancing algorithm. As of Cumulus Linux 2.5 only
|
||||||
|
layer3+4 policy is supported
|
||||||
|
default: layer3+4
|
||||||
|
location:
|
||||||
|
description:
|
||||||
|
- interface directory location
|
||||||
|
default:
|
||||||
|
- /etc/network/interfaces.d
|
||||||
|
|
||||||
|
requirements: [ Alternate Debian network interface manager - \
|
||||||
|
ifupdown2 @ github.com/CumulusNetworks/ifupdown2 ]
|
||||||
|
notes:
|
||||||
|
- because the module writes the interface directory location. Ensure that
|
||||||
|
``/etc/network/interfaces`` has a 'source /etc/network/interfaces.d/*' or
|
||||||
|
whatever path is mentioned in the ``location`` attribute.
|
||||||
|
|
||||||
|
- For the config to be activated, i.e installed in the kernel,
|
||||||
|
"service networking reload" needs be be executed. See EXAMPLES section.
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
# Options ['virtual_mac', 'virtual_ip'] are required together
|
||||||
|
# configure a bond interface with IP address
|
||||||
|
cl_bond: name=bond0 slaves="swp4-5" ipv4=10.1.1.1/24
|
||||||
|
notify: reload networking
|
||||||
|
|
||||||
|
# configure bond as a dual-connected clag bond
|
||||||
|
cl_bond: name=bond1 slaves="swp1s0 swp2s0" clag_id=1
|
||||||
|
notify: reload networking
|
||||||
|
|
||||||
|
# define cl_bond once in tasks file
|
||||||
|
# then write inteface config in variables file
|
||||||
|
# with just the options you want.
|
||||||
|
cl_bond:
|
||||||
|
name: "{{ item.key }}"
|
||||||
|
slaves: "{{ item.value.slaves }}"
|
||||||
|
clag_id: "{{ item.value.clag_id|default(omit) }}"
|
||||||
|
ipv4: "{{ item.value.ipv4|default(omit) }}"
|
||||||
|
ipv6: "{{ item.value.ipv6|default(omit) }}"
|
||||||
|
alias_name: "{{ item.value.alias_name|default(omit) }}"
|
||||||
|
addr_method: "{{ item.value.addr_method|default(omit) }}"
|
||||||
|
mtu: "{{ item.value.mtu|default(omit) }}"
|
||||||
|
vids: "{{ item.value.vids|default(omit) }}"
|
||||||
|
virtual_ip: "{{ item.value.virtual_ip|default(omit) }}"
|
||||||
|
virtual_mac: "{{ item.value.virtual_mac|default(omit) }}"
|
||||||
|
mstpctl_portnetwork: "{{ item.value.mstpctl_portnetwork|default('no') }}"
|
||||||
|
mstpctl_portadminedge: "{{ item.value.mstpctl_portadminedge|default('no') }}"
|
||||||
|
mstpctl_bpduguard: "{{ item.value.mstpctl_bpduguard|default('no') }}"
|
||||||
|
with_dict: cl_bonds
|
||||||
|
notify: reload networking
|
||||||
|
|
||||||
|
# In vars file
|
||||||
|
# ============
|
||||||
|
cl_bonds:
|
||||||
|
bond0:
|
||||||
|
alias_name: 'uplink to isp'
|
||||||
|
slaves: ['swp1', 'swp3']
|
||||||
|
ipv4: '10.1.1.1/24'
|
||||||
|
bond2:
|
||||||
|
vids: [1, 50]
|
||||||
|
clag_id: 1
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = '''
|
||||||
|
changed:
|
||||||
|
description: whether the interface was changed
|
||||||
|
returned: changed
|
||||||
|
type: bool
|
||||||
|
sample: True
|
||||||
|
msg:
|
||||||
|
description: human-readable report of success or failure
|
||||||
|
returned: always
|
||||||
|
type: string
|
||||||
|
sample: "interface bond0 config updated"
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
# handy helper for calling system calls.
|
||||||
|
# calls AnsibleModule.run_command and prints a more appropriate message
|
||||||
|
# exec_path - path to file to execute, with all its arguments.
|
||||||
|
# E.g "/sbin/ip -o link show"
|
||||||
|
# failure_msg - what message to print on failure
|
||||||
|
def run_cmd(module, exec_path):
|
||||||
|
(_rc, out, _err) = module.run_command(exec_path)
|
||||||
|
if _rc > 0:
|
||||||
|
if re.search('cannot find interface', _err):
|
||||||
|
return '[{}]'
|
||||||
|
failure_msg = "Failed; %s Error: %s" % (exec_path, _err)
|
||||||
|
module.fail_json(msg=failure_msg)
|
||||||
|
else:
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def current_iface_config(module):
|
||||||
|
# due to a bug in ifquery, have to check for presence of interface file
|
||||||
|
# and not rely solely on ifquery. when bug is fixed, this check can be
|
||||||
|
# removed
|
||||||
|
_ifacename = module.params.get('name')
|
||||||
|
_int_dir = module.params.get('location')
|
||||||
|
module.custom_current_config = {}
|
||||||
|
if os.path.exists(_int_dir + '/' + _ifacename):
|
||||||
|
_cmd = "/sbin/ifquery -o json %s" % (module.params.get('name'))
|
||||||
|
module.custom_current_config = module.from_json(
|
||||||
|
run_cmd(module, _cmd))[0]
|
||||||
|
|
||||||
|
|
||||||
|
def build_address(module):
|
||||||
|
# if addr_method == 'dhcp', dont add IP address
|
||||||
|
if module.params.get('addr_method') == 'dhcp':
|
||||||
|
return
|
||||||
|
_ipv4 = module.params.get('ipv4')
|
||||||
|
_ipv6 = module.params.get('ipv6')
|
||||||
|
_addresslist = []
|
||||||
|
if _ipv4 and len(_ipv4) > 0:
|
||||||
|
_addresslist += _ipv4
|
||||||
|
|
||||||
|
if _ipv6 and len(_ipv6) > 0:
|
||||||
|
_addresslist += _ipv6
|
||||||
|
if len(_addresslist) > 0:
|
||||||
|
module.custom_desired_config['config']['address'] = ' '.join(
|
||||||
|
_addresslist)
|
||||||
|
|
||||||
|
|
||||||
|
def build_vids(module):
|
||||||
|
_vids = module.params.get('vids')
|
||||||
|
if _vids and len(_vids) > 0:
|
||||||
|
module.custom_desired_config['config']['bridge-vids'] = ' '.join(_vids)
|
||||||
|
|
||||||
|
|
||||||
|
def build_pvid(module):
|
||||||
|
_pvid = module.params.get('pvid')
|
||||||
|
if _pvid:
|
||||||
|
module.custom_desired_config['config']['bridge-pvid'] = str(_pvid)
|
||||||
|
|
||||||
|
|
||||||
|
def conv_bool_to_str(_value):
|
||||||
|
if isinstance(_value, bool):
|
||||||
|
if _value is True:
|
||||||
|
return 'yes'
|
||||||
|
else:
|
||||||
|
return 'no'
|
||||||
|
return _value
|
||||||
|
|
||||||
|
|
||||||
|
def conv_array_to_str(_value):
|
||||||
|
if isinstance(_value, list):
|
||||||
|
return ' '.join(_value)
|
||||||
|
return _value
|
||||||
|
|
||||||
|
def build_generic_attr(module, _attr):
|
||||||
|
_value = module.params.get(_attr)
|
||||||
|
_value = conv_bool_to_str(_value)
|
||||||
|
_value = conv_array_to_str(_value)
|
||||||
|
if _value:
|
||||||
|
module.custom_desired_config['config'][
|
||||||
|
re.sub('_', '-', _attr)] = str(_value)
|
||||||
|
|
||||||
|
|
||||||
|
def build_alias_name(module):
|
||||||
|
alias_name = module.params.get('alias_name')
|
||||||
|
if alias_name:
|
||||||
|
module.custom_desired_config['config']['alias'] = alias_name
|
||||||
|
|
||||||
|
|
||||||
|
def build_addr_method(module):
|
||||||
|
_addr_method = module.params.get('addr_method')
|
||||||
|
if _addr_method:
|
||||||
|
module.custom_desired_config['addr_family'] = 'inet'
|
||||||
|
module.custom_desired_config['addr_method'] = _addr_method
|
||||||
|
|
||||||
|
|
||||||
|
def build_vrr(module):
|
||||||
|
_virtual_ip = module.params.get('virtual_ip')
|
||||||
|
_virtual_mac = module.params.get('virtual_mac')
|
||||||
|
vrr_config = []
|
||||||
|
if _virtual_ip:
|
||||||
|
vrr_config.append(_virtual_mac)
|
||||||
|
vrr_config.append(_virtual_ip)
|
||||||
|
module.custom_desired_config.get('config')['address-virtual'] = \
|
||||||
|
' '.join(vrr_config)
|
||||||
|
|
||||||
|
|
||||||
|
def add_glob_to_array(_bondmems):
|
||||||
|
"""
|
||||||
|
goes through each bond member if it sees a dash add glob
|
||||||
|
before it
|
||||||
|
"""
|
||||||
|
result = []
|
||||||
|
if isinstance(_bondmems, list):
|
||||||
|
for _entry in _bondmems:
|
||||||
|
if re.search('-', _entry):
|
||||||
|
_entry = 'glob ' + _entry
|
||||||
|
result.append(_entry)
|
||||||
|
return ' '.join(result)
|
||||||
|
return _bondmems
|
||||||
|
|
||||||
|
|
||||||
|
def build_bond_attr(module, _attr):
|
||||||
|
_value = module.params.get(_attr)
|
||||||
|
_value = conv_bool_to_str(_value)
|
||||||
|
_value = add_glob_to_array(_value)
|
||||||
|
if _value:
|
||||||
|
module.custom_desired_config['config'][
|
||||||
|
'bond-' + re.sub('_', '-', _attr)] = str(_value)
|
||||||
|
|
||||||
|
|
||||||
|
def build_desired_iface_config(module):
|
||||||
|
"""
|
||||||
|
take parameters defined and build ifupdown2 compatible hash
|
||||||
|
"""
|
||||||
|
module.custom_desired_config = {
|
||||||
|
'addr_family': None,
|
||||||
|
'auto': True,
|
||||||
|
'config': {},
|
||||||
|
'name': module.params.get('name')
|
||||||
|
}
|
||||||
|
|
||||||
|
for _attr in ['slaves', 'mode', 'xmit_hash_policy',
|
||||||
|
'miimon', 'lacp_rate', 'lacp_bypass_allow',
|
||||||
|
'lacp_bypass_period', 'lacp_bypass_all_active',
|
||||||
|
'min_links']:
|
||||||
|
build_bond_attr(module, _attr)
|
||||||
|
|
||||||
|
build_addr_method(module)
|
||||||
|
build_address(module)
|
||||||
|
build_vids(module)
|
||||||
|
build_pvid(module)
|
||||||
|
build_alias_name(module)
|
||||||
|
build_vrr(module)
|
||||||
|
|
||||||
|
for _attr in ['mtu', 'mstpctl_portnetwork', 'mstpctl_portadminedge'
|
||||||
|
'mstpctl_bpduguard', 'clag_id',
|
||||||
|
'lacp_bypass_priority']:
|
||||||
|
build_generic_attr(module, _attr)
|
||||||
|
|
||||||
|
|
||||||
|
def config_dict_changed(module):
|
||||||
|
"""
|
||||||
|
return true if 'config' dict in hash is different
|
||||||
|
between desired and current config
|
||||||
|
"""
|
||||||
|
current_config = module.custom_current_config.get('config')
|
||||||
|
desired_config = module.custom_desired_config.get('config')
|
||||||
|
return current_config != desired_config
|
||||||
|
|
||||||
|
|
||||||
|
def config_changed(module):
|
||||||
|
"""
|
||||||
|
returns true if config has changed
|
||||||
|
"""
|
||||||
|
if config_dict_changed(module):
|
||||||
|
return True
|
||||||
|
# check if addr_method is changed
|
||||||
|
return module.custom_desired_config.get('addr_method') != \
|
||||||
|
module.custom_current_config.get('addr_method')
|
||||||
|
|
||||||
|
|
||||||
|
def replace_config(module):
|
||||||
|
temp = tempfile.NamedTemporaryFile()
|
||||||
|
desired_config = module.custom_desired_config
|
||||||
|
# by default it will be something like /etc/network/interfaces.d/swp1
|
||||||
|
final_location = module.params.get('location') + '/' + \
|
||||||
|
module.params.get('name')
|
||||||
|
final_text = ''
|
||||||
|
_fh = open(final_location, 'w')
|
||||||
|
# make sure to put hash in array or else ifquery will fail
|
||||||
|
# write to temp file
|
||||||
|
try:
|
||||||
|
temp.write(module.jsonify([desired_config]))
|
||||||
|
# need to seek to 0 so that data is written to tempfile.
|
||||||
|
temp.seek(0)
|
||||||
|
_cmd = "/sbin/ifquery -a -i %s -t json" % (temp.name)
|
||||||
|
final_text = run_cmd(module, _cmd)
|
||||||
|
finally:
|
||||||
|
temp.close()
|
||||||
|
|
||||||
|
try:
|
||||||
|
_fh.write(final_text)
|
||||||
|
finally:
|
||||||
|
_fh.close()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec=dict(
|
||||||
|
slaves=dict(required=True, type='list'),
|
||||||
|
name=dict(required=True, type='str'),
|
||||||
|
ipv4=dict(type='list'),
|
||||||
|
ipv6=dict(type='list'),
|
||||||
|
alias_name=dict(type='str'),
|
||||||
|
addr_method=dict(type='str',
|
||||||
|
choices=['', 'dhcp']),
|
||||||
|
mtu=dict(type='str'),
|
||||||
|
virtual_ip=dict(type='str'),
|
||||||
|
virtual_mac=dict(type='str'),
|
||||||
|
vids=dict(type='list'),
|
||||||
|
pvid=dict(type='str'),
|
||||||
|
mstpctl_portnetwork=dict(type='bool', choices=BOOLEANS),
|
||||||
|
mstpctl_portadminedge=dict(type='bool', choices=BOOLEANS),
|
||||||
|
mstpctl_bpduguard=dict(type='bool', choices=BOOLEANS),
|
||||||
|
clag_id=dict(type='str'),
|
||||||
|
min_links=dict(type='int', default=1),
|
||||||
|
mode=dict(type='str', default='802.3ad'),
|
||||||
|
miimon=dict(type='int', default=100),
|
||||||
|
xmit_hash_policy=dict(type='str', default='layer3+4'),
|
||||||
|
lacp_rate=dict(type='int', default=1),
|
||||||
|
lacp_bypass_allow=dict(type='int', choices=[0, 1]),
|
||||||
|
lacp_bypass_all_active=dict(type='int', choices=[0, 1]),
|
||||||
|
lacp_bypass_priority=dict(type='list'),
|
||||||
|
lacp_bypass_period=dict(type='int'),
|
||||||
|
location=dict(type='str',
|
||||||
|
default='/etc/network/interfaces.d')
|
||||||
|
),
|
||||||
|
mutually_exclusive=[['lacp_bypass_priority', 'lacp_bypass_all_active']],
|
||||||
|
required_together=[['virtual_ip', 'virtual_mac']]
|
||||||
|
)
|
||||||
|
|
||||||
|
# if using the jinja default filter, this resolves to
|
||||||
|
# create an list with an empty string ['']. The following
|
||||||
|
# checks all lists and removes it, so that functions expecting
|
||||||
|
# an empty list, get this result. May upstream this fix into
|
||||||
|
# the AnsibleModule code to have it check for this.
|
||||||
|
for k, _param in module.params.iteritems():
|
||||||
|
if isinstance(_param, list):
|
||||||
|
module.params[k] = [x for x in _param if x]
|
||||||
|
|
||||||
|
_location = module.params.get('location')
|
||||||
|
if not os.path.exists(_location):
|
||||||
|
_msg = "%s does not exist." % (_location)
|
||||||
|
module.fail_json(msg=_msg)
|
||||||
|
return # for testing purposes only
|
||||||
|
|
||||||
|
ifacename = module.params.get('name')
|
||||||
|
_changed = False
|
||||||
|
_msg = "interface %s config not changed" % (ifacename)
|
||||||
|
current_iface_config(module)
|
||||||
|
build_desired_iface_config(module)
|
||||||
|
if config_changed(module):
|
||||||
|
replace_config(module)
|
||||||
|
_msg = "interface %s config updated" % (ifacename)
|
||||||
|
_changed = True
|
||||||
|
|
||||||
|
module.exit_json(changed=_changed, msg=_msg)
|
||||||
|
|
||||||
|
# import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
import tempfile
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
404
lib/ansible/modules/network/cumulus/cl_bridge.py
Executable file
404
lib/ansible/modules/network/cumulus/cl_bridge.py
Executable file
|
@ -0,0 +1,404 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# (c) 2016, Cumulus Networks <ce-ceng@cumulusnetworks.com>
|
||||||
|
#
|
||||||
|
# This file is part of Ansible
|
||||||
|
# Ansible is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
# Ansible is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: cl_bridge
|
||||||
|
version_added: "2.1"
|
||||||
|
author: "Cumulus Networks (@CumulusNetworks)"
|
||||||
|
short_description: Configures a bridge port on Cumulus Linux
|
||||||
|
description:
|
||||||
|
- Configures a bridge interface on Cumulus Linux To configure a bond port
|
||||||
|
use the cl_bond module. To configure any other type of interface use the
|
||||||
|
cl_interface module. Follow the guidelines for bridging found in the
|
||||||
|
Cumulus User Guide at http://docs.cumulusnetworks.com
|
||||||
|
options:
|
||||||
|
name:
|
||||||
|
description:
|
||||||
|
- name of the interface
|
||||||
|
required: true
|
||||||
|
alias_name:
|
||||||
|
description:
|
||||||
|
- add a port description
|
||||||
|
ipv4:
|
||||||
|
description:
|
||||||
|
- list of IPv4 addresses to configure on the interface.
|
||||||
|
use X.X.X.X/YY syntax.
|
||||||
|
ipv6:
|
||||||
|
description:
|
||||||
|
- list of IPv6 addresses to configure on the interface.
|
||||||
|
use X:X:X::X/YYY syntax
|
||||||
|
addr_method:
|
||||||
|
description:
|
||||||
|
- configures the port to use DHCP.
|
||||||
|
To enable this feature use the option 'dhcp'
|
||||||
|
choices: ['dhcp']
|
||||||
|
mtu:
|
||||||
|
description:
|
||||||
|
- set MTU. Configure Jumbo Frame by setting MTU to 9000.
|
||||||
|
virtual_ip:
|
||||||
|
description:
|
||||||
|
- define IPv4 virtual IP used by the Cumulus Linux VRR feature
|
||||||
|
virtual_mac:
|
||||||
|
description:
|
||||||
|
- define Ethernet mac associated with Cumulus Linux VRR feature
|
||||||
|
vids:
|
||||||
|
description:
|
||||||
|
- in vlan aware mode, lists vlans defined under the interface
|
||||||
|
pvid:
|
||||||
|
description:
|
||||||
|
- in vlan aware mode, defines vlan that is the untagged vlan
|
||||||
|
stp:
|
||||||
|
description:
|
||||||
|
- enables spanning tree. As of Cumulus Linux 2.5 the default
|
||||||
|
bridging mode, only per vlan RSTP or 802.1d is supported. For the
|
||||||
|
vlan aware mode, only common instance STP is supported
|
||||||
|
default: 'yes'
|
||||||
|
ports:
|
||||||
|
description:
|
||||||
|
- list of bridge members
|
||||||
|
required: True
|
||||||
|
vlan_aware:
|
||||||
|
description:
|
||||||
|
- enables vlan aware mode.
|
||||||
|
mstpctl_treeprio:
|
||||||
|
description:
|
||||||
|
- set spanning tree root priority. Must be a multiple of 4096
|
||||||
|
location:
|
||||||
|
description:
|
||||||
|
- interface directory location
|
||||||
|
default:
|
||||||
|
- /etc/network/interfaces.d
|
||||||
|
|
||||||
|
|
||||||
|
requirements: [ Alternate Debian network interface manager
|
||||||
|
ifupdown2 @ github.com/CumulusNetworks/ifupdown2 ]
|
||||||
|
notes:
|
||||||
|
- because the module writes the interface directory location. Ensure that
|
||||||
|
``/etc/network/interfaces`` has a 'source /etc/network/interfaces.d/*' or
|
||||||
|
whatever path is mentioned in the ``location`` attribute.
|
||||||
|
|
||||||
|
- For the config to be activated, i.e installed in the kernel,
|
||||||
|
"service networking reload" needs be be executed. See EXAMPLES section.
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
# Options ['virtual_mac', 'virtual_ip'] are required together
|
||||||
|
# configure a bridge vlan aware bridge.
|
||||||
|
cl_bridge: name=br0 ports='swp1-12' vlan_aware='yes'
|
||||||
|
notify: reload networking
|
||||||
|
|
||||||
|
# configure bridge interface to define a default set of vlans
|
||||||
|
cl_bridge: name=bridge ports='swp1-12' vlan_aware='yes' vids='1-100'
|
||||||
|
notify: reload networking
|
||||||
|
|
||||||
|
# define cl_bridge once in tasks file
|
||||||
|
# then write inteface config in variables file
|
||||||
|
# with just the options you want.
|
||||||
|
cl_bridge:
|
||||||
|
name: "{{ item.key }}"
|
||||||
|
ports: "{{ item.value.ports }}"
|
||||||
|
vlan_aware: "{{ item.value.vlan_aware|default(omit) }}"
|
||||||
|
ipv4: "{{ item.value.ipv4|default(omit) }}"
|
||||||
|
ipv6: "{{ item.value.ipv6|default(omit) }}"
|
||||||
|
alias_name: "{{ item.value.alias_name|default(omit) }}"
|
||||||
|
addr_method: "{{ item.value.addr_method|default(omit) }}"
|
||||||
|
mtu: "{{ item.value.mtu|default(omit) }}"
|
||||||
|
vids: "{{ item.value.vids|default(omit) }}"
|
||||||
|
virtual_ip: "{{ item.value.virtual_ip|default(omit) }}"
|
||||||
|
virtual_mac: "{{ item.value.virtual_mac|default(omit) }}"
|
||||||
|
mstpctl_treeprio: "{{ item.value.mstpctl_treeprio|default(omit) }}"
|
||||||
|
with_dict: cl_bridges
|
||||||
|
notify: reload networking
|
||||||
|
|
||||||
|
# In vars file
|
||||||
|
# ============
|
||||||
|
cl_bridge:
|
||||||
|
br0:
|
||||||
|
alias_name: 'vlan aware bridge'
|
||||||
|
ports: ['swp1', 'swp3']
|
||||||
|
vlan_aware: true
|
||||||
|
vids: ['1-100']
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = '''
|
||||||
|
changed:
|
||||||
|
description: whether the interface was changed
|
||||||
|
returned: changed
|
||||||
|
type: bool
|
||||||
|
sample: True
|
||||||
|
msg:
|
||||||
|
description: human-readable report of success or failure
|
||||||
|
returned: always
|
||||||
|
type: string
|
||||||
|
sample: "interface bond0 config updated"
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
# handy helper for calling system calls.
|
||||||
|
# calls AnsibleModule.run_command and prints a more appropriate message
|
||||||
|
# exec_path - path to file to execute, with all its arguments.
|
||||||
|
# E.g "/sbin/ip -o link show"
|
||||||
|
# failure_msg - what message to print on failure
|
||||||
|
def run_cmd(module, exec_path):
|
||||||
|
(_rc, out, _err) = module.run_command(exec_path)
|
||||||
|
if _rc > 0:
|
||||||
|
if re.search('cannot find interface', _err):
|
||||||
|
return '[{}]'
|
||||||
|
failure_msg = "Failed; %s Error: %s" % (exec_path, _err)
|
||||||
|
module.fail_json(msg=failure_msg)
|
||||||
|
else:
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def current_iface_config(module):
|
||||||
|
# due to a bug in ifquery, have to check for presence of interface file
|
||||||
|
# and not rely solely on ifquery. when bug is fixed, this check can be
|
||||||
|
# removed
|
||||||
|
_ifacename = module.params.get('name')
|
||||||
|
_int_dir = module.params.get('location')
|
||||||
|
module.custom_current_config = {}
|
||||||
|
if os.path.exists(_int_dir + '/' + _ifacename):
|
||||||
|
_cmd = "/sbin/ifquery -o json %s" % (module.params.get('name'))
|
||||||
|
module.custom_current_config = module.from_json(
|
||||||
|
run_cmd(module, _cmd))[0]
|
||||||
|
|
||||||
|
|
||||||
|
def build_address(module):
|
||||||
|
# if addr_method == 'dhcp', dont add IP address
|
||||||
|
if module.params.get('addr_method') == 'dhcp':
|
||||||
|
return
|
||||||
|
_ipv4 = module.params.get('ipv4')
|
||||||
|
_ipv6 = module.params.get('ipv6')
|
||||||
|
_addresslist = []
|
||||||
|
if _ipv4 and len(_ipv4) > 0:
|
||||||
|
_addresslist += _ipv4
|
||||||
|
|
||||||
|
if _ipv6 and len(_ipv6) > 0:
|
||||||
|
_addresslist += _ipv6
|
||||||
|
if len(_addresslist) > 0:
|
||||||
|
module.custom_desired_config['config']['address'] = ' '.join(
|
||||||
|
_addresslist)
|
||||||
|
|
||||||
|
|
||||||
|
def build_vids(module):
|
||||||
|
_vids = module.params.get('vids')
|
||||||
|
if _vids and len(_vids) > 0:
|
||||||
|
module.custom_desired_config['config']['bridge-vids'] = ' '.join(_vids)
|
||||||
|
|
||||||
|
|
||||||
|
def build_pvid(module):
|
||||||
|
_pvid = module.params.get('pvid')
|
||||||
|
if _pvid:
|
||||||
|
module.custom_desired_config['config']['bridge-pvid'] = str(_pvid)
|
||||||
|
|
||||||
|
|
||||||
|
def conv_bool_to_str(_value):
|
||||||
|
if isinstance(_value, bool):
|
||||||
|
if _value is True:
|
||||||
|
return 'yes'
|
||||||
|
else:
|
||||||
|
return 'no'
|
||||||
|
return _value
|
||||||
|
|
||||||
|
|
||||||
|
def build_generic_attr(module, _attr):
|
||||||
|
_value = module.params.get(_attr)
|
||||||
|
_value = conv_bool_to_str(_value)
|
||||||
|
if _value:
|
||||||
|
module.custom_desired_config['config'][
|
||||||
|
re.sub('_', '-', _attr)] = str(_value)
|
||||||
|
|
||||||
|
|
||||||
|
def build_alias_name(module):
|
||||||
|
alias_name = module.params.get('alias_name')
|
||||||
|
if alias_name:
|
||||||
|
module.custom_desired_config['config']['alias'] = alias_name
|
||||||
|
|
||||||
|
|
||||||
|
def build_addr_method(module):
|
||||||
|
_addr_method = module.params.get('addr_method')
|
||||||
|
if _addr_method:
|
||||||
|
module.custom_desired_config['addr_family'] = 'inet'
|
||||||
|
module.custom_desired_config['addr_method'] = _addr_method
|
||||||
|
|
||||||
|
|
||||||
|
def build_vrr(module):
|
||||||
|
_virtual_ip = module.params.get('virtual_ip')
|
||||||
|
_virtual_mac = module.params.get('virtual_mac')
|
||||||
|
vrr_config = []
|
||||||
|
if _virtual_ip:
|
||||||
|
vrr_config.append(_virtual_mac)
|
||||||
|
vrr_config.append(_virtual_ip)
|
||||||
|
module.custom_desired_config.get('config')['address-virtual'] = \
|
||||||
|
' '.join(vrr_config)
|
||||||
|
|
||||||
|
|
||||||
|
def add_glob_to_array(_bridgemems):
|
||||||
|
"""
|
||||||
|
goes through each bridge member if it sees a dash add glob
|
||||||
|
before it
|
||||||
|
"""
|
||||||
|
result = []
|
||||||
|
if isinstance(_bridgemems, list):
|
||||||
|
for _entry in _bridgemems:
|
||||||
|
if re.search('-', _entry):
|
||||||
|
_entry = 'glob ' + _entry
|
||||||
|
result.append(_entry)
|
||||||
|
return ' '.join(result)
|
||||||
|
return _bridgemems
|
||||||
|
|
||||||
|
|
||||||
|
def build_bridge_attr(module, _attr):
|
||||||
|
_value = module.params.get(_attr)
|
||||||
|
_value = conv_bool_to_str(_value)
|
||||||
|
_value = add_glob_to_array(_value)
|
||||||
|
if _value:
|
||||||
|
module.custom_desired_config['config'][
|
||||||
|
'bridge-' + re.sub('_', '-', _attr)] = str(_value)
|
||||||
|
|
||||||
|
|
||||||
|
def build_desired_iface_config(module):
|
||||||
|
"""
|
||||||
|
take parameters defined and build ifupdown2 compatible hash
|
||||||
|
"""
|
||||||
|
module.custom_desired_config = {
|
||||||
|
'addr_family': None,
|
||||||
|
'auto': True,
|
||||||
|
'config': {},
|
||||||
|
'name': module.params.get('name')
|
||||||
|
}
|
||||||
|
|
||||||
|
for _attr in ['vlan_aware', 'pvid', 'ports', 'stp']:
|
||||||
|
build_bridge_attr(module, _attr)
|
||||||
|
|
||||||
|
build_addr_method(module)
|
||||||
|
build_address(module)
|
||||||
|
build_vids(module)
|
||||||
|
build_alias_name(module)
|
||||||
|
build_vrr(module)
|
||||||
|
for _attr in ['mtu', 'mstpctl_treeprio']:
|
||||||
|
build_generic_attr(module, _attr)
|
||||||
|
|
||||||
|
|
||||||
|
def config_dict_changed(module):
|
||||||
|
"""
|
||||||
|
return true if 'config' dict in hash is different
|
||||||
|
between desired and current config
|
||||||
|
"""
|
||||||
|
current_config = module.custom_current_config.get('config')
|
||||||
|
desired_config = module.custom_desired_config.get('config')
|
||||||
|
return current_config != desired_config
|
||||||
|
|
||||||
|
|
||||||
|
def config_changed(module):
|
||||||
|
"""
|
||||||
|
returns true if config has changed
|
||||||
|
"""
|
||||||
|
if config_dict_changed(module):
|
||||||
|
return True
|
||||||
|
# check if addr_method is changed
|
||||||
|
return module.custom_desired_config.get('addr_method') != \
|
||||||
|
module.custom_current_config.get('addr_method')
|
||||||
|
|
||||||
|
|
||||||
|
def replace_config(module):
|
||||||
|
temp = tempfile.NamedTemporaryFile()
|
||||||
|
desired_config = module.custom_desired_config
|
||||||
|
# by default it will be something like /etc/network/interfaces.d/swp1
|
||||||
|
final_location = module.params.get('location') + '/' + \
|
||||||
|
module.params.get('name')
|
||||||
|
final_text = ''
|
||||||
|
_fh = open(final_location, 'w')
|
||||||
|
# make sure to put hash in array or else ifquery will fail
|
||||||
|
# write to temp file
|
||||||
|
try:
|
||||||
|
temp.write(module.jsonify([desired_config]))
|
||||||
|
# need to seek to 0 so that data is written to tempfile.
|
||||||
|
temp.seek(0)
|
||||||
|
_cmd = "/sbin/ifquery -a -i %s -t json" % (temp.name)
|
||||||
|
final_text = run_cmd(module, _cmd)
|
||||||
|
finally:
|
||||||
|
temp.close()
|
||||||
|
|
||||||
|
try:
|
||||||
|
_fh.write(final_text)
|
||||||
|
finally:
|
||||||
|
_fh.close()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec=dict(
|
||||||
|
ports=dict(required=True, type='list'),
|
||||||
|
name=dict(required=True, type='str'),
|
||||||
|
ipv4=dict(type='list'),
|
||||||
|
ipv6=dict(type='list'),
|
||||||
|
alias_name=dict(type='str'),
|
||||||
|
addr_method=dict(type='str',
|
||||||
|
choices=['', 'dhcp']),
|
||||||
|
mtu=dict(type='str'),
|
||||||
|
virtual_ip=dict(type='str'),
|
||||||
|
virtual_mac=dict(type='str'),
|
||||||
|
vids=dict(type='list'),
|
||||||
|
pvid=dict(type='str'),
|
||||||
|
mstpctl_treeprio=dict(type='str'),
|
||||||
|
vlan_aware=dict(type='bool', choices=BOOLEANS),
|
||||||
|
stp=dict(type='bool', default='yes', choices=BOOLEANS),
|
||||||
|
location=dict(type='str',
|
||||||
|
default='/etc/network/interfaces.d')
|
||||||
|
),
|
||||||
|
required_together=[
|
||||||
|
['virtual_ip', 'virtual_mac']
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# if using the jinja default filter, this resolves to
|
||||||
|
# create an list with an empty string ['']. The following
|
||||||
|
# checks all lists and removes it, so that functions expecting
|
||||||
|
# an empty list, get this result. May upstream this fix into
|
||||||
|
# the AnsibleModule code to have it check for this.
|
||||||
|
for k, _param in module.params.iteritems():
|
||||||
|
if isinstance(_param, list):
|
||||||
|
module.params[k] = [x for x in _param if x]
|
||||||
|
|
||||||
|
_location = module.params.get('location')
|
||||||
|
if not os.path.exists(_location):
|
||||||
|
_msg = "%s does not exist." % (_location)
|
||||||
|
module.fail_json(msg=_msg)
|
||||||
|
return # for testing purposes only
|
||||||
|
|
||||||
|
ifacename = module.params.get('name')
|
||||||
|
_changed = False
|
||||||
|
_msg = "interface %s config not changed" % (ifacename)
|
||||||
|
current_iface_config(module)
|
||||||
|
build_desired_iface_config(module)
|
||||||
|
if config_changed(module):
|
||||||
|
replace_config(module)
|
||||||
|
_msg = "interface %s config updated" % (ifacename)
|
||||||
|
_changed = True
|
||||||
|
|
||||||
|
module.exit_json(changed=_changed, msg=_msg)
|
||||||
|
|
||||||
|
# import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
import tempfile
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
312
lib/ansible/modules/network/cumulus/cl_img_install.py
Executable file
312
lib/ansible/modules/network/cumulus/cl_img_install.py
Executable file
|
@ -0,0 +1,312 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# (c) 2016, Cumulus Networks <ce-ceng@cumulusnetworks.com>
|
||||||
|
#
|
||||||
|
# This file is part of Ansible
|
||||||
|
# Ansible is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
# Ansible is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: cl_img_install
|
||||||
|
version_added: "2.1"
|
||||||
|
author: "Cumulus Networks (@CumulusLinux)"
|
||||||
|
short_description: Install a different Cumulus Linux version.
|
||||||
|
description:
|
||||||
|
- install a different version of Cumulus Linux in the inactive slot. For
|
||||||
|
more details go the Image Management User Guide @
|
||||||
|
http://docs.cumulusnetworks.com/
|
||||||
|
options:
|
||||||
|
src:
|
||||||
|
description:
|
||||||
|
- full path to the Cumulus Linux binary image. Can be a local path,
|
||||||
|
http or https URL. If the code version is in the name of the file,
|
||||||
|
the module will assume this is the version of code you wish to
|
||||||
|
install.
|
||||||
|
required: true
|
||||||
|
version:
|
||||||
|
description:
|
||||||
|
- inform the module of the exact version one is installing. This
|
||||||
|
overrides the automatic check of version in the file name. For
|
||||||
|
example, if the binary file name is called CumulusLinux-2.2.3.bin,
|
||||||
|
and version is set to '2.5.0', then the module will assume it is
|
||||||
|
installing '2.5.0' not '2.2.3'. If version is not included, then
|
||||||
|
the module will assume '2.2.3' is the version to install.
|
||||||
|
switch_slot:
|
||||||
|
description:
|
||||||
|
- Switch slots after installing the image.
|
||||||
|
To run the installed code, reboot the switch
|
||||||
|
choices: ['yes', 'no']
|
||||||
|
default: 'no'
|
||||||
|
|
||||||
|
requirements: ["Cumulus Linux OS"]
|
||||||
|
|
||||||
|
'''
|
||||||
|
EXAMPLES = '''
|
||||||
|
Example playbook entries using the cl_img_install module
|
||||||
|
|
||||||
|
## Download and install the image from a webserver.
|
||||||
|
|
||||||
|
- name: install image using using http url. Switch slots so the subsequent
|
||||||
|
will load the new version
|
||||||
|
cl_img_install: version=2.0.1
|
||||||
|
src='http://10.1.1.1/CumulusLinux-2.0.1.bin'
|
||||||
|
switch_slot=yes
|
||||||
|
|
||||||
|
## Copy the software from the ansible server to the switch.
|
||||||
|
## The module will get the code version from the filename
|
||||||
|
## The code will be installed in the alternate slot but the slot will not be primary
|
||||||
|
## A subsequent reload will not run the new code
|
||||||
|
|
||||||
|
- name: download cumulus linux to local system
|
||||||
|
get_url: src=ftp://cumuluslinux.bin dest=/root/CumulusLinux-2.0.1.bin
|
||||||
|
|
||||||
|
- name: install image from local filesystem. Get version from the filename
|
||||||
|
cl_img_install: src='/root/CumulusLinux-2.0.1.bin'
|
||||||
|
|
||||||
|
|
||||||
|
## If the image name has been changed from the original name, use the `version` option
|
||||||
|
## to inform the module exactly what code version is been installed
|
||||||
|
|
||||||
|
- name: download cumulus linux to local system
|
||||||
|
get_url: src=ftp://CumulusLinux-2.0.1.bin dest=/root/image.bin
|
||||||
|
|
||||||
|
- name: install image and switch slots. only reboot needed
|
||||||
|
cl_img_install: version=2.0.1 src=/root/image.bin switch_slot=yes'
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = '''
|
||||||
|
changed:
|
||||||
|
description: whether the interface was changed
|
||||||
|
returned: changed
|
||||||
|
type: bool
|
||||||
|
sample: True
|
||||||
|
msg:
|
||||||
|
description: human-readable report of success or failure
|
||||||
|
returned: always
|
||||||
|
type: string
|
||||||
|
sample: "interface bond0 config updated"
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def check_url(module, url):
|
||||||
|
parsed_url = urlparse(url)
|
||||||
|
if len(parsed_url.path) > 0:
|
||||||
|
sch = parsed_url.scheme
|
||||||
|
if (sch == 'http' or sch == 'https' or len(parsed_url.scheme) == 0):
|
||||||
|
return True
|
||||||
|
module.fail_json(msg="Image Path URL. Wrong Format %s" % (url))
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def run_cl_cmd(module, cmd, check_rc=True):
|
||||||
|
try:
|
||||||
|
(rc, out, err) = module.run_command(cmd, check_rc=check_rc)
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg=e.strerror)
|
||||||
|
# trim last line as it is always empty
|
||||||
|
ret = out.splitlines()
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def get_slot_info(module):
|
||||||
|
slots = {}
|
||||||
|
slots['1'] = {}
|
||||||
|
slots['2'] = {}
|
||||||
|
active_slotnum = get_active_slot(module)
|
||||||
|
primary_slotnum = get_primary_slot_num(module)
|
||||||
|
for _num in range(1, 3):
|
||||||
|
slot = slots[str(_num)]
|
||||||
|
slot['version'] = get_slot_version(module, str(_num))
|
||||||
|
if _num == int(active_slotnum):
|
||||||
|
slot['active'] = True
|
||||||
|
if _num == int(primary_slotnum):
|
||||||
|
slot['primary'] = True
|
||||||
|
return slots
|
||||||
|
|
||||||
|
|
||||||
|
def get_slot_version(module, slot_num):
|
||||||
|
lsb_release = check_mnt_root_lsb_release(slot_num)
|
||||||
|
switch_firm_ver = check_fw_print_env(module, slot_num)
|
||||||
|
_version = module.sw_version
|
||||||
|
if lsb_release == _version or switch_firm_ver == _version:
|
||||||
|
return _version
|
||||||
|
elif lsb_release:
|
||||||
|
return lsb_release
|
||||||
|
else:
|
||||||
|
return switch_firm_ver
|
||||||
|
|
||||||
|
|
||||||
|
def check_mnt_root_lsb_release(slot_num):
|
||||||
|
_path = '/mnt/root-rw/config%s/etc/lsb-release' % (slot_num)
|
||||||
|
try:
|
||||||
|
lsb_release = open(_path)
|
||||||
|
lines = lsb_release.readlines()
|
||||||
|
for line in lines:
|
||||||
|
_match = re.search('DISTRIB_RELEASE=([0-9a-zA-Z.]+)', line)
|
||||||
|
if _match:
|
||||||
|
return _match.group(1).split('-')[0]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def check_fw_print_env(module, slot_num):
|
||||||
|
cmd = None
|
||||||
|
if platform.machine() == 'ppc':
|
||||||
|
cmd = "/usr/sbin/fw_printenv -n cl.ver%s" % (slot_num)
|
||||||
|
fw_output = run_cl_cmd(module, cmd)
|
||||||
|
return fw_output[0].split('-')[0]
|
||||||
|
elif platform.machine() == 'x86_64':
|
||||||
|
cmd = "/usr/bin/grub-editenv list"
|
||||||
|
grub_output = run_cl_cmd(module, cmd)
|
||||||
|
for _line in grub_output:
|
||||||
|
_regex_str = re.compile('cl.ver' + slot_num + '=([\w.]+)-')
|
||||||
|
m0 = re.match(_regex_str, _line)
|
||||||
|
if m0:
|
||||||
|
return m0.group(1)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def get_primary_slot_num(module):
|
||||||
|
cmd = None
|
||||||
|
if platform.machine() == 'ppc':
|
||||||
|
cmd = "/usr/sbin/fw_printenv -n cl.active"
|
||||||
|
return ''.join(run_cl_cmd(module, cmd))
|
||||||
|
elif platform.machine() == 'x86_64':
|
||||||
|
cmd = "/usr/bin/grub-editenv list"
|
||||||
|
grub_output = run_cl_cmd(module, cmd)
|
||||||
|
for _line in grub_output:
|
||||||
|
_regex_str = re.compile('cl.active=(\d)')
|
||||||
|
m0 = re.match(_regex_str, _line)
|
||||||
|
if m0:
|
||||||
|
return m0.group(1)
|
||||||
|
|
||||||
|
|
||||||
|
def get_active_slot(module):
|
||||||
|
try:
|
||||||
|
cmdline = open('/proc/cmdline').readline()
|
||||||
|
except:
|
||||||
|
module.fail_json(msg='Failed to open /proc/cmdline. ' +
|
||||||
|
'Unable to determine active slot')
|
||||||
|
|
||||||
|
_match = re.search('active=(\d+)', cmdline)
|
||||||
|
if _match:
|
||||||
|
return _match.group(1)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def install_img(module):
|
||||||
|
src = module.params.get('src')
|
||||||
|
_version = module.sw_version
|
||||||
|
app_path = '/usr/cumulus/bin/cl-img-install -f %s' % (src)
|
||||||
|
run_cl_cmd(module, app_path)
|
||||||
|
perform_switch_slot = module.params.get('switch_slot')
|
||||||
|
if perform_switch_slot is True:
|
||||||
|
check_sw_version(module)
|
||||||
|
else:
|
||||||
|
_changed = True
|
||||||
|
_msg = "Cumulus Linux Version " + _version + " successfully" + \
|
||||||
|
" installed in alternate slot"
|
||||||
|
module.exit_json(changed=_changed, msg=_msg)
|
||||||
|
|
||||||
|
|
||||||
|
def switch_slot(module, slotnum):
|
||||||
|
_switch_slot = module.params.get('switch_slot')
|
||||||
|
if _switch_slot is True:
|
||||||
|
app_path = '/usr/cumulus/bin/cl-img-select %s' % (slotnum)
|
||||||
|
run_cl_cmd(module, app_path)
|
||||||
|
|
||||||
|
|
||||||
|
def determine_sw_version(module):
|
||||||
|
_version = module.params.get('version')
|
||||||
|
_filename = ''
|
||||||
|
# Use _version if user defines it
|
||||||
|
if _version:
|
||||||
|
module.sw_version = _version
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
_filename = module.params.get('src').split('/')[-1]
|
||||||
|
_match = re.search('\d+\W\d+\W\w+', _filename)
|
||||||
|
if _match:
|
||||||
|
module.sw_version = re.sub('\W', '.', _match.group())
|
||||||
|
return
|
||||||
|
_msg = 'Unable to determine version from file %s' % (_filename)
|
||||||
|
module.exit_json(changed=False, msg=_msg)
|
||||||
|
|
||||||
|
|
||||||
|
def check_sw_version(module):
|
||||||
|
slots = get_slot_info(module)
|
||||||
|
_version = module.sw_version
|
||||||
|
perform_switch_slot = module.params.get('switch_slot')
|
||||||
|
for _num, slot in slots.items():
|
||||||
|
if slot['version'] == _version:
|
||||||
|
if 'active' in slot:
|
||||||
|
_msg = "Version %s is installed in the active slot" \
|
||||||
|
% (_version)
|
||||||
|
module.exit_json(changed=False, msg=_msg)
|
||||||
|
else:
|
||||||
|
_msg = "Version " + _version + \
|
||||||
|
" is installed in the alternate slot. "
|
||||||
|
if 'primary' not in slot:
|
||||||
|
if perform_switch_slot is True:
|
||||||
|
switch_slot(module, _num)
|
||||||
|
_msg = _msg + \
|
||||||
|
"cl-img-select has made the alternate " + \
|
||||||
|
"slot the primary slot. " +\
|
||||||
|
"Next reboot, switch will load " + _version + "."
|
||||||
|
module.exit_json(changed=True, msg=_msg)
|
||||||
|
else:
|
||||||
|
_msg = _msg + \
|
||||||
|
"Next reboot will not load " + _version + ". " + \
|
||||||
|
"switch_slot keyword set to 'no'."
|
||||||
|
module.exit_json(changed=False, msg=_msg)
|
||||||
|
else:
|
||||||
|
if perform_switch_slot is True:
|
||||||
|
_msg = _msg + \
|
||||||
|
"Next reboot, switch will load " + _version + "."
|
||||||
|
module.exit_json(changed=False, msg=_msg)
|
||||||
|
else:
|
||||||
|
_msg = _msg + \
|
||||||
|
'switch_slot set to "no". ' + \
|
||||||
|
'No further action to take'
|
||||||
|
module.exit_json(changed=False, msg=_msg)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec=dict(
|
||||||
|
src=dict(required=True, type='str'),
|
||||||
|
version=dict(type='str'),
|
||||||
|
switch_slot=dict(type='bool', choices=BOOLEANS, default=False),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
determine_sw_version(module)
|
||||||
|
_url = module.params.get('src')
|
||||||
|
|
||||||
|
check_sw_version(module)
|
||||||
|
|
||||||
|
check_url(module, _url)
|
||||||
|
|
||||||
|
install_img(module)
|
||||||
|
|
||||||
|
|
||||||
|
# import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
# incompatible with ansible 1.4.4 - ubuntu 12.04 version
|
||||||
|
# from ansible.module_utils.urls import *
|
||||||
|
from urlparse import urlparse
|
||||||
|
import re
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
438
lib/ansible/modules/network/cumulus/cl_interface.py
Normal file
438
lib/ansible/modules/network/cumulus/cl_interface.py
Normal file
|
@ -0,0 +1,438 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# (c) 2016, Cumulus Networks <ce-ceng@cumulusnetworks.com>
|
||||||
|
#
|
||||||
|
# This file is part of Ansible
|
||||||
|
# Ansible is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
# Ansible is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: cl_interface
|
||||||
|
version_added: "2.1"
|
||||||
|
author: "Cumulus Networks (@CumulusNetworks)"
|
||||||
|
short_description: Configures a front panel port, loopback or
|
||||||
|
management port on Cumulus Linux.
|
||||||
|
description:
|
||||||
|
- Configures a front panel, sub-interface, SVI, management or loopback port
|
||||||
|
on a Cumulus Linux switch. For bridge ports use the cl_bridge module. For
|
||||||
|
bond ports use the cl_bond module. When configuring bridge related
|
||||||
|
features like the "vid" option, please follow the guidelines for
|
||||||
|
configuring "vlan aware" bridging. For more details review the Layer2
|
||||||
|
Interface Guide at http://docs.cumulusnetworks.com
|
||||||
|
options:
|
||||||
|
name:
|
||||||
|
description:
|
||||||
|
- name of the interface
|
||||||
|
required: true
|
||||||
|
alias_name:
|
||||||
|
description:
|
||||||
|
- add a port description
|
||||||
|
ipv4:
|
||||||
|
description:
|
||||||
|
- list of IPv4 addresses to configure on the interface.
|
||||||
|
use X.X.X.X/YY syntax.
|
||||||
|
ipv6:
|
||||||
|
description:
|
||||||
|
- list of IPv6 addresses to configure on the interface.
|
||||||
|
use X:X:X::X/YYY syntax
|
||||||
|
addr_method:
|
||||||
|
description:
|
||||||
|
- can be loopback for loopback interfaces or dhcp for dhcp
|
||||||
|
interfaces.
|
||||||
|
speed:
|
||||||
|
description:
|
||||||
|
- set speed of the swp(front panel) or management(eth0) interface.
|
||||||
|
speed is in MB
|
||||||
|
mtu:
|
||||||
|
description:
|
||||||
|
- set MTU. Configure Jumbo Frame by setting MTU to 9000.
|
||||||
|
|
||||||
|
virtual_ip:
|
||||||
|
description:
|
||||||
|
- define IPv4 virtual IP used by the Cumulus VRR feature
|
||||||
|
virtual_mac:
|
||||||
|
description:
|
||||||
|
- define Ethernet mac associated with Cumulus VRR feature
|
||||||
|
vids:
|
||||||
|
description:
|
||||||
|
- in vlan aware mode, lists vlans defined under the interface
|
||||||
|
mstpctl_bpduguard:
|
||||||
|
description:
|
||||||
|
- Enables BPDU Guard on a port in vlan-aware mode
|
||||||
|
mstpctl_portnetwork:
|
||||||
|
description:
|
||||||
|
- Enables bridge assurance in vlan-aware mode
|
||||||
|
mstpctl_portadminedge:
|
||||||
|
description:
|
||||||
|
- Enables admin edge port
|
||||||
|
clagd_enable:
|
||||||
|
description:
|
||||||
|
- Enables the clagd daemon. This command should only be applied to
|
||||||
|
the clag peerlink interface
|
||||||
|
clagd_priority:
|
||||||
|
description:
|
||||||
|
- Integer that changes the role the switch has in the clag domain.
|
||||||
|
The lower priority switch will assume the primary role. The number
|
||||||
|
can be between 0 and 65535
|
||||||
|
clagd_peer_ip:
|
||||||
|
description:
|
||||||
|
- IP address of the directly connected peer switch interface
|
||||||
|
clagd_sys_mac:
|
||||||
|
description:
|
||||||
|
- Clagd system mac address. Recommended to use the range starting
|
||||||
|
with 44:38:39:ff. Needs to be the same between 2 Clag switches
|
||||||
|
pvid:
|
||||||
|
description:
|
||||||
|
- in vlan aware mode, defines vlan that is the untagged vlan
|
||||||
|
location:
|
||||||
|
description:
|
||||||
|
- interface directory location
|
||||||
|
default:
|
||||||
|
- /etc/network/interfaces.d
|
||||||
|
|
||||||
|
requirements: [ Alternate Debian network interface manager - \
|
||||||
|
ifupdown2 @ github.com/CumulusNetworks/ifupdown2 ]
|
||||||
|
notes:
|
||||||
|
- because the module writes the interface directory location. Ensure that
|
||||||
|
``/etc/network/interfaces`` has a 'source /etc/network/interfaces.d/*' or
|
||||||
|
whatever path is mentioned in the ``location`` attribute.
|
||||||
|
|
||||||
|
- For the config to be activated, i.e installed in the kernel,
|
||||||
|
"service networking reload" needs be be executed. See EXAMPLES section.
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
# Options ['virtual_mac', 'virtual_ip'] are required together
|
||||||
|
# configure a front panel port with an IP
|
||||||
|
cl_interface: name=swp1 ipv4=10.1.1.1/24
|
||||||
|
notify: reload networking
|
||||||
|
|
||||||
|
# configure front panel to use DHCP
|
||||||
|
cl_interface: name=swp2 addr_family=dhcp
|
||||||
|
notify: reload networking
|
||||||
|
|
||||||
|
# configure a SVI for vlan 100 interface with an IP
|
||||||
|
cl_interface: name=bridge.100 ipv4=10.1.1.1/24
|
||||||
|
notify: reload networking
|
||||||
|
|
||||||
|
# configure subinterface with an IP
|
||||||
|
cl_interface: name=bond0.100 alias_name='my bond' ipv4=10.1.1.1/24
|
||||||
|
notify: reload networking
|
||||||
|
|
||||||
|
# define cl_interfaces once in tasks
|
||||||
|
# then write intefaces in variables file
|
||||||
|
# with just the options you want.
|
||||||
|
cl_interface:
|
||||||
|
name: "{{ item.key }}"
|
||||||
|
ipv4: "{{ item.value.ipv4|default(omit) }}"
|
||||||
|
ipv6: "{{ item.value.ipv6|default(omit) }}"
|
||||||
|
alias_name: "{{ item.value.alias_name|default(omit) }}"
|
||||||
|
addr_method: "{{ item.value.addr_method|default(omit) }}"
|
||||||
|
speed: "{{ item.value.link_speed|default(omit) }}"
|
||||||
|
mtu: "{{ item.value.mtu|default(omit) }}"
|
||||||
|
clagd_enable: "{{ item.value.clagd_enable|default(omit) }}"
|
||||||
|
clagd_peer_ip: "{{ item.value.clagd_peer_ip|default(omit) }}"
|
||||||
|
clagd_sys_mac: "{{ item.value.clagd_sys_mac|default(omit) }}"
|
||||||
|
clagd_priority: "{{ item.value.clagd_priority|default(omit) }}"
|
||||||
|
vids: "{{ item.value.vids|default(omit) }}"
|
||||||
|
virtual_ip: "{{ item.value.virtual_ip|default(omit) }}"
|
||||||
|
virtual_mac: "{{ item.value.virtual_mac|default(omit) }}"
|
||||||
|
mstpctl_portnetwork: "{{ item.value.mstpctl_portnetwork|default('no') }}"
|
||||||
|
mstpctl_portadminedge: "{{ item.value.mstpctl_portadminedge|default('no') }}"
|
||||||
|
mstpctl_bpduguard: "{{ item.value.mstpctl_bpduguard|default('no') }}"
|
||||||
|
with_dict: cl_interfaces
|
||||||
|
notify: reload networking
|
||||||
|
|
||||||
|
|
||||||
|
# In vars file
|
||||||
|
# ============
|
||||||
|
cl_interfaces:
|
||||||
|
swp1:
|
||||||
|
alias_name: 'uplink to isp'
|
||||||
|
ipv4: '10.1.1.1/24'
|
||||||
|
swp2:
|
||||||
|
alias_name: 'l2 trunk connection'
|
||||||
|
vids: [1, 50]
|
||||||
|
swp3:
|
||||||
|
speed: 1000
|
||||||
|
alias_name: 'connects to 1G link'
|
||||||
|
##########
|
||||||
|
# br0 interface is configured by cl_bridge
|
||||||
|
##########
|
||||||
|
br0.100:
|
||||||
|
alias_name: 'SVI for vlan 100'
|
||||||
|
ipv4: '10.2.2.2/24'
|
||||||
|
ipv6: '10:2:2::2/127'
|
||||||
|
virtual_ip: '10.2.2.254'
|
||||||
|
virtual_mac: '00:00:5E:00:10:10'
|
||||||
|
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = '''
|
||||||
|
changed:
|
||||||
|
description: whether the interface was changed
|
||||||
|
returned: changed
|
||||||
|
type: bool
|
||||||
|
sample: True
|
||||||
|
msg:
|
||||||
|
description: human-readable report of success or failure
|
||||||
|
returned: always
|
||||||
|
type: string
|
||||||
|
sample: "interface bond0 config updated"
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
# handy helper for calling system calls.
|
||||||
|
# calls AnsibleModule.run_command and prints a more appropriate message
|
||||||
|
# exec_path - path to file to execute, with all its arguments.
|
||||||
|
# E.g "/sbin/ip -o link show"
|
||||||
|
# failure_msg - what message to print on failure
|
||||||
|
def run_cmd(module, exec_path):
|
||||||
|
(_rc, out, _err) = module.run_command(exec_path)
|
||||||
|
if _rc > 0:
|
||||||
|
if re.search('cannot find interface', _err):
|
||||||
|
return '[{}]'
|
||||||
|
failure_msg = "Failed; %s Error: %s" % (exec_path, _err)
|
||||||
|
module.fail_json(msg=failure_msg)
|
||||||
|
else:
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def current_iface_config(module):
|
||||||
|
# due to a bug in ifquery, have to check for presence of interface file
|
||||||
|
# and not rely solely on ifquery. when bug is fixed, this check can be
|
||||||
|
# removed
|
||||||
|
_ifacename = module.params.get('name')
|
||||||
|
_int_dir = module.params.get('location')
|
||||||
|
module.custom_current_config = {}
|
||||||
|
if os.path.exists(_int_dir + '/' + _ifacename):
|
||||||
|
_cmd = "/sbin/ifquery -o json %s" % (module.params.get('name'))
|
||||||
|
module.custom_current_config = module.from_json(
|
||||||
|
run_cmd(module, _cmd))[0]
|
||||||
|
|
||||||
|
|
||||||
|
def build_address(module):
|
||||||
|
# if addr_method == 'dhcp', dont add IP address
|
||||||
|
if module.params.get('addr_method') == 'dhcp':
|
||||||
|
return
|
||||||
|
_ipv4 = module.params.get('ipv4')
|
||||||
|
_ipv6 = module.params.get('ipv6')
|
||||||
|
_addresslist = []
|
||||||
|
if _ipv4 and len(_ipv4) > 0:
|
||||||
|
_addresslist += _ipv4
|
||||||
|
if _ipv6 and len(_ipv6) > 0:
|
||||||
|
_addresslist += _ipv6
|
||||||
|
if len(_addresslist) > 0:
|
||||||
|
module.custom_desired_config['config']['address'] = ' '.join(
|
||||||
|
_addresslist)
|
||||||
|
|
||||||
|
|
||||||
|
def build_vids(module):
|
||||||
|
_vids = module.params.get('vids')
|
||||||
|
if _vids and len(_vids) > 0:
|
||||||
|
module.custom_desired_config['config']['bridge-vids'] = ' '.join(_vids)
|
||||||
|
|
||||||
|
|
||||||
|
def build_pvid(module):
|
||||||
|
_pvid = module.params.get('pvid')
|
||||||
|
if _pvid:
|
||||||
|
module.custom_desired_config['config']['bridge-pvid'] = str(_pvid)
|
||||||
|
|
||||||
|
|
||||||
|
def build_speed(module):
|
||||||
|
_speed = module.params.get('speed')
|
||||||
|
if _speed:
|
||||||
|
module.custom_desired_config['config']['link-speed'] = str(_speed)
|
||||||
|
module.custom_desired_config['config']['link-duplex'] = 'full'
|
||||||
|
|
||||||
|
|
||||||
|
def conv_bool_to_str(_value):
|
||||||
|
if isinstance(_value, bool):
|
||||||
|
if _value is True:
|
||||||
|
return 'yes'
|
||||||
|
else:
|
||||||
|
return 'no'
|
||||||
|
return _value
|
||||||
|
|
||||||
|
|
||||||
|
def build_generic_attr(module, _attr):
|
||||||
|
_value = module.params.get(_attr)
|
||||||
|
_value = conv_bool_to_str(_value)
|
||||||
|
if _value:
|
||||||
|
module.custom_desired_config['config'][
|
||||||
|
re.sub('_', '-', _attr)] = str(_value)
|
||||||
|
|
||||||
|
|
||||||
|
def build_alias_name(module):
|
||||||
|
alias_name = module.params.get('alias_name')
|
||||||
|
if alias_name:
|
||||||
|
module.custom_desired_config['config']['alias'] = alias_name
|
||||||
|
|
||||||
|
|
||||||
|
def build_addr_method(module):
|
||||||
|
_addr_method = module.params.get('addr_method')
|
||||||
|
if _addr_method:
|
||||||
|
module.custom_desired_config['addr_family'] = 'inet'
|
||||||
|
module.custom_desired_config['addr_method'] = _addr_method
|
||||||
|
|
||||||
|
|
||||||
|
def build_vrr(module):
|
||||||
|
_virtual_ip = module.params.get('virtual_ip')
|
||||||
|
_virtual_mac = module.params.get('virtual_mac')
|
||||||
|
vrr_config = []
|
||||||
|
if _virtual_ip:
|
||||||
|
vrr_config.append(_virtual_mac)
|
||||||
|
vrr_config.append(_virtual_ip)
|
||||||
|
module.custom_desired_config.get('config')['address-virtual'] = \
|
||||||
|
' '.join(vrr_config)
|
||||||
|
|
||||||
|
|
||||||
|
def build_desired_iface_config(module):
|
||||||
|
"""
|
||||||
|
take parameters defined and build ifupdown2 compatible hash
|
||||||
|
"""
|
||||||
|
module.custom_desired_config = {
|
||||||
|
'addr_family': None,
|
||||||
|
'auto': True,
|
||||||
|
'config': {},
|
||||||
|
'name': module.params.get('name')
|
||||||
|
}
|
||||||
|
|
||||||
|
build_addr_method(module)
|
||||||
|
build_address(module)
|
||||||
|
build_vids(module)
|
||||||
|
build_pvid(module)
|
||||||
|
build_speed(module)
|
||||||
|
build_alias_name(module)
|
||||||
|
build_vrr(module)
|
||||||
|
for _attr in ['mtu', 'mstpctl_portnetwork', 'mstpctl_portadminedge',
|
||||||
|
'mstpctl_bpduguard', 'clagd_enable',
|
||||||
|
'clagd_priority', 'clagd_peer_ip',
|
||||||
|
'clagd_sys_mac', 'clagd_args']:
|
||||||
|
build_generic_attr(module, _attr)
|
||||||
|
|
||||||
|
|
||||||
|
def config_dict_changed(module):
|
||||||
|
"""
|
||||||
|
return true if 'config' dict in hash is different
|
||||||
|
between desired and current config
|
||||||
|
"""
|
||||||
|
current_config = module.custom_current_config.get('config')
|
||||||
|
desired_config = module.custom_desired_config.get('config')
|
||||||
|
return current_config != desired_config
|
||||||
|
|
||||||
|
|
||||||
|
def config_changed(module):
|
||||||
|
"""
|
||||||
|
returns true if config has changed
|
||||||
|
"""
|
||||||
|
if config_dict_changed(module):
|
||||||
|
return True
|
||||||
|
# check if addr_method is changed
|
||||||
|
return module.custom_desired_config.get('addr_method') != \
|
||||||
|
module.custom_current_config.get('addr_method')
|
||||||
|
|
||||||
|
|
||||||
|
def replace_config(module):
|
||||||
|
temp = tempfile.NamedTemporaryFile()
|
||||||
|
desired_config = module.custom_desired_config
|
||||||
|
# by default it will be something like /etc/network/interfaces.d/swp1
|
||||||
|
final_location = module.params.get('location') + '/' + \
|
||||||
|
module.params.get('name')
|
||||||
|
final_text = ''
|
||||||
|
_fh = open(final_location, 'w')
|
||||||
|
# make sure to put hash in array or else ifquery will fail
|
||||||
|
# write to temp file
|
||||||
|
try:
|
||||||
|
temp.write(module.jsonify([desired_config]))
|
||||||
|
# need to seek to 0 so that data is written to tempfile.
|
||||||
|
temp.seek(0)
|
||||||
|
_cmd = "/sbin/ifquery -a -i %s -t json" % (temp.name)
|
||||||
|
final_text = run_cmd(module, _cmd)
|
||||||
|
finally:
|
||||||
|
temp.close()
|
||||||
|
|
||||||
|
try:
|
||||||
|
_fh.write(final_text)
|
||||||
|
finally:
|
||||||
|
_fh.close()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec=dict(
|
||||||
|
name=dict(required=True, type='str'),
|
||||||
|
ipv4=dict(type='list'),
|
||||||
|
ipv6=dict(type='list'),
|
||||||
|
alias_name=dict(type='str'),
|
||||||
|
addr_method=dict(type='str',
|
||||||
|
choices=['', 'loopback', 'dhcp']),
|
||||||
|
speed=dict(type='str'),
|
||||||
|
mtu=dict(type='str'),
|
||||||
|
virtual_ip=dict(type='str'),
|
||||||
|
virtual_mac=dict(type='str'),
|
||||||
|
vids=dict(type='list'),
|
||||||
|
pvid=dict(type='str'),
|
||||||
|
mstpctl_portnetwork=dict(type='bool', choices=BOOLEANS),
|
||||||
|
mstpctl_portadminedge=dict(type='bool', choices=BOOLEANS),
|
||||||
|
mstpctl_bpduguard=dict(type='bool', choices=BOOLEANS),
|
||||||
|
clagd_enable=dict(type='bool', choices=BOOLEANS),
|
||||||
|
clagd_priority=dict(type='str'),
|
||||||
|
clagd_peer_ip=dict(type='str'),
|
||||||
|
clagd_sys_mac=dict(type='str'),
|
||||||
|
clagd_args=dict(type='str'),
|
||||||
|
location=dict(type='str',
|
||||||
|
default='/etc/network/interfaces.d')
|
||||||
|
),
|
||||||
|
required_together=[
|
||||||
|
['virtual_ip', 'virtual_mac'],
|
||||||
|
['clagd_enable', 'clagd_priority',
|
||||||
|
'clagd_peer_ip', 'clagd_sys_mac']
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# if using the jinja default filter, this resolves to
|
||||||
|
# create an list with an empty string ['']. The following
|
||||||
|
# checks all lists and removes it, so that functions expecting
|
||||||
|
# an empty list, get this result. May upstream this fix into
|
||||||
|
# the AnsibleModule code to have it check for this.
|
||||||
|
for k, _param in module.params.iteritems():
|
||||||
|
if isinstance(_param, list):
|
||||||
|
module.params[k] = [x for x in _param if x]
|
||||||
|
|
||||||
|
_location = module.params.get('location')
|
||||||
|
if not os.path.exists(_location):
|
||||||
|
_msg = "%s does not exist." % (_location)
|
||||||
|
module.fail_json(msg=_msg)
|
||||||
|
return # for testing purposes only
|
||||||
|
|
||||||
|
ifacename = module.params.get('name')
|
||||||
|
_changed = False
|
||||||
|
_msg = "interface %s config not changed" % (ifacename)
|
||||||
|
current_iface_config(module)
|
||||||
|
build_desired_iface_config(module)
|
||||||
|
if config_changed(module):
|
||||||
|
replace_config(module)
|
||||||
|
_msg = "interface %s config updated" % (ifacename)
|
||||||
|
_changed = True
|
||||||
|
|
||||||
|
module.exit_json(changed=_changed, msg=_msg)
|
||||||
|
|
||||||
|
# import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
import tempfile
|
||||||
|
import os
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
147
lib/ansible/modules/network/cumulus/cl_interface_policy.py
Normal file
147
lib/ansible/modules/network/cumulus/cl_interface_policy.py
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# (c) 2016, Cumulus Networks <ce-ceng@cumulusnetworks.com>
|
||||||
|
#
|
||||||
|
# This file is part of Ansible
|
||||||
|
# Ansible is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
# Ansible is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: cl_interface_policy
|
||||||
|
version_added: "2.1"
|
||||||
|
author: "Cumulus Networks (@CumulusNetworks)"
|
||||||
|
short_description: Configure interface enforcement policy on Cumulus Linux
|
||||||
|
description:
|
||||||
|
- This module affects the configuration files located in the interfaces
|
||||||
|
folder defined by ifupdown2. Interfaces port and port ranges listed in the
|
||||||
|
"allowed" parameter define what interfaces will be available on the
|
||||||
|
switch. If the user runs this module and has an interface configured on
|
||||||
|
the switch, but not found in the "allowed" list, this interface will be
|
||||||
|
unconfigured. By default this is `/etc/network/interface.d`
|
||||||
|
For more details go the Configuring Interfaces at
|
||||||
|
http://docs.cumulusnetworks.com
|
||||||
|
notes:
|
||||||
|
- lo must be included in the allowed list.
|
||||||
|
- eth0 must be in allowed list if out of band management is done
|
||||||
|
options:
|
||||||
|
allowed:
|
||||||
|
description:
|
||||||
|
- list of ports to run initial run at 10G
|
||||||
|
location:
|
||||||
|
description:
|
||||||
|
- folder to store interface files.
|
||||||
|
default: '/etc/network/interfaces.d/'
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
Example playbook entries using the cl_interface_policy module.
|
||||||
|
|
||||||
|
- name: shows types of interface ranges supported
|
||||||
|
cl_interface_policy:
|
||||||
|
allowed: "lo eth0 swp1-9, swp11, swp12-13s0, swp12-30s1, swp12-30s2, bond0-12"
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = '''
|
||||||
|
changed:
|
||||||
|
description: whether the interface was changed
|
||||||
|
returned: changed
|
||||||
|
type: bool
|
||||||
|
sample: True
|
||||||
|
msg:
|
||||||
|
description: human-readable report of success or failure
|
||||||
|
returned: always
|
||||||
|
type: string
|
||||||
|
sample: "interface bond0 config updated"
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
# get list of interface files that are currently "configured".
|
||||||
|
# doesn't mean actually applied to the system, but most likely are
|
||||||
|
def read_current_int_dir(module):
|
||||||
|
module.custom_currentportlist = os.listdir(module.params.get('location'))
|
||||||
|
|
||||||
|
|
||||||
|
# take the allowed list and conver it to into a list
|
||||||
|
# of ports.
|
||||||
|
def convert_allowed_list_to_port_range(module):
|
||||||
|
allowedlist = module.params.get('allowed')
|
||||||
|
for portrange in allowedlist:
|
||||||
|
module.custom_allowedportlist += breakout_portrange(portrange)
|
||||||
|
|
||||||
|
|
||||||
|
def breakout_portrange(prange):
|
||||||
|
_m0 = re.match(r'(\w+[a-z.])(\d+)?-?(\d+)?(\w+)?', prange.strip())
|
||||||
|
# no range defined
|
||||||
|
if _m0.group(3) is None:
|
||||||
|
return [_m0.group(0)]
|
||||||
|
else:
|
||||||
|
portarray = []
|
||||||
|
intrange = range(int(_m0.group(2)), int(_m0.group(3)) + 1)
|
||||||
|
for _int in intrange:
|
||||||
|
portarray.append(''.join([_m0.group(1),
|
||||||
|
str(_int),
|
||||||
|
str(_m0.group(4) or '')
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return portarray
|
||||||
|
|
||||||
|
|
||||||
|
# deletes the interface files
|
||||||
|
def unconfigure_interfaces(module):
|
||||||
|
currentportset = set(module.custom_currentportlist)
|
||||||
|
allowedportset = set(module.custom_allowedportlist)
|
||||||
|
remove_list = currentportset.difference(allowedportset)
|
||||||
|
fileprefix = module.params.get('location')
|
||||||
|
module.msg = "remove config for interfaces %s" % (', '.join(remove_list))
|
||||||
|
for _file in remove_list:
|
||||||
|
os.unlink(fileprefix + _file)
|
||||||
|
|
||||||
|
|
||||||
|
# check to see if policy should be enforced
|
||||||
|
# returns true if policy needs to be enforced
|
||||||
|
# that is delete interface files
|
||||||
|
def int_policy_enforce(module):
|
||||||
|
currentportset = set(module.custom_currentportlist)
|
||||||
|
allowedportset = set(module.custom_allowedportlist)
|
||||||
|
return not currentportset.issubset(allowedportset)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec=dict(
|
||||||
|
allowed=dict(type='list', required=True),
|
||||||
|
location=dict(type='str', default='/etc/network/interfaces.d/')
|
||||||
|
),
|
||||||
|
)
|
||||||
|
module.custom_currentportlist = []
|
||||||
|
module.custom_allowedportlist = []
|
||||||
|
module.changed = False
|
||||||
|
module.msg = 'configured port list is part of allowed port list'
|
||||||
|
read_current_int_dir(module)
|
||||||
|
convert_allowed_list_to_port_range(module)
|
||||||
|
if int_policy_enforce(module):
|
||||||
|
module.changed = True
|
||||||
|
unconfigure_interfaces(module)
|
||||||
|
module.exit_json(changed=module.changed, msg=module.msg)
|
||||||
|
|
||||||
|
|
||||||
|
# import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
# from ansible.module_utils.urls import *
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
139
lib/ansible/modules/network/cumulus/cl_license.py
Executable file
139
lib/ansible/modules/network/cumulus/cl_license.py
Executable file
|
@ -0,0 +1,139 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# (c) 2016, Cumulus Networks <ce-ceng@cumulusnetworks.com>
|
||||||
|
#
|
||||||
|
# This file is part of Ansible
|
||||||
|
# Ansible is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
# Ansible is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: cl_license
|
||||||
|
version_added: "2.1"
|
||||||
|
author: "Cumulus Networks (@CumulusNetworks)"
|
||||||
|
short_description: Install Cumulus Linux license
|
||||||
|
description:
|
||||||
|
- Installs a Cumulus Linux license. The module reports no change of status
|
||||||
|
when a license is installed.
|
||||||
|
For more details go the Cumulus Linux License Documentation @
|
||||||
|
http://docs.cumulusnetwork.com and the Licensing KB Site @
|
||||||
|
https://support.cumulusnetworks.com/hc/en-us/sections/200507688
|
||||||
|
notes:
|
||||||
|
- to activate a license for the FIRST time, the switchd service must be
|
||||||
|
restarted. This action is disruptive. The license renewal process occurs
|
||||||
|
via the Cumulus Networks Customer Portal -
|
||||||
|
http://customers.cumulusnetworks.com.
|
||||||
|
- A non-EULA license is REQUIRED for automation. Manually install the
|
||||||
|
license on a test switch, using the command "cl-license -i <license_file>"
|
||||||
|
to confirm the license is a Non-EULA license.
|
||||||
|
See EXAMPLES, for the proper way to issue this notify action.
|
||||||
|
options:
|
||||||
|
src:
|
||||||
|
description:
|
||||||
|
- full path to the license. Can be local path or http url
|
||||||
|
force:
|
||||||
|
description:
|
||||||
|
- force installation of a license. Typically not needed.
|
||||||
|
It is recommended to manually run this command via the ansible
|
||||||
|
command. A reload of switchd is not required. Running the force
|
||||||
|
option in a playbook will break the idempotent state machine of
|
||||||
|
the module and cause the switchd notification to kick in all the
|
||||||
|
time, causing a disruption.
|
||||||
|
|
||||||
|
'''
|
||||||
|
EXAMPLES = '''
|
||||||
|
Example playbook using the cl_license module to manage licenses on Cumulus Linux
|
||||||
|
|
||||||
|
---
|
||||||
|
- hosts: all
|
||||||
|
tasks:
|
||||||
|
- name: install license using http url
|
||||||
|
cl_license: src='http://10.1.1.1/license.txt'
|
||||||
|
notify: restart switchd
|
||||||
|
|
||||||
|
- name: Triggers switchd to be restarted right away, before play, or role
|
||||||
|
is over. This is desired behaviour
|
||||||
|
meta: flush_handlers
|
||||||
|
|
||||||
|
- name: configure interfaces
|
||||||
|
template: src=interfaces.j2 dest=/etc/network/interfaces
|
||||||
|
notify: restart networking
|
||||||
|
|
||||||
|
handlers:
|
||||||
|
- name: restart switchd
|
||||||
|
service: name=switchd state=restarted
|
||||||
|
- name: restart networking
|
||||||
|
service: name=networking state=reloaded
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
# Force all switches to accept a new license. Typically not needed
|
||||||
|
ansible -m cl_license -a "src='http://10.1.1.1/new_lic' force=yes" -u root all
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = '''
|
||||||
|
changed:
|
||||||
|
description: whether the interface was changed
|
||||||
|
returned: changed
|
||||||
|
type: bool
|
||||||
|
sample: True
|
||||||
|
msg:
|
||||||
|
description: human-readable report of success or failure
|
||||||
|
returned: always
|
||||||
|
type: string
|
||||||
|
sample: "interface bond0 config updated"
|
||||||
|
'''
|
||||||
|
|
||||||
|
CL_LICENSE_PATH='/usr/cumulus/bin/cl-license'
|
||||||
|
|
||||||
|
def install_license(module):
|
||||||
|
# license is not installed, install it
|
||||||
|
_url = module.params.get('src')
|
||||||
|
(_rc, out, _err) = module.run_command("%s -i %s" % (CL_LICENSE_PATH, _url))
|
||||||
|
if _rc > 0:
|
||||||
|
module.fail_json(msg=_err)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec=dict(
|
||||||
|
src=dict(required=True, type='str'),
|
||||||
|
force=dict(type='bool', choices=BOOLEANS,
|
||||||
|
default=False)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# check if license is installed
|
||||||
|
# if force is enabled then set return code to nonzero
|
||||||
|
if module.params.get('force') is True:
|
||||||
|
_rc = 10
|
||||||
|
else:
|
||||||
|
(_rc, out, _err) = module.run_command(CL_LICENSE_PATH)
|
||||||
|
if _rc == 0:
|
||||||
|
module.msg = "No change. License already installed"
|
||||||
|
module.changed = False
|
||||||
|
else:
|
||||||
|
install_license(module)
|
||||||
|
module.msg = "License installation completed"
|
||||||
|
module.changed = True
|
||||||
|
module.exit_json(changed=module.changed, msg=module.msg)
|
||||||
|
|
||||||
|
|
||||||
|
# import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
# from ansible.module_utils.urls import *
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
210
lib/ansible/modules/network/cumulus/cl_ports.py
Executable file
210
lib/ansible/modules/network/cumulus/cl_ports.py
Executable file
|
@ -0,0 +1,210 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# (c) 2016, Cumulus Networks <ce-ceng@cumulusnetworks.com>
|
||||||
|
#
|
||||||
|
# This file is part of Ansible
|
||||||
|
# Ansible is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
# Ansible is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: cl_ports
|
||||||
|
version_added: "2.1"
|
||||||
|
author: "Cumulus Networks (@CumulusNetworks)"
|
||||||
|
short_description: Configure Cumulus Switch port attributes (ports.conf)
|
||||||
|
description:
|
||||||
|
- Set the initial port attribute defined in the Cumulus Linux ports.conf,
|
||||||
|
file. This module does not do any error checking at the moment. Be careful
|
||||||
|
to not include ports that do not exist on the switch. Carefully read the
|
||||||
|
original ports.conf file for any exceptions or limitations.
|
||||||
|
For more details go the Configure Switch Port Attribute Documentation at
|
||||||
|
http://docs.cumulusnetworks.com
|
||||||
|
options:
|
||||||
|
speed_10g:
|
||||||
|
description:
|
||||||
|
- list of ports to run initial run at 10G
|
||||||
|
speed_40g:
|
||||||
|
description:
|
||||||
|
- list of ports to run initial run at 40G
|
||||||
|
speed_4_by_10g:
|
||||||
|
description:
|
||||||
|
- list of 40G ports that will be unganged to run as 4 10G ports.
|
||||||
|
speed_40g_div_4:
|
||||||
|
description:
|
||||||
|
- list of 10G ports that will be ganged to form a 40G port
|
||||||
|
'''
|
||||||
|
EXAMPLES = '''
|
||||||
|
Example playbook entries using the cl_ports module to manage the switch
|
||||||
|
attributes defined in the ports.conf file on Cumulus Linux
|
||||||
|
|
||||||
|
## Unganged port config using simple args
|
||||||
|
- name: configure ports.conf setup
|
||||||
|
cl_ports: speed_4_by_10g="swp1, swp32" speed_40g="swp2-31"
|
||||||
|
notify: restart switchd
|
||||||
|
|
||||||
|
## Unganged port configuration on certain ports using complex args
|
||||||
|
|
||||||
|
- name: configure ports.conf setup
|
||||||
|
cl_ports:
|
||||||
|
speed_4_by_10g: ['swp1-3', 'swp6']
|
||||||
|
speed_40g: ['swp4-5', 'swp7-32']
|
||||||
|
notify: restart switchd
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = '''
|
||||||
|
changed:
|
||||||
|
description: whether the interface was changed
|
||||||
|
returned: changed
|
||||||
|
type: bool
|
||||||
|
sample: True
|
||||||
|
msg:
|
||||||
|
description: human-readable report of success or failure
|
||||||
|
returned: always
|
||||||
|
type: string
|
||||||
|
sample: "interface bond0 config updated"
|
||||||
|
'''
|
||||||
|
|
||||||
|
PORTS_CONF = '/etc/cumulus/ports.conf'
|
||||||
|
|
||||||
|
|
||||||
|
def hash_existing_ports_conf(module):
|
||||||
|
module.ports_conf_hash = {}
|
||||||
|
if not os.path.exists(PORTS_CONF):
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
existing_ports_conf = open(PORTS_CONF).readlines()
|
||||||
|
except IOError, error_msg:
|
||||||
|
_msg = "Failed to open %s: %s" % (PORTS_CONF, error_msg)
|
||||||
|
module.fail_json(msg=_msg)
|
||||||
|
return # for testing only should return on module.fail_json
|
||||||
|
|
||||||
|
for _line in existing_ports_conf:
|
||||||
|
_m0 = re.match(r'^(\d+)=(\w+)', _line)
|
||||||
|
if _m0:
|
||||||
|
_portnum = int(_m0.group(1))
|
||||||
|
_speed = _m0.group(2)
|
||||||
|
module.ports_conf_hash[_portnum] = _speed
|
||||||
|
|
||||||
|
|
||||||
|
def generate_new_ports_conf_hash(module):
|
||||||
|
new_ports_conf_hash = {}
|
||||||
|
convert_hash = {
|
||||||
|
'speed_40g_div_4': '40G/4',
|
||||||
|
'speed_4_by_10g': '4x10G',
|
||||||
|
'speed_10g': '10G',
|
||||||
|
'speed_40g': '40G'
|
||||||
|
}
|
||||||
|
for k in module.params.keys():
|
||||||
|
port_range = module.params[k]
|
||||||
|
port_setting = convert_hash[k]
|
||||||
|
if port_range:
|
||||||
|
port_range = [x for x in port_range if x]
|
||||||
|
for port_str in port_range:
|
||||||
|
port_range_str = port_str.replace('swp', '').split('-')
|
||||||
|
if len(port_range_str) == 1:
|
||||||
|
new_ports_conf_hash[int(port_range_str[0])] = \
|
||||||
|
port_setting
|
||||||
|
else:
|
||||||
|
int_range = map(int, port_range_str)
|
||||||
|
portnum_range = range(int_range[0], int_range[1]+1)
|
||||||
|
for i in portnum_range:
|
||||||
|
new_ports_conf_hash[i] = port_setting
|
||||||
|
module.new_ports_hash = new_ports_conf_hash
|
||||||
|
|
||||||
|
|
||||||
|
def compare_new_and_old_port_conf_hash(module):
|
||||||
|
ports_conf_hash_copy = module.ports_conf_hash.copy()
|
||||||
|
module.ports_conf_hash.update(module.new_ports_hash)
|
||||||
|
port_num_length = len(module.ports_conf_hash.keys())
|
||||||
|
orig_port_num_length = len(ports_conf_hash_copy.keys())
|
||||||
|
if port_num_length != orig_port_num_length:
|
||||||
|
module.fail_json(msg="Port numbering is wrong. \
|
||||||
|
Too many or two few ports configured")
|
||||||
|
return False
|
||||||
|
elif ports_conf_hash_copy == module.ports_conf_hash:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def make_copy_of_orig_ports_conf(module):
|
||||||
|
if os.path.exists(PORTS_CONF + '.orig'):
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
shutil.copyfile(PORTS_CONF, PORTS_CONF + '.orig')
|
||||||
|
except IOError, error_msg:
|
||||||
|
_msg = "Failed to save the original %s: %s" % (PORTS_CONF, error_msg)
|
||||||
|
module.fail_json(msg=_msg)
|
||||||
|
return # for testing only
|
||||||
|
|
||||||
|
def write_to_ports_conf(module):
|
||||||
|
"""
|
||||||
|
use tempfile to first write out config in temp file
|
||||||
|
then write to actual location. may help prevent file
|
||||||
|
corruption. Ports.conf is a critical file for Cumulus.
|
||||||
|
Don't want to corrupt this file under any circumstance.
|
||||||
|
"""
|
||||||
|
temp = tempfile.NamedTemporaryFile()
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
temp.write('# Managed By Ansible\n')
|
||||||
|
for k in sorted(module.ports_conf_hash.keys()):
|
||||||
|
port_setting = module.ports_conf_hash[k]
|
||||||
|
_str = "%s=%s\n" % (k, port_setting)
|
||||||
|
temp.write(_str)
|
||||||
|
temp.seek(0)
|
||||||
|
shutil.copyfile(temp.name, PORTS_CONF)
|
||||||
|
except IOError, error_msg:
|
||||||
|
module.fail_json(
|
||||||
|
msg="Failed to write to %s: %s" % (PORTS_CONF, error_msg))
|
||||||
|
finally:
|
||||||
|
temp.close()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec=dict(
|
||||||
|
speed_40g_div_4=dict(type='list'),
|
||||||
|
speed_4_by_10g=dict(type='list'),
|
||||||
|
speed_10g=dict(type='list'),
|
||||||
|
speed_40g=dict(type='list')
|
||||||
|
),
|
||||||
|
required_one_of=[['speed_40g_div_4',
|
||||||
|
'speed_4_by_10g',
|
||||||
|
'speed_10g',
|
||||||
|
'speed_40g']]
|
||||||
|
)
|
||||||
|
|
||||||
|
_changed = False
|
||||||
|
hash_existing_ports_conf(module)
|
||||||
|
generate_new_ports_conf_hash(module)
|
||||||
|
if compare_new_and_old_port_conf_hash(module):
|
||||||
|
make_copy_of_orig_ports_conf(module)
|
||||||
|
write_to_ports_conf(module)
|
||||||
|
_changed = True
|
||||||
|
_msg = "/etc/cumulus/ports.conf changed"
|
||||||
|
else:
|
||||||
|
_msg = 'No change in /etc/ports.conf'
|
||||||
|
module.exit_json(changed=_changed, msg=_msg)
|
||||||
|
|
||||||
|
|
||||||
|
# import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
# from ansible.module_utils.urls import *
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
214
lib/ansible/modules/network/cumulus/cl_prefix_check.py
Executable file
214
lib/ansible/modules/network/cumulus/cl_prefix_check.py
Executable file
|
@ -0,0 +1,214 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# (c) 2016, Cumulus Networks <ce-ceng@cumulusnetworks.com>
|
||||||
|
#
|
||||||
|
# This file is part of Ansible
|
||||||
|
# Ansible is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
# Ansible is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: cl_prefix_check
|
||||||
|
version_added: "2.1"
|
||||||
|
author: "Cumulus Networks (@CumulusNetworks)"
|
||||||
|
short_description: Check to see if route/prefix exists
|
||||||
|
description:
|
||||||
|
- Check to see if a route exists. This module can be used simply to check a
|
||||||
|
route and return if its present or absent. A larger timeout can be
|
||||||
|
provided to check if a route disappears. An example would be the user
|
||||||
|
could change the OSPF cost of a node within the network then utilize
|
||||||
|
cl_prefix_check of another (separate) node to verify the node (where the
|
||||||
|
OSPF cost was changed) is not being use to route traffic.
|
||||||
|
options:
|
||||||
|
prefix:
|
||||||
|
description:
|
||||||
|
- route/prefix that module is checking for. Uses format acceptable
|
||||||
|
to "ip route show" command. See manpage of "ip-route" for more
|
||||||
|
details
|
||||||
|
required: true
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- Describes if the prefix should be present.
|
||||||
|
choices: ['present', 'absent']
|
||||||
|
default: ['present']
|
||||||
|
timeout:
|
||||||
|
description:
|
||||||
|
- timeout in seconds to wait for route condition to be met
|
||||||
|
default: 5
|
||||||
|
poll_interval:
|
||||||
|
description:
|
||||||
|
- poll interval in seconds to check route.
|
||||||
|
default: 1
|
||||||
|
nonexthop:
|
||||||
|
description:
|
||||||
|
- address of node is not desired in result to prefix
|
||||||
|
default: ""
|
||||||
|
nexthop:
|
||||||
|
description:
|
||||||
|
- address of node is desired in result to prefix
|
||||||
|
default: ""
|
||||||
|
|
||||||
|
notes:
|
||||||
|
- IP Route Documentation -
|
||||||
|
http://manpages.ubuntu.com/manpages/precise/man8/route.8.html
|
||||||
|
'''
|
||||||
|
EXAMPLES = '''
|
||||||
|
Example playbook entries using the cl_prefix_check module to check if a prefix
|
||||||
|
exists
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Test if prefix is present.
|
||||||
|
cl_prefix_check: prefix=4.4.4.0/24
|
||||||
|
|
||||||
|
- name: Test if route is absent. poll for 200 seconds. Poll interval at
|
||||||
|
default setting of 1 second
|
||||||
|
cl_prefix_check: prefix=10.0.1.0/24 timeout=200 state=absent
|
||||||
|
|
||||||
|
- name: Test if route is present, with a timeout of 10 seconds and poll
|
||||||
|
interval of 2 seconds
|
||||||
|
cl_prefix_check: prefix=10.1.1.0/24 timeout=10 poll_interval=2
|
||||||
|
|
||||||
|
- name: Test if route is present, with a nexthop of 4.4.4.4 will fail if no
|
||||||
|
nexthop of 5.5.5.5
|
||||||
|
cl_prefix_check: prefix=4.4.4.4 nexthop=5.5.5.5
|
||||||
|
|
||||||
|
- name: Test if route is present, with no nexthop of 3.3.3.3 will fail if
|
||||||
|
there is a nexthop of 6.6.6.6
|
||||||
|
cl_prefix_check: prefix=3.3.3.3 nonexthop=6.6.6.6
|
||||||
|
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = '''
|
||||||
|
changed:
|
||||||
|
description: whether the interface was changed
|
||||||
|
returned: changed
|
||||||
|
type: bool
|
||||||
|
sample: True
|
||||||
|
msg:
|
||||||
|
description: human-readable report of success or failure
|
||||||
|
returned: always
|
||||||
|
type: string
|
||||||
|
sample: "interface bond0 config updated"
|
||||||
|
'''
|
||||||
|
|
||||||
|
def run_cl_cmd(module, cmd, check_rc=True):
|
||||||
|
try:
|
||||||
|
(rc, out, err) = module.run_command(cmd, check_rc=check_rc)
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg=e.strerror)
|
||||||
|
# trim last line as it is always empty
|
||||||
|
ret = out.splitlines()
|
||||||
|
f = open('workfile', 'w')
|
||||||
|
for a in ret:
|
||||||
|
f.write(a)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def route_is_present(result):
|
||||||
|
if len(result) > 0:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def route_is_absent(result):
|
||||||
|
if len(result) == 0:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def check_hop(result,hop):
|
||||||
|
for line in result:
|
||||||
|
if hop in line.split():
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def check_next_hops(module, result):
|
||||||
|
nexthop = module.params.get('nexthop')
|
||||||
|
nonexthop = module.params.get('nonexthop')
|
||||||
|
prefix = module.params.get('prefix')
|
||||||
|
|
||||||
|
if not nexthop and not nonexthop:
|
||||||
|
return True
|
||||||
|
elif not nexthop and nonexthop:
|
||||||
|
if check_hop(result,nonexthop)==False:
|
||||||
|
return True
|
||||||
|
elif nexthop and not nonexthop:
|
||||||
|
if check_hop(result,nexthop)==True:
|
||||||
|
return True
|
||||||
|
elif nexthop and nonexthop:
|
||||||
|
if check_hop(result,nexthop)==True and check_hop(result,nonexthop)==False:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return false
|
||||||
|
|
||||||
|
def loop_route_check(module):
|
||||||
|
prefix = module.params.get('prefix')
|
||||||
|
state = module.params.get('state')
|
||||||
|
timeout = int(module.params.get('timeout'))
|
||||||
|
poll_interval = int(module.params.get('poll_interval'))
|
||||||
|
|
||||||
|
# using ip route show instead of ip route get
|
||||||
|
# because ip route show will be blank if the exact prefix
|
||||||
|
# is missing from the table. ip route get tries longest prefix
|
||||||
|
# match so may match default route.
|
||||||
|
# command returns empty array if prefix is missing
|
||||||
|
cl_prefix_cmd = '/sbin/ip route show %s' % (prefix)
|
||||||
|
time_elapsed = 0
|
||||||
|
while True:
|
||||||
|
result = run_cl_cmd(module, cl_prefix_cmd)
|
||||||
|
if state == 'present' and route_is_present(result):
|
||||||
|
if check_next_hops(module, result)==True:
|
||||||
|
return True
|
||||||
|
if state == 'absent' and route_is_absent(result):
|
||||||
|
if check_next_hops(module, result)==True:
|
||||||
|
return True
|
||||||
|
time.sleep(poll_interval)
|
||||||
|
time_elapsed += poll_interval
|
||||||
|
if time_elapsed == timeout:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec=dict(
|
||||||
|
prefix=dict(required=True, type='str'),
|
||||||
|
state=dict(default='present', type='str',
|
||||||
|
choices=['present', 'absent']),
|
||||||
|
timeout=dict(default=2, type='int'),
|
||||||
|
poll_interval=dict(default=1, type='int'),
|
||||||
|
nexthop=dict(default='', type='str'),
|
||||||
|
nonexthop=dict(default='', type='str'),
|
||||||
|
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
_state = module.params.get('state')
|
||||||
|
_timeout = module.params.get('timeout')
|
||||||
|
_msg = "Testing whether route is %s. " % (_state)
|
||||||
|
_nexthop = module.params.get('nexthop')
|
||||||
|
_nonexthop = module.params.get('nonexthop')
|
||||||
|
|
||||||
|
#checking for bad parameters
|
||||||
|
if _nexthop == _nonexthop and _nexthop != '':
|
||||||
|
module.fail_json(msg='nexthop and nonexthop cannot be the same')
|
||||||
|
|
||||||
|
#the loop
|
||||||
|
if loop_route_check(module):
|
||||||
|
_msg += 'Condition Met'
|
||||||
|
module.exit_json(msg=_msg, changed=False)
|
||||||
|
else:
|
||||||
|
_msg += 'Condition not met %s second timer expired' % (_timeout)
|
||||||
|
module.fail_json(msg='paremeters not found')
|
||||||
|
|
||||||
|
# import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
import time
|
||||||
|
# from ansible.module_utils.urls import *
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
469
lib/ansible/modules/network/cumulus/cl_quagga_ospf.py
Executable file
469
lib/ansible/modules/network/cumulus/cl_quagga_ospf.py
Executable file
|
@ -0,0 +1,469 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# (c) 2016, Cumulus Networks <ce-ceng@cumulusnetworks.com>
|
||||||
|
#
|
||||||
|
# This file is part of Ansible
|
||||||
|
# Ansible is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
# Ansible is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: cl_quagga_ospf
|
||||||
|
version_added: "2.1"
|
||||||
|
author: "Cumulus Networks (@CumulusNetworks)"
|
||||||
|
short_description: Configure basic OSPFv2 parameters and interfaces using Quagga
|
||||||
|
description:
|
||||||
|
- Configures basic OSPFv2 global parameters such as
|
||||||
|
router id and bandwidth cost, or OSPFv2 interface configuration like
|
||||||
|
point-to-point settings or enabling OSPFv2 on an interface. Configuration
|
||||||
|
is applied to single OSPFv2 instance. Multiple OSPFv2 instance
|
||||||
|
configuration is currently not supported. It requires Quagga version
|
||||||
|
0.99.22 and higher with the non-modal Quagga CLI developed by Cumulus
|
||||||
|
Linux. For more details go to the Routing User Guide at
|
||||||
|
http://docs.cumulusnetworks.com/ and Quagga Docs at
|
||||||
|
http://www.nongnu.org/quagga/
|
||||||
|
options:
|
||||||
|
router_id:
|
||||||
|
description:
|
||||||
|
- Set the OSPFv2 router id
|
||||||
|
required: true
|
||||||
|
reference_bandwidth:
|
||||||
|
description:
|
||||||
|
- Set the OSPFv2 auto cost reference bandwidth
|
||||||
|
default: 40000
|
||||||
|
saveconfig:
|
||||||
|
description:
|
||||||
|
- Boolean. Issue write memory to save the config
|
||||||
|
choices: ['yes', 'no']
|
||||||
|
default: ['no']
|
||||||
|
interface:
|
||||||
|
description:
|
||||||
|
- define the name the interface to apply OSPFv2 services.
|
||||||
|
point2point:
|
||||||
|
description:
|
||||||
|
- Boolean. enable OSPFv2 point2point on the interface
|
||||||
|
choices: ['yes', 'no']
|
||||||
|
require_together:
|
||||||
|
- with interface option
|
||||||
|
area:
|
||||||
|
description:
|
||||||
|
- defines the area the interface is in
|
||||||
|
required_together:
|
||||||
|
- with interface option
|
||||||
|
cost:
|
||||||
|
description:
|
||||||
|
- define ospf cost.
|
||||||
|
required_together:
|
||||||
|
- with interface option
|
||||||
|
passive:
|
||||||
|
description:
|
||||||
|
- make OSPFv2 interface passive
|
||||||
|
choices: ['yes', 'no']
|
||||||
|
required_together:
|
||||||
|
- with interface option
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- Describes if OSPFv2 should be present on a particular interface.
|
||||||
|
Module currently does not check that interface is not associated
|
||||||
|
with a bond or bridge. User will have to manually clear the
|
||||||
|
configuration of the interface from the bond or bridge. This will
|
||||||
|
be implemented in a later release
|
||||||
|
choices: [ 'present', 'absent']
|
||||||
|
default: 'present'
|
||||||
|
required_together:
|
||||||
|
- with interface option
|
||||||
|
requirements: ['Cumulus Linux Quagga non-modal CLI, Quagga version 0.99.22 and higher']
|
||||||
|
'''
|
||||||
|
EXAMPLES = '''
|
||||||
|
Example playbook entries using the cl_quagga_ospf module
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: configure ospf router_id
|
||||||
|
cl_quagga_ospf: router_id=10.1.1.1
|
||||||
|
- name: enable OSPFv2 on swp1 and set it be a point2point OSPF
|
||||||
|
interface with a cost of 65535
|
||||||
|
cl_quagga_ospf: interface=swp1 point2point=yes cost=65535
|
||||||
|
- name: enable ospf on swp1-5
|
||||||
|
cl_quagga_ospf: interface={{ item }}
|
||||||
|
with_sequence: start=1 end=5 format=swp%d
|
||||||
|
- name: disable ospf on swp1
|
||||||
|
cl_quagga_ospf: interface=swp1 state=absent
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = '''
|
||||||
|
changed:
|
||||||
|
description: whether the interface was changed
|
||||||
|
returned: changed
|
||||||
|
type: bool
|
||||||
|
sample: True
|
||||||
|
msg:
|
||||||
|
description: human-readable report of success or failure
|
||||||
|
returned: always
|
||||||
|
type: string
|
||||||
|
sample: "interface bond0 config updated"
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def run_cl_cmd(module, cmd, check_rc=True, split_lines=True):
|
||||||
|
try:
|
||||||
|
(rc, out, err) = module.run_command(cmd, check_rc=check_rc)
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg=e.strerror)
|
||||||
|
# trim last line as it is always empty
|
||||||
|
if split_lines:
|
||||||
|
ret = out.splitlines()
|
||||||
|
else:
|
||||||
|
ret = out
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def check_dsl_dependencies(module, input_options,
|
||||||
|
dependency, _depend_value):
|
||||||
|
for _param in input_options:
|
||||||
|
if module.params.get(_param):
|
||||||
|
if not module.params.get(dependency):
|
||||||
|
_param_output = module.params.get(_param)
|
||||||
|
_msg = "incorrect syntax. " + _param + " must have an interface option." + \
|
||||||
|
" Example 'cl_quagga_ospf: " + dependency + "=" + _depend_value + " " + \
|
||||||
|
_param + "=" + _param_output + "'"
|
||||||
|
module.fail_json(msg=_msg)
|
||||||
|
|
||||||
|
|
||||||
|
def has_interface_config(module):
|
||||||
|
if module.params.get('interface') is not None:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_running_config(module):
|
||||||
|
running_config = run_cl_cmd(module, '/usr/bin/vtysh -c "show run"')
|
||||||
|
got_global_config = False
|
||||||
|
got_interface_config = False
|
||||||
|
module.interface_config = {}
|
||||||
|
module.global_config = []
|
||||||
|
for line in running_config:
|
||||||
|
line = line.lower().strip()
|
||||||
|
# ignore the '!' lines or blank lines
|
||||||
|
if len(line.strip()) <= 1:
|
||||||
|
if got_global_config:
|
||||||
|
got_global_config = False
|
||||||
|
if got_interface_config:
|
||||||
|
got_interface_config = False
|
||||||
|
continue
|
||||||
|
# begin capturing global config
|
||||||
|
m0 = re.match('router\s+ospf', line)
|
||||||
|
if m0:
|
||||||
|
got_global_config = True
|
||||||
|
continue
|
||||||
|
m1 = re.match('^interface\s+(\w+)', line)
|
||||||
|
if m1:
|
||||||
|
module.ifacename = m1.group(1)
|
||||||
|
module.interface_config[module.ifacename] = []
|
||||||
|
got_interface_config = True
|
||||||
|
continue
|
||||||
|
if got_interface_config:
|
||||||
|
module.interface_config[module.ifacename].append(line)
|
||||||
|
continue
|
||||||
|
if got_global_config:
|
||||||
|
m3 = re.match('\s*passive-interface\s+(\w+)', line)
|
||||||
|
if m3:
|
||||||
|
ifaceconfig = module.interface_config.get(m3.group(1))
|
||||||
|
if ifaceconfig:
|
||||||
|
ifaceconfig.append('passive-interface')
|
||||||
|
else:
|
||||||
|
module.global_config.append(line)
|
||||||
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
def get_config_line(module, stmt, ifacename=None):
|
||||||
|
if ifacename:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
for i in module.global_config:
|
||||||
|
if re.match(stmt, i):
|
||||||
|
return i
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def update_router_id(module):
|
||||||
|
router_id_stmt = 'ospf router-id '
|
||||||
|
actual_router_id_stmt = get_config_line(module, router_id_stmt)
|
||||||
|
router_id_stmt = 'ospf router-id ' + module.params.get('router_id')
|
||||||
|
if router_id_stmt != actual_router_id_stmt:
|
||||||
|
cmd_line = "/usr/bin/cl-ospf router-id set %s" %\
|
||||||
|
(module.params.get('router_id'))
|
||||||
|
run_cl_cmd(module, cmd_line)
|
||||||
|
module.exit_msg += 'router-id updated '
|
||||||
|
module.has_changed = True
|
||||||
|
|
||||||
|
|
||||||
|
def update_reference_bandwidth(module):
|
||||||
|
bandwidth_stmt = 'auto-cost reference-bandwidth'
|
||||||
|
actual_bandwidth_stmt = get_config_line(module, bandwidth_stmt)
|
||||||
|
bandwidth_stmt = bandwidth_stmt + ' ' + \
|
||||||
|
module.params.get('reference_bandwidth')
|
||||||
|
if bandwidth_stmt != actual_bandwidth_stmt:
|
||||||
|
cmd_line = "/usr/bin/cl-ospf auto-cost set reference-bandwidth %s" %\
|
||||||
|
(module.params.get('reference_bandwidth'))
|
||||||
|
run_cl_cmd(module, cmd_line)
|
||||||
|
module.exit_msg += 'reference bandwidth updated '
|
||||||
|
module.has_changed = True
|
||||||
|
|
||||||
|
|
||||||
|
def add_global_ospf_config(module):
|
||||||
|
module.has_changed = False
|
||||||
|
get_running_config(module)
|
||||||
|
if module.params.get('router_id'):
|
||||||
|
update_router_id(module)
|
||||||
|
if module.params.get('reference_bandwidth'):
|
||||||
|
update_reference_bandwidth(module)
|
||||||
|
if module.has_changed is False:
|
||||||
|
module.exit_msg = 'No change in OSPFv2 global config'
|
||||||
|
module.exit_json(msg=module.exit_msg, changed=module.has_changed)
|
||||||
|
|
||||||
|
|
||||||
|
def check_ip_addr_show(module):
|
||||||
|
cmd_line = "/sbin/ip addr show %s" % (module.params.get('interface'))
|
||||||
|
result = run_cl_cmd(module, cmd_line)
|
||||||
|
for _line in result:
|
||||||
|
m0 = re.match('\s+inet\s+\w+', _line)
|
||||||
|
if m0:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_interface_addr_config(module):
|
||||||
|
ifacename = module.params.get('interface')
|
||||||
|
cmd_line = "/sbin/ifquery --format json %s" % (ifacename)
|
||||||
|
int_config = run_cl_cmd(module, cmd_line, True, False)
|
||||||
|
ifquery_obj = json.loads(int_config)[0]
|
||||||
|
iface_has_address = False
|
||||||
|
if 'address' in ifquery_obj.get('config'):
|
||||||
|
for addr in ifquery_obj.get('config').get('address'):
|
||||||
|
try:
|
||||||
|
socket.inet_aton(addr.split('/')[0])
|
||||||
|
iface_has_address = True
|
||||||
|
break
|
||||||
|
except socket.error:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
iface_has_address = check_ip_addr_show(module)
|
||||||
|
if iface_has_address is False:
|
||||||
|
_msg = "interface %s does not have an IP configured. " +\
|
||||||
|
"Required for OSPFv2 to work"
|
||||||
|
module.fail_json(msg=_msg)
|
||||||
|
# for test purposes only
|
||||||
|
return iface_has_address
|
||||||
|
|
||||||
|
|
||||||
|
def enable_or_disable_ospf_on_int(module):
|
||||||
|
ifacename = module.params.get('interface')
|
||||||
|
_state = module.params.get('state')
|
||||||
|
iface_config = module.interface_config.get(ifacename)
|
||||||
|
if iface_config is None:
|
||||||
|
_msg = "%s is not found in Quagga config. " % (ifacename) + \
|
||||||
|
"Check that %s is active in kernel" % (ifacename)
|
||||||
|
module.fail_json(msg=_msg)
|
||||||
|
return False # for test purposes
|
||||||
|
found_area = None
|
||||||
|
for i in iface_config:
|
||||||
|
m0 = re.search('ip\s+ospf\s+area\s+([0-9.]+)', i)
|
||||||
|
if m0:
|
||||||
|
found_area = m0.group(1)
|
||||||
|
break
|
||||||
|
if _state == 'absent':
|
||||||
|
for i in iface_config:
|
||||||
|
if found_area:
|
||||||
|
cmd_line = '/usr/bin/cl-ospf clear %s area' % \
|
||||||
|
(ifacename)
|
||||||
|
run_cl_cmd(module, cmd_line)
|
||||||
|
module.has_changed = True
|
||||||
|
module.exit_msg += "OSPFv2 now disabled on %s " % (ifacename)
|
||||||
|
return False
|
||||||
|
area_id = module.params.get('area')
|
||||||
|
if found_area != area_id:
|
||||||
|
cmd_line = '/usr/bin/cl-ospf interface set %s area %s' % \
|
||||||
|
(ifacename, area_id)
|
||||||
|
run_cl_cmd(module, cmd_line)
|
||||||
|
module.has_changed = True
|
||||||
|
module.exit_msg += "OSPFv2 now enabled on %s area %s " % \
|
||||||
|
(ifacename, area_id)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def update_point2point(module):
|
||||||
|
ifacename = module.params.get('interface')
|
||||||
|
point2point = module.params.get('point2point')
|
||||||
|
iface_config = module.interface_config.get(ifacename)
|
||||||
|
found_point2point = None
|
||||||
|
for i in iface_config:
|
||||||
|
m0 = re.search('ip\s+ospf\s+network\s+point-to-point', i)
|
||||||
|
if m0:
|
||||||
|
found_point2point = True
|
||||||
|
break
|
||||||
|
if point2point:
|
||||||
|
if not found_point2point:
|
||||||
|
cmd_line = '/usr/bin/cl-ospf interface set %s network point-to-point' % \
|
||||||
|
(ifacename)
|
||||||
|
run_cl_cmd(module, cmd_line)
|
||||||
|
module.has_changed = True
|
||||||
|
module.exit_msg += 'OSPFv2 point2point set on %s ' % (ifacename)
|
||||||
|
else:
|
||||||
|
if found_point2point:
|
||||||
|
cmd_line = '/usr/bin/cl-ospf interface clear %s network' % \
|
||||||
|
(ifacename)
|
||||||
|
run_cl_cmd(module, cmd_line)
|
||||||
|
module.has_changed = True
|
||||||
|
module.exit_msg += 'OSPFv2 point2point removed on %s ' % \
|
||||||
|
(ifacename)
|
||||||
|
|
||||||
|
|
||||||
|
def update_passive(module):
|
||||||
|
ifacename = module.params.get('interface')
|
||||||
|
passive = module.params.get('passive')
|
||||||
|
iface_config = module.interface_config.get(ifacename)
|
||||||
|
found_passive = None
|
||||||
|
for i in iface_config:
|
||||||
|
m0 = re.search('passive-interface', i)
|
||||||
|
if m0:
|
||||||
|
found_passive = True
|
||||||
|
break
|
||||||
|
if passive:
|
||||||
|
if not found_passive:
|
||||||
|
cmd_line = '/usr/bin/cl-ospf interface set %s passive' % \
|
||||||
|
(ifacename)
|
||||||
|
run_cl_cmd(module, cmd_line)
|
||||||
|
module.has_changed = True
|
||||||
|
module.exit_msg += '%s is now OSPFv2 passive ' % (ifacename)
|
||||||
|
else:
|
||||||
|
if found_passive:
|
||||||
|
cmd_line = '/usr/bin/cl-ospf interface clear %s passive' % \
|
||||||
|
(ifacename)
|
||||||
|
run_cl_cmd(module, cmd_line)
|
||||||
|
module.has_changed = True
|
||||||
|
module.exit_msg += '%s is no longer OSPFv2 passive ' % \
|
||||||
|
(ifacename)
|
||||||
|
|
||||||
|
|
||||||
|
def update_cost(module):
|
||||||
|
ifacename = module.params.get('interface')
|
||||||
|
cost = module.params.get('cost')
|
||||||
|
iface_config = module.interface_config.get(ifacename)
|
||||||
|
found_cost = None
|
||||||
|
for i in iface_config:
|
||||||
|
m0 = re.search('ip\s+ospf\s+cost\s+(\d+)', i)
|
||||||
|
if m0:
|
||||||
|
found_cost = m0.group(1)
|
||||||
|
break
|
||||||
|
|
||||||
|
if cost != found_cost and cost is not None:
|
||||||
|
cmd_line = '/usr/bin/cl-ospf interface set %s cost %s' % \
|
||||||
|
(ifacename, cost)
|
||||||
|
run_cl_cmd(module, cmd_line)
|
||||||
|
module.has_changed = True
|
||||||
|
module.exit_msg += 'OSPFv2 cost on %s changed to %s ' % \
|
||||||
|
(ifacename, cost)
|
||||||
|
elif cost is None and found_cost is not None:
|
||||||
|
cmd_line = '/usr/bin/cl-ospf interface clear %s cost' % \
|
||||||
|
(ifacename)
|
||||||
|
run_cl_cmd(module, cmd_line)
|
||||||
|
module.has_changed = True
|
||||||
|
module.exit_msg += 'OSPFv2 cost on %s changed to default ' % \
|
||||||
|
(ifacename)
|
||||||
|
|
||||||
|
|
||||||
|
def config_ospf_interface_config(module):
|
||||||
|
enable_int_defaults(module)
|
||||||
|
module.has_changed = False
|
||||||
|
# get all ospf related config from quagga both globally and iface based
|
||||||
|
get_running_config(module)
|
||||||
|
# if interface does not have ipv4 address module should fail
|
||||||
|
get_interface_addr_config(module)
|
||||||
|
# if ospf should be enabled, continue to check for the remaining attrs
|
||||||
|
if enable_or_disable_ospf_on_int(module):
|
||||||
|
# update ospf point-to-point setting if needed
|
||||||
|
update_point2point(module)
|
||||||
|
# update ospf interface cost if needed
|
||||||
|
update_cost(module)
|
||||||
|
# update ospf interface passive setting
|
||||||
|
update_passive(module)
|
||||||
|
|
||||||
|
|
||||||
|
def saveconfig(module):
|
||||||
|
if module.params.get('saveconfig') is True and\
|
||||||
|
module.has_changed:
|
||||||
|
run_cl_cmd(module, '/usr/bin/vtysh -c "wr mem"')
|
||||||
|
module.exit_msg += 'Saving Config '
|
||||||
|
|
||||||
|
|
||||||
|
def enable_int_defaults(module):
|
||||||
|
if not module.params.get('area'):
|
||||||
|
module.params['area'] = '0.0.0.0'
|
||||||
|
if not module.params.get('state'):
|
||||||
|
module.params['state'] = 'present'
|
||||||
|
|
||||||
|
|
||||||
|
def check_if_ospf_is_running(module):
|
||||||
|
if not os.path.exists('/var/run/quagga/ospfd.pid'):
|
||||||
|
_msg = 'OSPFv2 process is not running. Unable to execute command'
|
||||||
|
module.fail_json(msg=_msg)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec=dict(
|
||||||
|
reference_bandwidth=dict(type='str',
|
||||||
|
default='40000'),
|
||||||
|
router_id=dict(type='str'),
|
||||||
|
interface=dict(type='str'),
|
||||||
|
cost=dict(type='str'),
|
||||||
|
area=dict(type='str'),
|
||||||
|
state=dict(type='str',
|
||||||
|
choices=['present', 'absent']),
|
||||||
|
point2point=dict(type='bool', choices=BOOLEANS),
|
||||||
|
saveconfig=dict(type='bool', choices=BOOLEANS, default=False),
|
||||||
|
passive=dict(type='bool', choices=BOOLEANS)
|
||||||
|
),
|
||||||
|
mutually_exclusive=[['reference_bandwidth', 'interface'],
|
||||||
|
['router_id', 'interface']]
|
||||||
|
)
|
||||||
|
check_if_ospf_is_running(module)
|
||||||
|
|
||||||
|
check_dsl_dependencies(module, ['cost', 'state', 'area',
|
||||||
|
'point2point', 'passive'],
|
||||||
|
'interface', 'swp1')
|
||||||
|
module.has_changed = False
|
||||||
|
module.exit_msg = ''
|
||||||
|
if has_interface_config(module):
|
||||||
|
config_ospf_interface_config(module)
|
||||||
|
else:
|
||||||
|
# Set area to none before applying global config
|
||||||
|
module.params['area'] = None
|
||||||
|
add_global_ospf_config(module)
|
||||||
|
saveconfig(module)
|
||||||
|
if module.has_changed:
|
||||||
|
module.exit_json(msg=module.exit_msg, changed=module.has_changed)
|
||||||
|
else:
|
||||||
|
module.exit_json(msg='no change', changed=False)
|
||||||
|
|
||||||
|
# import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
# incompatible with ansible 1.4.4 - ubuntu 12.04 version
|
||||||
|
# from ansible.module_utils.urls import *
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
212
lib/ansible/modules/network/cumulus/cl_quagga_protocol.py
Executable file
212
lib/ansible/modules/network/cumulus/cl_quagga_protocol.py
Executable file
|
@ -0,0 +1,212 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# (c) 2016, Cumulus Networks <ce-ceng@cumulusnetworks.com>
|
||||||
|
#
|
||||||
|
# This file is part of Ansible
|
||||||
|
# Ansible is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
# Ansible is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: cl_quagga_protocol
|
||||||
|
version_added: "2.1"
|
||||||
|
author: "Cumulus Networks (@CumulusNetworks)"
|
||||||
|
short_description: Enable routing protocol services via Quagga
|
||||||
|
description:
|
||||||
|
- Enable Quagga services available on Cumulus Linux. This includes OSPF
|
||||||
|
v2/v3 and BGP. Quagga services are defined in the /etc/quagga/daemons
|
||||||
|
file. This module creates a file that will only enable OSPF or BGP routing
|
||||||
|
protocols, because this is what Cumulus Linux currently supports. Zebra is
|
||||||
|
automatically enabled when a supported routing protocol is listed. If all
|
||||||
|
routing protocols are disabled, this module will disable zebra as well.
|
||||||
|
Using Ansible Templates you can run any supported or unsupported quagga
|
||||||
|
routing protocol. For more details go to the Quagga Documentation located
|
||||||
|
at http://docs.cumulusnetworks.com/ and at
|
||||||
|
http://www.nongnu.org/quagga/docs.html
|
||||||
|
options:
|
||||||
|
name:
|
||||||
|
description:
|
||||||
|
- name of the protocol to update
|
||||||
|
choices: ['ospfd', 'ospf6d', 'bgpd']
|
||||||
|
required: true
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- describe whether the protocol should be enabled or disabled
|
||||||
|
choices: ['present', 'absent']
|
||||||
|
required: true
|
||||||
|
activate:
|
||||||
|
description:
|
||||||
|
- restart quagga process to activate the change. If the service
|
||||||
|
is already configured but not activated, setting activate=yes will
|
||||||
|
not activate the service. This will be fixed in an upcoming
|
||||||
|
release
|
||||||
|
choices: ['yes', 'no']
|
||||||
|
default: ['no']
|
||||||
|
requirements: ['Quagga version 0.99.23 and higher']
|
||||||
|
'''
|
||||||
|
EXAMPLES = '''
|
||||||
|
Example playbook entries using the cl_quagga module
|
||||||
|
|
||||||
|
## Enable OSPFv2. Do not activate the change
|
||||||
|
cl_quagga_protocol name="ospfd" state=present
|
||||||
|
|
||||||
|
## Disable OSPFv2. Do not activate the change
|
||||||
|
cl_quagga_protocol name="ospf6d" state=absent
|
||||||
|
|
||||||
|
## Enable BGPv2. Do not activate the change. Activating the change requires a
|
||||||
|
## restart of the entire quagga process.
|
||||||
|
cl_quagga_protocol name="bgpd" state=present
|
||||||
|
|
||||||
|
## Enable OSPFv2 and activate the change as this might not start quagga when you
|
||||||
|
## want it to
|
||||||
|
cl_quagga_protocol name="ospfd" state=present activate=yes
|
||||||
|
|
||||||
|
## To activate a configured service
|
||||||
|
|
||||||
|
- name: disable ospfv2 service. Its configured but not enabled
|
||||||
|
cl_quagga_protocol name=ospfd state=absent
|
||||||
|
|
||||||
|
- name: enable ospfv2 service and activate it
|
||||||
|
cl_quagga_protocol name=ospfd state=present activate=yes
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = '''
|
||||||
|
changed:
|
||||||
|
description: whether the interface was changed
|
||||||
|
returned: changed
|
||||||
|
type: bool
|
||||||
|
sample: True
|
||||||
|
msg:
|
||||||
|
description: human-readable report of success or failure
|
||||||
|
returned: always
|
||||||
|
type: string
|
||||||
|
sample: "interface bond0 config updated"
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def run_cl_cmd(module, cmd, check_rc=True):
|
||||||
|
try:
|
||||||
|
(rc, out, err) = module.run_command(cmd, check_rc=check_rc)
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg=e.strerror)
|
||||||
|
# trim last line as it is always empty
|
||||||
|
ret = out.splitlines()
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def convert_to_yes_or_no(_state):
|
||||||
|
if _state == 'present':
|
||||||
|
_str = 'yes'
|
||||||
|
else:
|
||||||
|
_str = 'no'
|
||||||
|
return _str
|
||||||
|
|
||||||
|
|
||||||
|
def read_daemon_file(module):
|
||||||
|
f = open(module.quagga_daemon_file)
|
||||||
|
if f:
|
||||||
|
return f.readlines()
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def setting_is_configured(module):
|
||||||
|
_protocol = module.params.get('name')
|
||||||
|
_state = module.params.get('state')
|
||||||
|
_state = convert_to_yes_or_no(_state)
|
||||||
|
_daemon_output = read_daemon_file(module)
|
||||||
|
_str = "(%s)=(%s)" % (_protocol, 'yes|no')
|
||||||
|
_daemonstr = re.compile("\w+=yes")
|
||||||
|
_zebrastr = re.compile("zebra=(yes|no)")
|
||||||
|
_matchstr = re.compile(_str)
|
||||||
|
daemoncount = 0
|
||||||
|
module.disable_zebra = False
|
||||||
|
for _line in _daemon_output:
|
||||||
|
_match = re.match(_matchstr, _line)
|
||||||
|
_active_daemon_match = re.match(_daemonstr, _line)
|
||||||
|
_zebramatch = re.match(_zebrastr, _line)
|
||||||
|
if _active_daemon_match:
|
||||||
|
daemoncount += 1
|
||||||
|
if _zebramatch:
|
||||||
|
if _zebramatch.group(1) == 'no' and _state == 'yes':
|
||||||
|
return False
|
||||||
|
elif _match:
|
||||||
|
if _state == _match.group(2):
|
||||||
|
_msg = "%s is configured and is %s" % \
|
||||||
|
(_protocol, module.params.get('state'))
|
||||||
|
module.exit_json(msg=_msg, changed=False)
|
||||||
|
# for nosetests purposes only
|
||||||
|
if daemoncount < 3 and _state == 'no':
|
||||||
|
module.disable_zebra = True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def modify_config(module):
|
||||||
|
_protocol = module.params.get('name')
|
||||||
|
_state = module.params.get('state')
|
||||||
|
_state = convert_to_yes_or_no(_state)
|
||||||
|
_daemon_output = read_daemon_file(module)
|
||||||
|
_str = "(%s)=(%s)" % (_protocol, 'yes|no')
|
||||||
|
_zebrastr = re.compile("zebra=(yes|no)")
|
||||||
|
_matchstr = re.compile(_str)
|
||||||
|
write_to_file = open(module.quagga_daemon_file, 'w')
|
||||||
|
for _line in _daemon_output:
|
||||||
|
_match = re.match(_matchstr, _line)
|
||||||
|
_zebramatch = re.match(_zebrastr, _line)
|
||||||
|
if _zebramatch:
|
||||||
|
if module.disable_zebra is True and _state == 'no':
|
||||||
|
write_to_file.write('zebra=no\n')
|
||||||
|
elif _state == 'yes':
|
||||||
|
write_to_file.write('zebra=yes\n')
|
||||||
|
else:
|
||||||
|
write_to_file.write(_line)
|
||||||
|
elif _match:
|
||||||
|
if _state != _match.group(2):
|
||||||
|
_str = "%s=%s\n" % (_protocol, _state)
|
||||||
|
write_to_file.write(_str)
|
||||||
|
else:
|
||||||
|
write_to_file.write(_line)
|
||||||
|
write_to_file.close()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec=dict(
|
||||||
|
name=dict(type='str',
|
||||||
|
choices=['ospfd', 'ospf6d', 'bgpd'],
|
||||||
|
required=True),
|
||||||
|
state=dict(type='str',
|
||||||
|
choices=['present', 'absent'],
|
||||||
|
required=True),
|
||||||
|
activate=dict(type='bool', choices=BOOLEANS, default=False)
|
||||||
|
))
|
||||||
|
module.quagga_daemon_file = '/etc/quagga/daemons'
|
||||||
|
setting_is_configured(module)
|
||||||
|
modify_config(module)
|
||||||
|
_protocol = module.params.get('name')
|
||||||
|
_state = module.params.get('state')
|
||||||
|
_state = convert_to_yes_or_no(_state)
|
||||||
|
_msg = "%s protocol setting modified to %s" % \
|
||||||
|
(_protocol, _state)
|
||||||
|
if module.params.get('activate') is True:
|
||||||
|
run_cl_cmd(module, '/usr/sbin/service quagga restart')
|
||||||
|
_msg += '. Restarted Quagga Service'
|
||||||
|
module.exit_json(msg=_msg, changed=True)
|
||||||
|
|
||||||
|
# import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
# incompatible with ansible 1.4.4 - ubuntu 12.04 version
|
||||||
|
# from ansible.module_utils.urls import *
|
||||||
|
import re
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
Loading…
Reference in a new issue