implémentation initiale: host, group, attr
This commit is contained in:
parent
0b595837ba
commit
f3c028005a
|
@ -0,0 +1,489 @@
|
|||
#!/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'
|
||||
|
||||
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|(?<!\\)['"]''')
|
||||
def parse_string(self):
|
||||
mo = self.RE_EOS.search(self._line)
|
||||
if mo is not None:
|
||||
lexem = self._line[0:mo.start(0)]
|
||||
self._line = self._line[mo.start(0):]
|
||||
else:
|
||||
lexem = self._line
|
||||
self._line = ''
|
||||
lexem = lexem.replace('\\"', '"')
|
||||
lexem = lexem.replace("\\'", "'")
|
||||
lexem = lexem.replace('\\\\', '\\')
|
||||
return lexem
|
||||
|
||||
def parse(self):
|
||||
if self.lexems is not None: return self.lexems
|
||||
|
||||
lexems = self.lexems = []
|
||||
self._inf = open(self.file, 'rb')
|
||||
self._lcount = 0
|
||||
self._line = ''
|
||||
try:
|
||||
SOL = False
|
||||
while True:
|
||||
# Ignorer lignes vides et commentaires
|
||||
self.parse_ws()
|
||||
stop = False
|
||||
while self.is_empty():
|
||||
if SOL:
|
||||
lexems.append(EOL)
|
||||
SOL = False
|
||||
if self.next_line() is None:
|
||||
stop = True
|
||||
break
|
||||
self.parse_ws()
|
||||
if stop: break
|
||||
SOL = True
|
||||
# Construire une chaine
|
||||
lexem = ''
|
||||
while True:
|
||||
if self.is_empty(): break
|
||||
elif self.isa_space(): break
|
||||
if self.isa_squote(): lexem += self.parse_sstring()
|
||||
elif self.isa_dquote(): lexem += self.parse_dstring()
|
||||
else: lexem += self.parse_string()
|
||||
lexems.append(lexem)
|
||||
lexems.append(EOF)
|
||||
return lexems
|
||||
finally:
|
||||
self._inf.close()
|
||||
self._inf = None
|
||||
return lexems
|
||||
|
||||
def get_predicates(self):
|
||||
predicates = []
|
||||
predicate = []
|
||||
for lexem in self.parse():
|
||||
if lexem is EOF: break
|
||||
elif lexem is EOL:
|
||||
predicates.append(predicate)
|
||||
predicate = []
|
||||
else:
|
||||
predicate.append(lexem)
|
||||
return predicates
|
||||
|
||||
class HostObject(object):
|
||||
id = None
|
||||
dir = None
|
||||
hosts = None
|
||||
attrs = None
|
||||
|
||||
def __init__(self, id):
|
||||
self.id = id
|
||||
self.hosts = []
|
||||
self.attrs = {}
|
||||
def get_id(self): return self.id
|
||||
def get_dir(self): return self.dir
|
||||
def set_dir(self, dir): self.dir = dir
|
||||
def get_hosts(self): return self.hosts
|
||||
def add_host(self, host):
|
||||
if host not in self.hosts: self.hosts.append(host)
|
||||
def get_attrs(self): return self.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] = []
|
||||
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 copy_attrs(self, other, reset=True):
|
||||
for name, values in other.attrs.items():
|
||||
for value in values:
|
||||
if reset: self.reset_attr(name, value)
|
||||
else: self.set_attr(name, value)
|
||||
|
||||
def split_scalar(arg):
|
||||
if '=' in arg:
|
||||
name, value = arg.split('=', 1)
|
||||
else:
|
||||
name = arg
|
||||
value = None
|
||||
if name.endswith('%'):
|
||||
name = name[:-1]
|
||||
reset = True
|
||||
else:
|
||||
reset = False
|
||||
return name, reset, value
|
||||
|
||||
def split_list(arg):
|
||||
if '=' in arg:
|
||||
name, values = arg.split('=', 1)
|
||||
values = values.split(',')
|
||||
else:
|
||||
name = None
|
||||
values = arg.split(',')
|
||||
return name, values
|
||||
|
||||
class Parser(object):
|
||||
lexer = None
|
||||
predicates = None
|
||||
|
||||
known_profiles = None
|
||||
default_profile = None
|
||||
default_domain = None
|
||||
|
||||
objects = None
|
||||
groups = None
|
||||
attr_otype = None
|
||||
|
||||
def __init__(self, lexer, parse=True):
|
||||
self.lexer = lexer
|
||||
self.objects = {}
|
||||
self.groups = {}
|
||||
self.__setup_hosts()
|
||||
if parse: self.parse()
|
||||
|
||||
def parse(self):
|
||||
self.predicates = self.lexer.get_predicates()
|
||||
for p in self.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(args)
|
||||
|
||||
def get_objects(self, otype):
|
||||
objects = self.objects[otype]
|
||||
return [objects[id] for id in objects if id is not None]
|
||||
|
||||
############################################################################
|
||||
def reset_group(self, otype):
|
||||
self.groups[otype]['current'] = {}
|
||||
def reset_group_maybe(self, otype):
|
||||
if self.groups[otype]['type'] in ('defaults', 'once'):
|
||||
self.reset_group(otype)
|
||||
self.groups[otype]['type'] = 'once'
|
||||
def add_group(self, otype, id):
|
||||
self.groups[otype]['current'][id] = True
|
||||
|
||||
def handle_group(self, 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'
|
||||
"""
|
||||
otype = args[0:1] and args[0] or 'host'
|
||||
gtype = args[1:2] and args[1] or 'until'
|
||||
self.groups[otype]['type'] = gtype
|
||||
if gtype == 'defaults':
|
||||
self.groups[otype]['current'] = {None: True}
|
||||
elif gtype in ('once', 'until'):
|
||||
self.groups[otype]['current'] = {}
|
||||
|
||||
############################################################################
|
||||
def handle_attr(self, args):
|
||||
otype = self.attr_otype
|
||||
assert otype is not None, "attr_otype should not be None"
|
||||
for nv in args:
|
||||
name, reset, value = split_scalar(nv)
|
||||
if value is None:
|
||||
reset = True
|
||||
value = '1'
|
||||
for id in self.groups[otype]['current']:
|
||||
if reset: self.objects[otype][id].reset_attr(name)
|
||||
self.objects[otype][id].set_attr(name, value)
|
||||
|
||||
############################################################################
|
||||
def handle_default_profile(self, args):
|
||||
if not args or not args[0]:
|
||||
profile = None
|
||||
else:
|
||||
profile = args[0]
|
||||
if self.known_profiles is None: self.known_profiles = {}
|
||||
self.known_profiles[profile] = True
|
||||
self.default_profile = profile
|
||||
|
||||
############################################################################
|
||||
def handle_default_domain(self, args):
|
||||
if not args or not args[0]:
|
||||
domain = None
|
||||
else:
|
||||
domain = args[0]
|
||||
self.default_domain = domain
|
||||
|
||||
############################################################################
|
||||
def __setup_hosts(self):
|
||||
self.objects['host'] = {}
|
||||
self.__host_object(None)
|
||||
self.groups['host'] = {}
|
||||
self.handle_group(['host', 'defaults'])
|
||||
self.attr_otype = 'host'
|
||||
def __fix_host(self, host):
|
||||
if host.endswith('.'):
|
||||
host = host[:-1]
|
||||
elif '.' not in host and self.default_domain:
|
||||
host = '%s.%s' % (host, self.default_domain)
|
||||
return host
|
||||
def __hostid(self, host):
|
||||
host = re.sub(r'\..*', '', host)
|
||||
host = re.sub(r'[^a-zA-Z0-9]', '_', host)
|
||||
host = host.lower()
|
||||
return host
|
||||
def __hostib(self, id):
|
||||
if re.match(r'\d+$', id): return id
|
||||
ib = re.sub(r'^(.*?)\d+$', r'\1', id)
|
||||
return ib
|
||||
def __host_object(self, id):
|
||||
ho = self.objects['host'].get(id, None)
|
||||
if ho is None:
|
||||
ho = HostObject(id)
|
||||
self.objects['host'][id] = ho
|
||||
return ho
|
||||
|
||||
def handle_host(self, args):
|
||||
"""host [id=]host,...
|
||||
"""
|
||||
AP = ArgumentParser()
|
||||
AP.add_argument('-b', '--basedir', action='append', dest='basedirs')
|
||||
AP.add_argument('-d', '--dirspec', action='append', dest='dirspecs')
|
||||
AP.add_argument('nvss', nargs=REMAINDER)
|
||||
o = AP.parse_args(args)
|
||||
# construire la liste des hôtes à traiter
|
||||
nvss = []
|
||||
if o.basedirs is not None:
|
||||
for basedir in [path.expanduser(basedir) for basedir in o.basedirs]:
|
||||
nvss.extend([path.abspath(path.join(basedir, name))
|
||||
for name in os.listdir(basedir)
|
||||
if path.isdir(path.join(basedir, name))])
|
||||
if o.dirspecs is not None:
|
||||
for dirspec in [path.expanduser(dirspec) for dirspec in o.dirspecs]:
|
||||
nvss.extend([path.abspath(dir)
|
||||
for dir in glob(dirspec)
|
||||
if path.isdir(dir)])
|
||||
nvss.extend(o.nvss)
|
||||
# préparer la mise à jour du groupe courant
|
||||
self.reset_group_maybe('host')
|
||||
self.attr_otype = 'host'
|
||||
default_ho = self.__host_object(None)
|
||||
# traiter les hôtes
|
||||
for nvs in nvss:
|
||||
name, values = split_list(nvs)
|
||||
for host in values:
|
||||
if '/' in host:
|
||||
dir, host = path.split(path.abspath(path.expanduser(host)))
|
||||
else:
|
||||
dir = None
|
||||
host = self.__fix_host(host)
|
||||
if name:
|
||||
ho = self.__host_object(name)
|
||||
ho.add_host(host)
|
||||
ho.copy_attrs(default_ho)
|
||||
if dir is not None: ho.reset_attr('dir', dir)
|
||||
self.add_group('host', name)
|
||||
else:
|
||||
id = self.__hostid(host)
|
||||
ho = self.__host_object(id)
|
||||
ho.add_host(host)
|
||||
ho.copy_attrs(default_ho)
|
||||
if dir is not None: ho.reset_attr('dir', dir)
|
||||
self.add_group('host', id)
|
||||
ib = self.__hostib(id)
|
||||
ho = self.__host_object(ib)
|
||||
ho.add_host(host)
|
||||
ho.copy_attrs(default_ho)
|
||||
if dir is not None: ho.reset_attr('dir', dir)
|
||||
self.add_group('host', ib)
|
||||
|
||||
def run_qdd(o):
|
||||
# fichier de configuration
|
||||
confname = o.confname or 'deploy'
|
||||
if '/' in confname:
|
||||
conf = path.abspath(confname)
|
||||
elif path.exists('%s.conf' % confname):
|
||||
conf = '%s.conf' % confname
|
||||
else:
|
||||
conf = confname
|
||||
if not conf.endswith('.conf'): conf = '%s.conf' % conf
|
||||
user_conf = path.join(path.expanduser(USER_CONFDIR), conf)
|
||||
system_conf = path.join(SYSTEM_CONFDIR, conf)
|
||||
if path.exists(user_conf): conf = user_conf
|
||||
elif path.exists(system_conf): conf = system_conf
|
||||
lexer = Lexer(conf)
|
||||
|
||||
if o.action == 'nop':
|
||||
pass
|
||||
elif o.action == 'dump': #XXX
|
||||
parser = Parser(lexer)
|
||||
print "=== predicates"
|
||||
for p in parser.predicates:
|
||||
print ' '.join(map(repr, p))
|
||||
print "=== hosts"
|
||||
for ho in parser.get_objects('host'):
|
||||
id, dir, hosts = ho.get_id(), ho.get_dir(), ho.get_hosts()
|
||||
attrs = ho.get_attrs()
|
||||
print id
|
||||
if dir: print " dir=%s" % dir
|
||||
print " hosts=%r" % hosts
|
||||
if attrs: print " attrs=%r" % attrs
|
||||
elif o.action == 'query':
|
||||
parser = Parser(lexer)
|
||||
|
||||
if __name__ == '__main__':
|
||||
from argparse import ArgumentParser, HelpFormatter
|
||||
if sys.argv[1:2] == ['--compat']:
|
||||
# Avec l'argument --compat, désactiver la classe FancyHelpFormatter qui
|
||||
# se base sur une API non documentée
|
||||
sys.argv = sys.argv[0:1] + sys.argv[2:]
|
||||
FancyHelpFormatter = HelpFormatter
|
||||
else:
|
||||
class FancyHelpFormatter(HelpFormatter):
|
||||
"""Comme HelpFormatter, mais ne touche pas aux lignes qui commencent par les
|
||||
caractères '>>>'. 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)
|
Loading…
Reference in New Issue