Environment Variablen clonen


  • Mod

    Ich habe eine Datenbank, in der stehen allerlei Aufträge, die später von einem cronjob abgearbeitet werden. Das heißt in der Datenbank steht, wie und wann ein bestimmtes Programm aufgerufen wird und der cronjob überprüft regelmäßig, ob die Bedingungen erfüllt sind und handelt dann.

    Nun hatte ich schon oftmals das Problem, dass ein cronjob in einer ziemlich minimalen Umgebung startet und deswegen allerlei Kommandos fehlschlugen, weil irgendwelche Umgebungsvariablen nicht richtig gesetzt waren (PATH, LD_LIBRARY_PATH und viele mehr, an die man zuerst gar nicht denkt). Ich habe mittlerweile händisch eine Reihe typischer Variablen im crontab gesetzt, aber ich finde das keine gute Lösung. Zum einen muss ich voraussehen, welche Variablen wohl eine Rolle spielen können, zum anderen sind diese Werte dann fix, wenn sie erst einmal in dem Script stehen.

    Ich würde daher gerne das Environment, von wenn ich den Auftrag in die Datenbank schreibe, clonen. Gibt es da irgendeine besonders gute Methode? Derzeit wäre der Plan, in der Datenbank ein Feld hinzu zu fügen, in der ich den Inhalt von environ abspeichere, zu der Zeit zu der der Eintrag gemacht wird. Der cronjob führt da drauf dann eine Analyse aus, um die einzelnen Variablen und ihre Werte zu erhalten. Diese werden dann einzeln mit setenv gesetzt und dann das eigentlich gewünschte Kommando ausgeführt. Oder die Variablen werden in eine lange Liste gepackt und das Kommando wird dann mit einem entsprechenden env-Befehl gestartet. Letzteres ist wahrscheinlich besser.

    Insgesamt kommt mir mein Plan ziemlich umständlich vor. Gibt es da bessere Methoden, um mein Ziel zu erreichen?



  • wie wäre es, wenn in der Datenbank dann die Umgebungsvariables in einem Feld gespeichert werden, so: export PATH=...;LD_LIBRARY_PATH=...;...

    Du hast bestimmt ein wrapper script, der die Felder liest, dann kannst machen:

    ENV_VALS="`get_env_vars_from_database`"
    
    eval ${ENV_VALS}
    
    ...
    

    siehe

    $ cat a.sh 
    VARS="export X=1;Y=2"
    
    eval ${VARS}
    
    echo X=$X
    echo Y=$Y
    
    $ bash a.sh 
    X=1
    Y=2
    

  • Mod

    Das verschiebt leider nur das Problem auf das Programm, das den Eintrag in der Datenbank vornimmt. Leider ist es nämlich nicht damit getan, direkt die Ausgabe von getenv/env/environ oder einer der vielen anderen Möglichkeiten, um an die Umgebungsvariablen zu kommen, zu übernehmen. Denn Umgebungsvariablen können (und tun dies auch oft!) so ziemlich jede Art von Sonderzeichen enthalten. Unter anderem auch solche, die für die Shell eine bestimmte Bedeutung haben, wenn man sie nicht korrekt quoted. Da keine der mir bekannten Möglichkeiten, an die Umgebungsvariablen zu kommen, eine gequotete Zeichenkette als Rückgabewert liefert, muss das daher nach meinem Verständnis von Hand erfolgen.

    Tatsächlich sieht meine jetzige Implementierung auch vor, die Zerlegung beim Einfügen eines Eintrags vorzunehmen und die Daten als Vector in der Datenbank zu speichern. Der Code, der mit den Umgebungsvariablen hantiert, ist dann auch gar nicht mal so schlimm, wie ich mir das vorgestellt hätte:

    #ifndef ENVIRONMENT_H
    #define ENVIRONMENT_H
    
    #include <map>
    #include <utility>
    #include <string>
    
    class Environment : private std::map<std::string, std::string>
    {
     private:
      typedef std::map<std::string, std::string> Super;
     public:
      using Super::begin;
      using Super::end;
      using Super::operator[];
    };
    
    Environment get_current_environment();
    void set_environment(const Environment& env);
    
    #endif
    
    #include <unistd.h>
    
    extern char **environ;
    
    Environment get_current_environment()
    {
      Environment env;
      for (char **environ = ::environ; *environ; ++environ)
        {
          std::string line = *environ;
          std::size_t equal_pos = line.find_first_of('=');
          std::string name = line.substr(0, equal_pos);
          std::string val = line.substr(equal_pos + 1, std::string::npos);
          env[name] = val;
        }
      return env;
    }
    
    void set_environment(const Environment &env)
    {
      clearenv();
      for (const auto &it : env)
        {
          setenv(it.first.c_str(), it.second.c_str(), true);
        }
    }
    
    // Testprogramm:
    #include <iostream>
    
    int main() 
    {
      Environment test = get_current_environment();
      test["TESTVAR"] = "TESTVAL";
      test["PATH"] = "not the path you were looking for";
      set_environment(test);
      Environment test2 = get_current_environment();
      for (const auto& it : test2)
        std::cout << it.first << " = " << it.second << '\n';
    }
    

    Prinzipiell macht er zwar immer noch ungeheuer umständliches Zeug, aber zumindest der Code ist kurz und knackig und Geschwindigkeit spielt hier kaum eine Rolle.

    Ich bin übrigens davon abgekommen, die Variablen per env direkt beim Aufruf zu setzen, da ich befürchte, eventuell Probleme mit der maximalen Kommandozeilenlänge bekommen zu können. Ich vermute eval hat ebenfalls ein Maximum? Schließlich ist das für die Shell ein einziger großer Ausdruck.



  • Per Kommandozeile würde man einfach

    $ set >my_env_file
    

    zum Dumpen und

    . my_env_file
    

    zum wieder Einlesen machen.

    In der Datenbank könntest du dann halt den Inhalt von my_env_file speichern und den Befehl mit

    bash -c ". /tmp/my_env_file_from_database && cmd"
    

    ausführen (wobei /tmp/my_env_file_from_database eine temporäre Datei ist, die mit dem Inhalt des Datenbankfeldes gefüllt ist).

    Das macht im Grunde das gleiche wie dein Code, aber das Environment ist menschenlesbarer gespeichert und du würdest weniger in deinem Code tun.


  • Mod

    Ich habe es doch schon 2x geschrieben: Das funktioniert nicht! Die menschenlesbaren Ausgaben enthalten allerlei ungequotete Sonderzeichen. Die kann man nicht einfach wieder der Shell zur Ausführung vorsetzen und erwarten, dass es funktioniert.



  • SeppJ schrieb:

    cronjob in einer ziemlich minimalen Umgebung startet

    Das ist schon immer so gewesen.
    Was spricht gegen das Sourcen von .profile (und ggf. .bashrc) direkt vor jedem cron Kommando?

    * * * * * . ~user/.profile && command
    


  • Ich gehe davon aus, dass du dir das gut überlegt hast und möchte es dir deswegen natürlich auch nicht ausreden, aber ich persönlich habe die Erfahrung gemacht, dass sowas keine gute Idee ist.

    Wenn du die Environment einfach zu einem x-beliebigen Zeitpunkt einfach 1:1 klonst, dann fängst du dir ein klassisches Runtime-Changes-Problem ein. Klar ist die laaaange Umgebungsvariablenliste im Cronjob lästig, aber dafür ist auch jederzeit nachvollziehbar, was da drinsteht, wie das zustande kommt und was dein Programm zum laufen braucht.

    Wenn du einfach nur klonst, kannst du jederzeit irgendwelche Variablen setzen, die du dringend brauchst, sie das nächste Mal vergessen usw. Oder ein Kollege startet mal nicht aus der Shell-History oä.

    Ich hatte schon einige Projekte, die einfach ein .env -File hatten, wo eine Liste von Umgebungsvariablen in ganz simpler FOO=BAR -Notation gespeichert waren. Das File kann man dann zB. auch mit Minimaleinstellungen versionskontrolliert ablegen oä. Gibt einige Projekte, die ähnliche Dinge machen: https://github.com/bkeepers/dotenv#user-content-usage

    Wie gesagt, ich möchte dir nichts ausreden, aber ich persönlich würde das Klonen eher bleiben lassen; wäre mir zu wackelig.


  • Mod

    Wutz schrieb:

    SeppJ schrieb:

    cronjob in einer ziemlich minimalen Umgebung startet

    Das ist schon immer so gewesen.

    Das weiß ich auch und habe ich schon immer gewusst. Was willst du aussagen?

    Was spricht gegen das Sourcen von .profile (und ggf. .bashrc) direkt vor jedem cron Kommando?

    Dagegen spricht, dass nicht alle relevanten Variablen in diesen Dateien gesetzt werden.

    @nman: Danke, endlich jemand, der mein Problem versteht.

    Wenn du die Environment einfach zu einem x-beliebigen Zeitpunkt einfach 1:1 klonst, dann fängst du dir ein klassisches Runtime-Changes-Problem ein. Klar ist die laaaange Umgebungsvariablenliste im Cronjob lästig, aber dafür ist auch jederzeit nachvollziehbar, was da drinsteht, wie das zustande kommt und was dein Programm zum laufen braucht.

    Das ist sogar in diesem Fall genau das, was ich will. Da ist im Hintergrund ein Modulsystem aktiv. Bei verschiedenen Aufträgen kann es durchaus sein, dass ein anderes Modul geladen werden soll. Das ist zwar nicht das Einzige, was es zu erledigen gilt (sonst könnte ich den Auftrag schließlich den Befehl voran stellen, ein gewisses Modul zu laden), aber es ist ein hübscher, wünschenswerter Nebeneffekt, dass dies dadurch quasi automatisch mit erschlagen wird. Umgekehrt war das eben gerade der Nachteil der langen Liste im crontab, dass eben nicht klar war, welche Variablen überhaupt nötig sind. Die Programme sind teilweise nicht von mir und nutzen auf kaum oder gar nicht dokumentierte Weise die Umgebungsvariablen, die dann wiederum vom Systemadministrator auf passende Standardwerte gesetzt werden*. Es würde vermutlich sogar reichen, wenn ich im cron irgendwie an diese Standardwerte käme. Ich weiß aber nicht genau, mit welchem Mechanismus die Vorgabewerte gesetzt werden. Das Starten einer Login-Shell in cron war jedenfalls nicht genug.

    *: Zu denen dann noch meine persönlichen Einstellungen aus meinem Profil dazu kommen und die in einem cronjob eben nicht gesetzt werden. Um mal den Bezug zu Wutz' Beitrag herzustellen.



  • Ok, jetzt verstehe ich deine Absichten etwas besser. Leider fällt mir auch keine sehr elegante Art ein, das etwas unixiger zu lösen. Deine aktuelle Lösung ist vielleicht nicht perfekt, bei den Anforderungen aber eigentlich recht brauchbar in meinen Augen.


Log in to reply