un peu de nettoyage

This commit is contained in:
Jephté Clain 2017-07-18 17:33:32 +04:00
parent 5e0924aeb5
commit be0ec1c3ab
2 changed files with 298 additions and 38 deletions

39
TODO.md
View File

@ -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

View File

@ -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