[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:
parent
6172898d24
commit
d220fa370f
3 changed files with 156 additions and 66 deletions
76
client.py
76
client.py
|
@ -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):
|
||||||
|
|
|
@ -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
143
server.py
|
@ -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))
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue