summaryrefslogtreecommitdiff
path: root/network/cumulus/cl_ports.py
blob: 85b3ed94d0243a83f481c189eb1f66d3a903e96c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
#!/usr/bin/python
# -*- coding: utf-8 -*-

# (c) 2016, Cumulus Networks <ce-ceng@cumulusnetworks.com>
#
# This file is part of Ansible
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.

ANSIBLE_METADATA = {'status': ['preview'],
                    'supported_by': 'community',
                    'version': '1.0'}

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
      U(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 = get_exception()
        _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 = get_exception()
        _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 = get_exception()
            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()