Import users with csv is not working

What type of hardware are you using: Raspberry Pi 3, 4+
What YunoHost version are you running: 12.0.17
How are you able to access your server: The webadmin
SSH
Direct access via physical keyboard/screen
Are you in a special context or did you perform specific tweaking on your YunoHost instance ?: in the webadmin, using the csv import feature for users

Describe your issue

in the webadmin after uploading a simple csv formatted like et exported one, with only 3 entries (like titi, tata, toto), i get an 500 error:

Erreur: "500"

Action: "POST" /yunohost/api/users/import

Message d'erreur :
Erreur serveur inattendue

Share relevant logs or error messages

No direct option to share via yunopaste so here is the log

Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/moulinette/interfaces/api.py", line 498, in process
    ret = self.actionsmap.process(arguments, timeout=30, route=_route)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3/dist-packages/moulinette/actionsmap.py", line 517, in process
    arguments = vars(self.parser.parse_args(args, **kwargs))
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3/dist-packages/moulinette/interfaces/api.py", line 712, in parse_args
    ret = parser.parse_args(args, ret)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3/dist-packages/moulinette/interfaces/api.py", line 251, in parse_args
    return self._parser.parse_args(arg_strings, namespace)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/argparse.py", line 1874, in parse_args
    args, argv = self.parse_known_args(args, namespace)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/argparse.py", line 1907, in parse_known_args
    namespace, args = self._parse_known_args(args, namespace)
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/argparse.py", line 2122, in _parse_known_args
    stop_index = consume_positionals(start_index)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/argparse.py", line 2078, in consume_positionals
    take_action(action, args)
  File "/usr/lib/python3.11/argparse.py", line 1967, in take_action
    argument_values = self._get_values(action, argument_strings)
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3/dist-packages/moulinette/interfaces/__init__.py", line 285, in _get_values
    value = super(ExtendedArgumentParser, self)._get_values(action, arg_strings)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/argparse.py", line 2501, in _get_values
    value = self._get_value(action, arg_string)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/argparse.py", line 2534, in _get_value
    result = type_func(arg_string)
             ^^^^^^^^^^^^^^^^^^^^^
OSError: [Errno 36] File name too long: 'dXNlcm5hbWU7Zmlyc3RuYW1lO2xhc3RuYW1lO3Bhc3N3b3JkO21haWw7bWFpbC1hbGlhczttYWlsLWZvcndhcmQ7bWFpbGJveC1xdW90YTtncm91cHMKdG90bzt0b3RvO3RvdG87cXdxV0lxd2Vyd3FAZHNkYWZFZHNmM3JzZCZyd2Vyd2Vyd2VyMTM7dG90b0BtcmZsb3MucHc7OzswOwp0YXRhO3RhdGE7dGF0YTtxd3FXSXF3ZXJ3cUBkc2RhZkVkc2YzcnNkJnJ3ZXJ3ZXJ3ZXIxNDt0YXRhQG1yZmxvcy5wdzs7OzA7CnRpdGk7dGl0aTt0aXRpO3F3cVdJcXdlcndxQGRzZGFmRWRzZjNyc2QmcndlcndlcndlcjE1O3RpdGlAbXJmbG9zLnB3Ozs7MDsK'

Salut @mrflos

Le problÚme est connu mais pas encore résolu visiblement.

1 Like

Par contre, l’import via cli fonctionne (il faut penser Ă  sĂ©parer prĂ©nom et nom par un point de concatĂ©nation pour que l’adresse mail soit acceptĂ©e toutefois).

J’ai creusĂ© ce problĂšme d’import d’utilisateurs par fichier csv via la webadmin. Comme d’autres, je rencontre cette erreur sur trois de mes instances :
"OSError: [Errno 36] File name too long: "dXN..."
Le contenu du fichier encodĂ© en base64 est passĂ© comme nom du fichier, ce qui provoque l’erreur.

J’ai cherchĂ© (en toute honnĂȘtetĂ© avec l’aide de l’IA) une solution Ă  ce problĂšme. Voici ce Ă  quoi nous sommes arrivĂ©s.
@Aleks , pourrais-tu y jeter un coup d’Ɠil et dire si ces modifications sont viables ?

Elles ont permis de restaurer la fonction d’import d’utilisateurs par csv sur :

  • deux instances 12.0.17
  • une instance 12.1.6.1 testing en fonction
  • une instance 12.1.6.1 testing en dev
    NB: Sur 12.0.17, il y a un lĂ©ger bug lors de la crĂ©ation des utilisateurs dans le ldap si leur adresse mail est sur le modĂšle prenomnom@domain.tld : un message d’erreur s’affiche (mail soi disant vide: ‘info’: “object class ‘mailAccount’ requires attribute ‘mail’”}) mais le compte est quand mĂȘme créé. Pour Ă©viter l’affichage de cette erreur, il faut crĂ©er une adresse sur le modĂšle prenom.nom@domain.local. Une magie grise la transforme en prenomnom@domain.tld et ldap ne ronchonne pas.
    Ce bug n’est plus prĂ©sent sur 12.1.6.1.

Je n’ai pas de recul suffisant pour juger de potentielles consĂ©quences non dĂ©sirĂ©es ou d’effets de bord (quelques tests et quelques heures de mise en service).

D’avance, merci pour ton aide.


Modifications effectuées

  1. dans l’actionsmap, changement du type de l’argument csvfile pour faire passer le contenu du csv en chaüne de caractùres.

     ### user_import()
     import:
         action_help: Import several users from CSV
         api: POST /users/import
         arguments:
             csvfile:
                 help: "CSV file with columns username, firstname, lastname, password, mail, mailbox-quota, mail-alias, mail-forward, groups (separated by coma)"
                 type: str
    
  2. Dans user.py, ajouts

a) en-tĂȘte :

import base64
from io import StringIO

b) dans le bloc de la fonction user_import, entre

    existing_domains = domain_list()["domains"]

et

    reader = csv.DictReader(csvfile, delimiter=";", quotechar='"')

ajout de :

    if isinstance(csvfile, str):
        if os.path.exists(csvfile): # pour l'import via CLI dans le cas oĂč le fichier est stockĂ© sur le serveur et a un chemin.
            with open(csvfile, "r", encoding="utf-8") as f:
                csvfile = StringIO(f.read()) # ouverture, stockage, levture
            logger.info("Fichier CSV chargé depuis un chemin local.")
        else: # dans le cas oĂč le fichier n'est pas sur le serveur, on suppose qu'il est en base64 car importĂ© par l'interface web
            try:
                decoded = base64.b64decode(csvfile, validate=True).decode("utf-8") # decodage
                csvfile = StringIO(decoded) # stockage
                logger.info("Fichier CSV décodé depuis base64.")
            except Exception: # si échec
                csvfile = StringIO(csvfile)
    else:
        csvfile = StringIO(csvfile.read()) # stockage final avant lecture et traitement

c) Si le script est laissĂ© ainsi, une mise Ă  jour des utilisateurs par fichier csv Ă©crase les mots de passe existants, mĂȘme si la valeur de la colonne password du csv est laissĂ©e vide. Pour Ă©viter cela et faire en sorte que le mot de passe ne soit pas mis Ă  jour si la valeur password n’est pas dĂ©finie, remplacement dans def _import_update de

    user_update(
        new_infos["username"],
        fullname=(new_infos["firstname"] + " " + new_infos["lastname"]).strip(),
        change_password=new_infos["password"],
        mailbox_quota=new_infos["mailbox-quota"],
        mail=new_infos["mail"],
        add_mailalias=new_infos["mail-alias"],
        remove_mailalias=remove_alias,
        remove_mailforward=remove_forward,
        add_mailforward=new_infos["mail-forward"],
        from_import=True,
    )

par

    update_kwargs = {
        "fullname": (new_infos["firstname"] + " " + new_infos["lastname"]).strip(),
        "mailbox_quota": new_infos["mailbox-quota"],
        "mail": new_infos["mail"],
        "add_mailalias": new_infos["mail-alias"],
        "remove_mailalias": remove_alias,
        "remove_mailforward": remove_forward,
        "add_mailforward": new_infos["mail-forward"],
        "from_import": True,
    }

    # On ne change le mot de passe que s'il est défini dans le csv
    if new_infos.get("password"):
        update_kwargs["change_password"] = new_infos["password"]

    user_update(new_infos["username"], **update_kwargs)

hmmmm ça marche peut-ĂȘtre mais Ă  mon avis ça pĂšte la CLI
 normalement il y avait de la magie qui faisait qu’en CLI on pouvait passer un nom de fichier (comme tu l’as dit ça marche en l’état) et faisait ce qu’il faut pour que quand on recoit depuis l’API ça se comporte comme un fichier


 perso j’ai pas vraiment le temps de regarder j’ai dĂ©jĂ  beaucoup trop de trucs sur le feu, mais ça tourne autour de ce bloc ici :

Peut-ĂȘtre qu’un truc a changĂ© entre la v11 et v12 cĂŽtĂ© webadmin et que le format d’info reçues par l’API Yunohost n’est plus exactement le mĂȘme (si ça se trouve ce n’est plus un type “FileUpload”

Merci pour la réponse.

L’import d’un csv stockĂ© sur le serveur via la CLI reste bien opĂ©rationnel.

Sur un YNH 11 de test, l’import ne fonctionne pas non plus mais le message d’erreur webadmin est moins disert (“erreur interne”).

Bah, écoute, tant pis. Je vais laisser fonctionner avec cette modification, je verrai bien