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

Source Code for Module zephir.backend.entid

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