~junaidali/charms/trusty/plumgrid-director/pg-restart

« back to all changes in this revision

Viewing changes to hooks/charmhelpers/contrib/network/ufw.py

  • Committer: bbaqar at plumgrid
  • Date: 2015-07-29 18:07:31 UTC
  • Revision ID: bbaqar@plumgrid.com-20150729180731-ioynar8x3u5pxytc
Addressed reviews by Charmers

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright 2014-2015 Canonical Limited.
 
2
#
 
3
# This file is part of charm-helpers.
 
4
#
 
5
# charm-helpers is free software: you can redistribute it and/or modify
 
6
# it under the terms of the GNU Lesser General Public License version 3 as
 
7
# published by the Free Software Foundation.
 
8
#
 
9
# charm-helpers is distributed in the hope that it will be useful,
 
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
12
# GNU Lesser General Public License for more details.
 
13
#
 
14
# You should have received a copy of the GNU Lesser General Public License
 
15
# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
 
16
 
 
17
"""
 
18
This module contains helpers to add and remove ufw rules.
 
19
 
 
20
Examples:
 
21
 
 
22
- open SSH port for subnet 10.0.3.0/24:
 
23
 
 
24
  >>> from charmhelpers.contrib.network import ufw
 
25
  >>> ufw.enable()
 
26
  >>> ufw.grant_access(src='10.0.3.0/24', dst='any', port='22', proto='tcp')
 
27
 
 
28
- open service by name as defined in /etc/services:
 
29
 
 
30
  >>> from charmhelpers.contrib.network import ufw
 
31
  >>> ufw.enable()
 
32
  >>> ufw.service('ssh', 'open')
 
33
 
 
34
- close service by port number:
 
35
 
 
36
  >>> from charmhelpers.contrib.network import ufw
 
37
  >>> ufw.enable()
 
38
  >>> ufw.service('4949', 'close')  # munin
 
39
"""
 
40
import re
 
41
import os
 
42
import subprocess
 
43
from charmhelpers.core import hookenv
 
44
 
 
45
__author__ = "Felipe Reyes <felipe.reyes@canonical.com>"
 
46
 
 
47
 
 
48
class UFWError(Exception):
 
49
    pass
 
50
 
 
51
 
 
52
class UFWIPv6Error(UFWError):
 
53
    pass
 
54
 
 
55
 
 
56
def is_enabled():
 
57
    """
 
58
    Check if `ufw` is enabled
 
59
 
 
60
    :returns: True if ufw is enabled
 
61
    """
 
62
    output = subprocess.check_output(['ufw', 'status'],
 
63
                                     universal_newlines=True,
 
64
                                     env={'LANG': 'en_US',
 
65
                                          'PATH': os.environ['PATH']})
 
66
 
 
67
    m = re.findall(r'^Status: active\n', output, re.M)
 
68
 
 
69
    return len(m) >= 1
 
70
 
 
71
 
 
72
def is_ipv6_ok(soft_fail=False):
 
73
    """
 
74
    Check if IPv6 support is present and ip6tables functional
 
75
 
 
76
    :param soft_fail: If set to True and IPv6 support is broken, then reports
 
77
                      that the host doesn't have IPv6 support, otherwise a
 
78
                      UFWIPv6Error exception is raised.
 
79
    :returns: True if IPv6 is working, False otherwise
 
80
    """
 
81
 
 
82
    # do we have IPv6 in the machine?
 
83
    if os.path.isdir('/proc/sys/net/ipv6'):
 
84
        # is ip6tables kernel module loaded?
 
85
        lsmod = subprocess.check_output(['lsmod'], universal_newlines=True)
 
86
        matches = re.findall('^ip6_tables[ ]+', lsmod, re.M)
 
87
        if len(matches) == 0:
 
88
            # ip6tables support isn't complete, let's try to load it
 
89
            try:
 
90
                subprocess.check_output(['modprobe', 'ip6_tables'],
 
91
                                        universal_newlines=True)
 
92
                # great, we could load the module
 
93
                return True
 
94
            except subprocess.CalledProcessError as ex:
 
95
                hookenv.log("Couldn't load ip6_tables module: %s" % ex.output,
 
96
                            level="WARN")
 
97
                # we are in a world where ip6tables isn't working
 
98
                if soft_fail:
 
99
                    # so we inform that the machine doesn't have IPv6
 
100
                    return False
 
101
                else:
 
102
                    raise UFWIPv6Error("IPv6 firewall support broken")
 
103
        else:
 
104
            # the module is present :)
 
105
            return True
 
106
 
 
107
    else:
 
108
        # the system doesn't have IPv6
 
109
        return False
 
110
 
 
111
 
 
112
def disable_ipv6():
 
113
    """
 
114
    Disable ufw IPv6 support in /etc/default/ufw
 
115
    """
 
116
    exit_code = subprocess.call(['sed', '-i', 's/IPV6=.*/IPV6=no/g',
 
117
                                 '/etc/default/ufw'])
 
118
    if exit_code == 0:
 
119
        hookenv.log('IPv6 support in ufw disabled', level='INFO')
 
120
    else:
 
121
        hookenv.log("Couldn't disable IPv6 support in ufw", level="ERROR")
 
122
        raise UFWError("Couldn't disable IPv6 support in ufw")
 
123
 
 
124
 
 
125
def enable(soft_fail=False):
 
126
    """
 
127
    Enable ufw
 
128
 
 
129
    :param soft_fail: If set to True silently disables IPv6 support in ufw,
 
130
                      otherwise a UFWIPv6Error exception is raised when IP6
 
131
                      support is broken.
 
132
    :returns: True if ufw is successfully enabled
 
133
    """
 
134
    if is_enabled():
 
135
        return True
 
136
 
 
137
    if not is_ipv6_ok(soft_fail):
 
138
        disable_ipv6()
 
139
 
 
140
    output = subprocess.check_output(['ufw', 'enable'],
 
141
                                     universal_newlines=True,
 
142
                                     env={'LANG': 'en_US',
 
143
                                          'PATH': os.environ['PATH']})
 
144
 
 
145
    m = re.findall('^Firewall is active and enabled on system startup\n',
 
146
                   output, re.M)
 
147
    hookenv.log(output, level='DEBUG')
 
148
 
 
149
    if len(m) == 0:
 
150
        hookenv.log("ufw couldn't be enabled", level='WARN')
 
151
        return False
 
152
    else:
 
153
        hookenv.log("ufw enabled", level='INFO')
 
154
        return True
 
155
 
 
156
 
 
157
def disable():
 
158
    """
 
159
    Disable ufw
 
160
 
 
161
    :returns: True if ufw is successfully disabled
 
162
    """
 
163
    if not is_enabled():
 
164
        return True
 
165
 
 
166
    output = subprocess.check_output(['ufw', 'disable'],
 
167
                                     universal_newlines=True,
 
168
                                     env={'LANG': 'en_US',
 
169
                                          'PATH': os.environ['PATH']})
 
170
 
 
171
    m = re.findall(r'^Firewall stopped and disabled on system startup\n',
 
172
                   output, re.M)
 
173
    hookenv.log(output, level='DEBUG')
 
174
 
 
175
    if len(m) == 0:
 
176
        hookenv.log("ufw couldn't be disabled", level='WARN')
 
177
        return False
 
178
    else:
 
179
        hookenv.log("ufw disabled", level='INFO')
 
180
        return True
 
181
 
 
182
 
 
183
def default_policy(policy='deny', direction='incoming'):
 
184
    """
 
185
    Changes the default policy for traffic `direction`
 
186
 
 
187
    :param policy: allow, deny or reject
 
188
    :param direction: traffic direction, possible values: incoming, outgoing,
 
189
                      routed
 
190
    """
 
191
    if policy not in ['allow', 'deny', 'reject']:
 
192
        raise UFWError(('Unknown policy %s, valid values: '
 
193
                        'allow, deny, reject') % policy)
 
194
 
 
195
    if direction not in ['incoming', 'outgoing', 'routed']:
 
196
        raise UFWError(('Unknown direction %s, valid values: '
 
197
                        'incoming, outgoing, routed') % direction)
 
198
 
 
199
    output = subprocess.check_output(['ufw', 'default', policy, direction],
 
200
                                     universal_newlines=True,
 
201
                                     env={'LANG': 'en_US',
 
202
                                          'PATH': os.environ['PATH']})
 
203
    hookenv.log(output, level='DEBUG')
 
204
 
 
205
    m = re.findall("^Default %s policy changed to '%s'\n" % (direction,
 
206
                                                             policy),
 
207
                   output, re.M)
 
208
    if len(m) == 0:
 
209
        hookenv.log("ufw couldn't change the default policy to %s for %s"
 
210
                    % (policy, direction), level='WARN')
 
211
        return False
 
212
    else:
 
213
        hookenv.log("ufw default policy for %s changed to %s"
 
214
                    % (direction, policy), level='INFO')
 
215
        return True
 
216
 
 
217
 
 
218
def modify_access(src, dst='any', port=None, proto=None, action='allow',
 
219
                  index=None):
 
220
    """
 
221
    Grant access to an address or subnet
 
222
 
 
223
    :param src: address (e.g. 192.168.1.234) or subnet
 
224
                (e.g. 192.168.1.0/24).
 
225
    :param dst: destiny of the connection, if the machine has multiple IPs and
 
226
                connections to only one of those have to accepted this is the
 
227
                field has to be set.
 
228
    :param port: destiny port
 
229
    :param proto: protocol (tcp or udp)
 
230
    :param action: `allow` or `delete`
 
231
    :param index: if different from None the rule is inserted at the given
 
232
                  `index`.
 
233
    """
 
234
    if not is_enabled():
 
235
        hookenv.log('ufw is disabled, skipping modify_access()', level='WARN')
 
236
        return
 
237
 
 
238
    if action == 'delete':
 
239
        cmd = ['ufw', 'delete', 'allow']
 
240
    elif index is not None:
 
241
        cmd = ['ufw', 'insert', str(index), action]
 
242
    else:
 
243
        cmd = ['ufw', action]
 
244
 
 
245
    if src is not None:
 
246
        cmd += ['from', src]
 
247
 
 
248
    if dst is not None:
 
249
        cmd += ['to', dst]
 
250
 
 
251
    if port is not None:
 
252
        cmd += ['port', str(port)]
 
253
 
 
254
    if proto is not None:
 
255
        cmd += ['proto', proto]
 
256
 
 
257
    hookenv.log('ufw {}: {}'.format(action, ' '.join(cmd)), level='DEBUG')
 
258
    p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
 
259
    (stdout, stderr) = p.communicate()
 
260
 
 
261
    hookenv.log(stdout, level='INFO')
 
262
 
 
263
    if p.returncode != 0:
 
264
        hookenv.log(stderr, level='ERROR')
 
265
        hookenv.log('Error running: {}, exit code: {}'.format(' '.join(cmd),
 
266
                                                              p.returncode),
 
267
                    level='ERROR')
 
268
 
 
269
 
 
270
def grant_access(src, dst='any', port=None, proto=None, index=None):
 
271
    """
 
272
    Grant access to an address or subnet
 
273
 
 
274
    :param src: address (e.g. 192.168.1.234) or subnet
 
275
                (e.g. 192.168.1.0/24).
 
276
    :param dst: destiny of the connection, if the machine has multiple IPs and
 
277
                connections to only one of those have to accepted this is the
 
278
                field has to be set.
 
279
    :param port: destiny port
 
280
    :param proto: protocol (tcp or udp)
 
281
    :param index: if different from None the rule is inserted at the given
 
282
                  `index`.
 
283
    """
 
284
    return modify_access(src, dst=dst, port=port, proto=proto, action='allow',
 
285
                         index=index)
 
286
 
 
287
 
 
288
def revoke_access(src, dst='any', port=None, proto=None):
 
289
    """
 
290
    Revoke access to an address or subnet
 
291
 
 
292
    :param src: address (e.g. 192.168.1.234) or subnet
 
293
                (e.g. 192.168.1.0/24).
 
294
    :param dst: destiny of the connection, if the machine has multiple IPs and
 
295
                connections to only one of those have to accepted this is the
 
296
                field has to be set.
 
297
    :param port: destiny port
 
298
    :param proto: protocol (tcp or udp)
 
299
    """
 
300
    return modify_access(src, dst=dst, port=port, proto=proto, action='delete')
 
301
 
 
302
 
 
303
def service(name, action):
 
304
    """
 
305
    Open/close access to a service
 
306
 
 
307
    :param name: could be a service name defined in `/etc/services` or a port
 
308
                 number.
 
309
    :param action: `open` or `close`
 
310
    """
 
311
    if action == 'open':
 
312
        subprocess.check_output(['ufw', 'allow', str(name)],
 
313
                                universal_newlines=True)
 
314
    elif action == 'close':
 
315
        subprocess.check_output(['ufw', 'delete', 'allow', str(name)],
 
316
                                universal_newlines=True)
 
317
    else:
 
318
        raise UFWError(("'{}' not supported, use 'allow' "
 
319
                        "or 'delete'").format(action))