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