Understanding xargs

xargs [options] [command [initial-arguments]]

Linux is an OS that has been developed and oriented to provide users simple commands which can be joined to do more complex task.

Xarg is one of those commands that allows you to compose routines that would be tedious in some other way.

The logic of xarg is the same as a while loop that gets as input the result of a previous command by default using both spaces and newlines as delimiters. For instance, imagine you have to change read permissions of all files in a directory from root to otheruser. You can change them one by one, but it seems not a very enjoyable task, especially if there are a lot of. It would be great if you could filter the files you need and then change their permissions. Here is when xargs comes into action.

I guess you already know how to select files by the owner and then change permission of each one. If so, it is going to be quite easy to create a routine with xarg. First, you have to create a command that filters that files you need. So easy:

find . -user root

This should prompt all the files owned by root in the current directory and subdirectories. Now with the xargs command we can use each of those files as input for our next command:

chown otheruser {here will be placed each result of previous command}

Now we only have to connect this two simple command with xargs and a pipe this way:

find . -user root | xargs sudo chown otheruser

xargs, the backtick and the dollar syntax

Dollar syntax and backtick is absolutelly the same but dollar is prefereable because it leads to less mistakes on readibility. In some cases you can abbreviate above syntax with backtrick instead xargs like

command1 `command2` = command1 $(command2)

Where each single item of command2 output is treated as the argument of command1. This could be more readable and is preferable in some situations, but there are others whose use it is not possible.

The critical difference between backtick and xargs is that backtick syntax pass the output as it had been typed at the shell instead xargs that pass. This means that some commands like rm that allows varags may use backtick but other no.

Either you can’t put arguments to each iteration like you can in varargs.

And thats all!! A very powerful command that takes advantage of pipes and iteration to simplify

Primeros comandos con Mongo. Operaciones CRUD.

Iniciando el entorno

Para testear Mongo, vamos a usar Docker, creando un contenedor con la ultima imagen de Mongo y entrando en él para comenzar a ejecutar los primeros comandos.

docker container run –name mongo -d mongo
docker container exec -ti mongo bash

Para saber más de como instalar y el manejo básico de Docker, podéis ver las entradas relacionadas con Docker en este blog.

Creando los primeros documentos

Mongo es un gestor de bases de datos no relacionales. Las bases de datos en Mongo están compuestas por colecciones, que a su vez están compuestas por documentos.

Vamos a crear una base de datos llamada test y una colección inventory de documentos.

Para crear una base de datos nueva o cambiar a una existente, usamos el comando use. De esta forma, para crear una base de datos llamada test, escribiremos en la consola de Mongo use test.

Con la imagen anterior podemos darnos cuenta de algunas cosas, además de aprender algun comando útil de la shell de mongo:

  1. El comando para cambiar de base de datos, así como para crear una nueva es use.
  2. Para mostrar las bases de datos usamos el comando show seguido por el subcomando databases. No se muestran las bases de datos que no tengan colecciones en su interior, como la que acabamos de crear.
  3. Para insertar un conjunto de documentos en una colección no es necesario crear previamente la colección (aunque también existe la posibilidad de crear la colección vacía).
  4. Tras el comando ejecutado con éxito se nos muestra un resumen de los documentos creados y el id asignado por Mongo a cada uno de ellos.
  5. Para mostrar las colecciones dentro de la base de datos que estamos usando, usamos nuevamente el comando show seguido por el subcomando collections.
  6. Ahora, al tener una coleccion la base de datos que creamos al principio, se nos muestra al ejecutar el comando correspondiente.

El script inicial para insertar documentos está sacado de la documentación oficial de Mongo.

Operaciones CRUD

CRUD es el acrónimo de las palabras inglesas Create,  Read, Update y Delete.

Vamos a ver la sintaxis básica de cada una de estas operaciones en Mongo

Create

Ya vimos de pasada en el punto anterior para crear colecciones y documentos el procedimiento insertMany(), pero ¿y si queremos insertar sólo un documento? ¿Y si queremos guardar un documento en una variable y luego introducirla en la colección? Podemos hacer ambas cosas.

Los documentos de Mongo siguen una estructura determinada, fuertemente influenciada por JSON, con lo que un documento no es más que una sucesión de pares clave-valor, donde el valor puede ser cualquier tipo de datos BSON, incluyendo otros documentos o arrays. Eso si, todas las claves son de tipo String.

Veamos un ejemplo sencillo de introducción de un único documento. Ya que Mongo permite registros/documentos duplicados (asigna un id diferente a cada uno de ellos internamente que actúa como una suerte de clave primaria), vamos a usar el primer documento introducido en el paso anterior para ejemplificar el proceso.

Vemos como efectivamente, podemos crear una variable a la cual asignar un documento para despues introducir dicho documento en una colección, aunque ya lo hayamos introducido antes. Mongo se encargará de asignarle un ObjectId distinto a cada uno de ellos.

Read

El comando find se encarga de devolver los documentos en funcion de unas condiciones y mostrarlos, si así se desea, de distinta manera, por ejemplo, ocultando algún par clave-valor.

La sintaxis del comando es:

db.<collection>.find(filter, projection);

Ambos parámetros son opcionales, ¿pero qué significan cada uno de ellos?

El parámetro filter es el filtro por el que van a pasar los documentos de la colección. Un ejemplo podría ser: muestrame los documentos que tengan como valor de la clave «qty» el número 25.

El parámetro projection se encarga de mostrar o ocultar los pares clave-valor del documento. Por defecto muestra todos, pero si queremos filtrar la información podríamos hacerlo mediante este parámetro de forma que podríamos ampliar la sentencia anterior como: muestrame sólo los tags de los documentos que tengan como valor de la clave «qty» el número 25.

En definitiva, podríamos decir que el query es el where y el projection el select de una consulta MySQL.

Importante: Los parámetros de las funciones de mongo van entre llaves y separados por comas. Si queremos obviar un parámetro basta con poner dos llaves que no encierran nada.

  1. Seleccionamos los documentos con la clave qty igual a 25.
  2. Seleccionamos los documentos con qty igual a 25 y mostramos sólo sus tags. Como por defecto aparece el id de cada documento, tenemos que decir explícitamente que no lo deseamos mostrar.
  3. Si no queremos filtrar pero si mostrar sólo los tags de los documentos, dejamos el primer parámetro vacío.

Daros cuenta de que las llaves pueden ir entre comillas o sin comillas.

Si quisiéramos añadir una condición de filtro más compleja, la cosa cambia. Por ejemplo, comencemos por una condición de filtro basada en mayor o igual que un valor. Si antes simplemente poníamos dos puntos y el valor por el que queríamos filtrar, ahora ese valor deja de ser único e implica crear una condicion más compleja que un mero equals. Estas condiciones más complejas van encapsuladas entre llaves, como un objeto cuya clave es el operador y su valor, el valor de la condicion. La sintaxis sería:

db.<collection>.find({key:{$operator:value}}, projection);

En este enlace de la documentación oficial podemos ver cuales son los operadores que existen para filtrar y proyectar/seleccionar.

¿Y si queremos añadir condiciones de filtro? Por ejemplo, que el campo qty sea mayor que 10 o no tenga el tag «blue».

El parámetro query se puede expresar también como un objeto que tiene como clave un operador lógico y como valor un array de objetos que contienen los campos y sus condiciones. Se autoexplica con la sintaxis y un ejemplo.

db.<collection>.find({$logical_op:[{key:{$operator:value},key:{$operator:value}}]}, projection);

En la shell de mongo resulta complicado ejecutar consultas complejas, sobre todo las primeras veces debido a lo poco legible que resulta el formato JSON en una única linea. Si usamos la tecla enter podemos comenzar a tener un código más legible.

Nota: El operador lógico and se puede obviar, ya que es el operador por defecto. Simplemente poniendo en el parámetro query el conjunto de campos y los valores por los que filtrarlos, obtenemos el mismo resultado que haciendo explícito el operador and. Estas dos expresiones son equivalentes:

  • db.inventory.find( { $and: [ {qty:{$gt:10}}, {tags:{$in:[«blue»]}} ]});
  • db.inventory.find( {qty:{$gt:10},tags:{$in:[«blue»]}});

Update

Para actualizar documentos usamos los procedimientos updateMany(), updateOne() o replaceOne(). La sintaxis de todos ellos es la siguiente:

db.<collection>.replaceOne(<filter>, <update>, <options>);

  1. El parámetro filter funciona de la misma forma que en la query, filtrando los documentos en función de su campos y valores.
  2. El parámetro update es el encargado de cambiar el valor de una serie da campos que consideremos por otros valores que consideremos.
  3. El parámetro options es una serie de pares clave-valor en un objeto con propósitos muy diversos, que por simplicidad, no vamos a tratar.

Ya sabemos filtrar documentos por haberlo tratado en el apartado correspondiente a la consulta de documentos. El segundo parámetro (encargado de la actualización), es el más interesante en este caso. Podemos ver todos los operadores de actualización con una breve descripción aquí.

Veamos un pequeño ejemplo. Si queremos quitar el campo tags y setear a 0 la cantidad, a todos los documentos con una cantidad inferior a 30 unidades, podríamos poner esta expresión en la shell:

db.inventory.updateMany({qty:{$lte:30}},{$unset:{tags:»»}, $set:{qty:0}})

Existen muchos más operadores para actualizar los campos o los valores de los campos, pero por la simplicidad de la entrada y por existir una excelente documentacion oficial, no nos vamos a extender en este punto.

Delete

Si queremos eliminar un documento de una colección, al igual que a la hora de crearlos, tenemos dos procedimientos en función de si queremos eliminar uno o varios: deleteOne() y deleteMany().

El procedimiento tiene dos parámetros. El primer parámetro es el parámetro filter que ya hemos visto en los dos tipos de operaciones anteriores. En funcion de los documentos filtrados, podemos, en el segundo parámetro especificar una serie de opciones para el procedimiento, siendo este, nuevamente opcional.

No vamos a incidir más por tanto en este procedimiento ya que ya hemos visto cómo funciona el parámetro filter.

Volúmenes y datos persistentes en Docker

Hemos creado un contenedor a partir de una imagen, pero no es un contenedor cualquiera. Este contenedor maneja datos, ficheros y cambia constantemente. Tenemos un pequeño problema.

Hemos dicho en otras entradas, que los contenedores son objetos efímeros. Se crean y se destruyen. En su naturaleza está escrita su eventual parada por múltiples motivos: errores internos, balanceo de red, etc. Sin embargo los contenedores pueden exponer servicios que no tengan en cuanta la propia naturaleza de los contenedores. Si tenemos una aplicación que se conecta a una base de datos, no podemos permitir que el contenedor en el que está la base de datos desaparezca y con él todos los cambios.

Los contenedores crean una nueva capa sobre la imagen que utilizan y solo sobre esta ultima capa pueden escribir y modificar cosas, pero una vez el contenedor desaparece, esa capa no persiste, desaparece con él. ¿Cómo hacemos entonces si queremos guardar esos cambios?

Los volúmenes y los puntos de montaje

Es necesario conocer la naturaleza de Docker un mínimo para comprender este punto. Docker usa namespaces únicos para cada contenedor, y cada vez que lanzamos un contenedor, por defecto, crea una serie de namespaces propios ajeno a los del sistema host y un volumen. Uno de los namespaces es el namespace «mount»

El namespace mount es el encargado de manejar los puntos de montaje de un FileSystem. Esto nos permite que los contenedores estén completamente aislados y no puedan acceder a los puntos de montaje del host, pero tambien nos permite montar determinadas rutas del host en los contenedores (a esto se le denomina bind mount).

En esta imagen podemos apreciar cómo los volúmenes no dejan de ser áreas del FileSystem del host, el cual tambien puede tener rutas montadas en el contenedor. Los volúmenes son un objeto de Docker, igual que las redes, los contenedores, las imágenes o las etiquetas de contenedores, y se puede trabajar con ellos de forma aislada. Es importante tener en cuanta que con el borrado de un contenedor no se borra el volumen asociado.

Los volumenes

Existen determinadas imágenes que por los procesos que representarán sus contenedores, generan volúmenes automáticamente. Por ejemplo, si exploramos el dockerfile de mysql, veremos como contiene una sentencia VOLUME, que se encarga de crear un volumen y vinvularlo con una ruta del futuro contenedor.

Una vez creado el contenedor de la imagen de mysql, podemos inspeccionarlo (subcomandos container isnpect) y ver en su apartado Mounts, que tiene un volumen asociado.

El atributo source es la ruta de host y destination la ruta del contenedor. Si exploramos las carpetas desde el host y el contenedor respectivamente veremos exactamente los mismos datos.

El hecho de que un volumen sea un objeto separado de un contenedor en Docker, implica que un mismo volumen puede ser usado por más de un contenedor, incluso simultáneamente. Vamos a ver en un ejemplo este caso.

Como vemos arriba, no se ha creado un nuevo volumen al crear el segundo contenedor, ya que especificamos con el parámetro -v un volumen preexistente mediante el nombre, el cual se puede setear concatenando antes de la ruta destino de montaje del volumen en el contenedor y separado por dos puntos.

Los volúmenes, a diferencia de los bind mounts que veremos a continuacion pueden crearse desde el Dockerfile o en tiempo de creación del contenedor con el parámetro anteriormente mencionado.

Los binding mounts

Aparte de los directorios/volúmenes del host que podemos montar en los contenedores, podemos montar otras ubicaciones de nuestro host dentro del contenedor con los llamados binding mounts.

Estos binding mount funcionan de forma muy similar a los volúmenes del apartado anterior, pero tienen una serie de características propias.

  1. Como ya hemos dicho, los directorios están ubicados fuera de la ruta /var/lib/docker/volumes, en nuestro host.
  2. El montaje sobre un directorio del contenedor que no está vacío, reemplaza el contenido original del contenedor.
  3. Los cambios ejecutados desde el contenedor sobre los archivos del bind mount reemplazan, por defecto, los archivos originales del host. A todos los efectos, tanto Docker como el host están sincronizados en el acceso a una determinada ruta. Los puntos de montaje de Docker no se comportan como la capa superior que crean los contenedores durante su ejecución. No son una capa más de la imagen, son la propia ubicación del host.
  4. Mucho cuidado con el montaje de rutas críticas del host en contenedores, pues estos ejecutan procesos como usuario root, lo que implica un escalado de privilegios inmediato para cualquier usuario con permiso de ejecucion de Docker. Ver este artículo.

Como ejemplo, si quiero vincular una carpeta local del host que sirva como carpeta de un contenedor nginx, sólo tengo que usar el parámetro anteriormente visto para los volúmenes a la hora de crear dicho contenedor, con la salvedad de que tengo que usar la ruta completa o relativa a la izquierda de los dos puntos para mapear las localizaciones. Puedes ver cómo en la documentación de la imagen oficial de nginx en Docker Hub.

Otra forma de hacer los bind mounts

Además de usar el parámetro -v a la hora de lanzar el contenedor, podemos hilar más fino a la hora de crear estos puntos de montaje de los contenedores. El parámetro -v crea un punto de montaje con unos atributos por defecto, pero podemos cambiar estos atributos con un parámetro más verboso: –mount.

Dejo el enlace de la documentacion oficial de Docker para que podais bucear un poco más al respecto. 

Entender IPv6

Con la llelgada del famoso IoT (Internet of Things), se han multiplicado enormemente las direcciones IP necesarias para la identificación de dispositivos. Lo que parecía que podía durar para siempre hace 20 años con el protocolo IPv4, ha sido superado por la propia relaidad del desarrollo tecnológico. Cada vez más los dispositivos se conectan a la red global mediante este protocolo, y no tardaremos muchos años en ver una implementacion total para todos los dispositivos. Si has pasado horas entendiendo el protocolo IPv4, vete olvidando de él porque hay un nuevo muchachito en la capa de red del modelo OSI.

Manejar el direccionamiento de red con IPv6 no es complicado. Puede parecer ininteligible a primera vista, por la presencia de unos dobles dos puntos, hextetos variables, bits que no se saben donde están… pero es realmente sencillo.

Para comprender IPv6 vamos a dividir la entrada en dos grandes apartados: construcción de una dirección, y explicacion de cada uno de los hextetos.

Construcción de una dirección

Tengamos en cuenta las siguientes consideraciones iniciales:

  1. Una direccion IPv6 consta de ocho hextetos (128 bits agrupados en 8 grupos de 16 bits).
  2. Cada hexteto está separado por dos puntos de otro hexteto y todas las direcciones notan su red mediante CIDR (más comunmente conocido como mascara de red).
  3. Cada hexteto está representado hexadecimalmente.

Por tanto, las siguientes direcciones son direcciones IPv6 válidas:

Pero no las encontraremos nunca así. A partir de estas reglas se aplican otras para simplificar la representación de estas direcciones:

  1. Un hexteto nulo (todo ceros) o más hextetos nulos consecutivos se pueden representar como dos puntos dobles.
  2. Los ceros de los hextetos a la izquierda (menos significativos) se pueden obviar.
  3. En una misma direccion no pueden haber más de dos puntos dobles (podriamos no saber cuántos hextetos hay escondidos en cada uno de ellos para que la dirección sume 8 hextetos).

Veamos como con estas reglas podemos simplificar las direcciones anteriores.

Hextetos y dirección de red mediante CIDR

Es necesario simplificar y vendarse los ojos ante una mayor complejidad en este aspecto. Vamos a asumir que estamos trabajando con direcciones Global Unicast, es decir, direcciones de Internet. Si quieres profundizar un poco más en este aspecto, existen recursos muy interesantes en la red como este video donde explican precisamente esto con más detalle.

Ahora que sabemos cómo construir una dirección IPv6 y su notación, tenemos que saber que significan cada uno de sus hextetos en función de la máscara de red.

En una dirección IPv6 Global Unicast, existen tres grupos de hextetos configurados por defecto de la siguiente forma: los tres primeros hextetos especificarán el prefijo de red, el cuarto octeto, la subred, y los últimos cuatro octetos especificarán la dirección de la interfaz. Se suele denominar la regla 3-1-4. Hemos dicho que así es como se suelen configurar por defecto, pero no siempre es así.

Los tres primeros octetos (Global Routing Prefix) nos serán dados por nuestro proveedor de internet (ISP), al cual se le asigna un abanico de direcciones por parte de la entidad reguladora de IPv6. Estos tres primeros octetos son inamovibles, siempre serán 3, por lo que por defecto, una direccion IPv6 Global Unicast tendrá como minimo un prefijo de red de /64 (/48 de red global + /16 de subnet).

Sin embargo, el prefijo de subred se puede incrementar, dando lugar a un aumento de subredes, en detrimento del números de interfaces, tal y como pasaba en IPv4.

Veamos un ejemplo:

¿Quá pasa en local?

Pues en local pasa lo mismo, pero existen unas direcciones específicas, como en el anterior protocolo, que son necesarias tener en cuenta:

  1. ::1/128 está reservada para el loopback del localhost.
  2. fe80::xxxx:xxxx:xxxx:xxxx/64 define una interfaz accesible en nuestra red local. Se suele usar para redes tras una interfaz NAT, por ejemplo, la red LAN que se ubica tras nuestro router a internet.

Podeis ver otras IPv6 reservadas en este link.

Los contenedores en Docker

Los contenedores en Docker

Si ya tenemos Docker instalado en nuestro Sistema Operativo, ya podemos empezar a usar contenedores.

En este ejemplo vamos a instalar un contenedor con una imagen de Nginx. Es importante conceptualizar que un contenedor es una instancia de una imagen corriendo como un proceso, de forma que lo que tendremos es un proceso en nuestro sistema operativo ejecutado por Docker que nos proveerá todas las funcionalidades de Nginx. Esto quiere decir que podemos tener un numero indefinido de contenedores corriendo la misma imagen. Por ejemplo, podemos tener tres contenedores corriendo simultáneamente una imagen de Nginx en puertos distintos e independientes entre si. Como símil podemos entender las imagenes como .isos de una máquina virtual (elementos estáticos) y los contenedores como una instancia concreta de esa imagen corriendo como un proceso independiente de nuestro Sistema Operativo.

Vamos a gestionar poco a poco un contendor para ver las opciones básicas que tenemos y levantar un servicio nginx en medio minuto.

Vamos a ver qué implican las acciones marcadas con los numeros.

  1. Decimos a docker que cree un contenedor con la imagen llamada nginx y que sea accesible a traves del puerto 80 del host (veremos más adelante esto ultimo).
  2. Docker nos dice que el puerto 80 está ya ocupado en el host.
  3. Paramos el servicio que ocupa el puerto 80 previamente en nuestro host.
  4. Listamos las imágenes en el registro local (la descargamos en un inicio del registro en internet o docker hub).
  5. En las siguientes ordenes experimentamos con el modo attached. El modo attached es resumidamente, un modo mediante el cual la consola está vinculada al proceso, y podemos ver en tiempo real sus logs. Una vez salimos del modo attached mediante Ctrl+C, el proceso/contenedor se destruye.
  6. Si queremos llevar a un segundo plano la ejecucion del proceso, tenemos que añadir el argumento ­­detach al comando.

Si recuerdas, al comando docker container run le hemos añadido un argumento ­­publish. Este argumento es esencial para comunicarnos con el contenedor desde el host. Comunica el puerto 80 del container con el puerto 80 del host de forma que cualquier peticion al puerto 80 del host se derivará al puerto 80 del contenedor. La sintaxis del argumento es ­­publish hostport:containerport. Nota: Puedes ver más en detalle como funciona la publicación de puertos entendiendo Docker Network.

Además podemos modificar el nombre del contenedor  que se va a ejecutar. Por defecto Docker nos pone nombres aleatorios graciosos, pero podemos forzar un nombre con el argumento ­­name. Otra funcionalidad con la que podemos experimentar es la de las versiones de las imagenes que descargamos. Por defecto Docker si no tenemos una imagen concreta, nos descargará la ultima versión, pero podemos variar esto haciendo explicita esta característica. Veamos todos estos comandos juntos para ver como trabajan.

Podemos ver como en el primer comando seteamos unos determinados argumentos, tales como el nombre del contenedor, la versión de la imagen de nginx, el y el modo detach.
En este caso concreto, las peticiones al puerto 81 del host irían al puerto 80 del container que es donde escucha nginx por defecto. Cuando listamos tanto las imágenes como los contenedores en ejecución podemos ver todos los atributos y cómo cambian rgacias al seteo de los argumentos.

Hasta ahora solo hemos puesto un contenedor en marcha, pero como ya hemos dicho anteiriormente, desde una imagen podemos instanciar varios contenedores. Veamos tres contenedores de nginx corriendo comunicandose con el host a traves de los puertos 80, 81 y 82.

Ahora si quisiéramos borrar los contenedores de prueba solo tendríamos que usar un sencillo comando: docker container rm -f .