Adding files to ansible core modules.

This commit is contained in:
Scott Suehle 2016-01-25 15:59:45 -08:00 committed by Matt Clay
parent d3b826dda2
commit d9a071089b
10 changed files with 3013 additions and 0 deletions

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()