~canonical-ci-engineering/charms/trusty/core-image-publisher/trunk

« back to all changes in this revision

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

  • Committer: Celso Providelo
  • Date: 2015-03-25 04:13:43 UTC
  • Revision ID: celso.providelo@canonical.com-20150325041343-jw05jaz6jscs3c8f
fork of core-image-watcher

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 modify_access(src, dst='any', port=None, proto=None, action='allow'):
 
184
    """
 
185
    Grant access to an address or subnet
 
186
 
 
187
    :param src: address (e.g. 192.168.1.234) or subnet
 
188
                (e.g. 192.168.1.0/24).
 
189
    :param dst: destiny of the connection, if the machine has multiple IPs and
 
190
                connections to only one of those have to accepted this is the
 
191
                field has to be set.
 
192
    :param port: destiny port
 
193
    :param proto: protocol (tcp or udp)
 
194
    :param action: `allow` or `delete`
 
195
    """
 
196
    if not is_enabled():
 
197
        hookenv.log('ufw is disabled, skipping modify_access()', level='WARN')
 
198
        return
 
199
 
 
200
    if action == 'delete':
 
201
        cmd = ['ufw', 'delete', 'allow']
 
202
    else:
 
203
        cmd = ['ufw', action]
 
204
 
 
205
    if src is not None:
 
206
        cmd += ['from', src]
 
207
 
 
208
    if dst is not None:
 
209
        cmd += ['to', dst]
 
210
 
 
211
    if port is not None:
 
212
        cmd += ['port', str(port)]
 
213
 
 
214
    if proto is not None:
 
215
        cmd += ['proto', proto]
 
216
 
 
217
    hookenv.log('ufw {}: {}'.format(action, ' '.join(cmd)), level='DEBUG')
 
218
    p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
 
219
    (stdout, stderr) = p.communicate()
 
220
 
 
221
    hookenv.log(stdout, level='INFO')
 
222
 
 
223
    if p.returncode != 0:
 
224
        hookenv.log(stderr, level='ERROR')
 
225
        hookenv.log('Error running: {}, exit code: {}'.format(' '.join(cmd),
 
226
                                                              p.returncode),
 
227
                    level='ERROR')
 
228
 
 
229
 
 
230
def grant_access(src, dst='any', port=None, proto=None):
 
231
    """
 
232
    Grant access to an address or subnet
 
233
 
 
234
    :param src: address (e.g. 192.168.1.234) or subnet
 
235
                (e.g. 192.168.1.0/24).
 
236
    :param dst: destiny of the connection, if the machine has multiple IPs and
 
237
                connections to only one of those have to accepted this is the
 
238
                field has to be set.
 
239
    :param port: destiny port
 
240
    :param proto: protocol (tcp or udp)
 
241
    """
 
242
    return modify_access(src, dst=dst, port=port, proto=proto, action='allow')
 
243
 
 
244
 
 
245
def revoke_access(src, dst='any', port=None, proto=None):
 
246
    """
 
247
    Revoke access to an address or subnet
 
248
 
 
249
    :param src: address (e.g. 192.168.1.234) or subnet
 
250
                (e.g. 192.168.1.0/24).
 
251
    :param dst: destiny of the connection, if the machine has multiple IPs and
 
252
                connections to only one of those have to accepted this is the
 
253
                field has to be set.
 
254
    :param port: destiny port
 
255
    :param proto: protocol (tcp or udp)
 
256
    """
 
257
    return modify_access(src, dst=dst, port=port, proto=proto, action='delete')
 
258
 
 
259
 
 
260
def service(name, action):
 
261
    """
 
262
    Open/close access to a service
 
263
 
 
264
    :param name: could be a service name defined in `/etc/services` or a port
 
265
                 number.
 
266
    :param action: `open` or `close`
 
267
    """
 
268
    if action == 'open':
 
269
        subprocess.check_output(['ufw', 'allow', str(name)],
 
270
                                universal_newlines=True)
 
271
    elif action == 'close':
 
272
        subprocess.check_output(['ufw', 'delete', 'allow', str(name)],
 
273
                                universal_newlines=True)
 
274
    else:
 
275
        raise UFWError(("'{}' not supported, use 'allow' "
 
276
                        "or 'delete'").format(action))