41a9eb1a6b267f0049a895d7fdc4a0703429bbd7
[auf-refer.git] / auf-refer
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 """Copie et mise à jour des référentiels AuF.
4
5 Copyright ©2009  Agence universitaire de la Francophonie
6 Licence : GPL version 3
7 Auteur : Progfou <jean-christophe.andre@auf.org>
8
9 Dépendances Debian : python >= 2.5, python-simplejson
10 """
11
12 PROG_NAME = 'auf-refer'
13 RUN_USER = 'auf-refer'
14 DIR_BASE = '/var/lib/auf-refer/'
15 URL_BASE = 'http://intranet.auf/auf-refer/'
16
17 f = file('/etc/auf-refer/auf-refer.conf')
18 lines = filter(lambda l: not l.startswith('#'), f.readlines())
19 config = dict(map(lambda l: map(lambda s: s.strip(), l.split('=')), lines))
20
21 URL_BASE = config.get('URL_BASE', URL_BASE).rstrip('/') + '/'
22
23 __all__ = ( 'RUN_USER', 'DIR_BASE', 'URL_BASE' )
24
25 USAGE = """Usages (à lancer en tant que "root", par exemple via "sudo") :
26   auf-refer [-f] -a <ref> - copie un nouveau référentiel
27   auf-refer -d <ref>      - supprime un référentiel
28   auf-refer [-f] -u       - met à jour les référentiels
29   auf-refer -l            - liste les référentiels copiés
30   auf-refer -L            - liste les référentiels disponibles
31   auf-refer [-f] -A       - copie tous les référentiels disponibles
32
33 L'option -f permet de forcer le rechargement à travers un proxy/cache.
34 """
35
36 TIME_FORMAT = '%a, %d %b %Y %H:%M:%S GMT'
37
38 from sys import argv, exit, stderr
39 from getopt import getopt, GetoptError
40 from pwd import getpwnam
41 from os import getuid, setgid, setuid, listdir, utime, unlink
42 from os.path import join, exists, getmtime
43 from time import gmtime, strftime
44 from calendar import timegm
45 from urllib2 import Request, urlopen, HTTPError, URLError
46 from cStringIO import StringIO
47 from gzip import GzipFile
48 from simplejson import loads
49
50 def update_referentiel(referentiel, force=False):
51     headers = {}
52     headers['Accept-Encoding'] = 'x-gzip'
53     if force:
54         headers['Pragma'] = 'no-cache'
55     filename = join(DIR_BASE, referentiel)
56     if exists(filename):
57         # n'effectuer le chargement qu'en cas de nouvelle version
58         mtime = gmtime(getmtime(filename))
59         headers['If-Modified-Since'] = strftime(TIME_FORMAT, mtime)
60     else:
61         # fichier vide à date très ancienne pour déclencher la synchro
62         try:
63             switch_user()
64             file(filename, 'a').close()
65         except IOError, msg:
66             raise RuntimeError, \
67                 u"La création du référentiel a été refusée : %s" % msg
68         utime(filename, (0, 0))
69     url = URL_BASE + referentiel
70     req = Request(url, None, headers)
71     try:
72         u = urlopen(req)
73     except HTTPError, e:
74         if e.code == 304:
75             return
76         raise RuntimeError, \
77             u"L'URL suivante renvoie un code d'erreur %s :\n  %s" \
78                                                         % (e.code, url)
79     except URLError:
80         raise RuntimeError, u"L'URL suivante est inaccessible :\n  %s" % url
81     i = u.info()
82     if referentiel.endswith('.json') and i.type != 'application/json':
83         u.close()
84         raise RuntimeError, \
85             u"Le type des données chargées n'est pas JSON mais '%s'." % i.type
86     data = u.read()
87     if i.get('content-encoding') == 'x-gzip':
88         data = GzipFile('', 'r', 0, StringIO(data)).read()
89     u.close()
90     if referentiel.endswith('.json'):
91         try:
92             loads(data, encoding='utf-8')
93         except ValueError:
94             raise RuntimeError, u"Les données ne sont pas au format JSON."
95     # si on est arrivé jusqu'ici c'est que tout va bien... on enregistre !
96     try:
97         switch_user()
98         f = file(filename, 'wb')
99     except IOError, msg:
100         raise RuntimeError, \
101             u"L'écriture du référentiel a été refusée : %s" % msg
102     f.write(data)
103     f.close()
104     # on fixe la date donnée par le serveur, le cas échéant
105     mtime = i.getdate('last-modified')
106     if mtime:
107         mtime = timegm(mtime)
108         utime(filename, (mtime, mtime))
109
110 def add_referentiel(referentiel, force=False):
111     if referentiel in listdir(DIR_BASE):
112         raise RuntimeError, \
113             u"Le référentiel '%s' avait déjà été ajouté." % referentiel
114     update_referentiel(referentiel, force)
115
116 def delete_referentiel(referentiel):
117     if not referentiel in listdir(DIR_BASE):
118         raise RuntimeError, u"Le référentiel '%s' est absent." % referentiel
119     try:
120         switch_user()
121         unlink(join(DIR_BASE, referentiel))
122     except IOError, msg:
123         raise RuntimeError, \
124             u"La suppression du référentiel '%s' a été refusée :\n  %s" \
125                                                     % (referentiel, msg)
126
127 def update_referentiels(force=False):
128     error_messages = []
129     for referentiel in listdir(DIR_BASE):
130         try:
131             update_referentiel(referentiel, force)
132         except Exception, msg:
133             error_messages.append(unicode(msg))
134     if error_messages:
135         raise RuntimeError, u'\n'.join(error_messages)
136
137 def list_referentiels():
138     for referentiel in listdir(DIR_BASE):
139         print referentiel
140
141 def get_referentiels_available(force=False):
142     referentiel = 'auf-refer.json'
143     filename = join(DIR_BASE, referentiel)
144     if not exists(filename):
145         update_referentiel(referentiel, force)
146     try:
147         f = open(filename, 'rb')
148     except IOError:
149         raise RuntimeError, u"La liste des référentiels est indisponible."
150     try:
151         referentiels = loads(f.read(), encoding='utf-8')
152     except ValueError:
153         raise RuntimeError, \
154             u"La liste des référentiels n'est pas au format JSON.\n" \
155             u"Essayez les options -f et -u pour la mettre à jour."
156     f.close()
157     return referentiels
158
159 def list_referentiels_available(force=False):
160     referentiels = get_referentiels_available(force)
161     for referentiel in sorted(referentiels):
162         print "%-16s : %s" % (referentiel, referentiels[referentiel])
163
164 def add_referentiels_available(force=False):
165     referentiels = get_referentiels_available(force)
166     referentiels = set(referentiels) - set(listdir(DIR_BASE))
167     for referentiel in referentiels:
168         update_referentiel(referentiel, force)
169
170 _user_switched = False
171 def switch_user(username=RUN_USER):
172     global _user_switched
173     if _user_switched:
174         return
175     try:
176         pw = getpwnam(username)
177     except KeyError:
178         raise RuntimeError, u"L'utilisateur '%s' n'existe pas." % username
179     try:
180         setgid(pw.pw_gid)
181         setuid(pw.pw_uid)
182     except OSError:
183         raise RuntimeError, \
184             u"Impossible de basculer vers l'utilisateur '%s'.\n" \
185             u"Réessayez avec 'sudo' !" % username
186     _user_switched = True
187
188 if __name__ == '__main__':
189     # interdiction formelle de tourner sous 'root'
190     if getuid() == 0:
191         switch_user()
192
193     try:
194         opts, args = getopt(argv[1:], 'hfa:d:ulLA', ['help', 'force',
195             'add=', 'delete=', 'update', 'list',
196             'list-available', 'add-available'])
197     except GetoptError:
198         print USAGE
199         exit(1)
200
201     force = False
202     for opt, arg in opts:
203         if opt in ('-h', '--help'):
204             print USAGE
205             exit(0)
206         elif opt in ('-f', '--force'):
207             force = True
208         else:
209             try:
210                 if opt in ('-a', '--add'):
211                     add_referentiel(arg, force=force)
212                 elif opt in ('-d', '--delete'):
213                     delete_referentiel(arg)
214                 elif opt in ('-u', '--update'):
215                     update_referentiels(force=force)
216                 elif opt in ('-l', '--list'):
217                     list_referentiels()
218                 elif opt in ('-L', '--list-available'):
219                     list_referentiels_available(force=force)
220                 elif opt in ('-A', '--add-available'):
221                     add_referentiels_available(force=force)
222             except RuntimeError, msg:
223                 print >>stderr, u'%s' % msg
224
225     if not opts:
226         print USAGE
227     exit(0)
228