Mostrando entradas con la etiqueta sysadmin. Mostrar todas las entradas
Mostrando entradas con la etiqueta sysadmin. Mostrar todas las entradas
Puppet Types y Providers
Todos los que utilizamos puppet en algún momento nos encontraremos con la necesidad de crear un módulo que no se puede implementar sólo utilizando los recursos existentes. Es en ese momento donde surge la necesidad de crear nuestro propio tipo.

Puppet provee la posibilidad de crear nuestros propios tipos, desarrollados en ruby. Contar con la posibilidad de programar expande muchísimo nuestro horizonte de cosas que se pueden hacer con puppet.

Desde mi punto de vista, ruby es horrible para programar, pero es la única opción que existe actualmente en puppet, así que no nos quedará otra que aprender aunque sea las bases de este lenguaje.

Puppet separa la definición de un recurso en dos partes: types y providers
  • El archivo type define todas las propiedades y parámetros que se pueden usar en nuestro recurso. Es decir, define la estructura del recurso y como interactuaremos desde un manifest con él. 
  • Por otra parte, el provider se encarga de implementar el type que definimos.

Algo interesante es que podemos tener múltiples providers para el mismo type. El ejemplo más claro de esto es la implementación del type package. Existen varios providers para este tipo: apt, yum, pacman, etc. La forma en que interactuamos con el type es independiente de cómo luego se implemente.

Como todo en puppet, es importante la estructura y los nombres de nuestros directorios y archivos. Para definir nuestro type y provider, hay que seguir el siguiente árbol:
module
  |-- metadata.json
  |-- README.md
  |-- manifests
  |-- lib
       |-- puppet
             |-- provider
             |     |-- configjs
             |           |-- configjs.rb
             |-- type
                   |-- configjs.rb


La idea es que la explicación sea bastante dinámica, así que plantearé un ejemplo que seguiremos a lo largo del artículo.


Crear nuestro type

Como mencioné, los types deben ubicarse en el path <module name>/lib/puppet/type/<nombre del tipo>.rb para satisfacer la nomenclatura de puppet.

Por ejemplo, voy a definir un tipo para administrar configuraciones en formato JSON. Llamaré al tipo configjs:
  #configjs/lib/puppet/type/configjs.rb
  Puppet::Type.newtype(:configjs) do
    @doc = "Manage json configs"

    ensurable

    newparam(:path, :namevar => true) do
      desc "Path to the JSON File"

      newvalues(/\A\/\w+/)
    end

    newparam(:showdiff) do
      desc "Show a diff when changing values"

      defaultto :true
      newvalues(:true, :false)
    end

    newproperty(:config) do
      desc "The configuration to set"

      isrequired

      def insync?(is)
        is.sort == should.sort
      end
    end
  end
  
La forma de invocar un tipo es la misma que cuando lo definimos con define en un .pp. Este tipo se utilizaría de la siguiente manera en un manifest:
  $myconfig = {
    'name' => 'itfreek',
    'url' => 'http://itfreekzone.blogspot.com'
  }
  configjs {'/etc/myconfig.js':
    config => $myconfig
  }
  
Veamos un poco qué significa cada parte del tipo configjs.


Propiedades y parámetros

Para empezar declaramos un parámetro (path) y una propiedad (config). Si bien lucen similares, la diferencia entre ambos es muy importante:
  • El parámetro se utitliza para designar un valor desde puppet. En el ejemplo, path define la ubicación del archivo en el servidor. 
  • Una propiedad es algo que se puede inspeccionar y cambiar. La misma cuenta con dos valores. Por un lado, tenemos el valor que se indica desde puppet, denominado "should", y por otro el valor que tiene actualmente en el sistema, denominado "is". En el ejemplo, una config en el sistema será el JSON contenido en el file, que pudo haber sido modificado manualmente (esto es el "is"). Si puppet setea un JSON, este es el valor que esa propiedad debe tener, y por eso se denomina "should". La diferencia entre ambos valores es lo que puppet muestra al momento de ejecutar una corrida.


Namevar

Otro punto destacable es el uso de ":namevar => true" en el parámetro path. Como saben puppet usa el concepto namevar para distinguir un recurso en el sistema, y es el valor que asignamos al crear el recurso. En nuestro caso, el path es el nombre que nos permite distinguir un file de otro, lo mismo que sucede con el recurso file. En dicho ejemplo, el valor que tendrá el path es /etc/myconfig.js y será el nombre de nuestro recurso.


Otras opciones

Finalmente también encontramos "isrequired" y "defaultsto". Con el primero indicamos que la propiedad es requerida, es decir, si no se setea puppet dará error. El segundo indica cuál es el valor default que tomará el parámetro si no se especifica ninguno.


Validación y munging

Veamos el uso de newvalues. Con este método podemos restringir qué valores son aceptables para el parámetro. Podríamos incluir una lista de valores (newvalues(:true, :false)), o, como hice yo, una expresión regular a matchear. Más adelante veremos cómo invocar funciones a partir de valores con ensurable. Si el valor otorgado no cumple con esta restricción, puppet dará error.

Las otras opciones que tenemos son validar el input (validate) y transformar (o sanitizar) el valor (munge).

Por ejemplo, si queremos validar que el path sea un path absoluto, podemos agregar lo siguiente:
  newparam(:path, :namevar => true) do
    ...
    validate do |value|
      unless Puppet::Util.absolute_path? value
        raise ArgumentError, "archive path must be absolute: #{value}"
      end
    end
  end
  
Por otra parte, si quisieramos setear los permisos del file, podríamos agregar un parámetro mode. Mode debería ser un string en la definición del recurso, pero nosotros necesitamos que sea un integer en notación octal para poder setearlo en el filesystem (ej: 0644). Podríamos hacer esto con munge:
  newparam(:mode) do
    munge do |value|
      value.to_i(8)
    end
  end
  


Providers

Ahora que contamos con la definición del tipo, pasemos al provider. El mismo debe ubicarse en /lib/puppet/provider//.rb. Un tipo puede tener varios providers (como package, o vcsrepo).

En nuestro ejemplo, el provider se llamará igual al tipo (aunque podría llamarse diferente) y lo definimos así:

  #configjs/lib/puppet/provider/configjs/configjs.rb
  Puppet::Type.type(:configjs).provide(:configjs) do
    def config
      if File.exists?(resource[:path])
        content = File.read(resource[:path])
        PSON.load(content)
      else
        {}
      end
    end

    def config=(value)
      create
    end

    def exists?
      File.exists?(resource[:path])
    end

    def create
      config_file = File.new(resource[:path], 'w+')
      config_file.write(JSON.pretty_generate(resource[:config]))
      config_file.close
    end

    def destroy
      FileUtils.rm(resource[:path])
    end
  end
  
Acá aparece el hash 'resource'. Este hash contiene los valores para cada uno de los parámetros y las propiedades que se setearon. Dentro del type también se puede acceder a este hash, pero como self (ejemplo: self[:path]).


Definiendo la funcionalidad

Ensurable

Si prestaron atención, el tipo configjs invoca el método ensurable. Un recurso se considera 'ensurable' cuando su presencia se puede verificar. Esto es, si no existe y debería existir (ensure => present), hay que crearlo, mientras que si existe y no debería existir (ensure => absent), eliminarlo. Al realizar esta invocación, deberemos definir tres funciones:
  •  exists? debe retornar true si nuestro recurso existe en el sistema y falso sino.
  • create: crear el recurso si no existe y debería existir.
  • destroy: destruir el recurso si existe y no debería.

En el ejemplo anterior, exists? nos dice si el file existe o no. Si no existe, creamos el archivo con el contenido indicado en la propiedad config. Si queremos destruirlo, simplemente hacemos un rm del file.

Es posible redefinir los métodos invocados al utilizar "ensure => present/absent", e incluso agregar más valores. La forma de hacerlo es la siguiente:
  #configjs/lib/puppet/type/configjs.rb
  Puppet::Type.newtype(:configjs) do
    ...

    ensurable do
      newvalue(:present) do
        provider.generate
      end

      newvalue(:absent) do
        provider.remove
      end

      newvalue(:empty) do
        provider.empty
      end
    end
  end
  

Esto es, indicamos que si ensure es present invoque la función generate en lugar de create, mientras que si es absent invoque remove en lugar de destroy. Además, agregamos el valor emtpy que nos permite invocar la función emtpy en el provider.

Como habrán notado, es posible invocar funciones del provider desde la definición del type.


Refresheable

Ahora, qué pasa si realizamos un cambio en el file desde fuera del resource, como por ejemplo, un exec en un manifest? La simple lógica anterior nos indica que si el file existe en el filesystem, entonces no hay que realizar ninguna acción, no verifica si el contenido del file es el correcto.

Este comportamiento lo cambiaremos con los setters y getters (explicado a continuación), pero hay otra forma si no queremos utilizar propiedades, podemos hacerlo indicando que el recurso es refresheable.

Que nuestro tipo sea refresheable significa que se ejecutará algo con un cambio en la propiedad, o si tenemos una relación notify/subscribe. En el ejemplo podríamos realizar estos cambios:
  #configjs/lib/puppet/type/configjs.rb
  Puppet::Type.newtype(:configjs, :self_refresh => true)) do
    ...
    ...
    def refresh
      provider.create
    end
  end
  
Lo que hicimos es, primero indicar que nuestro tipo es refresheable con ":self_refresh => true". Luego definimos la función refresh, donde indicamos qué se debe ejecutar en un refresh. En nuestro caso lo que hacemos es invocar el método create del provider.

Con esta nueva funcionalidad, además de crearse el file si no existe en el filesystem, también se pisará el contenido si por alguna razón cambia una propiedad o algún otro recurso le notifica que debe hacer un refresh (notify).


Setters y Getters

Siempre que definamos una propiedad, puppet utilizará distintas funciones para determinar si el valor de la propiedad difiere de la que está actualmente en el host. Si este es el caso, seteará el valor que corresponda. Esto es, /etc/myconfig.js tiene el contenido que se indicó por puppet?

Para saber cuál es el valor actual de la propiedad y setear el que corresponde, puppet invoca métodos cuyos nombres son igual a la propiedad. Estos métodos se definen en el provider.

Para el ejemplo anterior, la propiedad se llama config, entonces las funciones get y set se definen de la siguiente manera:
  #Getter: decime qué valor tiene la config ahora en el server
  def config
  end

  #Setter: setea este value como contenido de la config
  def config=(value)
  end
  
Puppet determina si el valor actual es el que debería tener invocando el método "insync?", es decir, esta propiedad está sincronizada? Por defecto esta función realiza una comparación entre el valor actual y el que debería mediante el operador "==". Para comparar strgins va genial, pero si, por ejemplo, estamos comparando hashes, la comparación podría no resultar como esperamos. Para estos casos, podemos redefinir el método insync? en la definición de la propiedad (en el type).

Siguiendo nuestro ejemplo, vemos esta definición:
  #configjs/lib/puppet/type/configjs.rb
  Puppet::Type.newtype(:configjs) do
    ...
    newproperty(:config) do
      ...

      def insync?
        is.sort == should.sort
      end
    end
    ...
  end
  


Prefetch y flush

En ciertos casos no queremos definir setters y getters para todas las propiedades de nuestro tipo. Si estamos en esa situación, nuestro approach debería ser utilizar un esquema prefetch/flush.

Un provider prefetch/flush implementa sólo dos métodos:
  • prefetch: dada una lista de recursos, retornará y seteará las instancias con los valores obtenidos. 
  • flush: se invoca luego de que todos los valores se setearon.

Dado que puppet intentará invocar los setter/getters para cada una de las propiedades, podemos evitar la definición de todos estos métodos invocando mkresource_method, que define setter/getters default para todas las propiedades.

Luego definimos el método prefetch para realizar todas las tareas que haríamos con los getters, y flush para todos los setters.

No utilicé este esquema aún, pero el formato básico es:
  #configjs/lib/puppet/provider/configjs/configjs.rb
  Puppet::Type.type(:configjs).provide(:configjs) do
    mkresource_method

    def prefetch
      ...
    end

    def flush
      ...
    end
  end
  


Initialize

Otra opción útil para la definición de types y providers es el uso del método initialize. Este método se ejecuta cuando ruby crea un objeto, por lo que podremos realizar algunas inicializaciones ahí si lo necesitamos.


Ejecutar Comandos

En el ejemplo sólo utilizamos files y no necesitamos ejecutar ningún comando. Pero qué pasa si queremos tener un recurso que utilice comandos? Tomando nuestro ejemplo de referencia, tal vez utilicemos el config json generado como input de un comando.

Sólo para ejemplificar el uso de comandos, redefiniré el método create del provider agregando la creación de un container lxd a partir del json. No probé este comando, por lo que puede tener algún error, pero sirve para ejemplificar.
  #configjs/lib/puppet/provider/configjs/configjs.rb
  Puppet::Type.type(:configjs).provide(:configjs) do
    commands :curl => 'curl'

    def create
      config_file = File.new(resource[:path], 'w+')
      config_file.write(JSON.pretty_generate(resource[:config]))
      config_file.close

      curl("-k", "-L", "--cert", ""~/.config/lxc/client.crt", "--key", ~/.config/lxc/client.key", "-H", "Content-Type: application/json", "-X", "POST", "-d", "@#{resource[:path]}", "https://127.0.0.1:8443/1.0/containers)
    end

    ...
  end
  


Cambiar el output

Cuando puppet cambia un recurso, se registra un evento. Vemos la notificación de que algo cambió de la siguiente forma:
Notice: /Stage[main]/<resource>/Config_js[/etc/myconfig.js]/Configjs[/etc/myconfig.js]/config: config changed <IS VALUE> to <SHOULD VALUE>

La forma en que puppet crea el mensaje es convirtiendo el valor actual y el que debería tener en strings (usando el método to_s de ruby). Dependiendo el valor de la propiedad, la conversión a string puede no ser la deseada y el cambio no resulta claro.

Por suerte podemos customizar tanto la forma en que dichos valores se convierten a string, como el mensaje de lo que va a cambiar. Para ello debemos definir los métodos is_to_s, should_to_s y change_to_s dentro de la propiedad o parámetro:
  • is_to_s: se ejecuta para convertir a string el valor que actualmente tiene el server. 
  • should_to_s: debe convertir el valor a aplicar a string. 
  • change_to_s: se invoca para mostrar lo que va a cambiar. Si no redefinimos esta función puppet mostrará el valor actual devuelto por is_to_s y el aplicable retornado por should_to_s.
En general nos alcanzará con redefinir is_to_s y should_to_s, o sólamente change_to_s, es muy raro que utilicemos las tres, ya que si definimos change_to_s no tiene mucho sentido definir también las otras dos.

Siguiendo el ejemplo anterior, la propiedad config es un hash. La función to_s no nos retorna un string muy lindo de los hashes, así que en su lugar, podríamos usar inspect en is_to_s y should_to_s:

  #configjs/lib/puppet/type/configjs.rb
  Puppet::Type.newtype(:configjs) do
    ...

    newproperty(:config) do
      ...

      def is_to_s(value)
        value.inspect
      end

      def should_to_s(value)
        value.inspect
      end
    end
  end
  
En lugar de lo anterior, podríamos simplemente redefinir change_to_s:
  #configjs/lib/puppet/type/configjs.rb
  Puppet::Type.newtype(:configjs) do
    ...

    newproperty(:config) do
      ...

      def change_to_s(is, should)
        "old value #{(is).inspect},
        new #{(should).inspect}"
    end
  end
  

Update 1: descubrí que cuando ejecutamos puppet con --noop, no se ejecuta change_to_s, sino una función noop que no podemos redefinir y tiene un output predefinido de "current value %s, should be %s (noop), param.is_to_s(current_value), param.should_to_s(param.should)". Ver reporte.

Como no podemos cambiar esta funcionalidad, encontré un workaround en el tipo file. Lo que hace file es imprimir las diferencias entre los files en la función issync? y hacer que is_to_s y should_to_s devuelvan checksums en lugar de printear ambos files.

La forma de implementar esto sería:
  Puppet::Type.newtype(:configjs) do
    ...
    def is_to_s(is)
      '{md5}' + Digest::MD5.hexdigest(is.to_s)
    end

    def should_to_s(should)
      '{md5}' + Digest::MD5.hexdigest(should.to_s)
    end

    def insync?(is)
      return @is_insync unless @is_insync.nil?

      @is_insync = super(is)
      if ! @is_insync && Puppet[:show_diff]
        send @resource[:loglevel], "old value #{(is).inspect},
        new #{(should).inspect}"
      end
      @is_insync
    end
  end

Con esto logramos hacer nuestro propio print de los cambios tanto con --noop como si se van a aplicar de verdad.

Tuve que reescribir las funciones is_to_s y should_to_s para que no retornen el contenido, sino un md5. Ese cambio es necesario porque el método noop de puppet llama estas funciones siempre. Si no modifico su output, obtendremos un print de los cambios dos veces.

También tuve que agregar una variable a la clase para detectar si ya ejecuté insync anteriormente. La clase property llama el método insync? dos veces, así que si no determinamos que ya se ejecutó terminaremos imprimiendo el mismo diff dos veces.


Update 2: Utilizar gems externos en nuestro provider

Algo interesante que me surgió durante la creación de un módulo fue la necesidad de utilizar un gem externo. En mi caso, el gem era inifile.

Ej:
  Puppet::Type.type(:mytype).provide(:mytype) do
    ...
    require 'inifile'
    ini_file = IniFile.load('/etc/config.ini')
    ...
  end
Como muchas cosas en puppet, un poco de magia negra es necesaria, dado que puppet no provee una forma de definir dependencias de librerías ruby en la definición del módulo, así que debemos utilizar un workaround.

Luego de leer en comentarios en reportes de bugs y foros, llegué a esta solución.
Básicamente, lo que podemos hacer es:
  • Instalar la dependencia gem con puppet.
  • Hacer que nuestro recurso dependa de la instalación de este paquete.
  • Agregar una feature y un confine para que el provider no se ejecute si la librería no está instalada.

Vamos por partes, primero instalemos el gem necesario:
  package {'inifile':
    ensure   => 'installed',
    provider => 'puppet_gem',
  }

Agregamos la dependencia a la definición de nuestro tipo:
  mytype { 'nombre':
    ...
    require => Package['inifile'],
  }

Ahora, creemos una feature (lib/puppet/feature/inifilelib.rb) con el siguiente contenido:
  Puppet.features.add(:inifilelib, :libs => ["inifile"])

Por último agreguemos el confine en nuestro provider: 
  require 'inifile' if Puppet.features.inifilelib?
  Puppet::Type.type(:mytype).provide(:mytype) do
    confine :feature => :inifilelib
    ...
    require 'inifile'
    ini_file = IniFile.load('/etc/config.ini')
    ...
  end

Esto hará que el provider no se evalúe hasta que el gem inifile no esté instalado. Pueden encontrar una explicación un poco más detallada en el link.


Final, pero hay más para aprender

Si bien este artículo debería cubrir un buen porcentaje de lo que podemos necesitar al crear un type, surgiran cosas que no están acá. Una de ellas es la herencia de providers para crear nuevos providers, el uso de autorequire, features, etc. Recomiendo dar un vistaso a las referencias para más info.

Me decidí a escribir sobre types porque no encontré información muy buena. Si bien hay artículos interesantes que te introducen a la creación de tipos, a medida que avanzas y necesitas entender cómo funcionan ciertas cosas me costó bastante. La info está muy desparramada y se asumen muchas cosas que cuesta entender de dónde salen. Reconozco que no leí completamente el libro Puppet Types and Providers by Nan Liu, Dan Bode que supuestamente es la referencia más completa al respecto.


Referencias

Puppet Types and Providers

Puppet Custom Types 

Puppet Provider Development

Puppet Complete Resource Example

Fun With Puppet Providers - Part 1 of Whatever

Who Abstracted My Ruby?

Seriously, What Is This Provider Doing?

Puppet Extension Points - Part 2

Chapter 4. Advanced Types and Providers

Writing Custom Puppet Types and Providers to Manage Web-Based Applications (slides)
Configurar slaves OpenLDAP para aplicar lock de usuarios por logins fallidos (ppolicy)
Quienes utilicen LDAP para autenticación de servidores/workstations, seguramente se han topado o se toparan con el overlay ppolicy, el cual permite configurar policies de password como largo mínimo, complegidad, history, lock por cantidad de intentos, etc. Cómo habilitar y configurar ppolicy está bastante bien explicado en diversos sites como este, por lo que no lo incluiré acá. El problema es que ninguno explica bien qué hacer si tenes servidores slave que se utilizan para autenticación.

Cuando tenés una configuración master -> slaves, los slaves son read-only, por lo que no guardarán ningún atributo por si mismos, sino que replican lo que tiene el master. Esto es, si por ejemplo están utilizando un slave como ldap server, varios intentos fallidos de login nunca bloquearán la cuenta, porque el tipo no guardará el atributo pwdAccountLockedTime en ningún lado. Esto es lo mismo que sucede cuando intentan hacer un change password y están usando un slave, el pass no se puede cambiar.

Para solucionar este problema, OpenLDAP provee la alternativa de que los slave redirijan los updates al master. Esto es, si alguien quiere actualizar un valor en el slave, el slave redirigirá el update al ldap master, y lo obtendrá por replicación. La solución comprende utilizar updateref, enconjunto con el overlay chain y la opción ppolicy_forward_updates, donde:
  • updateref "<master ldap server>" indica a qué servidor LDAP enviar los updates (se utiliza sólo en slaves). Esta opción por sí sola retorna al cliente la URL del master ldap, y es el cliente quien se debe encargar de tomar esa URL y hacer el update allí.
  • El overlay chain facilita lo anterior, haciendo que el update lo haga el mismo slave, en lugar de retornar la URL del master al cliente. Con chain podemos configurar que si alguna acción dispara un update, el slave se conecte al master y lo aplique. Para ello, hay que configurar un usuario de binding que en el master tenga permisos de escritura para los atributos que deberá actualizar. Este overlay también se usa para resolver DNs que no están replicados en el slave, ya que sin él, OpenLDAP retornará un redirect que el cliente final deberá seguir.
  • ppolicy_forward_updates es la opción del overlay ppolicy que le indica que debe forwardear los password failures, ya que sino asume que es un master y no dispara el hook de redirect.

Veamos todo junto en un ejemplo, donde el ldap master se llama masterserver.dvpem.org. Esta config es para el slave y va en slapd.conf. Incluí sólo las líneas relevantes de la configuración, no es una configuración slapd.conf completa.

IMPORTANTE:
  • La configuración del overlay chain debe ir lo más arriba posible, antes de la configuración de cualquier base de datos y de la configuración de replicación.
  • updateref debe ir luego de la configuración de replicación (syncrelp).
  • El usuario que utilicen para el overlay chain debe tener permiso manage sobre los atributos de password. Por ejemplo, este es el permiso necesario para un user SlaveUpdater:
    {0}to dn.subtree="ou=people,dc=dvpem,dc=org" attrs=pwdChangedTime,pwdAccountLockedTime,pwdFailureTime,pwdHistory,pwdGraceUseTime,pwdReset by dn.base="cn=SlaveUpdater,dc=dvpem,dc=org" manage by * +0 break
...
#importamos el módulo que contiene el overlay chain
moduleload back_ldap.la

#cargamos el overlay
overlay chain
#definimos cuál es la URL del master y con qué usuario bindear. Recuerden que el user debe tener permiso manage.
chain-uri "ldap://masterserver.dvpem.org"
chain-idassert-bind     bindmethod="simple"
                        binddn="cn=SlaveUpdater,dc=dvpem,dc=org"
                        credentials="elpassdeSlaveUpdater"
                        mode="self"
                        #starttls=yes        #necesario si usamos ldaps como protocolo
                        #tls_reqcert=allow   #necesario si usamos ldaps como protocolo
chain-return-error TRUE

...
...

#cargamos el overlay ppolicy
moduleload ppolicy.la
overlay ppolicy

#indicamos dónde está la pólicy default de password (largo, cantidad de intentos, etc). Misma config que en el master.
ppolicy_default "cn=default,ou=Policies,dc=dvpem,dc=org"

#indicamos que se deben forwardear los updates al master
ppolicy_forward_updates

...
...

#configuración estándar syncrelp
syncrelp rid=001
    provider=ldap://masterserver.dvpem.org
    type=refreshAndPersist
    interval=00:00:02:00
    retry="30 10 120 +"
    searchbase="dc=dvpem,dc=org"
    attrs="*,+"
    bindmethod=simple
    binddn="cn=replicator,dc=dvpem,dc=org"
    credentials=elpassdelreplicator
    tls_reqcert=allow

#especificamos a dónde redirigir los updates
updateref "ldap://masterserver.dvpem.org"

Si tienen configurado el slave para utilizar la base cn=config, deberán ejecutar el diguiente ldif (aquí lo llamo enable-ppolicy-forward.ldif):
dn: cn=module{0},cn=config
changetype: modify
add: olcModuleLoad
olcModuleLoad: back_ldap

dn: olcOverlay={0}chain,olcDatabase={-1}frontend,cn=config
changetype: add
objectClass: olcOverlayConfig
objectClass: olcChainConfig
olcOverlay: {0}chain
olcChainCacheURI: FALSE
olcChainMaxReferralDepth: 1
olcChainReturnError: TRUE

dn: olcDatabase=ldap,olcOverlay={0}chain,olcDatabase={-1}frontend,cn=config
changetype: add
objectClass: olcLDAPConfig
objectClass: olcChainDatabase
olcDatabase: ldap
olcDbURI: "ldaps://masterserver.dvpem.org"
olcDbStartTLS: none  starttls=no
olcDbIDAssertBind: mode=self
  flags=prescriptive,proxy-authz-non-critical 
  bindmethod=simple
  timeout=0
  network-timeout=0
  binddn="cn=SlaveUpdater,dc=dvpem,dc=org"
  credentials="elpassdeSlaveUpdater"
  keepalive=0:0:0
  starttls=yes
  tls_cert="/etc/ldap/ssl/certificado.pem"
  tls_key="/etc/ldap/ssl/certificado.pem"
  tls_cacert="/etc/ldap/ssl/certificado.pem"
  tls_reqcert=allow
  tls_cipher_suite=NORMAL
olcDbRebindAsUser: FALSE
olcDbChaseReferrals: TRUE
olcDbTFSupport: no
olcDbProxyWhoAmI: FALSE
olcDbProtocolVersion: 3
olcDbSingleConn: FALSE
olcDbCancel: abandon
olcDbUseTemporaryConn: FALSE
olcDbConnectionPoolMax: 16
olcDbSessionTrackingRequest: FALSE
olcDbNoRefs: FALSE
olcDbNoUndefFilter: FALSE

dn: olcDatabase={1}hdb,cn=config
changetype: modify
add: olcUpdateRef
olcUpdateRef: ldaps://masterserver.dvpem.org

dn: olcOverlay={0}ppolicy,olcDatabase={1}hdb,cn=config
changetype: modify
replace: olcPPolicyForwardUpdates
olcPPolicyForwardUpdates: TRUE

Ejecutamos el ldif con ldapmodify (cambien el usuario admin por el que corresponda a ustedes):
ldapmodify -D "cn=admin,cn=config" -W -f enable-ppolicy-forward.ldif

Como siempre, espero que les haya resultado útil. Yo perdí un buen rato hasta entender cómo funciona esta lógica.
Ejecutar como servicio un programa que no es demonizable
Hoy estuve lidiando con una aplicación que se ejecuta desde consola y queda escuchando en un puerto, pero que no provee la posibilidad de correr como demonio (tipo Apache, sshd, etc). Para no tener que loguear un usuario y dejar la aplicación en ejecución en alguna consola colgada, mejor es convertirla en un servicio. Mi caso en particular se dió con la aplicación CherryMusic, que permite compartir musica a través de una interfaz web.

Como primer paso, vamos a crear un directorio donde alojar la aplicación, y para un caso como el que describo, creo que el mejor lugar es /opt:
# mkdir /opt/cherrymusic
Para cherrymusic, si hacen un clone del proyecto, el directorio se crea sólo, lo mismo si descomprimen un tar.gz.

A continuación creemos el usuario con el que se ejecutará la aplicación:
# useradd cherrymusic -d /opt/cherrymusic -s /bin/false
El comando anterior indica que el home del usuario será /opt/cherrymusic y que utilice /bin/false como shell... es decir, que no tenga shell.

Para ver qué sucede con nuestra aplicación, estaría bueno ver algún log, así que creemos uno, con los permisos necesarios para que la aplicación pueda escribir:
# touch /var/log/cherrymusic
# chown cherrymusic /var/log/cherrymusic
La ejecución del programa la haremos de la siguiente forma:
# sudo -u cherrymusic -H /usr/bin/python /opt/cherrymusic/cherrymusic --port 8080 &>>/var/log/cherrymusic
Esto es, le decimos que ejecute la aplicación con el usuario cherrymusic (-u), usando el home de dicho usuario (-H), y redirigimos la salida a /var/log/cherrymusic

Como último paso, creamos un init script. Si usamos el viejo estándar, podemos meter un script como el siguiente en /etc/init.d/cherrymusic
#!/bin/bash
### BEGIN INIT INFO
# Provides:          cherrymusic
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
### END INIT INFO
case "$1" in
  start)
    iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8080
    sudo -u cherrymusic -H /usr/bin/python /opt/cherrymusic/cherrymusic --port 8080 &>>/var/log/cherrymusic
    ;;
  stop)
    iptables -t nat -D PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8080
    killall -u cherrymusic
    ;;
  *)
    echo "usage: $0 <start | stop>"
    ;;
esac
Ok, tal vez un killall no es lo mejor, pero sirve para mostrar un script muy simple.
Al script le agregué un "plus" que es levantar una regla iptables que redirija lo que llegue por el puerto 80, al 8080. Esto se debe a que, al ejecutar el script con un usuario no privilegiado, el mismo no se puede hookear al puerto 80.
Faltaría sólo agregarlo para que se ejecute al inicio:
  # update-rc.d cherrymusic defaults
Si quisieramos hacer lo mismo, pero utilizando upstart, podemos crear el siguiente archivo en /etc/init/cherrymusic.conf:
# Cherry Music
#
start on runlevel [2345]
stop on runlevel [!2345]
script
  iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8080
  sudo -u cherrymusic -H /usr/bin/python /opt/cherrymusic/cherrymusic --port 8080 &>>/var/log/cherrymusic
end script
post-stop script
  iptables -t nat -D PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8080
end script
Eso es todo, ya pueden utilizar "service cherrymusic start" y "service cherrymusic stop", y además la aplicación se ejecutará cada vez que se encienda el equipo. Los pasos serían los mismos si ejecutaran cualquier otra aplicación =)
Mounting lvm partitions: get into the dark depths of disk storage
Buenas amigos, hoy traigo este artículo desde las oscuras tierras donde moran los sysadmins. Producto de romperme la cabeza para lograr un mayor entendimiento sobre cómo funciona un sistema de archivos. Antes que nada debo excusarme por mi escasa actividad en el blog, últimamente mi tiempo se escapa desarrollando Web (PHP+MySQL+HTML+CSS+JavaScript), mi trabajo en seguridad, estudiando Java para tratar de certificar este año, y lo que me resulta más interesante: incursionando en virtualización de servidores utilizando KVM sobre host CentOS. Varias veces pensé en escribir sobre virtualización en KVM, pero el tema es tan amplio que no sé cómo encararlo. Tal vez debería empezar con mini-artículos sobre temas puntuales, como este que escribo, pero me falta tiempo. Prometo más adelante abordar el tema...


Breve intro

Para experimentar con virtualización tomé la decisión de utilizar CentOS, clon a nivel binario de RedHat, y utilizar KVM en favor de Xen. Para las máquinas virtuales decidí utilizar volúmenes lógicos de LVM como discos virtuales en lugar de archivos de imagen. No voy a explicar en este artículo las ventajas de utilizar volúmenes lógicos así que vamos directo a lo técnico...


Necesito un backup de mi máquina virtual

(Si necesitas un tutorial de LVM no lo vas a encontrar en este artículo, Google it!, hay muchos buenos tutoriales dando vueltas: 1, 2, 3)

Necesito hacer un backup de una máquina virtual cuyo disco es un volumen lógico y no un archivo de imagen. Además necesito mantener la máquina virtual en ejecución. Es decir, hacer un backup online. No hay problema! Una de las ventajas de LVM es que permite tomar snapshots de volúmenes lógicos, una especie de "instantánea" del contenido del volumen.

En la figura superior vemos un esquema de ejemplo de un disco. Una de las particiones se marca como tipo 8e y queda disponible como espacio para volúmenes lógicos. Supongamos que el volumen lógico asignado a la vm se llama "vm01" y pertenece al grupo de volúmenes lógicos "vm_group". Para crear un snapshot del volumen lógico utilizamos "lvcreate":

lvcreate --size 16G --snapshot --name vm01-snapshot /dev/vm_group/vm01

Donde --size indica el tamaño máximo del volumen, --name indica el nombre del volumen lógico correspondiente al snapshot y el último parámetro indica la ruta al dispositivo. El resultado es una copia del volumen en un instante determinado.


Listo el backup!

Eso es todo, aunque... Si el guest posee una base de datos, se debe hacer un lock para que cualquier aplicación en ejecución no escriba en tablas, y además se debe hacer un flush para escribir a disco todos los registros que se encuentran en memoria. Esto es necesario para que la base de datos quede en un estado consistente al momento de tomar el snapshot. Se puede hacer utilizando la herramienta ShellSQL (en CentOS se debe agregar el repositorio RPMforge para instalarla)


Que hago con el backup?

Una vez que tenemos la snapshot seguramente queremos moverla a un servidor de backup. Para esto necesitamos montar la imagen y copiarla al servidor de backup utilizando ftp (quise decir sftp, ver este artículo) o inclusive scp.

En Linux no es posible montar una partición LVM (tipo 8e), sólo es posible montar volúmenes lógicos. Esto es fácil si el volumen lógico posee un sistema de archivos como ext2, ext3 o NTFS. Los problemas surgen cuando el volumen lógico (partición lógica) contiene varias particiones en lugar de un sistema de archivos (las cosas se vuelven algo confusas).

¿Por qué un volumen lógico tendría varias particiones? Simple, si es utilizado por una máquina virtual tiene al menos una partición, ya que todo sistema operativo necesita al menos una o varias particiones para funcionar (/boot, /, /var, /home, etc.)

Si tratamos de montar un volumen lógico particionado obtendremos el siguiente resultado:

hfs: unable to find HFS+ superblock

Significa que mount no encuentra ningún sistema de archivos, ya que para encontrar un sistema de archivos habrá que buscarlo en la tabla de particiones. Recordemos que lo que para el host (el volumen lógico) es una partición, para el guest virtual es un dispositivo. El resultado es que tenemos particiones dentro de una partición lógica :S

Montar una partición que se encuentra dentro de un volumen lógico

Gracias a esta guía pude lograr lo que necesitaba. Primero veamos qué particiones contiene el volumen lógico correspondiente al snapshot:
[root@myserver ~]# fdisk -l -u /dev/vm_group/vm01-snapshot

Disk /dev/vm_group/vm01-snapshot: 10.4 GB, 10485760000 bytes
255 heads, 63 sectors/track, 1274 cylinders, total 20480000 sectors
Units = sectors of 1 * 512 = 512 bytes

Device Boot Start End Blocks Id System
/dev/vm_group/vm01-snapshot1 * 63 20450744 10225341 7 HPFS/NTFS

Se observa que el volumen lógico posee una única partición NTFS. El parámetro -u se utiliza para que las unidades sean en sectores y no en cilindros. Esta partición se puede montar si le indicamos a mount que el volumen lógico vm01 es un loopback device y le pasamos un offset (medido en bytes) al comienzo de la partición deseada.
El sistema de archivos NTFS comienza en el sector 63 (se observa en la captura anterior) y cada sector posee 512 bytes. Por lo tanto el offset es 32256.
Con los siguientes parámetros logramos montar la partición deseada:
[root@myserver ~]# mount -o loop,offset=32256 -t ntfs /dev/vm_group/vm01-snapshot /media/vm01-snapshot/
The disk contains an unclean file system (0, 0).
The file system wasn't safely closed on Windows. Fixing.

Voila! Tenemos la partición montada!
[root@myserver ~]# la /media/vm01-snapshot/
total 1310609
drwxrwxrwx 1 root root 8192 Feb 16 07:39 Archivos de programa
-rwxrwxrwx 1 root root 0 Feb 3 11:32 AUTOEXEC.BAT
-rwxrwxrwx 1 root root 4952 Aug 24 2001 Bootfont.bin
-rwxrwxrwx 1 root root 211 Feb 3 11:29 boot.ini
-rwxrwxrwx 1 root root 0 Feb 3 11:32 CONFIG.SYS
drwxrwxrwx 1 root root 4096 Feb 3 11:37 Documents and Settings
-rwxrwxrwx 1 root root 536399872 Mar 30 12:46 hiberfil.sys
-rwxrwxrwx 1 root root 0 Feb 3 11:32 IO.SYS
-rwxrwxrwx 1 root root 0 Feb 3 11:32 MSDOS.SYS
drwxrwxrwx 1 root root 0 Feb 16 07:37 MSOCache
-rwxrwxrwx 1 root root 47564 Aug 3 2004 NTDETECT.COM
-rwxrwxrwx 1 root root 250640 Aug 3 2004 ntldr
-rwxrwxrwx 1 root root 805306368 Mar 30 12:46 pagefile.sys
drwxrwxrwx 1 root root 4096 Feb 3 11:36 System Volume Information
drwxrwxrwx 1 root root 28672 Feb 16 14:48 WINDOWS

Teniendo acceso a la partición podemos copiar el backup del disco donde deseamos (usando scp o sftp).

Espero que les sirva!