diff --git a/lib/ansible/modules/network/cumulus/cl_bond.py b/lib/ansible/modules/network/cumulus/cl_bond.py new file mode 100755 index 00000000000..c6844bd6c17 --- /dev/null +++ b/lib/ansible/modules/network/cumulus/cl_bond.py @@ -0,0 +1,468 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2016, Cumulus Networks +# +# 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 . + +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() diff --git a/lib/ansible/modules/network/cumulus/cl_bridge.py b/lib/ansible/modules/network/cumulus/cl_bridge.py new file mode 100755 index 00000000000..076c3f56d66 --- /dev/null +++ b/lib/ansible/modules/network/cumulus/cl_bridge.py @@ -0,0 +1,404 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2016, Cumulus Networks +# +# 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 . + +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() diff --git a/lib/ansible/modules/network/cumulus/cl_img_install.py b/lib/ansible/modules/network/cumulus/cl_img_install.py new file mode 100755 index 00000000000..79c0eaf0d80 --- /dev/null +++ b/lib/ansible/modules/network/cumulus/cl_img_install.py @@ -0,0 +1,312 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2016, Cumulus Networks +# +# 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 . + +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() diff --git a/lib/ansible/modules/network/cumulus/cl_interface.py b/lib/ansible/modules/network/cumulus/cl_interface.py new file mode 100644 index 00000000000..71c1a75ecac --- /dev/null +++ b/lib/ansible/modules/network/cumulus/cl_interface.py @@ -0,0 +1,438 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2016, Cumulus Networks +# +# 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 . + +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() diff --git a/lib/ansible/modules/network/cumulus/cl_interface_policy.py b/lib/ansible/modules/network/cumulus/cl_interface_policy.py new file mode 100644 index 00000000000..307c477ccfa --- /dev/null +++ b/lib/ansible/modules/network/cumulus/cl_interface_policy.py @@ -0,0 +1,147 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2016, Cumulus Networks +# +# 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 . + +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() diff --git a/lib/ansible/modules/network/cumulus/cl_license.py b/lib/ansible/modules/network/cumulus/cl_license.py new file mode 100755 index 00000000000..deafed5e49a --- /dev/null +++ b/lib/ansible/modules/network/cumulus/cl_license.py @@ -0,0 +1,139 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2016, Cumulus Networks +# +# 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 . + +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 " + 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() diff --git a/lib/ansible/modules/network/cumulus/cl_ports.py b/lib/ansible/modules/network/cumulus/cl_ports.py new file mode 100755 index 00000000000..e632cdddac2 --- /dev/null +++ b/lib/ansible/modules/network/cumulus/cl_ports.py @@ -0,0 +1,210 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2016, Cumulus Networks +# +# 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 . + +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() diff --git a/lib/ansible/modules/network/cumulus/cl_prefix_check.py b/lib/ansible/modules/network/cumulus/cl_prefix_check.py new file mode 100755 index 00000000000..87d26584b63 --- /dev/null +++ b/lib/ansible/modules/network/cumulus/cl_prefix_check.py @@ -0,0 +1,214 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2016, Cumulus Networks +# +# 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 . + +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() diff --git a/lib/ansible/modules/network/cumulus/cl_quagga_ospf.py b/lib/ansible/modules/network/cumulus/cl_quagga_ospf.py new file mode 100755 index 00000000000..03cea7a7221 --- /dev/null +++ b/lib/ansible/modules/network/cumulus/cl_quagga_ospf.py @@ -0,0 +1,469 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2016, Cumulus Networks +# +# 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 . + +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() diff --git a/lib/ansible/modules/network/cumulus/cl_quagga_protocol.py b/lib/ansible/modules/network/cumulus/cl_quagga_protocol.py new file mode 100755 index 00000000000..15a5410ecbb --- /dev/null +++ b/lib/ansible/modules/network/cumulus/cl_quagga_protocol.py @@ -0,0 +1,212 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2016, Cumulus Networks +# +# 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 . + +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()