Package zephir :: Package backend :: Module entid
[frames] | no frames]

Source Code for Module zephir.backend.entid

  1  #!/bin/env python 
  2  # -*- coding: UTF-8 -*- 
  3   
  4  from twisted.internet import reactor 
  5  from zephir.backend.config import log 
  6  from zephir.entpool import IdPool 
  7  from zephir.backend.uucp_utils import uucp_pool, UUCPError 
  8  from zephir.backend.lib_backend import cx_pool 
  9  import random, os, md5, time, traceback, base64 
 10   
11 -class ZephirIdPool(IdPool):
12
13 - def __init__(self, code_ent):
14 super(ZephirIdPool,self).__init__(code_ent) 15 # liste des plages déjà réservées 16 self.reserved = [] 17 self.pending = [] 18 self.free = [(0,self.max_id)] 19 self.free_space = self.max_id + 1
20
21 - def get_range(self, length = 100):
22 """renvoie les identifiants minimum/maximum des nouvelles plages 23 et met en place la réservation en attendant une confirmation 24 """ 25 if length <= 0: 26 raise InternalError, "Intervalle invalide : %s" % length 27 #if self.pending: 28 # # une réservation est déjà en cours 29 # raise InternalError, "une réservation est déjà en cours" 30 if length > self.free_space: 31 raise InternalError, "Pas assez d'identifiants disponibles" 32 # recherche de plages disponibles 33 ranges = [] 34 for r_min, r_max in self.free: 35 r_len = r_max - r_min + 1 36 if length <= r_len: 37 ranges.append((r_min, r_min+length-1)) 38 break 39 else: 40 # plage insuffisante, il faut utiliser aussi la suivante 41 ranges.append((r_min, r_max)) 42 length = length - r_len 43 self.pending.append(ranges) 44 # recalcul des plages libres 45 return [(self.id_to_string(minid), self.id_to_string(maxid)) for minid, maxid in ranges]
46
47 - def _add_reserved(self, minid, maxid, id_serveur = -1):
48 """ajoute une plage dans la liste des plages réservées 49 """ 50 # recherche de l'emplacement où insérer la plage 51 insert_index = 0 52 # mise à jour des plages réservées 53 if self.reserved: 54 for (r_min, r_max, id_s) in self.reserved: 55 if r_min > maxid: 56 # on est sur la plage précédent celle à insérer 57 break 58 insert_index += 1 59 # ajout de la plage réservée avant insert_index 60 self.reserved.insert(insert_index, (minid, maxid, id_serveur)) 61 self.update_free_ranges()
62
63 - def update_free_ranges(self):
64 """met à jour la liste des plages disponibles 65 en fonction des plages réservées 66 """ 67 reserved = [(r_min, r_max) for r_min, r_max, id_s in self.reserved] 68 # on considère les plages en attente de validation comme non disponibles 69 for p_rng in self.pending: 70 reserved.extend(p_rng) 71 # recalcul des plages libres 72 free = [] 73 start = 0 74 free_space = 0 75 for r_min, r_max in reserved: 76 if r_min != start: 77 free.append((start, r_min - 1)) 78 free_space += r_max - r_min + 1 79 start = r_max + 1 80 if start <= self.max_id: 81 free.append((start, self.max_id)) 82 free_space += self.max_id - start +1 83 self.free = free 84 self.free_space = free_space
85
86 - def reserve_range(self, minid, maxid):
87 """réserve une plage spécifique si elle est disponible 88 (utile pour bloquer des intervalles non gérés par zephir) 89 """ 90 # vérification de la disponibilité de la plage 91 valid = False 92 for r_min, r_max in self.free: 93 if r_min <= minid and r_max > minid: 94 if r_max >= maxid: 95 # plage libre 96 valid = True 97 # on enregistre la nouvelle plage 98 self._add_reserved(minid, maxid) 99 break 100 return valid
101
102 - def cancel(self,ranges):
103 """annule une réservation de plages 104 """ 105 if ranges in self.pending: 106 self.pending.remove(ranges) 107 self.update_free_ranges()
108
109 - def validate(self, id_serveur, ranges):
110 """valide la prise en compte de la plage réservée 111 """ 112 if ranges in self.pending: 113 self.pending.remove(ranges) 114 for r_min, r_max in ranges: 115 self._add_reserved(r_min, r_max, id_serveur) 116 return True 117 return False
118
119 - def __repr__(self):
120 """représentation par défaut de l'objet 121 """ 122 descr = "ENT %s" % self.code_ent 123 if not self.pending: 124 descr += " - identifiants disponibles: %s" % (self.free_space) 125 if self.pending: 126 rngs=[] 127 for ranges in self.pending: 128 for r_min, r_max in ranges: 129 rngs.append("%s->%s" % (self.id_to_string(r_min), self.id_to_string(r_max))) 130 descr += " - plage en attente de validation : %s" % " - ".join(rngs) 131 return descr
132
133 - def to_dict(self):
134 """renvoie les données actuelles sous forme de dictionnaire pour sauvegarde en base de données 135 """ 136 return {'code_ent':self.code_ent, 'free_space':str(self.free_space)}
137
138 -class IdPoolManager:
139 """classe de gestion d'un ensemble de pool d'identifiants pour les ENT 140 se reporter au Schéma Directeur des Espaces Numériques de Travail 141 http://www.educnet.education.fr/services/ent/sdet 142 """ 143
144 - def __init__(self, serveur_pool):
145 # chargement des plages déjà réservées depuis la base 146 self.serveur_pool = serveur_pool 147 self.id_pools = self.load_pools() 148 log.msg("chargement des pools d'identifiants ENT") 149 for pool in self.id_pools.values(): 150 log.msg(pool) 151 self.timeouts = {}
152
153 - def load_pools(self):
154 """initialise les pools d'identifiant depuis les données stockées en base 155 """ 156 # récupération des plages réservées dans la base 157 cursor = cx_pool.create() 158 cursor.execute("select code_ent, serveur, min, max from ent_id_ranges order by code_ent, min") 159 ranges = cursor.fetchall() 160 cx_pool.close(cursor) 161 # initialisation des pools 162 pools = {} 163 if ranges: 164 for code_ent, id_serveur, minid, maxid in ranges: 165 if code_ent not in pools: 166 pools[code_ent] = ZephirIdPool(code_ent) 167 pools[code_ent]._add_reserved(minid, maxid, id_serveur) 168 return pools
169
170 - def get_pool(self, code_ent = None):
171 """renvoie les informations d'un pool (plages réservées et nombre d'identifiants disponibles) 172 code_ent: code ENT du pool à renvoyer (tous si rien) 173 """ 174 data = [] 175 if code_ent: 176 if code_ent in self.id_pools: 177 pools = [self.id_pools[code_ent]] 178 else: 179 pools = [] 180 else: 181 pools = self.id_pools.values() 182 for pool in pools: 183 ranges = [] 184 for minid, maxid, id_s in pool.reserved: 185 ranges.append([pool.id_to_string(minid), pool.id_to_string(maxid), id_s]) 186 free_ranges = [] 187 for minid, maxid in pool.free: 188 free_ranges.append([pool.id_to_string(minid), pool.id_to_string(maxid)]) 189 data.append([pool.code_ent, pool.free_space, ranges, free_ranges]) 190 return 1, data
191
192 - def get_code_ent(self, code_ent = None):
193 """renvoie la liste des codes ent connus 194 """ 195 return self.id_pools.keys()
196
197 - def get_id_range(self, id_serveur, cle_pub, nb_id = 100):
198 """réserve une plage dans le pool d'adresses d'un ent 199 - le code ent est récupéré dans la configuration du serveur 200 - si le pool n'a jamais été utilisé, on l'initialise 201 - le pool se met en attente de validation et bloque les autres demandes en attendant 202 (si la validation n'est pas faite après un certain temps, on annule la réservation) 203 - les données sont envoyées par uucp dans un fichier dont on garde le md5 204 (une action de validation est aussi programmée) 205 """ 206 # récupération du code ent 207 try: 208 serv = self.serveur_pool[int(id_serveur)] 209 except: 210 return 0, "Serveur inconnu" 211 # lecture de la clé publique du serveur 212 f_cle = os.path.join(serv.get_confdir(), 'cle_publique') 213 data_cle = file(f_cle).read().strip() 214 try: 215 assert data_cle == """command="sudo /usr/sbin/uucico2 -D -l" %s""" % base64.decodestring(cle_pub) 216 except: 217 return 0, "Cle incorrecte pour le serveur %s" % str(id_serveur) 218 try: 219 code_ent = serv.get_config().parsedico()['code_ent'] 220 assert len(code_ent) == 2 221 except: 222 return 0, "Code ENT invalide pour ce serveur" 223 # si une réservation est déjà en cours pour ce serveur, on en interdit une autre 224 if code_ent not in self.timeouts: 225 self.timeouts[code_ent] = {} 226 if self.timeouts[code_ent].get(id_serveur,None): 227 return 0, "Une réservation est encore en cours pour ce serveur" 228 # récupération de la plage 229 try: 230 if not code_ent in self.id_pools: 231 # initialisation d'un nouveau pool 232 self.id_pools[code_ent] = ZephirIdPool(code_ent) 233 pool = self.id_pools[code_ent] 234 ranges = pool.get_range(nb_id) 235 except Exception, e: 236 return 0, (str(e)) 237 # préparation de l'envoi du fichier et de la commande de prise en compte 238 random_id = self.send_interval(serv, ranges) 239 # mise en place d'un timeout pour annuler la réservation 240 # dans 30 secondes si elle n'est pas validée. On stocke le un md5 aléatoire pour 241 # servir de 'mot de passe' lors de la validation par le client 242 self.timeouts[code_ent][id_serveur] = (random_id, reactor.callLater(30, self.cancel, pool, id_serveur, ranges)) 243 log.msg(str(pool)) 244 return 1, "OK"
245
246 - def cancel(self, pool, id_serveur, ranges):
247 """annule une réservation après un délai d'attente 248 """ 249 code_ent = pool.code_ent 250 if id_serveur in self.timeouts.get(code_ent,{}): 251 log.msg("ENT %s - timeout de la reservation (serveur %s)" % (str(code_ent), str(id_serveur))) 252 pool.cancel(ranges) 253 del self.timeouts[code_ent][id_serveur]
254
255 - def validate_id_range(self, code_ent, id_serveur, md5_id, ranges):
256 """Valide une réservation envoyée à un serveur 257 - vérifie que l'id du serveur et le md5 renvoyé correspond à la réservation en cours 258 - met à jour le pool d'identifiant de ce code ent 259 """ 260 err = "Réservation invalide" 261 if id_serveur in self.timeouts.get(code_ent,{}): 262 pool = self.id_pools[code_ent] 263 if pool.pending: 264 stored_id, call_id = self.timeouts[code_ent][id_serveur] 265 try: 266 # calcul des plages envoyées 267 num_ranges = [(pool.string_to_id(minid),pool.string_to_id(maxid)) for minid, maxid in ranges] 268 assert num_ranges in pool.pending 269 assert md5_id == stored_id 270 if pool.validate(id_serveur, num_ranges): 271 # suppression de la réservation et du callback de timeout 272 call_id.cancel() 273 del self.timeouts[code_ent][id_serveur] 274 return self.store_range(code_ent, id_serveur, pool, num_ranges) 275 except: 276 traceback.print_exc() 277 # par sécurité, on ne donne pas le détail de l'erreur 278 pass 279 log.msg("ENT %s - Plage(s) invalide(s) demandée par le serveur %s : " % (str(code_ent), str(id_serveur)), str(ranges)) 280 return 0, err
281
282 - def store_range(self, code_ent, id_serveur, pool, ranges):
283 """enregistre une plage réservée dans la base de données""" 284 date = str(time.ctime()) 285 cursor = cx_pool.create() 286 for cur_min, cur_max in ranges: 287 query = """insert into ent_id_ranges (code_ent, serveur, min, max, date_valid) values(%s, %s, %s, %s, %s)""" 288 params = (code_ent, id_serveur, cur_min, cur_max, date) 289 cursor.execute(query, params) 290 if id_serveur == -1: 291 # plage non rattachée à un serveur (définie manuellement) 292 log.msg("ENT %s - plage bloquée : " % str(code_ent), pool.id_to_string(cur_min), "->", pool.id_to_string(cur_max)) 293 else: 294 log.msg("ENT %s - plage validée par le serveur %s : " % (str(code_ent), str(id_serveur)) \ 295 , pool.id_to_string(cur_min) , "->", pool.id_to_string(cur_max)) 296 cx_pool.commit(cursor) 297 return 1, "OK"
298
299 - def reserve_range(self, minid, maxid):
300 """Réservation manuelle d'une plage d'identifiant 301 """ 302 code_ent = minid[0]+minid[3] 303 if (code_ent == maxid[0] + maxid[3]): 304 if not code_ent in self.id_pools: 305 #initialisation d'un nouveau pool 306 self.id_pools[code_ent] = ZephirIdPool(code_ent) 307 pool = self.id_pools[code_ent] 308 else: 309 # XXX FIXME : ajouter l'ent si inconnu ? 310 return 0, "code ENT invalide" 311 minid = pool.string_to_id(minid) 312 maxid = pool.string_to_id(maxid) 313 if minid > maxid: 314 return 0, "Plage d'identifiants invalide" 315 else: 316 # réservation de la plage 317 if pool.reserve_range(minid, maxid): 318 return self.store_range(code_ent, -1, pool, [(minid, maxid)]) 319 else: 320 return 0, "Plage non disponible"
321
322 - def send_interval(self, serv, ranges):
323 """envoi d'un fichier indiquant l'intervalle réservé 324 """ 325 # génération d'un hash aléatoire 326 rand = random.SystemRandom().random() 327 random_id = md5.md5(str(rand)) 328 for rng in ranges: 329 random_id.update(str(rng)) 330 random_id = random_id.hexdigest() 331 id_uucp = str(serv.get_rne()) + '-' + str(serv.id_s) 332 try: 333 serveur_dir = serv.get_confdir() 334 # écriture fichier de transfert 335 archive = "ent_ids" 336 content = random_id 337 for r_min, r_max in ranges: 338 content += "\n%s\t%s" % (r_min, r_max) 339 f_arc = open(os.path.join(serveur_dir, archive), 'w') 340 f_arc.write(content) 341 f_arc.close() 342 # préparation de l'envoi par uucp 343 cmd_tar = ['cd ', serveur_dir, ';', '/bin/tar', '--same-owner', '-chpf', archive+'.tar', 'ent_ids'] 344 # création du fichier tar à envoyer 345 cmd_tar.append('>/dev/null 2>&1') 346 res = os.system(" ".join(cmd_tar)) 347 cmd_checksum = """cd %s ;md5sum -b %s.tar > %s.md5""" % (serveur_dir, archive, archive) 348 res = os.system(cmd_checksum) 349 # préparation des commandes 350 res = uucp_pool.add_cmd(id_uucp, "zephir_client update_ent_ids") 351 res = uucp_pool.add_file(id_uucp, os.path.join(serveur_dir, archive+".tar")) 352 os.unlink(os.path.join(serveur_dir, archive)) 353 except Exception, e: 354 return 0, "Erreur de préparation du fichier (%s)" % str(e) 355 return random_id
356