#!/usr/bin/env python # -*- coding: utf-8 mode: python -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 import os, sys, re from os import path from argparse import ArgumentParser from glob import glob from fnmatch import fnmatch USER_CONFDIR = '~/etc/deploy' SYSTEM_CONFDIR = '/var/local/deploy' ################################################################################ # Diverses fonctions def isseq(t): """Tester si t est une séquence """ return isinstance(t, list) or isinstance(t, tuple) or isinstance(t, set) __SEQOF_UNDEF = object() def seqof(o, noneValue=__SEQOF_UNDEF): """Retourner un tuple à parti de o * si o est une séquence, retourner tuple(o) * si noneValue est défini, et que o is noneValue, retourner noneValue * sinon, retourner le tuple (o,) """ if isseq(o): return tuple(o) elif o is noneValue and noneValue is not __SEQOF_UNDEF: return noneValue else: return (o,) 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 src is None: return None dest = [] for items in seqof(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 def qshell(values): if isseq(values): return map(qshell, values) elif not values: return '' else: return "'%s'" % values.replace("'", "'\\''") def strip_domain(a): if a.find('.') == -1: return a else: return a[:a.find('.')] def match_host(a, b): """Les hôtes sont matchés de façon particulière: NAME matche NAME et NAME.DOMAIN NAME. matche NAME NAME.DOMAIN matche NAME.DOMAIN """ a = a.lower() b = b.lower() if a == b: return True if a.find('.') == -1: # NAME return a == strip_domain(b) elif a.endswith('.'): # NAME. return a[:-1] == b else: # NAME.DOMAIN return a == b ################################################################################ # 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) """ OTYPE = 'object' VALUESNAME = 'values' ATTRPREFIX = 'object_' _id = None _values = None _attrs = None def __init__(self, id): self._id = id self._values = set() self._attrs = {} def get_id(self): return self._id id = property(get_id) def is_defaults(self): """Cet objet contient-il les attributs par défaut pour le type? """ return self._id is None def get_values(self): return tuple(self._values) values = property(get_values) def add_value(self, value): self._values.add(value) def get_attrs(self): return self._attrs attrs = property(get_attrs) 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) def invalid_match(self, match): return ValueError("%s: invalid match type") def match_values(self, values, match='any', reverse=False): """Tester si cet objet a l'une des valeurs spécifiées (avec match=='any' qui est la valeur par défaut) """ values = seqof(values) if reverse: Yes, No = False, True else: Yes, No = True, False if match == 'any': for value in values: if value in self._values: return Yes return No elif match == 'all': for value in values: if value not in self._values: return No return Yes else: raise self.invalid_match(match) def match_attrs(self, attrs, match='any', reverse=False): """Tester si cet objet a un des attributs correspondant aux valeurs spécifiées (avec match=='any' qui est la valeur par défaut) """ if reverse: Yes, No = False, True else: Yes, No = True, False if match == 'any': for name, value in attrs.items(): values = self._attrs.get(name, None) if values is None: continue if value in values: return Yes return No elif match == 'all': for name, value in attrs.items(): values = self._attrs.get(name, None) if values is None: return No if value not in values: return No return Yes else: raise self.invalid_match(match) def _dump_id(self, indent): print "%s%s" % (indent, self.id) def _dump_values(self, indent, name): print "%s %s: %s" % (indent, name, ' '.join(self.values)) def _dump_attrs(self, indent): if self.attrs: print "%s attrs:" % indent for name, values in self.attrs.items(): if len(values) == 1: print "%s %s=%s" % (indent, name, repr(values[0])) 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, self.VALUESNAME) self._dump_attrs(indent) class HostObject(GenericObject): """Un hôte """ OTYPE = 'host' VALUESNAME = 'hosts' ATTRPREFIX = 'host_' @staticmethod def genid(host): """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): """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 def match_values(self, values, match='any', reverse=False): values = seqof(values) if reverse: Yes, No = False, True else: Yes, No = True, False if match == 'any': for value in values: for host in self._values: if match_host(value, host): return Yes return No elif match == 'all': for value in values: for host in self._values: if not match_host(value, host): return No return Yes else: raise self.invalid_match(match) class ModuleObject(GenericObject): """Un module, uinst-allable """ OTYPE = 'module' VALUESNAME = 'modules' ATTRPREFIX = 'module_' @staticmethod def genid(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 class WobundleObject(GenericObject): """Un bundle Webobjects, woinst-allable """ OTYPE = 'wobundle' VALUESNAME = 'wobundles' ATTRPREFIX = 'wobundle_' @staticmethod def genid(wobundle): """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 class WebappObject(GenericObject): """Une webapp, toinst-allable """ OTYPE = 'webapp' VALUESNAME = 'webapps' ATTRPREFIX = 'webapp_' @staticmethod def genid(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 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 _ttype = None _tos = None _attrs = None def __init__(self, ftype, ttype): self._ftype = ftype self._fos = set() self._ttype = ttype self._tos = set() 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 def get_profile(self): return self._profile def set_profile(self, profile): self._profile = profile profile = property(get_profile, set_profile) def get_ftype(self): return self._ftype ftype = property(get_ftype) def get_fos(self): return self._fos fos = property(get_fos) def add_fo(self, fo): self._fos.add(fo) def get_ttype(self): return self._ttype ttype = property(get_ttype) def get_tos(self): return self._tos tos = property(get_tos) def add_to(self, to): self._tos.add(to) def get_attrs(self): return self._attrs attrs = property(get_attrs) 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 invalid_match(self, match): return ValueError("%s: invalid match type") 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 profiles = seqof(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', reverse=False): """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) """ fos = seqof(fos) if reverse: Yes, No = False, True else: Yes, No = True, False if match == 'any': for fo in fos: if fo in self._fos: return Yes return No elif match == 'all': for fo in fos: if fo not in self._fos: return No return Yes else: raise self.invalid_match(match) def match_tos(self, tos, match='any', reverse=False): """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) """ tos = seqof(tos) if reverse: Yes, No = False, True else: Yes, No = True, False if match == 'any': for to in tos: if to in self._tos: return Yes return No elif match == 'all': for to in tos: if to not in self._tos: return No return Yes else: raise self.invalid_match(match) def match_attrs(self, attrs, match='any', reverse=False): """Tester si ce lien a un des attributs correspondant aux valeurs spécifiées (avec match=='any' qui est la valeur par défaut) """ if reverse: Yes, No = False, True else: Yes, No = True, False if match == 'any': for name, value in attrs.items(): values = self._attrs.get(name, None) if values is None: continue if value in values: return Yes return No elif match == 'all': for name, value in attrs.items(): values = self._attrs.get(name, None) if values is None: return No if value not in values: return No return Yes else: raise self.invalid_match(match) def _dump_profile(self, indent): profile = self.profile or 'ALL' print "%s profile: %s" % (indent, profile) def _dump_fos(self, indent): fos = [fo or '(default)' for fo in self.fos] print "%s from: %s" % (indent, ' '.join(fos)) def _dump_tos(self, indent): print "%s to: %s" % (indent, ' '.join(self.tos)) def _dump_attrs(self, indent): if self.attrs: print "%s attrs:" % indent for name, values in self.attrs.items(): if len(values) == 1: print "%s %s=%s" % (indent, name, repr(values[0])) 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) self._dump_tos(indent) 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') def dump(self, indent=''): print "%suinst" % indent self._dump_profile(indent) self._dump_fos(indent) self._dump_tos(indent) 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') def dump(self, indent=''): print "%sruinst" % indent self._dump_profile(indent) self._dump_fos(indent) self._dump_tos(indent) 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') def dump(self, indent=''): print "%srwoinst bundle" % indent self._dump_profile(indent) self._dump_fos(indent) self._dump_tos(indent) 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') def dump(self, indent=''): print "%srwoinst webres" % indent self._dump_profile(indent) self._dump_fos(indent) self._dump_tos(indent) 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') def dump(self, indent=''): print "%srtoinst" % indent self._dump_profile(indent) self._dump_fos(indent) self._dump_tos(indent) self._dump_attrs(indent) class UNDEF(object): __repr__ = __string__ = lambda self: 'UNDEF' UNDEF = UNDEF() class Database(object): """La base de données des objets et des liens """ _default_profile = None _default_domain = None _objects_ids_otype = None _objects_classes = None _links_ltype = None _links_classes = None def __init__(self): self._objects_ids_otype = {} self._objects_classes = {} self._links_ltype = {} 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:] if domain.endswith('.'): domain = domain[:-1] self._default_domain = domain or None 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. Si value ou attrs sont définis, filtrer sur ces valeurs. """ 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, 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 and create: object_class = self._objects_classes.get(otype, None) if object_class is not None: object = object_class(id) self._objects_ids_otype[otype][id] = object return object def find_objects(self, otype=UNDEF, value=UNDEF, attrs=UNDEF): """Chercher tous les objets correspondant aux critères. """ objects = [] otypes = seqof(otype, UNDEF) for otype in self.get_known_otypes(): if otypes is not UNDEF and otype not in otypes: continue objects.extend(self.get_objects(otype)) return [object for object in objects if (value is UNDEF or object.match_values(value)) and (attrs is UNDEF or object.match_attrs(attrs))] 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 """ #XXX si ltype is None, faire un traitement récursif avec get_known_ltypes links = self._links_ltype.get(ltype, None) if links is None: return None found = None for link in links: if (profile is UNDEF or link.match_profiles(profile)) \ and (fo is UNDEF or link.match_fos(fo)) \ and (to is UNDEF or link.match_tos(to)) \ and (attrs is UNDEF or link.match_attrs(attrs)): if found is None: found = [] found.append(link) if found is not None: return found if not create: return [] # ici, create==True if profile is UNDEF: profile = None link = self._links_classes[ltype]() link.profile = profile if fo is not UNDEF: for fo in seqof(fo): link.add_fo(fo) if to is not UNDEF: for to in seqof(to): link.add_to(to) if attrs is not UNDEF: for name, value in attrs.items(): link.set_attr(name, value) links.append(link) return [link] ################################################################################ # Analyse des fichiers de configuration class EOL(object): __repr__ = __string__ = lambda self: 'EOL' EOL = EOL() class EOF(object): __repr__ = __string__ = lambda self: 'EOF' EOF = EOF() class Lexer(object): file = None lexems = None _inf = None _lcount = None _line = None def __init__(self, file, parse=True): self.file = file if parse: self.parse() def next_line(self): line = self._inf.readline() if line == '': return None if line.endswith("\r\n"): line = line[:-2] elif line.endswith("\n"): line = line[:-1] elif line.endswith("\r"): line = line[:-1] self._lcount += 1 self._line = line return line def is_empty(self): return self._line == '' def isa_comment(self): return self._line[:1] == '#' def isa_squote(self): return self._line[:1] == "'" def isa_dquote(self): return self._line[:1] == '"' RE_SPACE = re.compile(r'\s+') RE_COMMENT = re.compile(r'#.*') def parse_ws(self): mo = self.RE_SPACE.match(self._line) if mo is not None: self._line = self._line[mo.end(0):] mo = self.RE_COMMENT.match(self._line) if mo is not None: self._line = self._line[mo.end(0):] def isa_space(self): return self.RE_SPACE.match(self._line) is not None def isa_comment(self): return self.RE_COMMENT.match(self._line) is not None RE_SQUOTE = re.compile(r"'") def parse_sstring(self): slos = self._lcount lexem = '' self._line = self._line[1:] mo = self.RE_SQUOTE.search(self._line) while mo is None: lexem += self._line if self.next_line() is None: raise ValueError("unterminated quoted string starting at line %i" % slos) lexem += "\n" mo = self.RE_SQUOTE.search(self._line) lexem += self._line[0:mo.start(0)] self._line = self._line[mo.end(0):] return lexem RE_DQUOTE = re.compile(r'"') def parse_dstring(self): slos = self._lcount lexem = '' self._line = self._line[1:] mo = self.RE_DQUOTE.search(self._line) while mo is None: lexem += self._line if self.next_line() is None: raise ValueError("unterminated double-quoted string starting at line %i" % slos) lexem += "\n" mo = self.RE_DQUOTE.search(self._line) lexem += self._line[0:mo.start(0)] self._line = self._line[mo.end(0):] lexem = lexem.replace('\\"', '"') lexem = lexem.replace("\\'", "'") lexem = lexem.replace('\\\\', '\\') return lexem RE_EOS = re.compile(r'''\s|(?>>'. Cela permet de mixer du texte formaté et du texte non formaté. """ def _fill_text(self, text, width, indent): return ''.join([indent + line for line in text.splitlines(True)]) def _split_lines(self, text, width): lines = [''] for line in text.splitlines(): if line.startswith('>>>'): lines.append(line) lines.append('') else: lines[-1] += '\n' + line lines = filter(None, lines) texts = [] for line in lines: if line.startswith('>>>'): line = line[3:] if line: texts.append(line) else: texts.extend(super(FancyHelpFormatter, self)._split_lines(line, width)) return texts AP = ArgumentParser( usage=u"%(prog)s --query FILTER", description=__doc__, formatter_class=FancyHelpFormatter, ) AP.set_defaults(action='query') AP.add_argument('-Q', '--query-action', action='store_const', dest='action', const='query', help=u"Interroger la base de données. C'est l'option par défaut") AP.add_argument('-N', '--nop-action', action='store_const', dest='action', const='nop', help=u"Ne rien faire. Utile pour vérifier si le fichier ne contient pas d'erreur de syntaxe.") AP.add_argument('-P', '--dump-action', action='store_const', dest='action', const='dump', help=u"Afficher le contenu de la base de données.") AP.add_argument('-c', '--config', dest='confname', help=u"Spécifier le nom de la configuration à utiliser. Par défaut, utiliser le nom générique deploy.") AP.add_argument('-q', '--query-type', dest='query_type', help=u"""\ Spécifier le type d'information à afficher et donc le type d'information à chercher: >>> * 'object' pour des informations sur les objets. le filtre est la valeur de l'objet ou d'un de ses attributs. >>> * 'source' pour des informations sur les objets sources d'un lien. le filtre est la valeur d'un objet destination ou de l'un de ses attributs. >>> * 'dest' pour des informations sur les objets destinations d'un lien. le filtre est la valeur d'un objet source et de l'un de ses attributs. >>> * 'link' pour des informations sur un lien. >>> La valeur par défaut est '-q dest -j host'""") AP.add_argument('-j', '--object-type', dest='object_type', help=u"Spécifier le type d'objet à chercher pour -q object|source|dest") AP.add_argument('-t', '--link-type', dest='link_type', help=u"Spécifier le type de lien à chercher pour -q source|dest|link") AP.add_argument('-p', '--profile', dest='profile', help=u"Spécifier le profil de déploiement pour -q source|dest|link. Utiliser une valeur vide pour ne sélectionner que les liens sans profils. Ne pas spécifier l'option pour afficher tous les profils définis.") AP.add_argument('-F', '--format', dest='format', help=u"""\ Spécifier le format pour la sortie: >>> * 'shell' affiche les définitions de variables >>> * 'lines' affiches les valeurs à raison d'une par lignes. les attributs ne sont pas affichés. >>> * 'spaces', 'path' et 'comma' affichent les valeurs séparées respectivement par un espace ' ', deux point ':' et une virgule ','. les attributs ne sont pas affichés. >>> La valeur par défaut est shell""") AP.add_argument('-v', '--include-vars', action='append', dest='vars', metavar='VARS...', help=u"Spécifier les variables qui doivent être affichées sous forme de chaine glob e.g 'prefix_*' pour toutes les variables qui commencent par prefix. Plusieurs spécifications peuvent être données en les séparant par une virgule. Par défaut, toutes les variables sont affichées.") AP.add_argument('-o', '--object-vars', action='store_true', dest='object_vars', help=u"Afficher uniquement les variables associées aux objets.") AP.add_argument('-l', '--link-vars', action='store_true', dest='link_vars', help=u"Afficher uniquement les variables associées aux liens.") AP.add_argument('--of', '--object-func', dest='object_func', metavar='FUNC', help=u"Avec le format shell, spécifier le nom d'une fonction à afficher après les variables associées aux objets.") AP.add_argument('--lf', '--link-func', dest='link_func', metavar='FUNC', help=u"Avec le format shell, spécifier le nom d'une fonction à afficher après les variables associées aux liens.") AP.add_argument('args', nargs='*') o = AP.parse_args() run_qdd(o)