Infrastructure Behavior Driven-Development

(WIP)

Ahí más inventores que inventos dice un amigo mio.

Como Desarrollador y Operador de plataformas de software orientadas a telefonía (VOIP) y como practicante de TDD, me he visto envuelto en mayores responsabilidades y por lo tanto en la necesidad de mejorar el proceso de configuración y mantenimiento de los diferentes servicios, en este ejercicio he llegado a la conclusión que muchos otros ya han llegado y es usar la práctica de un entorno de pruebas automatizadas para configurar y probar los servicios.

Lo que buscamos de esta práctica es:

  • que la configuración de servicios entre en un ciclo de integración y despliegue continuo.
  • que la configuración de los servicios este orientado a comportamientos esperados.
  • tener un mecanismo para obtener rápidamente feedback en la configuración de los servicios.

Para ilustrar como proceder vamos a configurar el servicio pure-ftpd en base a una serie de requerimientos, usando una librería de pruebas automatizadas, en este caso usare rspec y ruby para el ejercicio.

inicializamos el entorno de pruebas de rspec

rspec --init

iniciamos con una prueba fundamental y es verificar la sintaxis de configuración.

require 'spec_helper'
require 'tempfile'

def validate_syntax(config_path)
  %x[timeout 1 /usr/sbin/pure-ftpd #{config_path}]
  $?.exitstatus == 0 || $?.exitstatus == 124
end

describe 'pure-ftpd' do
  let (:conf) { Tempfile.new('pure-ftpd') }
  
  describe 'configuracion' do
    it 'al verificar archivo invalido falla' do
      conf.write('invalidline')
      conf.flush
      
      expect(validate_syntax(conf.path)).to be false
    end

    it 'al verificar archivo valido ok' do
      conf.write('ChrootEveryone yes')
      conf.flush
      
      expect(validate_syntax(conf.path)).to be true
    end
  end
end

una vez tenemos un mecanismo para confirmar que la sintaxis del archivo es correcta procedemos a confirmar que el servicio inicializa y finaliza correctamente en presencia del archivo de configuracion indicado.

require 'spec_helper'
require 'tempfile'

def ftpd_start(config_path)
  pid = Process.spawn("/usr/sbin/pure-ftpd #{config_path}")
  Process.detach(pid)

  sleep 1

  port = %x{lsof -p #{pid} -itcp -a -P -n 2> /dev/null}.chomp[/TCP.+:(\d+)/,1].to_i

  {pid: pid, port: port}
end

def ftpd_alive?(server)
  # http://dev.housetrip.com/2014/03/24/ruby-pid-tip/
  Process.kill(0, server[:pid])
  true
rescue Errno::ESRCH
  false
end

def ftpd_stop(server)
  Process.kill(9, server[:pid])
rescue Errno::ESRCH
  false
end

describe 'pure-ftpd' do
  let (:conf) { Tempfile.new('pure-ftpd') }
  
  describe 'gestión del servicio' do
    it 'iniciar cuando el archivo de configuracion es correcto' do
      conf.write('Bind 127.0.0.1,0')
      conf.flush
      
      pid = ftpd_start(conf.path)
      
      expect(ftpd_alive?(pid)).to be true
    ensure
      ftpd_stop(pid)
    end

    it 'not iniciar cuando el archivo de configuracion es invalido' do
      conf.write('asdfs')
      conf.flush

      pid = ftpd_start(conf.path)
      expect(ftpd_alive?(pid)).to be false
    ensure
      ftpd_stop(pid)
    end
  end
end

los ejercicios anteriores nos empiezan a dar una idea de como vamos a controlar el servicio durante las pruebas, ahora vamos a proceder a configurar el servicio en base los requerimientos.

pure-ftpd.conf

Bind 127.0.0.1,8021
# funcion de utilidad para reescribir archivos de configuracion durante las pruebas
def substitute(path, match, replace)
  content = File.read(path)
  File.write(path, content.sub(match, replace))
end

before { substitute('pure-ftpd.conf', '/etc/pure-ftpd.pdb', "#{Dir.pwd}/pure-ftpd.pdb") }

no se permite ingreso anonimo

pure-ftpd.conf

NoAnonymous yes
it 'no se permite logeo anonimo' do
  server = ftpd_start(conf)
  
  expect do
    Net::FTP.open("127.0.0.1", port: server[:port]) do |ftp|
      ftp.login
    end
  end.to raise_error(Net::FTPPermError)
ensure
  ftpd_stop(server)
end

ingreso solo a usuarios autorizados

pure-pw useradd foo -f pure-ftpd.users -u nobody -d /tmp/foo
pure-pw mkdb pure-ftpd.pdb -f pure-ftpd.users

pure-ftpd.conf

PureDB /etc/pure-ftpd.pdb
it 'ingreso a usuarios registrados' do
  server = ftpd_start(conf)
  
  expect do
    Net::FTP.open("127.0.0.1", port: server[:port]) do |ftp|
      ftp.login('foo', 'foo')
    end
  end.not_to raise_error
ensure
  ftpd_stop(server)
end
it 'subir archivos solo usuarios registrados' do
  server = ftpd_start(conf)
  
  expect do
    Net::FTP.open("127.0.0.1", port: server[:port]) do |ftp|
      ftp.login('foo', 'foo')
      ftp.puttextfile(__FILE__, 'demo.txt')
    end
  end.not_to raise_error
ensure
  ftpd_stop(server)
end

Como vemos es posible usar las pruebas automatizadas como mecanismo para confirmar el comportamiento esperado del servicio, algunos beneficios que tenemos son:

  • pruebas automatizadas de confirmación.
  • verificar que el servicio siempre inicie (cuantas veces nos ha pasado que hemos modificado incorrectamente un archivo de configuración y luego no inicia?).
  • nos incentiva a llevar los archivos de configuraciones en control de versiones.

Más inventores que inventos