]> git.llucax.com Git - software/pymin.git/blob - services/dns/__init__.py
Merge /home/luca/pymin
[software/pymin.git] / services / dns / __init__.py
1 # vim: set encoding=utf-8 et sw=4 sts=4 :
2
3 # TODO COMMENT
4 from mako.template import Template
5 from mako.runtime import Context
6 from os import path
7 from os import unlink
8
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
20 try:
21     from dispatcher import handler, HandlerError
22 except ImportError:
23     class HandlerError(RuntimeError): pass
24     def handler(f): return f # NOP for testing
25
26
27
28 __ALL__ = ('DnsHandler',)
29
30 pickle_ext = '.pkl'
31
32 pickle_vars = 'vars'
33 pickle_zones = 'zones'
34
35 config_filename = 'named.conf'
36 zone_filename = 'zoneX.zone'
37 zone_filename_ext = '.zone'
38
39 template_dir = path.join(path.dirname(__file__), 'templates')
40
41
42 class Error(HandlerError):
43     r"""
44     Error(command) -> Error instance :: Base DnsHandler exception class.
45
46     All exceptions raised by the DnsHandler inherits from this one, so you can
47     easily catch any DnsHandler exception.
48
49     message - A descriptive error message.
50     """
51
52     def __init__(self, message):
53         r"Initialize the Error object. See class documentation for more info."
54         self.message = message
55
56     def __str__(self):
57         return self.message
58
59 class ZoneError(Error, KeyError):
60     r"""
61     ZoneError(zonename) -> ZoneError instance
62
63     This is the base exception for all zone related errors.
64     """
65
66     def __init__(self, zonename):
67         r"Initialize the object. See class documentation for more info."
68         self.message = 'Zone error: "%s"' % zonename
69
70
71 class ZoneNotFoundError(ZoneError):
72     r"""
73     ZoneNotFoundError(hostname) -> ZoneNotFoundError instance
74
75     This exception is raised when trying to operate on a zone that doesn't
76     exists.
77     """
78
79     def __init__(self, zonename):
80         r"Initialize the object. See class documentation for more info."
81         self.message = 'zone not found: "%s"' % zonename
82
83
84 class ZoneAlreadyExistsError(ZoneError):
85     r"""
86     ZoneAlreadyExistsError(hostname) -> ZoneAlreadyExistsError instance
87
88     This exception is raised when trying to add a zonename that already exists.
89     """
90
91     def __init__(self, zonename):
92         r"Initialize the object. See class documentation for more info."
93         self.message = 'Zone already exists: "%s"' % zonename
94
95
96 class HostError(Error, KeyError):
97     r"""
98     HostError(hostname) -> HostError instance
99
100     This is the base exception for all host related errors.
101     """
102
103     def __init__(self, hostname):
104         r"Initialize the object. See class documentation for more info."
105         self.message = 'Host error: "%s"' % hostname
106
107 class HostAlreadyExistsError(HostError):
108     r"""
109     HostAlreadyExistsError(hostname) -> HostAlreadyExistsError instance
110
111     This exception is raised when trying to add a hostname that already exists.
112     """
113
114     def __init__(self, hostname):
115         r"Initialize the object. See class documentation for more info."
116         self.message = 'Host already exists: "%s"' % hostname
117
118 class HostNotFoundError(HostError):
119     r"""
120     HostNotFoundError(hostname) -> HostNotFoundError instance
121
122     This exception is raised when trying to operate on a hostname that doesn't
123     exists.
124     """
125
126     def __init__(self, hostname):
127         r"Initialize the object. See class documentation for more info."
128         self.message = 'Host not found: "%s"' % hostname
129
130
131 class MailExchangeError(Error, KeyError):
132     r"""
133     MailExchangeError(hostname) -> MailExchangeError instance
134
135     This is the base exception for all mail exchange related errors.
136     """
137
138     def __init__(self, mx):
139         r"Initialize the object. See class documentation for more info."
140         self.message = 'Mail Exchange error: "%s"' % mx
141
142
143 class MailExchangeAlreadyExistsError(MailExchangeError):
144     r"""
145     MailExchangeAlreadyExistsError(hostname) -> MailExchangeAlreadyExistsError instance
146
147     This exception is raised when trying to add a mail exchange that already exists.
148     """
149
150     def __init__(self, mx):
151         r"Initialize the object. See class documentation for more info."
152         self.message = 'Mail Exchange already exists: "%s"' % mx
153
154
155 class MailExchangeNotFoundError(MailExchangeError):
156     r"""
157     MailExchangeNotFoundError(hostname) -> MailExchangeNotFoundError instance
158
159     This exception is raised when trying to operate on a mail exchange that doesn't
160     exists.
161     """
162
163     def __init__(self, mx):
164         r"Initialize the object. See class documentation for more info."
165         self.message = 'Mail Exchange not found: "%s"' % mx
166
167
168
169 class NameServerError(Error, KeyError):
170     r"""
171     NameServerError(ns) -> NameServerError instance
172
173     This is the base exception for all name server related errors.
174     """
175
176     def __init__(self, ns):
177         r"Initialize the object. See class documentation for more info."
178         self.message = 'Name Server error: "%s"' % ns
179
180 class NameServerAlreadyExistsError(NameServerError):
181     r"""
182     NameServerAlreadyExistsError(hostname) -> NameServerAlreadyExistsError instance
183
184     This exception is raised when trying to add a name server that already exists.
185     """
186
187     def __init__(self, ns):
188         r"Initialize the object. See class documentation for more info."
189         self.message = 'Name server already exists: "%s"' % ns
190
191 class NameServerNotFoundError(NameServerError):
192     r"""
193     NameServerNotFoundError(hostname) -> NameServerNotFoundError instance
194
195     This exception is raised when trying to operate on a name server that doesn't
196     exists.
197     """
198
199     def __init__(self, ns):
200         r"Initialize the object. See class documentation for more info."
201         self.message = 'Mail Exchange not found: "%s"' % ns
202
203
204 class ParameterError(Error, KeyError):
205     r"""
206     ParameterError(paramname) -> ParameterError instance
207
208     This is the base exception for all DhcpHandler parameters related errors.
209     """
210
211     def __init__(self, paramname):
212         r"Initialize the object. See class documentation for more info."
213         self.message = 'Parameter error: "%s"' % paramname
214
215 class ParameterNotFoundError(ParameterError):
216     r"""
217     ParameterNotFoundError(hostname) -> ParameterNotFoundError instance
218
219     This exception is raised when trying to operate on a parameter that doesn't
220     exists.
221     """
222
223     def __init__(self, paramname):
224         r"Initialize the object. See class documentation for more info."
225         self.message = 'Parameter not found: "%s"' % paramname
226
227 class Host(Sequence):
228     def __init__(self, name, ip):
229         self.name = name
230         self.ip = ip
231
232     def as_tuple(self):
233         return (self.name, self.ip)
234
235 class HostHandler:
236     def __init__(self,zones):
237         self.zones = zones
238
239     @handler
240     def add(self, name, hostname, ip):
241         if not name in self.zones:
242             raise ZoneNotFoundError(name)
243         if hostname in self.zones[name].hosts:
244             raise HostAlreadyExistsError(hostname)
245         self.zones[name].hosts[hostname] = Host(hostname, ip)
246         self.zones[name].mod = True
247
248     @handler
249     def update(self, name, hostname, ip):
250         if not name in self.zones:
251             raise ZoneNotFoundError(name)
252         if not hostname in self.zones[name].hosts:
253              raise HostNotFoundError(name)
254         self.zones[name].hosts[hostname].ip = ip
255         self.zones[name].mod = True
256
257     @handler
258     def delete(self, name, hostname):
259         if not name in self.zones:
260             raise ZoneNotFoundError(name)
261         if not hostname in self.zones[name].hosts:
262              raise HostNotFoundError(name)
263         del self.zones[name].hosts[hostname]
264         self.zones[name].mod = True
265
266     @handler
267     def list(self):
268         return self.zones.keys()
269
270     @handler
271     def show(self):
272         return self.zones.values()
273
274
275 class MailExchange(Sequence):
276
277     def __init__(self, mx, prio):
278         self.mx = mx
279         self.prio = prio
280
281     def as_tuple(self):
282         return (self.mx, self.prio)
283
284 class MailExchangeHandler:
285
286     def __init__(self, zones):
287         self.zones = zones
288
289     @handler
290     def add(self, zonename, mx, prio):
291         if not zonename in self.zones:
292             raise ZoneNotFoundError(zonename)
293         if mx in self.zones[zonename].mxs:
294             raise MailExchangeAlreadyExistsError(mx)
295         self.zones[zonename].mxs[mx] = MailExchange(mx, prio)
296         self.zones[zonename].mod = True
297
298     @handler
299     def update(self, zonename, mx, prio):
300         if not zonename in self.zones:
301             raise ZoneNotFoundError(zonename)
302         if not mx in self.zones[zonename].mxs:
303             raise MailExchangeNotFoundError(mx)
304         self.zones[zonename].mxs[mx].prio = prio
305         self.zones[zonename].mod = True
306
307     @handler
308     def delete(self, zonename, mx):
309         if not zonename in self.zones:
310             raise ZoneNotFoundError(zonename)
311         if not mx in self.zones[zonename].mxs:
312             raise MailExchangeNotFoundError(mx)
313         del self.zones[zonename].mxs[mx]
314         self.zones[zonename].mod = True
315
316     @handler
317     def list(self):
318         return self.zones.keys()
319
320     @handler
321     def show(self):
322         return self.zones.values()
323
324
325 class NameServer(Sequence):
326
327     def __init__(self, name):
328         self.name = name
329
330     def as_tuple(self):
331         return (self.name)
332
333 class NameServerHandler:
334
335     def __init__(self, zones):
336         self.zones = zones
337
338     @handler
339     def add(self, zone, ns):
340         if not zone in self.zones:
341             raise ZoneNotFoundError(zone)
342         if ns in self.zones[zone].nss:
343             raise NameServerAlreadyExistsError(ns)
344         self.zones[zone].nss[ns] = NameServer(ns)
345         self.zones[zone].mod = True
346
347     @handler
348     def delete(self, zone, ns):
349         if not zone in self.zones:
350             raise ZoneNotFoundError(zone)
351         if not ns in self.zones[zone].nss:
352             raise NameServerNotFoundError(ns)
353         del self.zones[zone].nss[ns]
354         self.zones[zone].mod = True
355
356     @handler
357     def list(self):
358         return self.zones.keys()
359
360     @handler
361     def show(self):
362         return self.zones.values()
363
364
365 class Zone(Sequence):
366     def __init__(self, name):
367         self.name = name
368         self.hosts = dict()
369         self.mxs = dict()
370         self.nss = dict()
371         self.new = False
372         self.mod = False
373         self.dele = False
374
375     def as_tuple(self):
376         return (self.name, self.hosts, self.mxs, self.nss)
377
378 class ZoneHandler:
379
380     r"""ZoneHandler(zones) -> ZoneHandler instance :: Handle a list of zones.
381
382     This class is a helper for DnsHandler to do all the work related to zone
383     administration.
384
385     zones - A dictionary with string keys (zone name) and Zone instances values.
386     """
387     def __init__(self, zones):
388         self.zones = zones
389
390     @handler
391     def add(self, name):
392         if name in self.zones:
393             if self.zones[name].dele = True:
394                 self.zones[name].dele = False
395             else:
396                 raise ZoneAlreadyExistsError(name)
397         self.zones[name] = Zone(name)
398         self.zones[name].mod = True
399         self.zones[name].new = True
400
401
402     @handler
403     def delete(self, name):
404         r"delete(name) -> None :: Delete a zone from the zone list."
405         if not name in self.zones:
406             raise ZoneNotFoundError(name)
407         self.zones[name].dele = True
408
409     @handler
410     def list(self):
411         return self.zones.keys()
412
413     @handler
414     def show(self):
415         return self.zones.values()
416
417 class DnsHandler:
418     r"""DnsHandler([pickle_dir[, config_dir]]) -> DnsHandler instance.
419
420     Handles DNS service commands for the dns program.
421
422     pickle_dir - Directory where to write the persistent configuration data.
423
424     config_dir - Directory where to store de generated configuration files.
425
426     Both defaults to the current working directory.
427     """
428
429     def __init__(self, pickle_dir='.', config_dir='.'):
430         r"Initialize DnsHandler object, see class documentation for details."
431         self.pickle_dir = pickle_dir
432         self.config_dir = config_dir
433         c_filename = path.join(template_dir, config_filename)
434         z_filename = path.join(template_dir, zone_filename)
435         self.config_template = Template(filename=c_filename)
436         self.zone_template = Template(filename=z_filename)
437         try :
438             self._load()
439         except IOError:
440             self.zones = dict()
441             self.vars = dict(
442                 isp_dns1 = '',
443                 isp_dns2 = '',
444                 bind_addr1 = '',
445                 bind_addr2 = ''
446             )
447
448         self.host = HostHandler(self.zones)
449         self.zone = ZoneHandler(self.zones)
450         self.mx = MailExchangeHandler(self.zones)
451         self.ns = NameServerHandler(self.zones)
452         self.mod = False
453
454     @handler
455     def set(self, param, value):
456         r"set(param, value) -> None :: Set a DNS parameter."
457         if not param in self.vars:
458             raise ParameterNotFoundError(param)
459         self.vars[param] = value
460         self.mod = True
461
462     @handler
463     def get(self, param):
464         r"get(param) -> None :: Get a DNS parameter."
465         if not param in self.vars:
466             raise ParameterNotFoundError(param)
467         return self.vars[param]
468
469     @handler
470     def list(self):
471         return self.vars.keys()
472
473     @handler
474     def show(self):
475         return self.vars.values()
476
477     @handler
478     def start(self):
479         r"start() -> None :: Start the DNS service."
480         #esto seria para poner en una interfaz
481         #y seria el hook para arrancar el servicio
482         pass
483
484     @handler
485     def stop(self):
486         r"stop() -> None :: Stop the DNS service."
487         #esto seria para poner en una interfaz
488         #y seria el hook para arrancar el servicio
489         pass
490
491     @handler
492     def restart(self):
493         r"restart() -> None :: Restart the DNS service."
494         #esto seria para poner en una interfaz
495         #y seria el hook para arrancar el servicio
496         pass
497
498     @handler
499     def reload(self):
500         r"reload() -> None :: Reload the configuration of the DNS service."
501         #esto seria para poner en una interfaz
502         #y seria el hook para arrancar el servicio
503         pass
504
505     @handler
506     def commit(self):
507         r"commit() -> None :: Commit the changes and reload the DNS service."
508         #esto seria para poner en una interfaz
509         #y seria que hace el pickle deberia llamarse
510         #al hacerse un commit
511         self._dump()
512         self._write_config()
513         self.reload()
514
515     @handler
516     def rollback(self):
517         r"rollback() -> None :: Discard the changes not yet commited."
518         self._load()
519
520     def _dump(self):
521         r"_dump() -> None :: Dump all persistent data to pickle files."
522         # XXX podría ir en una clase base
523         self._dump_var(self.vars, pickle_vars)
524         self._dump_var(self.zones, pickle_zones)
525
526     def _load(self):
527         r"_load() -> None :: Load all persistent data from pickle files."
528         # XXX podría ir en una clase base
529         self.vars = self._load_var(pickle_vars)
530         self.zones = self._load_var(pickle_zones)
531
532     def _pickle_filename(self, name):
533         r"_pickle_filename() -> string :: Construct a pickle filename."
534         # XXX podría ir en una clase base
535         return path.join(self.pickle_dir, name) + pickle_ext
536
537     def _dump_var(self, var, name):
538         r"_dump_var() -> None :: Dump a especific variable to a pickle file."
539         # XXX podría ir en una clase base
540         pkl_file = file(self._pickle_filename(name), 'wb')
541         pickle.dump(var, pkl_file, 2)
542         pkl_file.close()
543
544     def _load_var(self, name):
545         r"_load_var() -> object :: Load a especific pickle file."
546         # XXX podría ir en una clase base
547         return pickle.load(file(self._pickle_filename(name)))
548
549     def _write_config(self):
550         r"_write_config() -> None :: Generate all the configuration files."
551         # XXX podría ir en una clase base, ver como generalizar variables a
552         # reemplazar en la template
553         #archivos de zona
554         delete_zones = list()
555         for a_zone in self.zones.values():
556             if a_zone.mod:
557                 if not a_zone.new:
558                     # TODO freeze de la zona
559                     print 'Freezing zone ' + a_zone.name + zone_filename_ext
560                 zone_out_file = file(path.join(self.config_dir, a_zone.name + zone_filename_ext), 'w')
561                 ctx = Context(
562                     zone_out_file,
563                     zone = a_zone,
564                     hosts = a_zone.hosts.values(),
565                     mxs = a_zone.mxs.values(),
566                     nss = a_zone.nss.values()
567                     )
568                 self.zone_template.render_context(ctx)
569                 zone_out_file.close()
570                 a_zone.mod = False
571                 if not a_zone.new:
572                     # TODO unfreeze de la zona
573                     print 'Unfreezing zone ' + a_zone.name + zone_filename_ext
574                 else :
575                     self.mod = True
576                     a_zone.new = False
577             if a_zone.dele:
578                 #borro el archivo .zone
579                 try:
580                     self.mod = True
581                     unlink(path.join(self.config_dir, a_zone.name + zone_filename_ext))
582                 except OSError:
583                     #la excepcion pude darse en caso que haga un add de una zona y
584                     #luego el del, como no hice commit, no se crea el archivo
585                     pass
586                 delete_zones.append(a_zone.name)
587         #borro las zonas
588         for z in delete_zones:
589             del self.zones[z]
590         #archivo general
591         if self.mod :
592             cfg_out_file = file(path.join(self.config_dir, config_filename), 'w')
593             ctx = Context(cfg_out_file, zones=self.zones.values(), **self.vars)
594             self.config_template.render_context(ctx)
595             cfg_out_file.close()
596             self.mod = False
597             print 'Restarting service'
598
599
600
601 if __name__ == '__main__':
602
603     dns = DnsHandler();
604
605     dns.set('isp_dns1','la_garcha.com')
606     dns.set('bind_addr1','localhost')
607     dns.zone.add('zona_loca.com')
608     #dns.zone.update('zona_loca.com','ns1.dominio.com')
609
610     dns.host.add('zona_loca.com','hostname_loco','192.168.0.23')
611     dns.host.update('zona_loca.com','hostname_loco','192.168.0.66')
612
613     dns.host.add('zona_loca.com','hostname_kuak','192.168.0.23')
614     dns.host.delete('zona_loca.com','hostname_kuak')
615
616     dns.host.add('zona_loca.com','hostname_kuang','192.168.0.24')
617     dns.host.add('zona_loca.com','hostname_chan','192.168.0.25')
618     dns.host.add('zona_loca.com','hostname_kaine','192.168.0.26')
619
620     dns.mx.add('zona_loca.com','mx1.sarasa.com',10)
621     dns.mx.update('zona_loca.com','mx1.sarasa.com',20)
622     dns.mx.add('zona_loca.com','mx2.sarasa.com',30)
623     dns.mx.add('zona_loca.com','mx3.sarasa.com',40)
624     dns.mx.delete('zona_loca.com','mx3.sarasa.com')
625
626     dns.ns.add('zona_loca.com','ns1.jua.com')
627     dns.ns.add('zona_loca.com','ns2.jua.com')
628     dns.ns.add('zona_loca.com','ns3.jua.com')
629     dns.ns.delete('zona_loca.com','ns3.jua.com')
630
631     dns.zone.add('zona_oscura')
632
633     dns.host.add('zona_oscura','hostname_a','192.168.0.24')
634     dns.host.add('zona_oscura','hostname_b','192.168.0.25')
635     dns.host.add('zona_oscura','hostname_c','192.168.0.26')
636
637     dns.zone.delete('zona_oscura')
638
639     dns.commit()
640
641     print 'ZONAS :'
642     print dns.zone.show() + '\n'
643     print 'HOSTS :'
644     print dns.host.show()
645
646     #test zone errors
647     try:
648         dns.zone.update('zone-sarasa','lalal')
649     except ZoneNotFoundError, inst:
650         print 'Error: ', inst
651
652     try:
653         dns.zone.delete('zone-sarasa')
654     except ZoneNotFoundError, inst:
655         print 'Error: ', inst
656
657     try:
658         dns.zone.add('zona_loca.com','ns1.dom.com','ns2.dom.com')
659     except ZoneAlreadyExistsError, inst:
660         print 'Error: ', inst
661
662     #test hosts errors
663     try:
664         dns.host.update('zone-sarasa','kuak','192.68')
665     except ZoneNotFoundError, inst:
666         print 'Error: ', inst
667
668     try:
669         dns.host.update('zona_loca.com','kuak','192.68')
670     except HostNotFoundError, inst:
671         print 'Error: ', inst
672
673     try:
674         dns.host.delete('zone-sarasa','lala')
675     except ZoneNotFoundError, inst:
676         print 'Error: ', inst
677
678     try:
679         dns.host.delete('zona_loca.com','lala')
680     except HostNotFoundError, inst:
681         print 'Error: ', inst
682
683     try:
684         dns.host.add('zona','hostname_loco','192.168.0.23')
685     except ZoneNotFoundError, inst:
686         print 'Error: ', inst
687
688     try:
689         dns.host.add('zona_loca.com','hostname_loco','192.168.0.23')
690     except HostAlreadyExistsError, inst:
691         print 'Error: ', inst