Renombrar volumen Docker

El cliente docker tiene un conjunto de opciones destinadas a administrar volúmenes (docker volume), pero ninguna de ellas permite renombrar un volumen existente.

Me puse a investigar cómo hacerlo y me fui a ver el filesystem y a buscar la carpeta que representa el volumen y hacer el cambio allí, pero cuando investigaba para no dañar la configuración me encontré con esta solución que me pareció muy filosofía docker, inclusive es lo que se hace para copiar un disco en una nueva unidad:

docker volume create --name <nuevo_volumen>
docker run --rm -it -v <viejo_volumen>:/from -v <nuevo_volumen>:/to alpine ash -c "cd /from ; cp -av . /to"
docker volume rm <viejo_volumen>

En definitiva se trata de crear un nuevo volumen, lanzar un contenedor que monta el volumen viejo y el volumen nuevo y copia los datos de uno a otro, y finalizar borrando el viejo volumen. Simple, seguro, y elegante.

La idea no es mía, la obtuve de este issue de Github, donde incluso alguien propone un script:

$ docker-rename-vol viejo-volumen nuevo-volumen

#/bin/bash
docker volume create --name $2
docker run --rm -it -v $1:/from -v $2:/to alpine ash -c "cd /from ; cp -av . /to"
[ $? -eq 0 ] && docker volume rm $1

Y ahora, está aquí mi blog para tenerlo presente.

Sobre escribir variables al invocar el shell script

Todos utilizamos variables de tipo constantes en nuestros shell scripts, que en principio no son modificables pues están en el código, un ejemplo simple:

#!/bin/bash
HOLA="Hola, soy el script"
echo $HOLA

así en cada ejecución de este script se mostrará el contenido de la variable HOLA:

$ ./hola
hola soy el script
$

Pero al definir/crear la variable podemos hacerlo con un contenido por defecto, que se usa en caso que la variable no tenga otro valor, mediante esta sintaxis de definición:

HOLA=${HOLA:-"Hola, soy el script"}

de esta forma HOLA tomará el valor que ya traiga o, en caso de ser nula, asignará el string indicado luego del :-

Esta sintaxis para la definición de las variables en nuestros script permite que se le pueda cambiar el valor al invocar el script, así:

$ HOLA="Hola, soy el shell" ./hola
Hola, soy el shell
$

También esta sintaxis de valor por defecto de las variables puede ser asignado directamente al invocarlas, utilizando una sintaxis equivalente:

#!/bin/bash
echo ${HOLA:-"Hola, soy el script"}

lo que simplifica el script, aunque puede distribuir los valores de las variables a lo largo y ancho del código así que a usarlo con cuidado.

Obviamente es algo muy documentado y explicado, aquí unos ejemplos:

Ejecutar un script bash remoto sin instalar

Me ha sido muy útil ejecutar scripts remotos sin instalarlos localmente. Esto me permite, por ejemplo, hacer la instalación inicial del cliente Puppet o poner Ansible para completar la configuración del sistema hasta llevarlo a estado de producción.

Para descargar el script se puede usar tanto el comando curl como wget. Uno u otro suelen venir instalados por defecto en cualquier distribución Linux.

La idea es simple: correr el comando (wget o curl) y obtener la salida (script) limpia (es decir, sin datos extra de transferencia o ejecución) y pasarlo como entrada a bash para su interpretación y ejecución local.

He armado un simple script, cuyo código puede ser visto aqui: script-remoto.txt (la terminación txt es solamente para que lo muestre el navegador, pero no necesita ninguna extensión en particular), que puede ser ejecutado con cualquiera de estos comandos:

con curl:

source <(curl -s http://pilas.guru/wp-content/uploads/script-remoto.txt)

bash <(curl -s http://pilas.guru/wp-content/uploads/script-remoto.txt)

curl -s http://pil.as/1h1n | source /dev/stdin

curl -sL http://pilas.guru/wp-content/uploads/script-remoto.txt | bash -s

con wget:

source <(wget -qO- http://pilas.guru/wp-content/uploads/script-remoto.txt)

bash <(wget -qO- http://pilas.guru/wp-content/uploads/script-remoto.txt)

wget -qO- http://pil.as/1h1n | source /dev/stdin

wget -qO- http://pilas.guru/wp-content/uploads/script-remoto.txt | bash -s

Pueden ver que he creado un enlace corto que redirecciona al mismo archivo http://pil.as/1h1n, pero ATENCION, no se debe confiar en los enlaces cortos livianamente y MENOS con la intención de ejecutar comandos ajenos en el equipo propio.

Retorno de valores en funciones de Bash

El lenguaje de scripting de bash permite el uso de funciones que deben estar declaradas siempre antes de ser llamadas, pero a diferencia de otros lenguajes, no permite retornar valores. Siempre que una función de bash finaliza devuelve el valor de estado de salida, RC o $?, que es cero en caso de ejecución correcta y cualquier otro valor en caso de error.

Aquí se describen algunas formas de retornar valores. Se suelen usar variables globales, pero también se pueden usar sustitución de comandos o se puede pasar una variable donde el resultado estará cargado.

Variable global

El método común y más simple es cargar una variable global, pues en bash todas las variables son globales (salvo definición explícita de local):

function mifuncion()
{
    RESULTADO='un valor'
}

mifuncion
echo $RESULTADO

Simple y claro. Pero las variables globales se vuelven complejas de manejar en códigos extensos, principalmente a la hora de encontrar errores de ejecución.

Entonces es posible usar variables locales a las funciones, pero el problema es devolver el valor al código que llamó a la función.

Sustitución de comando (STDOUT)

function mifuncion()
{
    local  miresultado='un valor'
    echo "$miresultado"
}

RESULTADO=$(mifuncion)   # o RESULTADO=`mifuncion`
echo $RESULTADO

Aquí el resultado se muestra en STDOUT y se produce la sustitución del comando al llamar a la función para cargarlo en una variable.

Pasar una variable (eval)

Otra forma de pasar un resultado es escribir la función para que reciba una variable al ser llamada y entonces cambiar el valor de la variable como resultado de la función:

function mifuncion()
{
    local  __resultadovar=$1
    local  miresultado='un valor'
    eval $__resultadovar="'$miresultado'"
}

mifuncion RESULTADO
echo $RESULTADO

Aqui lo que hacemos es guardar el nombre de una variable en una variable; por eso no podemos configurar la variable directamente, necesitamos usar eval para configurarla. El comando eval le dice a bash que interprete la linea dos veces: la primera vez devuelve el string miresultado=’un valor’, el cual es interpretado una vez mas para obtener el valor de la variable.

Cuando se guarda el nombre de una variable que se pasa en al linea de comando, debemos asegurarnos que es guardado en una variable de tipo local (local __resultadovar=$1) con un nombre que no sea usado cuando se llama a la función (por eso el nombre __resultadovar). Si no tenemos cuidado con esto, y se usa el mismo nombre de variable, la variable de resultado no será cargada; por ejemplo, lo siguiente no funciona:

function mifuncion()
{
    local  RESULTADO=$1
    local  miresultado='un valor'
    eval $RESULTADO="'$miresultado'"
}

mifuncion RESULTADO
echo $RESULTADO

La razón por la que no funciona es que cuando eval realiza la segunda interpretación evalúa RESULTADO=’un valor, pero RESULTADO es en ese momento una variable local a la función, y entonces queda establecida así, en lugar de configurar la variable que se pasa al llamar a la función.

Otra precaución para evitar este funcionamiento no deseado, es utilizar variables en mayúsculas cuando son globales y minúsculas cuando son locales.

Para mayor flexibilidad, las funciones pueden ser escritas para combinar ambos métodos:

function mifuncion()
{
    local  __resultadovar=$1
    local  miresultado='un valor'
    if [[ "$__resultadovar" ]]; then
        eval $__resultadovar="'$miresultado'"
    else
        echo "$miresultado"
    fi
}

mifuncion RESULTADO
echo $RESULTADO
RESULTADO2=$(mifuncion)
echo $RESULTADO2

Entonces, si ninguna variable es pasada a la función, el valor de salida es colocado en STDOUT.

Basado (casi traducción textual) del artículo del 2009 de Mitch Frazier, publicado en Linux Journal con el título «Returning Values from Bash Functions«, pues ha sido el artículo con el que comprendí claramente cómo gestionar las funciones en bash.

Copiar base de datos MySQL de un servidor a otro

Tengo un servidor en producción por allá en la nube y quiero tener otra réplica (asincrónica) de algunas bases de datos MySQL. Buscando la solución más sencilla y rápida llegue a este script que se ejecuta en el servidor de réplica y que trae la base de datos remota y la deja activa en el MySQL local.

ssh shelluser@server.remoto.com «mysqldump -uusuario -pclave123 basedatos | gzip -c1» | gunzip -c | mysql -uroot -ppassword basedatos

Los comandos ente comillas mysqldump -uusuario -pclave123 basedatos | gzip -c1 se ejecutan en el servidor remoto mediante la conexión ssh que debe poder realizar el usuario ‘shelluser’, para sacar por estándar output el dump compactado de la base de datos.

Localmente se recibe por estandar input con los comandos gunzip -c | mysql -uroot -ppassword basedatos, que descomprimen y ejecutan los comandos del dump en el mysql local contra la base de datos que se indica.

El gzip y el gunzip es opcional y lo que busca hacer es comprimir el tráfico que se transfiere mediante SSH, y solo sería necesario si la base que se está bajando es de cierto volumen.

Automatizando SSH

Algnas veces me ha tocado escriir líneas de ssh bastante complejas, como por ejemplo:

ssh -1 -p 23 -l root -i /etc/ssh/key01 -o CheckHostIP=no 192.134.27.24

que realmente terminan siendo un incordio, cuando esa conexión debe ser vuelta a realizar para repetir tareas de administración remota. Entonces, este artículo es un resúmen de algunos trucos de automatización que he logrado aprender hasta ahora, para hacer mi ssh más fácil y mi vida también.

1. el nombre

Escribir la IP 192.134.27.24 cada vez que me conecto es antidiluviano y no escala a IPv6. Así que lo ideal es disponer de un DNS bien configurado al cuál recurrir.

Lamentablemente en redes internas he visto que asignan nombres a las máquinas que resultan más difíciles que la dirección IPv4 misma, por ejemplo: sssd2p1r3db.sss.com, que significa sss es el nombre de la empresa, datacenter 2, piso 1, rack 3, db pues es el server de base de datos. Fantástico! Cómo no recordarlo? Claro, despues uno ve que terminan usando la IP.

Así que llamemos a nuestro servidor 192.134.27.24 sssd2p1r3db como «tito» (o cualquier otro nombre que me resulte fácil de recordar)

Para que funcione el ssh a tito debo llegar a la IP. Y esta tarea de personalización de los nombres en la red la hace el archivo /etc/hosts. Edito el archivo y agrego:

# Servidor sssd2p3r3db
192.134.27.24 tito

A partir de ahora, todo lo que yo haga a nivel de red con el nombre «tito» va a funcionar desde ssh tito, ping tito, hasta http://tito en el Firefox.

2. las opciones ssh

Las opciones del ssh que son necesarias para esta conexión de ejemplo son: -1 -p 23 -l root -i /etc/ssh/key01 -o CheckHostIP=no, que no es necesario explicar su significado, pues todas estas opciones están a un man de distancia.

El archivo que permite automatizar todo esto para todos los usuarios es /etc/ssh/ssh_config y para un usuario en específico es el archivo $HOME/.ssh/config. Edito el archivo y creo una entrada Host acorde a mis necesidades:

Host tito
Protocol 1
Port 23
User root
IdentityFile /etc/ssh/key01
CheckHostIP no

Y ya está!

Ahora puedo escribir ssh tito y mi conexión va a ser realizada exactamente como si escribiera toda la linea del principio.

Solo una aclaración: la sentencia Host funciona por string, es decir que si escribo ssh 192.134.27.24 no va a producir el mismo efecto de levantar los datos del ssh_config, aunque me esté conectando al mismo servidor.

3. Bash completion

Y por último, hacer que mi Bash permita usar tabulador para los nombres caprichozos y fáciles que he colocado a mis servidores… porque?, a ver: nadie pretende que me acuerde que el server se llama «tito», es mucho más acordarme que empieza con «t» y sigue con «algo» 😉

Edito ~/.bashrc y agrego:

_completossh ()
{
actual=${COMP_WORDS[COMP_CWORD]};
COMPREPLY=($(compgen -W ‘$(cat /etc/ssh/config | grep «^Hostb» – | sed -e «s/Host //»)’ — $actual))
}
complete -F _completossh ssh

Y ahora puedo escribir ssh t + tabulador, y me completará «tito». Y si tengo más de un servidor que comience con «t» pues dar dos veces tabulador y me muestra la lista.

4. Algunos documentos para leer del tema