]> git.llucax.com Git - software/pymin.git/blob - services/firewall/__init__.py
Merge /home/luca/pymin
[software/pymin.git] / services / firewall / __init__.py
1 # vim: set encoding=utf-8 et sw=4 sts=4 :
2
3 # TODO See if it's better (more secure) to execute commands via python instead
4 # of using script templates.
5
6 from mako.template import Template
7 from mako.runtime import Context
8 from os import path
9 try:
10     import cPickle as pickle
11 except ImportError:
12     import pickle
13
14 try:
15     from seqtools import Sequence
16 except ImportError:
17     # NOP for testing
18     class Sequence: pass
19 try:
20     from dispatcher import Handler, handler, HandlerError
21 except ImportError:
22     # NOP for testing
23     class HandlerError(RuntimeError): pass
24     class Handler: pass
25     def handler(help):
26         def wrapper(f):
27             return f
28         return wrapper
29
30 __ALL__ = ('FirewallHandler', 'Error', 'RuleError', 'RuleAlreadyExistsError',
31            'RuleNotFoundError')
32
33 pickle_ext = '.pkl'
34 pickle_rules = 'rules'
35
36 config_filename = 'iptables.sh'
37
38 template_dir = path.join(path.dirname(__file__), 'templates')
39
40 class Error(HandlerError):
41     r"""
42     Error(command) -> Error instance :: Base FirewallHandler exception class.
43
44     All exceptions raised by the FirewallHandler inherits from this one, so you can
45     easily catch any FirewallHandler exception.
46
47     message - A descriptive error message.
48     """
49
50     def __init__(self, message):
51         r"Initialize the Error object. See class documentation for more info."
52         self.message = message
53
54     def __str__(self):
55         return self.message
56
57 class RuleError(Error, KeyError):
58     r"""
59     RuleError(rule) -> RuleError instance.
60
61     This is the base exception for all rule related errors.
62     """
63
64     def __init__(self, rule):
65         r"Initialize the object. See class documentation for more info."
66         self.message = 'Rule error: "%s"' % rule
67
68 class RuleAlreadyExistsError(RuleError):
69     r"""
70     RuleAlreadyExistsError(rule) -> RuleAlreadyExistsError instance.
71
72     This exception is raised when trying to add a rule that already exists.
73     """
74
75     def __init__(self, rule):
76         r"Initialize the object. See class documentation for more info."
77         self.message = 'Rule already exists: "%s"' % rule
78
79 class RuleNotFoundError(RuleError):
80     r"""
81     RuleNotFoundError(rule) -> RuleNotFoundError instance.
82
83     This exception is raised when trying to operate on a rule that doesn't
84     exists.
85     """
86
87     def __init__(self, rule):
88         r"Initialize the object. See class documentation for more info."
89         self.message = 'Rule not found: "%s"' % rule
90
91 class Rule(Sequence):
92     r"""Rule(chain, target[, src[, dst[, ...]]]) -> Rule instance.
93
94     chain - INPUT, OUTPUT or FORWARD.
95     target - ACCEPT, REJECT or DROP.
96     src - Source subnet as IP/mask.
97     dst - Destination subnet as IP/mask.
98     protocol - ICMP, UDP, TCP or ALL.
99     src_port - Source port (only for UDP or TCP protocols).
100     dst_port - Destination port (only for UDP or TCP protocols).
101     """
102
103     def __init__(self, chain, target, src=None, dst=None, protocol=None,
104                        src_port=None, dst_port=None):
105         r"Initialize object, see class documentation for details."
106         self.chain = chain
107         self.target = target
108         self.src = src
109         self.dst = dst
110         self.protocol = protocol
111         # TODO Validate that src_port and dst_port could be not None only
112         # if the protocol is UDP or TCP
113         self.src_port = src_port
114         self.dst_port = dst_port
115
116     def update(self, chain=None, target=None, src=None, dst=None, protocol=None,
117                        src_port=None, dst_port=None):
118         r"update([chain[, ...]]) -> Update the values of a rule (see Rule doc)."
119         if chain is not None: self.chain = chain
120         if target is not None: self.target = target
121         if src is not None: self.src = src
122         if dst is not None: self.dst = dst
123         if protocol is not None: self.protocol = protocol
124         # TODO Validate that src_port and dst_port could be not None only
125         # if the protocol is UDP or TCP
126         if src_port is not None: self.src_port = src_port
127         if dst_port is not None: self.dst_port = dst_port
128
129     def __cmp__(self, other):
130         r"Compares two Rule objects."
131         if self.chain == other.chain \
132                 and self.target == other.target \
133                 and self.src == other.src \
134                 and self.dst == other.dst \
135                 and self.protocol == other.protocol \
136                 and self.src_port == other.src_port \
137                 and self.dst_port == other.dst_port:
138             return 0
139         return cmp(id(self), id(other))
140
141     def as_tuple(self):
142         r"Return a tuple representing the rule."
143         return (self.chain, self.target, self.src, self.dst, self.protocol,
144                     self.src_port)
145
146 class RuleHandler(Handler):
147     r"""RuleHandler(rules) -> RuleHandler instance :: Handle a list of rules.
148
149     This class is a helper for FirewallHandler to do all the work related to rules
150     administration.
151
152     rules - A list of Rule objects.
153     """
154
155     def __init__(self, rules):
156         r"Initialize the object, see class documentation for details."
157         self.rules = rules
158
159     @handler(u'Add a new rule.')
160     def add(self, *args, **kwargs):
161         r"add(rule) -> None :: Add a rule to the rules list (see Rule doc)."
162         rule = Rule(*args, **kwargs)
163         if rule in self.rules:
164             raise RuleAlreadyExistsError(rule)
165         self.rules.append(rule)
166
167     @handler(u'Update a rule.')
168     def update(self, index, *args, **kwargs):
169         r"update(index, rule) -> None :: Update a rule (see Rule doc)."
170         # TODO check if the modified rule is the same of an existing one
171         index = int(index) # TODO validation
172         try:
173             self.rules[index].update(*args, **kwargs)
174         except IndexError:
175             raise RuleNotFoundError(index)
176
177     @handler(u'Delete a rule.')
178     def delete(self, index):
179         r"delete(index) -> Rule :: Delete a rule from the list returning it."
180         index = int(index) # TODO validation
181         try:
182             return self.rules.pop(index)
183         except IndexError:
184             raise RuleNotFoundError(index)
185
186     @handler(u'Get information about a rule.')
187     def get(self, index):
188         r"get(rule) -> Rule :: Get all the information about a rule."
189         index = int(index) # TODO validation
190         try:
191             return self.rules[index]
192         except IndexError:
193             raise RuleNotFoundError(index)
194
195     @handler(u'Get information about all rules.')
196     def show(self):
197         r"show() -> list of Rules :: List all the complete rules information."
198         return self.rules
199
200 class FirewallHandler(Handler):
201     r"""FirewallHandler([pickle_dir[, config_dir]]) -> FirewallHandler instance.
202
203     Handles firewall commands using iptables.
204
205     pickle_dir - Directory where to write the persistent configuration data.
206
207     config_dir - Directory where to store de generated configuration files.
208
209     Both defaults to the current working directory.
210     """
211
212     def __init__(self, pickle_dir='.', config_dir='.'):
213         r"Initialize FirewallHandler object, see class documentation for details."
214         self.pickle_dir = pickle_dir
215         self.config_dir = config_dir
216         filename = path.join(template_dir, config_filename)
217         self.template = Template(filename=filename)
218         try:
219             self._load()
220         except IOError:
221             # This is the first time the handler is used, create a basic
222             # setup using some nice defaults
223             self.rules = list() # TODO defaults?
224             self._dump()
225             self._write_config()
226         self.rule = RuleHandler(self.rules)
227
228     # Does this (start, stop, restart, reload) makes sense??? 
229     # Implement a "try" command that apply the changes for some time and
230     # then goes back to the previous configuration if the changes are not
231     # commited. TODO
232     @handler(u'Start the service.')
233     def start(self):
234         r"start() -> None :: Start the firewall."
235         #esto seria para poner en una interfaz
236         #y seria el hook para arrancar el servicio
237         pass
238
239     @handler(u'Stop the service.')
240     def stop(self):
241         r"stop() -> None :: Stop the firewall."
242         #esto seria para poner en una interfaz
243         #y seria el hook para arrancar el servicio
244         pass
245
246     @handler(u'Restart the service.')
247     def restart(self):
248         r"restart() -> None :: Restart the firewall."
249         #esto seria para poner en una interfaz
250         #y seria el hook para arrancar el servicio
251         pass
252
253     @handler(u'Reload the service config (without restarting, if possible).')
254     def reload(self):
255         r"reload() -> None :: Reload the configuration of the firewall."
256         #esto seria para poner en una interfaz
257         #y seria el hook para arrancar el servicio
258         pass
259
260     @handler(u'Commit the changes (reloading the service, if necessary).')
261     def commit(self):
262         r"commit() -> None :: Commit the changes and reload the firewall."
263         #esto seria para poner en una interfaz
264         #y seria que hace el pickle deberia llamarse
265         #al hacerse un commit
266         self._dump()
267         self._write_config()
268         self.reload() # TODO exec the script
269
270     @handler(u'Discard all the uncommited changes.')
271     def rollback(self):
272         r"rollback() -> None :: Discard the changes not yet commited."
273         self._load()
274
275     def _dump(self):
276         r"_dump() -> None :: Dump all persistent data to pickle files."
277         # XXX podría ir en una clase base
278         self._dump_var(self.rules, pickle_rules)
279
280     def _load(self):
281         r"_load() -> None :: Load all persistent data from pickle files."
282         # XXX podría ir en una clase base
283         self.rules = self._load_var(pickle_rules)
284
285     def _pickle_filename(self, name):
286         r"_pickle_filename() -> string :: Construct a pickle filename."
287         # XXX podría ir en una clase base
288         return path.join(self.pickle_dir, name) + pickle_ext
289
290     def _dump_var(self, var, name):
291         r"_dump_var() -> None :: Dump a especific variable to a pickle file."
292         # XXX podría ir en una clase base
293         pkl_file = file(self._pickle_filename(name), 'wb')
294         pickle.dump(var, pkl_file, 2)
295         pkl_file.close()
296
297     def _load_var(self, name):
298         r"_load_var() -> object :: Load a especific pickle file."
299         # XXX podría ir en una clase base
300         return pickle.load(file(self._pickle_filename(name)))
301
302     def _write_config(self):
303         r"_write_config() -> None :: Generate all the configuration files."
304         # XXX podría ir en una clase base, ver como generalizar variables a
305         # reemplazar en la template
306         out_file = file(path.join(self.config_dir, config_filename), 'w')
307         ctx = Context(out_file, rules=self.rules)
308         self.template.render_context(ctx)
309         out_file.close()
310
311 if __name__ == '__main__':
312
313     import os
314
315     fw_handler = FirewallHandler()
316
317     def dump():
318         print '-' * 80
319         print 'Rules:'
320         print fw_handler.rule.show()
321         print '-' * 80
322
323     dump()
324
325     fw_handler.rule.add('input','drop','icmp')
326
327     fw_handler.rule.update(0, dst='192.168.0.188/32')
328
329     fw_handler.rule.add('output','accept', '192.168.1.0/24')
330
331     fw_handler.commit()
332
333     dump()
334
335     for f in (pickle_rules + pickle_ext, config_filename):
336         os.unlink(f)
337