[keep-alive] feature connexion unique au serveur

C'était plus pratique que de faire un merge de la branche devel-keep-alive.
La feature n'est pas activée automatiquement pour les gens qui ont déjà
un fichier de config, mais elle est présente pour ceux qui en crée un.
This commit is contained in:
Daniel STAN 2013-08-04 13:33:59 +02:00
parent 6172898d24
commit d220fa370f
3 changed files with 156 additions and 66 deletions

View file

@ -239,7 +239,12 @@ class simple_memoize(object):
mais il faudra s'en préoccuper si un jour on veut changer le comportement.""" mais il faudra s'en préoccuper si un jour on veut changer le comportement."""
if self.val == None: if self.val == None:
self.val = self.f(*args, **kwargs) self.val = self.f(*args, **kwargs)
return copy.deepcopy(self.val) # On évite de tout deepcopier. Typiquement, un subprocess.Popen
# ne devrait pas l'être (comme dans get_keep_alive_connection)
if type(self.val) in [dict, list]:
return copy.deepcopy(self.val)
else:
return self.val
###### ######
@ -258,6 +263,10 @@ def remote_proc(options, command, arg=None):
full_command.append(command) full_command.append(command)
if arg: if arg:
full_command.append(arg) full_command.append(arg)
if options.verbose and not options.quiet:
print("Running command %s ..." % " ".join(full_command))
proc = subprocess.Popen(full_command, proc = subprocess.Popen(full_command,
stdin = subprocess.PIPE, stdin = subprocess.PIPE,
stdout = subprocess.PIPE, stdout = subprocess.PIPE,
@ -265,34 +274,67 @@ def remote_proc(options, command, arg=None):
close_fds = True) close_fds = True)
return proc return proc
@simple_memoize
def get_keep_alive_connection(options):
"""Fabrique un process parlant avec le serveur suivant la commande
'keep-alive'. On utilise une fonction séparée pour cela afin
de memoizer le résultat, et ainsi utiliser une seule connexion"""
proc = remote_proc(options, 'keep-alive', None)
atexit.register(proc.stdin.close)
return proc
def remote_command(options, command, arg=None, stdin_contents=None): def remote_command(options, command, arg=None, stdin_contents=None):
"""Exécute la commande distante, et retourne la sortie de cette """Exécute la commande distante, et retourne la sortie de cette
commande""" commande"""
detail = options.verbose and not options.quiet detail = options.verbose and not options.quiet
keep_alive = options.serverdata.get('keep-alive', False)
proc = remote_proc(options, command, arg) if keep_alive:
if stdin_contents is not None: conn = get_keep_alive_connection(options)
proc.stdin.write(json.dumps(stdin_contents)) args = filter(None, [arg, stdin_contents])
proc.stdin.close() msg = {u'action': unicode(command), u'args': args }
ret = proc.wait() conn.stdin.write('%s\n' % json.dumps(msg))
raw_out = proc.stdout.read() conn.stdin.flush()
if ret != 0: raw_out = conn.stdout.readline()
if not options.quiet: else:
print((u"Mauvais code retour côté serveur, voir erreur " + proc = remote_proc(options, command, arg)
u"ci-dessus").encode('utf-8'), if stdin_contents is not None:
file=sys.stderr) proc.stdin.write(json.dumps(stdin_contents))
if options.verbose: proc.stdin.close()
print("raw_output: %s" % raw_out) ret = proc.wait()
sys.exit(ret) raw_out = proc.stdout.read()
if ret != 0:
if not options.quiet:
print((u"Mauvais code retour côté serveur, voir erreur " +
u"ci-dessus").encode('utf-8'),
file=sys.stderr)
if detail:
print("raw_output: %s" % raw_out)
sys.exit(ret)
try: try:
return json.loads(raw_out) answer = json.loads(raw_out.strip())
except ValueError: except ValueError:
if not options.quiet: if not options.quiet:
print(u"Impossible de parser le résultat".encode('utf-8'), print(u"Impossible de parser le résultat".encode('utf-8'),
file=sys.stderr) file=sys.stderr)
if options.verbose: if detail:
print("raw_output: %s" % raw_out) print("raw_output: %s" % raw_out)
sys.exit(42) sys.exit(42)
if not keep_alive:
return answer
else:
try:
if answer[u'status'] != u'ok':
raise KeyError('Bad answer status')
return answer[u'content']
except KeyError:
if not options.quiet:
print(u"Réponse erronée du serveur".encode('utf-8'),
file=sys.stderr)
if detail:
print("answer: %s" % repr(answer))
sys.exit(-1)
@simple_memoize @simple_memoize
def all_keys(options): def all_keys(options):

View file

@ -30,6 +30,7 @@ servers = {
# Utile pour tester # Utile pour tester
'localhost': { 'localhost': {
'server_cmd': [ssh_path, 'localhost'] + distant_cmd, 'server_cmd': [ssh_path, 'localhost'] + distant_cmd,
'keep-alive': True, # <-- experimental, n'ouvre qu'une connexion
}, },
'ovh': { 'ovh': {
'server_cmd': [ssh_path, 'ovh.crans.org'] + distant_cmd, 'server_cmd': [ssh_path, 'ovh.crans.org'] + distant_cmd,

143
server.py
View file

@ -25,6 +25,8 @@ MYUID = pwd.getpwuid(os.getuid())[0]
if MYUID == 'root': if MYUID == 'root':
MYUID = os.environ['SUDO_USER'] MYUID = os.environ['SUDO_USER']
## Fonctions internes au serveur
def validate(roles, mode='r'): def validate(roles, mode='r'):
"""Vérifie que l'appelant appartient bien aux roles précisés """Vérifie que l'appelant appartient bien aux roles précisés
Si mode mode='w', recherche un rôle en écriture Si mode mode='w', recherche un rôle en écriture
@ -47,6 +49,73 @@ def writefile(filename, contents):
f.write(contents.encode("utf-8")) f.write(contents.encode("utf-8"))
f.close() f.close()
class server_command(object):
"""
Une instance est un décorateur pour la fonction servant de commande
externe du même nom"""
#: nom de la commande
name = None
#: fonction wrappée
decorated = None
#: (static) dictionnaire name => fonction
by_name = {}
#: rajoute un argument en fin de fonction à partir de stdin (si standalone)
stdin_input = False
#: Est-ce que ceci a besoin d'écrire ?
write = False
def __init__(self, name, stdin_input = False, write=False):
"""
* ``name`` nom de l'action telle qu'appelée par le client
* ``stdin_input`` si True, stdin sera lu en mode non-keepalive, et
remplira le dernier argument de la commande.
* ``write`` s'agit-il d'une commande en écriture ?
"""
self.name = name
self.stdin_input = stdin_input
self.write = write
server_command.by_name[name] = self
def __call__(self, fun):
self.decorated = fun
return fun
## Fonction exposées par le serveur
@server_command('keep-alive')
def keepalive():
""" Commande permettant de réaliser un tunnel json (un datagramme par ligne)
Un message entre le client et le serveur consiste en l'échange de dico
Message du client: {'action': "nom_de_l'action",
'args': liste_arguments_passes_a_la_fonction}
Réponse du serveur: {'status': 'ok',
'content': retour_de_la_fonction,
}
"""
for line in iter(sys.stdin.readline, ''):
data = json.loads(line.rstrip())
try:
# Une action du protocole = de l'ascii
action = data['action'].encode('ascii')
content = server_command.by_name[action].decorated(*data['args'])
status = u'ok'
except Exception as e:
status = u'error'
content = repr(e)
out = {
'status': status,
'content': content,
}
print(json.dumps(out, encoding='utf-8'))
sys.stdout.flush()
@server_command('listroles')
def listroles(): def listroles():
"""Liste des roles existant et de leurs membres. """Liste des roles existant et de leurs membres.
Renvoie également un rôle particulier ``"whoami"``, contenant l'username de l'utilisateur qui s'est connecté.""" Renvoie également un rôle particulier ``"whoami"``, contenant l'username de l'utilisateur qui s'est connecté."""
@ -56,10 +125,12 @@ def listroles():
d["whoami"] = MYUID d["whoami"] = MYUID
return d return d
@server_command('listkeys')
def listkeys(): def listkeys():
"""Liste les usernames et les (mail, fingerprint) correspondants""" """Liste les usernames et les (mail, fingerprint) correspondants"""
return serverconfig.KEYS return serverconfig.KEYS
@server_command('listfiles')
def listfiles(): def listfiles():
"""Liste les fichiers dans l'espace de stockage, et les roles qui peuvent y accéder""" """Liste les fichiers dans l'espace de stockage, et les roles qui peuvent y accéder"""
os.chdir(serverconfig.STORE) os.chdir(serverconfig.STORE)
@ -71,6 +142,7 @@ def listfiles():
files[filename[:-5]] = file_dict["roles"] files[filename[:-5]] = file_dict["roles"]
return files return files
@server_command('getfile')
def getfile(filename): def getfile(filename):
"""Récupère le fichier ``filename``""" """Récupère le fichier ``filename``"""
filepath = getpath(filename) filepath = getpath(filename)
@ -83,13 +155,12 @@ def getfile(filename):
except IOError: except IOError:
return [False, u"Le fichier %s n'existe pas." % filename] return [False, u"Le fichier %s n'existe pas." % filename]
@server_command('getfiles', stdin_input=True)
def getfiles(): def getfiles(filenames):
"""Récupère plusieurs fichiers, lit la liste des filenames demandés sur stdin""" """Récupère plusieurs fichiers, lit la liste des filenames demandés sur stdin"""
stdin = sys.stdin.read()
filenames = json.loads(stdin)
return [getfile(f) for f in filenames] return [getfile(f) for f in filenames]
# TODO ça n'a rien à faire là, à placer plus haut dans le code
def _putfile(filename, roles, contents): def _putfile(filename, roles, contents):
"""Écrit ``contents`` avec les roles ``roles`` dans le fichier ``filename``""" """Écrit ``contents`` avec les roles ``roles`` dans le fichier ``filename``"""
gotit, old = getfile(filename) gotit, old = getfile(filename)
@ -109,10 +180,9 @@ def _putfile(filename, roles, contents):
writefile(filepath, json.dumps({'roles': roles, 'contents': contents})) writefile(filepath, json.dumps({'roles': roles, 'contents': contents}))
return [True, u"Modification effectuée."] return [True, u"Modification effectuée."]
def putfile(filename): @server_command('putfile', stdin_input=True, write=True)
def putfile(filename, parsed_stdin):
"""Écrit le fichier ``filename`` avec les données reçues sur stdin.""" """Écrit le fichier ``filename`` avec les données reçues sur stdin."""
stdin = sys.stdin.read()
parsed_stdin = json.loads(stdin)
try: try:
roles = parsed_stdin['roles'] roles = parsed_stdin['roles']
contents = parsed_stdin['contents'] contents = parsed_stdin['contents']
@ -120,10 +190,9 @@ def putfile(filename):
return [False, u"Entrée invalide"] return [False, u"Entrée invalide"]
return _putfile(filename, roles, contents) return _putfile(filename, roles, contents)
def putfiles(): @server_command('putfiles', stdin_input=True, write=True)
def putfiles(parsed_stdin):
"""Écrit plusieurs fichiers. Lit les filenames sur l'entrée standard avec le reste.""" """Écrit plusieurs fichiers. Lit les filenames sur l'entrée standard avec le reste."""
stdin = sys.stdin.read()
parsed_stdin = json.loads(stdin)
results = [] results = []
for fichier in parsed_stdin: for fichier in parsed_stdin:
try: try:
@ -136,7 +205,7 @@ def putfiles():
results.append(_putfile(filename, roles, contents)) results.append(_putfile(filename, roles, contents))
return results return results
@server_command('rmfile', write=True)
def rmfile(filename): def rmfile(filename):
"""Supprime le fichier filename après avoir vérifié les droits sur le fichier""" """Supprime le fichier filename après avoir vérifié les droits sur le fichier"""
gotit, old = getfile(filename) gotit, old = getfile(filename)
@ -153,6 +222,7 @@ def rmfile(filename):
return u"Suppression effectuée" return u"Suppression effectuée"
# TODO monter plus haut
def backup(corps, fname, old): def backup(corps, fname, old):
"""Backupe l'ancienne version du fichier""" """Backupe l'ancienne version du fichier"""
os.umask(0077) os.umask(0077)
@ -162,6 +232,7 @@ def backup(corps, fname, old):
back.write((u'* %s: %s\n' % (str(datetime.datetime.now()), corps)).encode("utf-8")) back.write((u'* %s: %s\n' % (str(datetime.datetime.now()), corps)).encode("utf-8"))
back.close() back.close()
# TODO monter plus haut
def notification(subject, corps, fname, old): def notification(subject, corps, fname, old):
"""Envoie par mail une notification de changement de fichier""" """Envoie par mail une notification de changement de fichier"""
conn = smtplib.SMTP('localhost') conn = smtplib.SMTP('localhost')
@ -180,43 +251,19 @@ def notification(subject, corps, fname, old):
conn.sendmail(frommail, tomail, msg.as_string()) conn.sendmail(frommail, tomail, msg.as_string())
conn.quit() conn.quit()
WRITE_COMMANDS = ["putfile", "rmfile"]
if __name__ == "__main__": if __name__ == "__main__":
argv = sys.argv[1:] argv = sys.argv[0:]
if len(argv) not in [1, 2]: command_name = argv[1]
sys.exit(1)
command = argv[0]
if serverconfig.READONLY and command in WRITE_COMMANDS:
raise IOError("Ce serveur est read-only.")
filename = None
try:
filename = argv[1]
except IndexError:
pass
answer = None command = server_command.by_name[command_name]
if command == "listroles": if serverconfig.READONLY and command.write:
answer = listroles() raise IOError("Ce serveur est read-only.")
elif command == "listkeys":
answer = listkeys() args = argv[2:]
elif command == "listfiles": # On veut des unicode partout
answer = listfiles() args = [ s.decode('utf-8') for s in args ]
elif command == "getfiles": if command.stdin_input:
answer = getfiles() args.append(json.loads(sys.stdin.read()))
elif command == "putfiles": answer = command.decorated(*args)
answer = putfiles() if answer is not None:
else:
if not filename:
print("filename nécessaire pour cette opération", file=sys.stderr)
sys.exit(1)
if command == "getfile":
answer = getfile(filename)
elif command == "putfile":
answer = putfile(filename)
elif command == "rmfile":
answer = rmfile(filename)
else:
sys.exit(1)
if not answer is None:
print(json.dumps(answer)) print(json.dumps(answer))