zephir.backend.entid

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