Automágica: durante 2017 estoy trabajando bastante en Automágica, mi software para editar libros: Más información - Posts relacionados


Charla: Desarrollando aplicaciones de red con Twisted

Hoy di en Junín una charla introductoria Twisted.

Cuando los organizadores me contactaron hace un para de meses para invitarme a venir, me preguntaron sobre qué podía hablar. Mi primera idea era una charla titulada algo así como Python para diversión, trabajo y estudio. Mandé un guión con los temas que incluiría, en en el track trabajo mensioné Twisted, el framework que venía usando hacía un tiempo ya. La respuesta que recibí fue.

"Dale, date una charla de Twisted"

:)

Si bien venía usándolo hacía casi dos años, mi forma de aprenderlo no fue la más pedagógica, sino que fue la forma dura. Empecé a desarrollar y fui conociendo lo puntudo de sus aristas a medida que iba necesitando agregar nuevas funcionalidades.

Preparar la charla mi vino muy bien para organizar lo que sabía y aprender algunas que había venido evitando. Hoy di la charla en Junín y creo que salió bastante bien.

Introducción a Twisted, un framework escrito en Python que permite

crear aplicaciones de red (clientes y servidores) asincrónicas.

Veremos sus conceptos principales y ejemplos que permitirán al

auditorio volver a su casa con las herramientas básicas para empezar a programar utilizándolo.

Dejo el material de la charla:

Slides

Ejemplos

Y una propaganda que fue mi última slide. PyAr, el grupo de usuarios de Python de Argentina está dando charlas abiertas (de 2hs c/u) en La Tribu todos los sábados. El próximo sábado tenemos justamenet una clase de Twisted!

http://python.org.ar/pyar/CharlasAbiertas2010#Twisted

Sábado 13 de Noviembre - Charlas Abiertas de Python en La Tribu (Lambaré 873, Capital Federal)

Twisted

Vamos a ver porque el modelo de twisted es necesario, como se programa en modo asyncronico usando deferreds y conocer el api de red de twisted para hacer servicios.

Disertante: Lucio Torre

13 a 15 horas


Twitter updates desde Twisted

Este artículo es una re-edición del publicado el 24/08/2010 ya que cuando lo publiqué, Twitter estaba terminando su proceso apagar el sistema de autenticación básica para pasar al más complejo sistema de la danza oAuth; lo cual convirtió al artículo en inservible. El artículo anterior no está más disponible. Gracias a Pupeno por revisar esta versión.

¿Tenés un servidor escrito en Twisted? ¿Tenés eventos críticos o importantes que mandás por mail o a celulares? ¿Qué tal publicarlos en Twitter?

La versión original de este artículo utilizaba la biblioteca Twitty Twister, la única pensada para usar Twitter desde Twisted. Lamentablemente no funciona bien con el nuevo sistema de autenticación de Twitter por lo que voy a usar Tweepy, una librería con la cual hacer la danza de oAuth es muy sencillo.

1) Registrar un usuario dónde publicar las notificaciones.

2) Con ese usuario, registrar una aplicación. Es muy importante usar el mismo usuario para ahorrarnos algunos pasos en la danza oAuth.

3) De la página de la aplicación creada, tomar la siguiente información: consumer key, consumer secret (desde la home de la aplicación), access token y access token secret (de la página My Access Token).

4) Código:

Con los datos del punto 3, creamos el módulo twitterupdates.py:

import tweepy



TWITTER_KEY = 'xxxxxxxxxxxxxxxx'

TWITTER_SECRET = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'

MY_KEY = 'xxxxxxxxxxxxxxxxxxxxxxxx'

MY_SECRET = 'xxxxxxxxxxxxxxxxxxxxxxxxxx'



auth = tweepy.OAuthHandler(TWITTER_KEY, TWITTER_SECRET)

auth.set_access_token(MY_KEY, MY_SECRET)

twitter = tweepy.API(auth)

y desde nuestra aplicación Twisted lo importamos:

from twitterupdates import twitter

Postear una actualización en Twitter o cualquier otra petición a un servidor externo tiene una naturaleza bloqueante y en un framework de concurrencia colaborativa, como es Twisted, no podemos darnos ese lujo. Para solucionar esto, utilizamos deferToThread:

deferToThread(twitter.update_status, "Mensaje a postear en Twitter")

Esta llamada retorna un objeto Deferred al cual se le pueden añadir callbacks o errbacks. La llamada bloqueante es corrida en un hilo aparte.

Enjoy!


Cambiando el formato de los logs en Twisted

En Twisted se puede tener un log de todo lo que pasa en un programa de forma bastante fácil:

from twisted.python import log

from twisted.python.logfile import DailyLogFile

log.startLogging(DailyLogFile('log.txt', LOGDIR))

Todos los prints que anden dando vuelta ahora son entradas en el log y de yapa estoy usando DailyLogFile en lugar de un archivo común y corriente para que al final del día rote a un nuevo archivo en disco. Hay varias opciones, esta es la que me sirve a mí.

Por defecto los logs se ven algo así:

2010-05-28 07:41:00-0500 [__main__.TModBusFactory] Nuevo cliente: 190.136.29.16:12101

2010-05-28 07:41:00-0500 [__main__.TModBusFactory] Total: 1

2010-05-28 07:41:01-0500 [TModBus,0,190.136.29.16] G24 dice:  :0090SFE00

2010-05-28 07:41:01-0500 [TModBus,0,190.136.29.16] SITIO SFE

2010-05-28 07:41:01-0500 [TModBus,0,190.136.29.16] Nuevo sitio registrado en MBProxy SFE

2010-05-28 07:45:49-0500 [__main__.TModBusFactory] Nuevo cliente: 190.136.29.16:30519

2010-05-28 07:45:49-0500 [__main__.TModBusFactory] Total: 2

2010-05-28 07:45:49-0500 [TModBus,1,190.136.29.16] G24 dice:  :0090SFE00

2010-05-28 07:45:49-0500 [TModBus,1,190.136.29.16] SITIO SFE

2010-05-28 07:45:49-0500 [TModBus,1,190.136.29.16] SFE ya estaba conectado. Borrando anterior.

2010-05-28 07:45:51-0500 [TModBus,1,190.136.29.16] Nuevo sitio registrado en MBProxy SFE

¿Cómo podemos cambiar el formato de salida? Algunos tips.

Para cambiar el formato de la fecha y hora:

log.FileLogObserver.timeFormat = '%Y-%m-%d %H:%M:%S'

Para cambiar lo que aparece entre corchetes: utilizar log.msg y el keyword system:

log.msg("Nuevo cliente: %s:%d" % (self.peer.host, self.peer.port), system=' - ')

Con estos dos cambios se puede tener un log como este:

2010-05-28 07:41:00 [ - ] Nuevo cliente: 190.136.29.16:12101

2010-05-28 07:41:00 [ - ] Total: 1

2010-05-28 07:41:01 [SFE] G24 dice:  :0090SFE00

2010-05-28 07:41:01 [SFE] SITIO SFE

2010-05-28 07:41:01 [SFE] Nuevo sitio registrado en MBProxy SFE

2010-05-28 07:45:49 [ - ] Nuevo cliente: 190.136.29.16:30519

2010-05-28 07:45:49 [ - ] Total: 2

2010-05-28 07:45:49 [ - ] G24 dice:  :0090SFE00

2010-05-28 07:45:49 [SFE] SITIO SFE

2010-05-28 07:45:49 [SFE] SFE ya estaba conectado. Borrando anterior.

2010-05-28 07:45:51 [SFE] Nuevo sitio registrado en MBProxy SFE

Para cambiar aún más el formato de la salida, la única forma que encontré es extender FileLogObserver y sobreescribir su método emit. Discutimos un poco esto en StackOverflow.

nota: En Twisted también podemos usar el sistema de logging de Python, esto tiene la ventaja de que podemos trabajar con niveles de log y controlar mejor el formato y la forma en que se generan los logs, pero el problema de que no está preparado para funcionar en forma asincrónica y esto puede traer algunos problemas con Twisted.


Puedo disfrazar cualquier cosa de Modbus

Hace más de un año participo en un proyecto de desarrollo de un sistema SCADA, específicamente en la capa de comunicación. La distribución física es algo como esto:

En cada edificio hay una red 485 con dispositivos de hardware propio (aka Robot) y un módulo celular (Motorolla G24) con salida a Internet vía GPRS. Esos módulos se registran en un servidor (escrito con Twisted) para ser luego sondeados: se registran sus mediciones, hay gráficos en tiempo real, se puede hacer telecomando y encender un equipo a la distancia o cambiar un set point; entre otras cosas.

Nuestra primera implementación estaba inspirada en el protocolo Modbus ASCII, pero bastante alejada del estándar. Nos sirvió por bastante tiempo y no apegarnos a una especificación nos permitió acelerar el desarrollo; especialmente del hardware.

Modbus es un protocolo de línea maestro/esclavo en donde el maestro realiza peticiones de lectura o escritura sobre algún o algunos registros en un esclavo. El protocolo define 4 tipos de registros (analógicos o digitales, de solo lectura o lectura-escritura) y fue pensado para funcionar sobre una línea serie; existen dos sabores: RTU, dónde los datos viajan en forma binaria y la ocupación del canal es optimizada y ASCII, menos eficiente pero legible por humanos. Se puedo leer más al respecto en Wikipedia o en la página oficial del protocolo.

Hace algunos meses empezamos a utilizar Mango m2m. Mango tiene todos los componentes de interfaz de usuario que se pueden necesitar en un SCADA en adición a la posibilidad de conectarle distintas fuentes de datos (desde Modbus Serie a un motor de base de datos); las más interesante para nosotros es Modbus TCP.

Modbus TCP extiende el protocolo original para poder utilizarlo sobre la red de redes en lugar de sobre una línea serie. Por ejemplo, una de las cosas que hace es agregar al paquete enviado un número consecutivo para solucionar el problema de paquetes perdidos o duplicados.

Teníamos entonces, por un lado Mango que sabe hablar Modbus TCP y por el otro los robotitos hablando un dialecto de Modbus ASCII. En el medio, el servidor de comunicaciones. La pregunta en este punto era cómo unir ambos extremos, de manera de aprovechar esta herramienta que soluciona muchos de nuestros requerimientos.

Allí es donde entra en acción pymodbus, una implementación en Python del stack Modbus. No solo está escrita en Python, sino que está implementada utilizando el framework Twisted, el mismo que venía usando para el servidor de comunicaciones. Utilizando pymodbus pude engañar a Mango y hacerle creer que hablaba contar dispositivos Modbus TCP cuando en realidad interceptaba sus peticiones, enviaba los comandos necesarios a los robots y contestaba con la respuesta correcta.

En el camino encontré varios problemas en la librería, los fui reportando y enviando parches para solucionarlos. A lo último eran tanto los cambios que enviaba y empecé a modificar el core de la librería para que funcione con deferrers (es decir, siguiendo la filosofía de Twisted) que recibí permiso de commit en la rama devel.

Nota: el esquema descripto funcionó bastante tiempo. Actualmente seguimos avanzando y los robots ya hablan Modbus ASCII estándar a la vez que el componente intermedio adelgazó para convertirse en un traductor o gateway.


Charla relámpago: Comet en Twisted

Ayer en las charlas relámpago de PyCon Argentina 2009 mostré cómo utilizar Comet desde Twisted. Tenía un slide disparador que no pude mostrar por que OpenOffice no se abrió y como las charlas relámpago duran 5 minutos, no podía darme el lujo de investigar que pasaba.

"No importa", dije, "de todas formas el slide tenía solo cuatro palabras". Y para hacer justicia sobre las Leyes de Murphy publico aquí ese slide:

cometEl ejemplo que mostré en vivo puede bajarse del sitio web de Nevow/Athena.


Shell Python administrativo sobre SSH para tu servidor Twisted en 10 líneas

entre tu reactor.listeTCP(puerto, factory) y reactor.run():

from twisted.conch import manhole, manhole_ssh

from twisted.cred import portal, checkers

def getManholeFactory(namespace, **passwords):

realm = manhole_ssh.TerminalRealm()

def getManhole(_): return manhole.Manhole(namespace)

realm.chainedProtocolFactory.protocolFactory = getManhole

p = portal.Portal(realm)

p.registerChecker(

checkers.InMemoryUsernamePasswordDatabaseDontUse(**passwords))

f = manhole_ssh.ConchFactory(p)

return f

reactor.listenTCP(2222, getManholeFactory(globals(), admin='aaa'))

Los aplausos para este tutorial.


Servidor SSH con Twisted

Actualicé este ejemplo, tamibién incluido en el libro de Twisted para que no tire Deprecation Warnings:

from twisted.cred import portal, checkers, credentials

from twisted.conch import error, avatar, recvline, interfaces as conchinterfaces

from twisted.conch.ssh import factory, userauth, connection, keys, session, common

from twisted.conch.insults import insults

from twisted.application import service, internet

from zope.interface import implements

import os

class SSHDemoProtocol(recvline.HistoricRecvLine):

def __init__(self, user):

    self.user = user



def connectionMade(self) :

    recvline.HistoricRecvLine.connectionMade(self)

    self.terminal.write("Welcome to my test SSH server.")

    self.terminal.nextLine()

    self.do_help()

    self.showPrompt()



def showPrompt(self):

    self.terminal.write("$ ")



def getCommandFunc(self, cmd):

    return getattr(self, 'do_' + cmd, None)



def lineReceived(self, line):

    line = line.strip()

    if line:

        cmdAndArgs = line.split()

        cmd = cmdAndArgs[0]

        args = cmdAndArgs[1:]

        func = self.getCommandFunc(cmd)

        if func:

           try:

               func(*args)

           except Exception, e:

               self.terminal.write("Error: %s" % e)

               self.terminal.nextLine()

        else:

           self.terminal.write("No such command.")

           self.terminal.nextLine()

    self.showPrompt()



def do_help(self, cmd=''):

    "Get help on a command. Usage: help command"

    if cmd:

        func = self.getCommandFunc(cmd)

        if func:

            self.terminal.write(func.__doc__)

            self.terminal.nextLine()

            return



    publicMethods = filter(

        lambda funcname: funcname.startswith('do_'), dir(self))

    commands = [cmd.replace('do_', '', 1) for cmd in publicMethods]

    self.terminal.write("Commands: " + " ".join(commands))

    self.terminal.nextLine()



def do_echo(self, *args):

    "Echo a string. Usage: echo my line of text"

    self.terminal.write(" ".join(args))

    self.terminal.nextLine()



def do_whoami(self):

    "Prints your user name. Usage: whoami"

    self.terminal.write(self.user.username)

    self.terminal.nextLine()



def do_quit(self):

    "Ends your session. Usage: quit"

    self.terminal.write("Thanks for playing!")

    self.terminal.nextLine()

    self.terminal.loseConnection()



def do_clear(self):

    "Clears the screen. Usage: clear"

    self.terminal.reset()

class SSHDemoAvatar(avatar.ConchUser):

implements(conchinterfaces.ISession)



def __init__(self, username):

    avatar.ConchUser.__init__(self)

    self.username = username

    self.channelLookup.update({'session':session.SSHSession})



def openShell(self, protocol):

    serverProtocol = insults.ServerProtocol(SSHDemoProtocol, self)

    serverProtocol.makeConnection(protocol)

    protocol.makeConnection(session.wrapProtocol(serverProtocol))



def getPty(self, terminal, windowSize, attrs):

    return None



def execCommand(self, protocol, cmd):

    raise NotImplementedError



def closed(self):

    pass

class SSHDemoRealm:

implements(portal.IRealm)



def requestAvatar(self, avatarId, mind, *interfaces):

    if conchinterfaces.IConchUser in interfaces:

        return interfaces[0], SSHDemoAvatar(avatarId), lambda: None

    else:

        raise Exception, "No supported interfaces found."

def getRSAKeys():

if not (os.path.exists('public.key') and os.path.exists('private.key')):

    # generate a RSA keypair

    print "Generating RSA keypair..."

    from Crypto.PublicKey import RSA

    KEY_LENGTH = 1024

    rsaKey = RSA.generate(KEY_LENGTH, common.entropy.get_bytes)

    publicKeyString = keys.Key(rsaKey).toString()

    privateKeyString = keys.makePrivateKeyString(rsaKey)

    # save keys for next time

    file('public.key', 'w+b').write(publicKeyString)

    file('private.key', 'w+b').write(privateKeyString)

    print "done."

else:

    publicKeyString = file('public.key').read()

    privateKeyString = file('private.key').read()

return publicKeyString, privateKeyString

if name == "main":

sshFactory = factory.SSHFactory()

sshFactory.portal = portal.Portal(SSHDemoRealm())

users = {'admin': 'aaa', 'guest': 'bbb'}

sshFactory.portal.registerChecker(

checkers.InMemoryUsernamePasswordDatabaseDontUse(**users))

pubKeyString, privKeyString = getRSAKeys()

sshFactory.publicKeys = {

    'ssh-rsa': keys.Key.fromString(pubKeyString)}

sshFactory.privateKeys = {

    'ssh-rsa': keys.Key.fromString(privKeyString)}



from twisted.internet import reactor

reactor.listenTCP(2222, sshFactory)

reactor.run()</pre></body></html>