#!/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, REMAINDER from glob import glob USER_CONFDIR = '~/etc/deploy' SYSTEM_CONFDIR = '/var/local/deploy' ################################################################################ # Diverses fonctions def isseq(t): return isinstance(t, list) or isinstance(t, tuple) or isinstance(t, set) ################################################################################ # Base de données class UNDEFINED(object): __repr__ = __string__ = lambda self: 'UNDEFINED' UNDEFINED = UNDEFINED() class GenericObject(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 get_values(self): return 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): return self._attrs.get(name, None) def set_attr(self, name, value): 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): 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): if name not in self._attrs: self._attrs[name] = [] self._attrs[name].append(value) def del_attr(self, name, value): 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'): 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'): for name, values in other.attrs.items(): for value in values: self.modify_attr(name, value, method) 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=''): self._dump_id(indent) self._dump_attrs(indent) class HostObject(GenericObject): @staticmethod def genid(host): "MY-Host42.self --> 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" if re.match(r'\d+$', id): return id ib = re.sub(r'^(.*?)\d+$', r'\1', id) return ib def dump(self, indent=''): self._dump_id(indent) self._dump_values(indent, 'hosts') self._dump_attrs(indent) class ModuleObject(GenericObject): @staticmethod def genid(module): "MY-Module --> MY_Module" module = re.sub(r'[^a-zA-Z0-9]', '_', module) return module def dump(self, indent=''): self._dump_id(indent) self._dump_values(indent, 'modules') self._dump_attrs(indent) class WobundleObject(GenericObject): @staticmethod def genid(wobundle): "MY-App.woa --> MY_App" wobundle = re.sub(r'\.(woa|framework)$', '', wobundle) wobundle = re.sub(r'[^a-zA-Z0-9]', '_', wobundle) return wobundle def dump(self, indent=''): self._dump_id(indent) self._dump_values(indent, 'wobundles') self._dump_attrs(indent) class WebappObject(GenericObject): @staticmethod def genid(webapp): "MY-Webapp --> MY_Webapp" webapp = re.sub(r'[^a-zA-Z0-9]', '_', webapp) return webapp def dump(self, indent=''): self._dump_id(indent) self._dump_values(indent, 'webapps') self._dump_attrs(indent) class GenericLink(object): _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 get_profile(self): return self._profile def set_profile(self): 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): return self._attrs.get(name, None) def set_attr(self, name, value): 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): 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): if name not in self._attrs: self._attrs[name] = [] self._attrs[name].append(value) def del_attr(self, name, value): 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'): 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'): for name, values in other.attrs.items(): for value in values: self.modify_attr(name, value, method) def match_fos(self, fos, match='any'): 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'): 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'): for name, value in attrs.items(): values = self._attrs.get(name, None) if values is not None: if value in values: return True return False def _dump_profile(self, indent): profile = self.profile or 'ALL' print "%s profile: %s" % (indent, profile) def _dump_fos(self, indent): print "%s from: %s" % (indent, ' '.join(self.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=''): print "%slink" % indent self._dump_profile(indent) self._dump_fos(indent) self._dump_tos(indent) self._dump_attrs(indent) class UinstLink(GenericLink): 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): def __init__(self): super(RuinstLink, self).__init__('module', 'host') def dump(self, indent=''): print "%suinst" % indent self._dump_fos(indent) self._dump_tos(indent) self._dump_attrs(indent) class Database(object): _default_profile = None _default_domain = None _objects = None _objects_classes = None _links = None _links_classes = None def __init__(self): self._objects = {} self._objects_classes = {} self._links = {} self._links_classes = {} def has_default_profile(self): return self._default_profile is not None def get_default_profile(self): return self._default_profile def set_default_profile(self, profile): self._default_profile = profile or None default_profile = property(get_default_profile, set_default_profile) def has_default_domain(self): return self._default_domain is not None def get_default_domain(self): return self._default_domain def set_default_domain(self, domain): 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): if not self._objects.has_key(otype): self._objects[otype] = {} if oclass is None: oclass = GenericObject self._objects_classes[otype] = oclass def get_known_objects(self): return self._objects.keys() known_objects = property(get_known_objects) def get_objects(self, otype): objects = self._objects.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): objects = self._objects.get(otype, None) if objects is None: return False return objects.has_key(id) def get_object(self, otype, id): objects = self._objects.get(otype, None) if objects is None: return None object = objects.get(id, None) if object is None: object_class = self._objects_classes.get(otype, None) if object_class is not None: object = object_class(id) self._objects[otype][id] = object return object def register_link(self, ltype, lclass): if not self._links.has_key(ltype): self._links[ltype] = {} self._links_classes[ltype] = lclass def get_known_profiles(self, ltype): return self._links[ltype].keys() def get_links(self, ltype, profile, fo=UNDEFINED, to=UNDEFINED, attrs=UNDEFINED, create=False): if fo is UNDEFINED and to is UNDEFINED and attrs is UNDEFINED: raise ValueError("you must set either fo, to ou attrs") plinks = self._links.get(ltype, None) if plinks is None: return None links = plinks.get(profile, None) if links is None: links = plinks[profile] = [] found = None for link in links: if (fo is UNDEFINED or link.match_fos(fo)) \ and (to is UNDEFINED or link.match_tos(to)) \ and (attrs is UNDEFINED or link.match_attrs(attrs)): if found is None: found = [] found.append(link) if found is not None: return found if not create: return None link = self._links_classes[ltype]() if fo is not UNDEFINED: if isseq(fo): fos = fo else: fos = [fo] for fo in fos: link.add_fo(fo) if to is not UNDEFINED: if isseq(to): tos = to else: tos = [to] for to in tos: link.add_to(to) if attrs is not UNDEFINED: 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"") AP.add_argument('-j', '--object-type', dest='object_type', help=u"") AP.add_argument('-t', '--link-type', dest='link_type', help=u"") AP.add_argument('-p', '--profile', dest='profile', help=u"Spécifier le profil de déploiement") AP.add_argument('-F', '--format', dest='format', help=u"Spécifier le format pour la sortie. La valeur par défaut est shell.") AP.add_argument('-v', '--include-vars', dest='vars', metavar='VARS...', help=u"Spécifier les variables qui doivent être affichées. Par défaut, toutes les variables sont affichées.") AP.add_argument('-o', '--object-vars', action='store_true', dest='object_vars', help=u"Afficher les variables associées aux objets.") AP.add_argument('-d', '--dest-vars', action='store_true', dest='dest_vars', help=u"Afficher les variables associées aux hôtes destination.") AP.add_argument('-l', '--link-vars', action='store_true', dest='link_vars', help=u"Afficher 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('--df', '--dest-func', dest='dest_func', metavar='FUNC', help=u"Avec le format shell, spécifier le nom d'une fonction à afficher après les variables associées aux hôtes destination.") 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.") o = AP.parse_args() run_qdd(o)