From be0ec1c3ab134a7aa1d57efc8b64f82e987972e0 Mon Sep 17 00:00:00 2001 From: Jephte Clain Date: Tue, 18 Jul 2017 17:33:32 +0400 Subject: [PATCH] un peu de nettoyage --- TODO.md | 39 +++++ lib/ulib/support/deploydb.py | 297 ++++++++++++++++++++++++++++++----- 2 files changed, 298 insertions(+), 38 deletions(-) diff --git a/TODO.md b/TODO.md index 32dea67..635d0a5 100644 --- a/TODO.md +++ b/TODO.md @@ -1,3 +1,42 @@ # TODO +* faire la différence entre "pas de profil défini" (=aucun profil) et + "définition par défaut pour les profils" (si pas de définition plus précise + pour un profil, prendre celle-là) + +* mettre à jour l'algorithme pour la prise en compte du type de groupe. + peut-être garder en mémoire la dernière commande ou le dernier type de + commande. + +* il faut distinguer: définitions par défaut globales, définitions par défaut + pour le groupe, définition locale + ~~~ + host dh=dest-host.univ.run + + group module defaults + attr odef=value + ruinst ldef=value + ruinst -p PROFILE pldef=value + + # group #1 + group module once + module MyModule + attr ovar=value + ruinst ldef2=value + ruinst -p PROFILE pldef2=value + ruinst host=dh lvar=value + + # group #2 + module OtherModule + ruinst host=dh + ~~~ + dans l'exemple ci-dessus: + * odef est un attribut global à tous les modules + * ovar est spécifique à MyModule + * ldef est un attribut global à tous les liens + * pldef est un attribut global à tous les liens dans le profil PROFILE + * ldef2 est attribut global à tous les liens du groupe #1 + * pldef2 est un attribut global à tous les liens du groupe #1 dans le profil PROFILE + * lvar est spécifique au lien MyModule --> dh + -*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary \ No newline at end of file diff --git a/lib/ulib/support/deploydb.py b/lib/ulib/support/deploydb.py index 67ecc85..3846620 100755 --- a/lib/ulib/support/deploydb.py +++ b/lib/ulib/support/deploydb.py @@ -9,20 +9,44 @@ from glob import glob USER_CONFDIR = '~/etc/deploy' SYSTEM_CONFDIR = '/var/local/deploy' -#XXX faire la différence entre "pas de profil défini" (=aucun profil) et -# "définition par défaut pour les profils" (si pas de définition plus précise -# pour un profil, prendre celle-là) - ################################################################################ # Diverses fonctions def isseq(t): return isinstance(t, list) or isinstance(t, tuple) or isinstance(t, set) +def flatten(src, unique=True, clean=True, sep=','): + """découper chaque élément du tableau src selon sep et les aplatir dans une + seule liste. + + Si unique==True, supprimer les doublons. + Si clean==True, supprimer les valeurs vides et les espaces périphériques + + e.g flatten(['a , b', 'c,']) --> ['a', 'b', 'c'] + + """ + if not isseq(src): src = [src] + dest = [] + for items in src: + items = items.split(sep) + if clean: items = filter(None, map(lambda item: item.strip(), items)) + if unique: + for item in items: + if item not in dest: dest.append(item) + else: + dest.extend(items) + return dest + ################################################################################ # Base de données class GenericObject(object): + """Un objet générique + + Un objet a un identifiant (propriété id), une liste de valeurs (propriété + values) et des attributs (propriété attrs) + """ + _id = None _values = None _attrs = None @@ -43,26 +67,45 @@ class GenericObject(object): def get_attrs(self): return self._attrs attrs = property(get_attrs) - def get_attr(self, name): return self._attrs.get(name, None) + def get_attr(self, name): + """Obtenir la valeur d'un attribut ou None si l'attribut n'existe pas + """ + return self._attrs.get(name, None) def set_attr(self, name, value): + """Ajouter une valeur à la liste des valeurs d'un attribut si elle n'y est pas + déjà. + """ if name not in self._attrs: self._attrs[name] = [] if value not in self._attrs[name]: self._attrs[name].append(value) def reset_attr(self, name, value=None): + """Recommencer à zéro la valeur d'un attribut + """ if name in self._attrs: del self._attrs[name] if value is not None: self.set_attr(name, value) def add_attr(self, name, value): + """Ajouter une valeur à la liste des valeurs d'un attribut + """ if name not in self._attrs: self._attrs[name] = [] self._attrs[name].append(value) def del_attr(self, name, value): + """Supprimer une valeur d'un attribut + """ if name not in self._attrs: return self._attrs[name].remove(value) if not self._attrs[name]: del self._attrs[name] def modify_attr(self, name, value, method='set'): + """Méthode générique pour modifier les valeurs d'un attribut + + method peut valoir set, reset, add, del + """ if method == 'set': self.set_attr(name, value) elif method == 'reset': self.reset_attr(name, value) elif method == 'add': self.add_attr(name, value) elif method == 'del': self.del_attr(name, value) def merge_attrs(self, other, method='set'): + """Fusionner dans cet objet les attributs d'un autre objet, avec la méthode + spécifiée. + """ for name, values in other.attrs.items(): for value in values: self.modify_attr(name, value, method) @@ -80,20 +123,30 @@ class GenericObject(object): else: print "%s %s=(%s)" % (indent, name, ', '.join(map(repr, values))) def dump(self, indent=''): + """Afficher l'identifiant, les valeurs et les attributs de cet objet + """ self._dump_id(indent) + self._dump_values(indent, 'values') self._dump_attrs(indent) class HostObject(GenericObject): + """Un hôte + """ + @staticmethod def genid(host): - "MY-Host42.self --> my_host42" + """Générer un identifiant à partir du nom d'hôte + e.g. MY-Host42.tld --> my_host42 + """ host = re.sub(r'\..*', '', host) host = re.sub(r'[^a-zA-Z0-9]', '_', host) host = host.lower() return host @staticmethod def genib(id): - "my_host42 --> my_host" + """Générer un identifiant générique à partir d'un identifiant qualifié + e.g. my_host42 --> my_host + """ if re.match(r'\d+$', id): return id ib = re.sub(r'^(.*?)\d+$', r'\1', id) return ib @@ -104,9 +157,14 @@ class HostObject(GenericObject): self._dump_attrs(indent) class ModuleObject(GenericObject): + """Un module, uinst-allable + """ + @staticmethod def genid(module): - "MY-Module --> MY_Module" + """Générér un identifiant à partir du nom du module + e.g. MY-Module --> MY_Module + """ module = re.sub(r'[^a-zA-Z0-9]', '_', module) return module @@ -116,9 +174,14 @@ class ModuleObject(GenericObject): self._dump_attrs(indent) class WobundleObject(GenericObject): + """Un bundle Webobjects, woinst-allable + """ + @staticmethod def genid(wobundle): - "MY-App.woa --> MY_App" + """Générér un identifiant à partir du nom du bundle + e.g. MY-App.woa --> MY_App + """ wobundle = re.sub(r'\.(woa|framework)$', '', wobundle) wobundle = re.sub(r'[^a-zA-Z0-9]', '_', wobundle) return wobundle @@ -129,9 +192,14 @@ class WobundleObject(GenericObject): self._dump_attrs(indent) class WebappObject(GenericObject): + """Une webapp, toinst-allable + """ + @staticmethod def genid(webapp): - "MY-Webapp --> MY_Webapp" + """Générér un identifiant à partir du nom de la webapp + e.g. MY-Webapp --> MY_Webapp + """ webapp = re.sub(r'[^a-zA-Z0-9]', '_', webapp) return webapp @@ -141,6 +209,14 @@ class WebappObject(GenericObject): self._dump_attrs(indent) class GenericLink(object): + """Un lien générique + + Un lien est valide dans un profil (propriété profile), a un type d'objets de + départ (propriété ftype), un type d'objet d'arrivée (propriété ttype), un + ensemble d'objets de départ (propriété fos), un ensemble d'objets d'arrivée + (propriété tos), et des attributs (propriété attrs). + """ + _profile = None _ftype = None _fos = None @@ -156,6 +232,9 @@ class GenericLink(object): self._attrs = {} def is_defaults(self): + """Cet objet contient-il les attributs par défaut pour les liens provenant du + type d'objet ftype + """ fos = list(self._fos) return len(fos) == 1 and fos[0] is None @@ -180,46 +259,89 @@ class GenericLink(object): def get_attrs(self): return self._attrs attrs = property(get_attrs) - def get_attr(self, name): return self._attrs.get(name, None) + def get_attr(self, name): + """Obtenir la valeur d'un attribut ou None si l'attribut n'existe pas + """ + return self._attrs.get(name, None) def set_attr(self, name, value): + """Ajouter une valeur à la liste des valeurs d'un attribut si elle n'y est pas + déjà. + """ if name not in self._attrs: self._attrs[name] = [] if value not in self._attrs[name]: self._attrs[name].append(value) def reset_attr(self, name, value=None): + """Recommencer à zéro la valeur d'un attribut + """ if name in self._attrs: del self._attrs[name] if value is not None: self.set_attr(name, value) def add_attr(self, name, value): + """Ajouter une valeur à la liste des valeurs d'un attribut + """ if name not in self._attrs: self._attrs[name] = [] self._attrs[name].append(value) def del_attr(self, name, value): + """Supprimer une valeur d'un attribut + """ if name not in self._attrs: return self._attrs[name].remove(value) if not self._attrs[name]: del self._attrs[name] def modify_attr(self, name, value, method='set'): + """Méthode générique pour modifier les valeurs d'un attribut + + method peut valoir set, reset, add, del + """ if method == 'set': self.set_attr(name, value) elif method == 'reset': self.reset_attr(name, value) elif method == 'add': self.add_attr(name, value) elif method == 'del': self.del_attr(name, value) def merge_attrs(self, other, method='set'): + """Fusionner dans ce lien les attributs d'un autre lien, avec la méthode + spécifiée. + """ for name, values in other.attrs.items(): for value in values: self.modify_attr(name, value, method) def match_profiles(self, profiles): + """Tester si ce lien est dans l'un des profils spécifiés + + Si profiles == None, alors l'utilisateur ne demande aucun profil en + particulier. Dans ce cas, la réponse est OUI. + + Si profiles == [], alors l'utilisateur demande tous les liens pour + lesquels aucun profil n'est défini. Dans ce cas, la réponse est OUI si + ce lien ne définit aucun profil. + + Sinon, profiles est une liste des profils recherchés. Dans ce cas, la + réponse n'est OUI que si le profil de ce lien fait partie des profils + spécifiés. + """ + if profiles is None: return True if not isseq(profiles): profiles = [profiles] + if profiles == []: return self._profile is None for profile in profiles: if profile == self._profile: return True return False def match_fos(self, fos, match='any'): + """Tester si ce lien a l'un des objets spécifiés dans ses objets de départ (avec + match=='any' qui est la valeur par défaut) + """ if not isseq(fos): fos = [fos] for fo in fos: if fo in self._fos: return True return False def match_tos(self, tos, match='any'): + """Tester si ce lien a l'un des objets spécifiés dans ses objets d'arrivée (avec + match=='any' qui est la valeur par défaut) + """ if not isseq(tos): tos = [tos] for to in tos: if to in self._tos: return True return False def match_attrs(self, attrs, match='any'): + """Tester si ce lien a un des attributs correspondant aux valeurs spécifiées + (avec match=='any' qui est la valeur par défaut) + """ for name, value in attrs.items(): values = self._attrs.get(name, None) if values is not None: @@ -243,6 +365,9 @@ class GenericLink(object): else: print "%s %s=(%s)" % (indent, name, ', '.join(map(repr, values))) def dump(self, indent=''): + """Afficher le profil, les objets de départ, les objets d'arrivée, et les + attributs de ce lien + """ print "%slink" % indent self._dump_profile(indent) self._dump_fos(indent) @@ -250,6 +375,9 @@ class GenericLink(object): self._dump_attrs(indent) class UinstLink(GenericLink): + """Un module déployé sur un hôte avec le type uinst:rsync + """ + def __init__(self): super(UinstLink, self).__init__('module', 'host') @@ -261,6 +389,9 @@ class UinstLink(GenericLink): self._dump_attrs(indent) class RuinstLink(GenericLink): + """Un module déployé sur un hôte avec ruinst + """ + def __init__(self): super(RuinstLink, self).__init__('module', 'host') @@ -272,6 +403,9 @@ class RuinstLink(GenericLink): self._dump_attrs(indent) class RwoinstBundleLink(GenericLink): + """Un bundle déployé sur un hôte avec rwoinst + """ + def __init__(self): super(RwoinstBundleLink, self).__init__('wobundle', 'host') @@ -283,6 +417,9 @@ class RwoinstBundleLink(GenericLink): self._dump_attrs(indent) class RwoinstWebresLink(GenericLink): + """Les resources web d'un bundle déployées sur un hôte avec rwoinst + """ + def __init__(self): super(RwoinstWebresLink, self).__init__('wobundle', 'host') @@ -294,6 +431,9 @@ class RwoinstWebresLink(GenericLink): self._dump_attrs(indent) class RtoinstLink(GenericLink): + """Une webapp déployée sur un hôte avec rtoinst + """ + def __init__(self): super(RtoinstLink, self).__init__('webapp', 'host') @@ -309,6 +449,9 @@ class UNDEF(object): UNDEF = UNDEF() class Database(object): + """La base de données des objets et des liens + """ + _default_profile = None _default_domain = None @@ -325,18 +468,38 @@ class Database(object): self._links_classes = {} def has_default_profile(self): + """Vérifier si un profil par défaut a été spécifié + """ return self._default_profile is not None def get_default_profile(self): + """Obtenir le profil par défaut, ou None si aucun profil par défaut n'a été + défini + """ return self._default_profile def set_default_profile(self, profile): + """Spécifier le profil par défaut + + Si un lien est spécifié sans profil, il est réputé défini dans le + profil par défaut. + """ self._default_profile = profile or None default_profile = property(get_default_profile, set_default_profile) def has_default_domain(self): + """Vérifier si un domaine par défaut a été spécifié + """ return self._default_domain is not None def get_default_domain(self): + """Obtenir le domaine par défaut, ou None si aucun domaine par défaut n'a été + défini + """ return self._default_domain def set_default_domain(self, domain): + """Spécifier le domaine par défaut + + Si un hôte est spécifié sans domaine, il est réputé défini avec le + domaine par défaut. + """ if domain is not None: #XXX si le domaine a été corrigé, l'indiquer en warning if domain.startswith('.'): domain = domain[1:] @@ -345,26 +508,42 @@ class Database(object): default_domain = property(get_default_domain, set_default_domain) def register_object(self, otype, oclass=None): + """Enregistrer un type d'objet et la classe utilisée pour l'instancier. + + Par défaut, le classe utilisé est GenericObject. + """ if not self._objects_ids_otype.has_key(otype): self._objects_ids_otype[otype] = {} if oclass is None: oclass = GenericObject self._objects_classes[otype] = oclass def get_known_otypes(self): + """Obtenir la liste des types d'objets connus + """ return self._objects_ids_otype.keys() known_otypes = property(get_known_otypes) def get_objects(self, otype): + """Obtenir tous les objets définis du type spécifié ou None si le type d'objets + est invalide. + """ objects = self._objects_ids_otype.get(otype, None) if objects is None: return None return [objects[id] for id in objects if id is not None] def has_object(self, otype, id): + """Vérifier si l'objet avec l'identifiant spécifié est défini + """ objects = self._objects_ids_otype.get(otype, None) if objects is None: return False return objects.has_key(id) - def get_object(self, otype, id): + def get_object(self, otype, id, create=True): + """Obtenir l'objet avec l'identifiant spécifié, ou None si l'objet n'existe pas. + + Si create==True, l'objet est créé s'il n'existe pas, et cette méthode ne + retourne pas None. + """ objects = self._objects_ids_otype.get(otype, None) if objects is None: return None object = objects.get(id, None) - if object is None: + if object is None and create: object_class = self._objects_classes.get(otype, None) if object_class is not None: object = object_class(id) @@ -372,13 +551,24 @@ class Database(object): return object def register_link(self, ltype, lclass): + """Enregister un type de lien et la classe utilisée pour l'instancier. + + Il n'y a pas de classe par défaut, il faut absolument spécifier une + classe valide. + """ if not self._links_ltype.has_key(ltype): self._links_ltype[ltype] = [] self._links_classes[ltype] = lclass def get_known_ltypes(self): + """Obtenir la liste des types de liens connus + """ return self._links_ltype.keys() known_ltypes = property(get_known_ltypes) def get_links(self, ltype, profile=UNDEF, fo=UNDEF, to=UNDEF, attrs=UNDEF, create=False): + """Obtenir les liens correspondant aux critères. + + XXX compléter la doc + """ links = self._links_ltype.get(ltype, None) if links is None: return None @@ -616,22 +806,24 @@ class Parser(object): if db is None: db = Database() self.db = db self.groups = {} - self.commands = {} + self.commands = { + 'default_profile': self.handle_default_profile, + 'default_domain': self.handle_default_domain, + 'group': self.handle_group, + 'attr': self.handle_attr, + } self.__setup_hosts() self.__setup_uinst() self.__setup_woinst() self.__setup_toinst() + self.__setup_query() def parse(self, predicates): for p in predicates: cmd = p[0] args = p[1:] - if cmd == 'default_profile': self.handle_default_profile(args) - elif cmd == 'default_domain': self.handle_default_domain(args) - elif cmd == 'group': self.handle_group(args) - elif cmd == 'attr': self.handle_attr(args) - elif cmd == 'host': self.handle_host(*p) - elif cmd in self.commands: self.commands[cmd](*p) + if cmd in self.commands: self.commands[cmd](*p) + else: raise ValueError("%s: unknown command" % cmd) return self def register_command(self, name, method): @@ -647,18 +839,21 @@ class Parser(object): def add_group(self, otype, id): self.groups[otype]['current'].add(id) - def handle_group(self, args): - """group otype gtype + def handle_group(self, cmd, *args): + """group otype [gtype] gtype peut valoir: - defaults: revenir à l'état initial, permettant de spécifier les attributs et liens pour tous les hôtes définis à partir de cette ligne - - once: un nouveau groupe est défini à chaque nouvelle ligne 'group' - - until: définir un groupe qui va jusqu'à la prochaine ligne 'group' + - once: un nouveau groupe est défini à chaque nouvelle définition + d'objet. en d'autres termes, ne font partie du groupe que les objets + faisant partie d'une même définition. c'est la valeur par défaut + - until: définir un groupe qui va jusqu'à la prochaine définition de + groupe ou de lien. """ otype = args[0:1] and args[0] or 'host' if otype not in self.groups: raise ValueError('%s: invalid object type' % otype) - gtype = args[1:2] and args[1] or 'until' + gtype = args[1:2] and args[1] or 'once' self.groups[otype]['type'] = gtype if gtype == 'defaults': self.groups[otype]['current'] = set([None]) @@ -669,7 +864,7 @@ class Parser(object): self.attr_otype = otype ############################################################################ - def handle_attr(self, args): + def handle_attr(self, cmd, *args): otype = self.attr_otype assert otype is not None, "attr_otype should not be None" for nv in args: @@ -683,13 +878,13 @@ class Parser(object): self.db.get_object(otype, id).modify_attr(name, value, method) ############################################################################ - def handle_default_profile(self, args): + def handle_default_profile(self, cmd, *args): if not args or not args[0]: profile = None else: profile = args[0] self.db.default_profile = profile ############################################################################ - def handle_default_domain(self, args): + def handle_default_domain(self, cmd, *args): if not args or not args[0]: domain = None else: domain = args[0] self.db.default_domain = domain @@ -699,7 +894,8 @@ class Parser(object): self.db.register_object('host', HostObject) self.db.get_object('host', None) self.groups['host'] = {} - self.handle_group(['host', 'defaults']) + self.handle_group('group', 'host', 'defaults') + self.register_command('host', self.handle_host) def __fix_host(self, host): if host.endswith('.'): host = host[:-1] @@ -779,7 +975,7 @@ class Parser(object): self.db.register_link('ruinst', RuinstLink) self.db.get_object('module', None) self.groups['module'] = {} - self.handle_group(['module', 'defaults']) + self.handle_group('group', 'module', 'defaults') self.register_command('module', self.handle_module) self.register_command('uinst', self.handle_xuinst) self.register_command('ruinst', self.handle_xuinst) @@ -852,13 +1048,25 @@ class Parser(object): def handle_xuinst(self, ltype, *args): """uinst -p profile attrs* ruinst -p profile attrs* + + usage de l'option -p: + pas d'option -p + définir les liens dans le profil par défaut. équivalent à -p '' s'il + n'y a pas de profil par défaut. + -p '' + définir les liens en ne les associant à aucun profil + -p PROFILE,... + définir les liens dans les profils spécifiés + """ AP = ArgumentParser() AP.add_argument('-p', '--profile', action='append', dest='profiles') AP.add_argument('nvss', nargs=REMAINDER) o = AP.parse_args(args) - profiles = o.profiles or [None] - #XXX spliter profiles sur ',' + + if o.profiles is None: profiles = [self.db.default_profile] + else: profiles = flatten(o.profiles) + if profiles == []: profiles = [None] for profile in profiles: # préparer la mise à jour du groupe courant currentls = self.db.get_links(ltype, profile, fo=self.groups['module']['current'], create=True) @@ -900,7 +1108,7 @@ class Parser(object): self.db.register_link('rwoinst-webres', RwoinstWebresLink) self.db.get_object('wobundle', None) self.groups['wobundle'] = {} - self.handle_group(['wobundle', 'defaults']) + self.handle_group('group', 'wobundle', 'defaults') self.register_command('wobundle', self.handle_wobundle) self.register_command('rwoinst', self.handle_rwoinst) def __fix_wobundle(self, wobundle): @@ -980,9 +1188,11 @@ class Parser(object): AP.add_argument('stype', nargs=1) AP.add_argument('nvss', nargs=REMAINDER) o = AP.parse_args(args) + + if o.profiles is None: profiles = [self.db.default_profile] + else: profiles = flatten(o.profiles) + if profiles == []: profiles = [None] ltype = "%s-%s" % (ltype, o.stype[0]) - profiles = o.profiles or [None] - #XXX spliter profiles sur ',' for profile in profiles: # préparer la mise à jour du groupe courant currentls = self.db.get_links(ltype, profile, fo=self.groups['wobundle']['current'], create=True) @@ -1023,7 +1233,7 @@ class Parser(object): self.db.register_link('rtoinst', RtoinstLink) self.db.get_object('webapp', None) self.groups['webapp'] = {} - self.handle_group(['webapp', 'defaults']) + self.handle_group('group', 'webapp', 'defaults') self.register_command('webapp', self.handle_webapp) self.register_command('rtoinst', self.handle_rtoinst) def __fix_webapp(self, webapp): @@ -1099,8 +1309,10 @@ class Parser(object): AP.add_argument('-p', '--profile', action='append', dest='profiles') AP.add_argument('nvss', nargs=REMAINDER) o = AP.parse_args(args) - profiles = o.profiles or [None] - #XXX spliter profiles sur ',' + + if o.profiles is None: profiles = [self.db.default_profile] + else: profiles = flatten(o.profiles) + if profiles == []: profiles = [None] for profile in profiles: # préparer la mise à jour du groupe courant currentls = self.db.get_links(ltype, profile, fo=self.groups['webapp']['current'], create=True) @@ -1135,6 +1347,15 @@ class Parser(object): for currentl in currentls: currentl.modify_attr(name, value, method) + ############################################################################ + def __setup_query(self): + self.register_command('query', self.handle_query) + + def handle_query(self, cmd, *args): + """query config + """ + print "WARNING: query not implemented" #XXX + ################################################################################ # Programme principal