StringIO als Alternative zu einer Datei nutzen

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
Hellstorm
User
Beiträge: 231
Registriert: Samstag 22. Juni 2013, 15:01

Sonntag 14. September 2014, 15:15

Hallo,

in Paramiko werden die SSH-Hostkeys leider nur aus einer Datei gelesen (ist nur leider blöd, wenn man das ganze nicht unbedingt in einer Datei stehen hat. (Ich möchte in meinem Fall den Key aus dem Putty-Eintrag in der Windows-Registry ins Open-SSH-Format konvertieren und dann anschließend laden, aber ohne das ganze auf die Festplatte zu speichern)). Ich würde dies gerne so umschreiben, dass auch ein StringIO benutzt werden kann.

Die relevante Funktion ist folgendermaßen:

Code: Alles auswählen

    def load(self, filename):
        """
        Read a file of known SSH host keys, in the format used by OpenSSH.
        This type of file unfortunately doesn't exist on Windows, but on
        posix, it will usually be stored in
        ``os.path.expanduser("~/.ssh/known_hosts")``.

        If this method is called multiple times, the host keys are merged,
        not cleared.  So multiple calls to `load` will just call `add`,
        replacing any existing entries and adding new ones.

        :param str filename: name of the file to read host keys from

        :raises IOError: if there was an error reading the file
        """
        with open(filename, 'r') as f:
            for lineno, line in enumerate(f):
                line = line.strip()
                if (len(line) == 0) or (line[0] == '#'):
                    continue
                e = HostKeyEntry.from_line(line, lineno)
                if e is not None:
                    _hostnames = e.hostnames
                    for h in _hostnames:
                        if self.check(h, e.key):
                            e.hostnames.remove(h)
                    if len(e.hostnames):
                        self._entries.append(e)
(https://github.com/paramiko/paramiko/bl ... ostkeys.py, Zeile 79.)

Das ganze geht einfach und funktioniert gut, wenn ich die Zeile „with open(filename, 'r') as f:“ entferne und das f direkt als StringIO übergebe. Nur dann kann man natürlich nicht mehr aus Dateien lesen.

Da in Python ja oft eigentlich beides gehen sollte (Duck Typing, oder nicht?), soll der eben je nachdem beides lesen können. Wenn ich der Methode einfach ein StringIO übergebe, bekomme ich ein TypeError: invalid file: <_io.StringIO object at 0x00000000043515E8>.

Wie soll man sowas nun implementieren? Das einfachste wäre einfach

Code: Alles auswählen

    def load(self, filename):
        try:
            with open(filename, 'r') as f:
                for lineno, line in enumerate(f):
                    line = line.strip()
                    if (len(line) == 0) or (line[0] == '#'):
                        continue
                    e = HostKeyEntry.from_line(line, lineno)
                    if e is not None:
                        _hostnames = e.hostnames
                        for h in _hostnames:
                            if self.check(h, e.key):
                                e.hostnames.remove(h)
                        if len(e.hostnames):
                            self._entries.append(e)
        except TypeError:
            for lineno, line in enumerate(filename):
                line = line.strip()
                if (len(line) == 0) or (line[0] == '#'):
                    continue
                e = HostKeyEntry.from_line(line, lineno)
                if e is not None:
                    _hostnames = e.hostnames
                    for h in _hostnames:
                        if self.check(h, e.key):
                            e.hostnames.remove(h)
                    if len(e.hostnames):
                        self._entries.append(e)
aber das ist natürlich stilistisch sehr schlecht. Ich habe halt irgendwie ein Problem mit dem „with“. Wenn man das manuell machen würde, könnte ich ja das try... except nur für den Datei-Öffnen-Befehl machen, aber müsste nicht alles neu schreiben.

Danke!
EyDu
User
Beiträge: 4871
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

Sonntag 14. September 2014, 15:44

Code: Alles auswählen

def load(self, stream):
    ...


def load_from_file(self, filename):
    with open(filename) as fp:
        return self.load(fp)
Um das load kannst du dir natürlich noch immer eine Wrapper-Funktion schreiben, welche selbst zwischen Strings und Streams unterscheidet.
Das Leben ist wie ein Tennisball.
Hellstorm
User
Beiträge: 231
Registriert: Samstag 22. Juni 2013, 15:01

Sonntag 14. September 2014, 16:51

Ok, danke. Ich wollte den Quelltext da nicht zu sehr verunstalten (ist auch sowieso die Frage, ob das dann aufgenommen wird :D). Ich werde das mal implementieren und dann dort als Pull Request abschicken. Mal sehen ob es was wird. Danke!
Sirius3
User
Beiträge: 8625
Registriert: Sonntag 21. Oktober 2012, 17:20

Sonntag 14. September 2014, 18:27

@Hellstorm: was Du gemacht hast, ist den ganzen Code zu kopieren und nur eine Zeile zu ändern. Damit verdoppelst Du nicht nur den Code, Du mußt in Zukunft auch immer daran denken, Änderungen an zwei Stellen zu machen. Wenn Du auf so ein Problem triffst, hilft es, nachzuschauen, wie andere dieses Problem gelöst haben. Hier zum Beispiel ElementTree.parse:

Code: Alles auswählen

    def parse(self, source, parser=None):
        close_source = False
        if not hasattr(source, "read"):
            source = open(source, "rb")
            close_source = True
        try:
            if not parser:
                parser = XMLParser(target=TreeBuilder())
            while 1:
                data = source.read(65536)
                if not data:
                    break
                parser.feed(data)
            self._root = parser.close()
            return self._root
        finally:
            if close_source:
                source.close()
oder hier, wie es ZipFile macht:

Code: Alles auswählen

        # Check if we were passed a file-like object
        if isinstance(file, basestring):
            self._filePassed = 0
            self.filename = file
            modeDict = {'r' : 'rb', 'w': 'wb', 'a' : 'r+b'}
            try:
                self.fp = open(file, modeDict[mode])
            except IOError:
                if mode == 'a':
                    mode = key = 'w'
                    self.fp = open(file, modeDict[mode])
                else:
                    raise
        else:
            self._filePassed = 1
            self.fp = file
            self.filename = getattr(file, 'name', None)
Hellstorm
User
Beiträge: 231
Registriert: Samstag 22. Juni 2013, 15:01

Mittwoch 17. September 2014, 17:14

Hab jetzt gemerkt, dass mein ganzes Vorhaben unnütz war. Die Funktion gibt es nämlich schon, ist nur ein wenig unbequem versteckt :D

Und zwar nimmt man zuerst den Schlüssel des Servers, trägt den in ein bytes-Objekt ein, macht dann ein base64.decodebytes() und kann dann mit paramiko.RSAKey(data=schlüssel) ein RSAKey-Objekt erstellen. Anschließend dann mit get_host_keys().add() den Schlüssel zum SSHClient hinzufügen.

Etwas umständlich, aber klappt so auch ganz gut. Wäre nur sehr schön, wenn man das auch einfach per String übergeben könnte, aber naja...
Benutzeravatar
Hyperion
Moderator
Beiträge: 7472
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

Mittwoch 17. September 2014, 18:20

Hellstorm hat geschrieben: Etwas umständlich, aber klappt so auch ganz gut. Wäre nur sehr schön, wenn man das auch einfach per String übergeben könnte, aber naja...
Naja, das schlägt das Problem der Serialisierung und der damit verbundenen impliziten Umkodierungsoperationen zu! Du kannst zwischen Programmen ja nur Bytes austauschen und keine Unicode-Objekte (das sind ja lediglich interne *Konzepte*, die sonst wie intern codiert sein können!). Ergo muss man auf Byte-Ebene arbeiten.
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3))
assert encoding_kapiert
Hellstorm
User
Beiträge: 231
Registriert: Samstag 22. Juni 2013, 15:01

Mittwoch 17. September 2014, 19:08

Ich finde es nur irgendwie komisch, dass Base64 so oder so alle Bytes auf ASCII reduziert (oder nicht?). Wieso muss ich dann, wenn ich base64.decodebytes() mache, dort Bytes übergeben (mal abgesehen davon, dass es der Name impliziert)? Der Sinn von Base64 ist doch gerade der, danach nicht mehr mit Werten außerhalb ASCIIs rechnen zu müssen, oder nicht?

Naja, natürlich ist das Vorgehen so wie es ist logisch und ich habe keine Ahnung, das gebe ich zu (Die Python-Entwickler haben natürlich mehr Ahnung) :D. Ich finde es nur für den Anfang etwas kontraintuitiv, dass ein Verfahren, dass dafür sorgt, dass die Bytes auf ASCII (=Text) reduziert werden, dann eben kein „Text“ akzeptiert.

Zu Paramiko wäre halt praktisch, wenn ich dem SSHClient in einem Rutsch den Hostkey mitsamt dem Hostname und dem Typen (d.h. im Grunde einer Zeile aus der known_hosts) übergeben könnte. Letzten Endes holt der SSHClient sich das ja auch selber aus der known_hosts und wandelt das in ein passendes Format um, nur wenn man es manuell macht, ist es so umständlich.
Antworten