Cómo inicié

Cuando no sabemos nada de cómo construir algo, hay que comenzar por las partes más pequeñas posibles. En mi caso «alguna idea» ya tenía sobre REST, Python y algunas de las otras tecnologías con lo cual ciertas cosas podía inferirlas fácilmente y otras no. Comencé leyendo publicaciones como Rest APIS with Flask and SQL Alchemy, Building a Basic Restful API in Python pero solo mostraban código y estaban algo incompletas. Finalmente la mejor que encontré fue Flask Parsing JSON Data que me dió una mejor visión de lo que quería lograr, a partir de ahí pude construir todo lo que verán a continuación.

Dado que el interés de esta publicación es explicarlo todo al detalle entonces iremos paso a paso.

Primero respondamos las preguntas básicas

¿Cómo funciona un REST API? ¿Para qué quiero el REST API? ¿Qué quiero lograr? ¿Cuál es mi objetivo? Estas preguntas son necesarias para poder avanzar.

Primero definamos que un REST API, son dos cosas «REST Representational State Transfer» y API (Application Interface Programming), en otras publicaciones también verán el término RESTful API.

Lo importante a entender primero que nada es que «REST» es un protocolo de comunicación muy liviano y simple, hay otro llamado «SOAP» que necesita «codificarse adicionalmente» para poder enviarlo. Claramente en el contexto de esta publicación usaremos REST.

REST VS SOAP

REST vs SOAP | Imagen tomada de https://www.codementor.io/sagaragarwal94/building-a-basic-restful-api-in-python-58k02xsiq

Viendo la imagen nos queda claro que REST viene siendo la forma, la manera (protocolo) en que un cliente envía datos a un servidor, este protocolo trabaja bajo HTTP y soporta las acciones POST, GET, UPDATE, DELETE.

Respecto a API es todo el código que permite a las aplicaciones comunicarse entre sí, en nuestro caso el API será el código en Python que puede interactuar con las peticiones escritas via REST/HTTP y la base de datos u otras aplicaciones.

Qué es un API

Qué es un API | Tomado de https://medium.com/@perrysetgo/what-exactly-is-an-api-69f36968a41f

La idea, qué quiero lograr con el API REST

Teniendo en cuenta la teoría anterior ahora puedo formalizar la idea. Lo que quiero lograr es tener acceso a todos los datos de múltiples equipos en todo el país de esta forma (en nuestra idea todas las notebooks serán Linux):

REST API, Cómo funcionará

REST API, Cómo funcionará

Algunos clientes se conectarán por medio de una VPN, otros a través de un servidor Gateway pero al final todos terminarán en el servidor donde tengo el «API REST».

Dado que en este escenario todos los equipos son Linux, me interesa obtener y centralizar todos estos datos:

  • Procesador, Memoria, Discos, Modelo del equipo (o Marca)
  • Dirección IP, MAC Address
  • Usuario de la notebook
  • Fecha (cuando el usuario inicie la sesión quiero que me envíe estos datos, todas las veces).
  • Etc…

Dadas mis necesidades particulares voy a agregar toda la información, todos los días, donde lo único que irá variando será la fecha.

¿Cómo extraigo esos datos de la notebook?

En windows podríamos utilizar «cmd» y ejecutar comandos para obtener información de los discos o memoria, en Linux usamos una consola o un script y ejecutamos otros comandos para la misma finalidad. En este caso creé el script «identificar_dispositivo.sh» que reúne la información que quiero en cada equipo (obivamente el script debe existir en cada equipo):

#!/bin/bash
# Miguel Ortiz
# Este script reune informacion en formato JSON.
#--------------------------------------- Archivo de destino
DEVICE_CURRENT = /tmp/deviceCurrent.json
#------------------------------------- reunir informacion del hardware
#
MACADD=`ip addr | awk '/eth0/{getline; print $2}' | head -1`
MACWIFI=`ip addr | awk '/wlan0/{getline;getline;getline;getline; print $2}'`
IPADDRETH0=`ip addr | awk '/eth0/{getline;getline; print $2}'`
VENDOR=`cat /sys/devices/virtual/dmi/id/sys_vendor 2> /dev/null`
PRODUCT=`cat /sys/devices/virtual/dmi/id/product_name | tr -d ' ' 2> /dev/null `
UUID=`cat /sys/devices/virtual/dmi/id/product_uuid 2> /dev/null`
SERIAL=`cat /sys/devices/virtual/dmi/id/product_serial 2> /dev/null`
MEMRAM=`free -m | grep "Memoria" | awk '{print $2}'`
CPUMODEL=`cat /proc/cpuinfo | grep "model name" | sed 's/.*://' | tr -d ' ' | head -1`
CPUCORES=`cat /proc/cpuinfo | grep "cpu cores" | head -1 | awk '{print $4}'`
FREEDISK=`df -h --output=pcent | paste -sd "," - | cut -d "," -f2-`

# ------------------------------------ convertir los datos a formato JSON

# JSON Format for REST API
# La primera linea borra el archivo tras cada ejecución

echo "{" > $DEVICE_CURRENT
        echo '"MACADD"'":\"$MACADD\"," >> $DEVICE_CURRENT
        echo '"MACWIFI"'":\"$MACTUN0\"," >> $DEVICE_CURRENT
        echo '"MEMRAM"'":\"$MEMRAM\"," >> $DEVICE_CURRENT
        echo '"CPUMODEL"'":\"$CPUMODEL\"," >> $DEVICE_CURRENT
        echo '"CPUCORES"'":\"$CPUCORES\"," >> $DEVICE_CURRENT
        echo '"FREEDISK"'":\"$FREEDISK\"," >> $DEVICE_CURRENT
        echo '"VENDOR"'":\"$VENDOR\"," >> $DEVICE_CURRENT
        echo '"UUID"'":\"$UUID\"," >> $DEVICE_CURRENT
        echo '"PRODUCT"'":\"$PRODUCT\"," >> $DEVICE_CURRENT
        echo '"SERIAL"'":\"$SERIAL\"," >> $DEVICE_CURRENT
        echo '"IPADDRETH0"'":\"$IPADDRETH0\"," >> $DEVICE_CURRENT
echo "}" >> $DEVICE_CURRENT

# ENVIAR LOS DATOS AL API REST

`curl -s -i -X POST <DIRECCION_IP_SERVIDOR>:5000/postjson -H 'content-type: application/json' --data-binary "@/tmp/deviceCurrent.json" --output /dev/null -m 10`

La primera parte del script ejecuta comandos para obtener los datos, por ejemplo «obtener la RAM» y lo guarda en una variable.

Posteriormente escribimos esos datos en formato JSON, el formato JSON siempre debe ser, pueden validar el código JSON en aplicaciones online como JsonFormatter

{
  "dato1" : "valor",
  "dato2" : "valor",
  "dato3" : "valor"
}

Además todas estas líneas terminarán en el archivo deviceCurrent.json en /tmp, el cual luce algo similar a esto:

{
"MACADD":"AA:BB:CC:DD:EE",
"MEMRAM":"7854",
"CPUMODEL":"Intel(R)Core(TM)i7-3520MCPU@2.90GHz",
"CPUCORES":"2",
"FREEDISK":"  0%, 11%, 88%,  5%,  1%,  0%, 72%,  2%, 17%,  1%, 96%",
"USBDEVICES":"11",
"VENDOR":"BANGHO",
"UUID":"",
"PRODUCT":"MOV",
"SERIAL":"",
"IPADDRETH0":"",
}

La última parte del script es  el llamado a curl que es una herramienta para comunicarnos con el servidor donde está el REST API. Básicamente le estamos enviando información en formato JSON, el cual nuestra aplicación recibirá, procesará e insertará en la base de datos. Todavía no construimos la aplicación de destino, pero esta será la forma en la que enviaremos los datos.

Curl se encargará de enviar la información vía HTTP, en este caso usaremos el método «POST» que es para el envío de información, el método «GET» sería para recibir datos, «UPDATE» para actualizar y «DELETE» para borrar, a medida que avancemos en la construcción de la aplicación entenderán cómo usar los otros métodos.

Podemos usar un equipo como cliente y otro como servidor, pero si solo disponemos de un equipo no hay problema, podemos hacer de cliente y servidor al mismo tiempo, solo bastará con cambiar la IP en el CURL para apuntar a «localhost» o nuestra dirección IP local.

Construyendo el REST API (Flask, Python, MongoDB).

Como vimos nuestro curl apunta a una dirección IP y el puerto 5000, ese servicio que corre es Flask. Básicamente Flask viene a ser un framework para escribir aplicaciones web, actúa como WSGI (Web Server Gateway Interface) y permite exponer un servicio que se encargará de recepcionar y redirigir las peticiones (GET, POST, DELETE, UPDATE) a nuestro código funcional en pyhton.

Para hacer esto al ejecutar flask este escuchará sobre la IP del equipo (servidor) expondrá el servicio de escucha en el puerto 5000 y finalmente publicará «rutas» donde podremos interactuar con el código y por consiguiente con las aplicaciones.

Veamos un ejemplo muy sencillo. Para esto necesitaremos (revisen según su distribución, yo utilizo Debian):

  1. Instalar Python (apt-get install python3)
  2. Instalar Python-Pip (apt-get install python3-pip)
  3. Instalar Flask  (pip install Flask)

Existen muchos tutoriales para instalar estas cosas, algunos recomendarán virtual environments, compilaciones directas, o instalaciones por repositorio, esto depende de cada uno.

Escribiendo la primera aplicación en Flask

Creamos el archivo hello.py y dentro copiamos y pegamos el siguiente código:

from flask import Flask
from flask import request
    print ('you are here')
app = Flask(__name__)

@app.route("/")
def hello():
        return "First Flask APP"

@app.route('/postjson', methods = ['POST'])
def postJsonHandler():
    print ('Getting RAW Data')
    print request.get_data()
    print ('Validate JSON Format')
    print (request.is_json)
    content = request.get_json()
    print (content)
    return 'JSON posted'

Ahora debemos iniciar el servicio, para lo cual primero seteamos la variable de entorno:

FLASK_APP=hello.py flask run

Y luego ejecutamos en la consola (0.0.0.0 significa que escuchará en todas las interfaces de red del equipo, en caso de tener dos o más placas de red)

python -m flask run --host=0.0.0.0
Si todo salió bien tendremos el mensaje: Running on http://0.0.0.0:5000/  | Es importante recordar que esto solo es válido para correr el servicio, para todo lo demás debemos usar la IP específica del servidor.
mortiz@tinylab:/srv/data$ python -m flask run --host=0.0.0.0
 * Serving Flask app "hello.py"
 * Environment: production
   WARNING: Do not use the development server in a production environment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)

El servicio está activo y escuchando. Probemos con un pequeño JSON de ejemplo antes de usar nuestro curl armado en el paso anterior:

{
    "device": "TemperatureSensor",
    "value": "20",
    "timestamp": "25/01/2017 10:10:05"
}

De forma tal que cuando hagamos un POST de esos datos los deberíamos ver del lado del servidor. Si les resulta complicado usar CURL en este momento, pueden probar inicialmente con el plugin de Chrome «Advanced REST Client» para enviarle la información al servidor de esta forma:

  1. Elijan el método POST
  2. Coloquen la URL con la ruta publicada en el servicio «postjson» (usen la IP específica del servidor)
  3. Elijan el Body content type: application/json
  4. En la segunda imagen configuren el TAB Header con los valores: Header Name: content-type  Header Value: application/json
  5. Elegir Editor View «Raw Input»
  6. Copien y peguen el ejemplo JSON
  7. Finalmente click en «SEND» si sale el mensaje 200 OK entonces la información se envió correctamente al servidor.

Y observamos en la consola donde estamos ejecutando el servicio cómo se recibe la información apropiadamente:

mortiz@tinylab:/srv/data$ python -m flask run --host=0.0.0.0
 * Serving Flask app "hello.py"
 * Environment: production
   WARNING: Do not use the development server in a production environment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
{"device": "TemperatureSensor","value": "20","timestamp": "25/01/2017 10:10:05"} you are here False None <IP_DEL_CLIENTE> - 
- [09/May/2019 18:01:59] "POST /postjson HTTP/1.1" 200 
- {"device": "TemperatureSensor","value": "20","timestamp": "25/01/2017 10:10:05"} you are here 
True {u'device': u'TemperatureSensor', u'timestamp': u'25/01/2017 10:10:05', u'value': u'20'} <IP_DEL_CLIENTE> - 
- [09/May/2019 18:03:26] "POST /postjson HTTP/1.1" 200 -

Explicando cómo funciona esta primera parte

Si lograste hacer todo bien ya tenés un REST API funcionando. Ahora es necesario entender qué hace el código.

En las primeras líneas cargamos los módulos de Flask y Request en python, luego imprimimos un mensaje para saber en qué parte de la ejecución de código estamos:

from flask import Flask
from flask import request
    print ('you are here')

Luego avanzamos y encontramos la primer «route»:

@app.route("/")
def hello():
        return "First Flask APP"

Básicamente es publicar algo en el «raíz» de la aplicación web. Dentro de esta aplicación no hay mucho, solo una función llamada «hello» que imprime en pantalla «First Flask APP», si vamos a la http://<IP_DEL_SERVIDOR>:5000 en nuestro navegador lo veremos así:

Primera aplicación Flask en ejecución

Primera aplicación Flask en ejecución

Por defecto las rutas utilizan el método GET, nuestro navegador «trae información» utilizando GET y Flask le entrega justamente lo que pide.

Ahora bien, nos interesa conocer cómo funcionó el POST, es decir, el que envió datos desde el navegador o desde CURL al servidor:

@app.route('/postjson', methods = ['POST'])
def postJsonHandler():
    print ('Getting RAW Data')
    print request.get_data()
    print ('Validate JSON Format')
    print (request.is_json)
    content = request.get_json()
    print (content)
    return 'JSON posted'

Observen que en esta ruta «/postjson» el método «POST» está puesto de forma explícita. La función que hay en esta ruta se encarga de interactuar con los datos en formato JSON (¿recuerdan que todo lo estamos armando en JSON?) Si intentamos acceder desde el navegador a esa URI obtendremos un error, porque no está diseñada para dar información sino para recibir.

Cada línea que dice «Print» es solo para escribir mensajes y entender qué es lo que va a pasar a continuación, de momento pueden omitirlas si no las entienden. Ahora bien, cuando ejecutamos CURL o utilizamos el Plugin de Advanced REST Client en Chrome los datos viajan por la red y llegan al servidor Flask que se está ejecutando, allí usaremos el módulo «Request» el cual se encarga de recepcionar esa información que enviamos:

print request.get_data()

En el código utilizo print para que me muestre exactamente «qué estamos recibiendo», así podríamos recibir datos en crudo que no sean JSON, cualquier cosa que queramos.

Sin embargo sabemos que todo lo queremos con JSON porque es el formato correcto para trabajar con REST y otras tecnologías, así que usamos nuevamente el módulo «request» pero le pedimos que nos muestre la información si es JSON:

print (request.is_json)

Esta línea va a validar si los datos que vienen están en un formato de JSON correcto, caso contrario dará un error.

Posteriormente con get_json ya estamos seguros de que aquello que venga es un formáto válido en JSON y se almacenará dentro de la variable «content»

content = request.get_json()

En último lugar imprimimos la variable content que dentro tiene un JSON válido:

print (content)

Hasta este punto hemos logrado:

  • Enviar un dato en formato JSON desde un cliente (Con el plugin de Chrome o CURL)
  • Recibirlo del lado del servidor
  • Validar si es un JSON correcto o no
  • Imprimir el dato del lado del servidor

¡Como ya estamos recibiendo el dato podemos entonces proceder a almacenarlo en la base de datos!

Almacenando información en MongoDB

Primero deberemos instalar MongoDB, en mi caso la versión 3.2 en Debian 9:

root@mortiz:/srv/proyecto# mongo --version
MongoDB shell version: 3.2.11

Luego crearemos el archivo de python que se conectará a la base de datos y las funciones de escritura, le he llamado usarMongoDB.py este se encargará de crear todo, la base y colecciones si no existen:

from pymongo import MongoClient
from datetime import datetime
import json

# establish connex
conn = MongoClient('localhost', 27017)
conn = MongoClient()

# create db
db = conn.baseDeDatos

# create collection
collection = db.InfoDeNotebooks

# <-------------------------------------------------------- INSERT 
# Funciones para insertar datos
def insertDatosNotebook(NBDataconFecha) :
    """
    Una vez que se agrego fecha y hora en la NBDataconFecha recibida por POST
    agregamos el documento "registro"  en la base de datos.
    """
    insertNotebook = collection.insert(NBDataconFecha)

Y en nuestro hello.py cambiamos /postjson para que haga el insert:

from flask import Flask
from flask import request
import usarMongoDB # aca importamos el archivo nuevo que interactua con la base
    print ('you are here')

app = Flask(__name__)


@app.route("/")
def hello():
        return "First Flask APP"

@app.route('/postjson', methods = ['POST'])
def postJsonHandler():

    NBData = request.get_json()
    # Registro la fecha desde el lado del servidor
    NBData.update({"FECHA":datetime.datetime.now().strftime("%m/%d/%Y, %H:%M:%S")})
    # Inserto el valor en la base
    usarMongoDB.insertDatosNotebook(NBData)
    return 'JSON posted'

Y el valor aparecerá en nuestra base mongo. Entramos en mongo:

root@tinylab:/srv/proyecto# mongo

Luego seleccionamos la base que fue creada:

> use baseDeDatos

Y podemos imprimir todos los registros de la colección que también fue creada:

> db.InfoDeNotebooks.find()