]> www.wagner.pp.ru Git - oss/dyngo.git/blob - dyngo
Fixed handling of situation where address for some network is not found
[oss/dyngo.git] / dyngo
1 #!/usr/bin/env python3
2 import ipaddress, subprocess, sys, getopt,dbm
3 from configparser import ConfigParser
4 import urllib.request,urllib.parse
5 import logging,os.path,time
6 class DyndnsNetwork(object):
7     """
8     This class represents dynamic DNS server and network.
9     It has attributes hostname network server user and password
10     """
11     def __init__(self,config_section):
12         self.hostname = config_section['hostname']
13         self.network = ipaddress.ip_network(config_section['network'])
14         self.server = config_section['server']
15         if 'user' in config_section:
16             self.user=config_section["user"]
17             self.password=config_section["password"]
18
19
20     def contains(self,address):
21         if self.network.prefixlen==0 and address.is_private:
22             return False
23         if self.network.version != address.version:
24             return False
25         return address in self.network
26     def nsupdate(self, address):
27         """
28         Sends a get query to specified server.
29         Raises HTTPError if something goes wrong
30         """
31         #construct opener
32         logging.debug("Going to send message to %s",self.server)
33         if hasattr(self,'user'):
34             password_mgr =urllib.request.HTTPPasswordMgrWithDefaultRealm()
35             password_mgr.add_password(None,self.server,self.user,self.password)
36             handler=urllib.request.HTTPBasicAuthHandler(password_mgr)
37             opener = urllib.request.build_opener(handler)
38         else:
39             opener = urllib.request.build_opener()
40         with opener.open('%s?%s'%(self.server,
41                      urllib.parse.urlencode({'hostname':self.hostname,
42                      'myip':str(address)}))) as req:
43                req.read().decode("utf-8")
44                
45            
46
47 def get_current_addresses():
48     result=[]    
49     logging.debug("Acquire current IPs")
50     for line in subprocess.run(['ip','-o','addr','show'],check=True,stdout=subprocess.PIPE).stdout.decode('utf-8').split("\n"):
51         if not len(line): 
52             continue
53         (no,iface,family,addr,rest)=line.split(maxsplit=4)
54         address=ipaddress.ip_address(addr.split('/')[0])
55         if address.is_loopback or address.is_link_local:
56             continue
57         result.append(address)
58     logging.debug("Got following addresses: %s",repr(result))
59     return result
60 def check_for_update():
61     addrlist=get_current_addresses()
62     for name,net in networks.items():
63         found=False
64         for a in addrlist:
65             if net.contains(a):
66                 found =True
67                 if name in database:
68                     old_addr=ipaddress.ip_address(database[name].decode("utf-8"))
69                     if old_addr==a:
70                         # Nothing changed go, to next net
71                         logging.debug("Address for net %s not changed",name)
72                         break
73                 # address changed
74                 try:
75                      logging.info("Doing update for net %s with address %s",name, str(a))
76                      net.nsupdate(a)
77                      database[name]=str(a)
78                 except urllib.error.HTTPError as e:
79                     logging.exception("Http error doing nsupdate code %s",e.code)
80                 break     
81         if not found:
82             logging.info("Address for net %s no more found",name)
83             if name in database:
84                 del database[name]
85
86 config=ConfigParser()
87 config['dyngo']={'interval':'60','database':'/var/lib/dyngo/dyngo.db',
88                  'ca':'/etc/ssl/certs','loglevel':'WARNING'}
89 options=dict(getopt.getopt(sys.argv,"f:")[0])
90 if not '-f' in options:
91     options["-f"]="/etc/dyngo.conf"
92 if len(config.read(options["-f"]))!=1:
93     print("Cannot read config %s"%options["-f"],file=sys.stderr)
94     sys.exit(1)
95 conf=config['dyngo']
96 if conf['loglevel'].isdigit():
97     level=int(conf['loglevel'])
98 else:
99     try:
100         logging.NOTICE=25
101         level=getattr(logging,conf['loglevel'].upper())
102     except AttributeError:
103         print("Invalid logleevel '%s'"%conf('loglevel'))
104         sys.exit(1)
105
106 logging.basicConfig(format="%(asctime)s %(message)s",
107       level=level, stream=sys.stderr)
108 interval=int(conf['interval'])
109 database=dbm.open(conf['database'],"c")
110 https_params={}
111 if 'ca' in conf and len(conf['ca']):
112     path=conf['ca']
113     if os.path.isdir(path):
114         https_params['capath']=path
115     else:
116         http_params['cafile']=path
117 # Convert all other config sections to DyndnsNetwork objects
118 networks={}
119 for sect in config.sections():
120     if sect == 'dyngo' or sect == 'DEFAULT':
121         continue
122     logging.debug("Processing network %s",sect)    
123     networks[sect]=DyndnsNetwork(config[sect])
124 # Remove stale items from persistent database, which are no more
125 # mentioned in the config file
126 for i in set([x.decode("utf-8") for x in database.keys()])-set(networks.keys()):
127     logging.info("Removing from persistent state network %s which is no more  in config",i)
128     del database[i]
129
130 while True:
131     check_for_update()
132     time.sleep(interval)
133