EXTENDIENDO Y EMBEBIENDO ANSIBLE CON Python
OCTUBRE '15
INTRODUCCIÓN A ANSIBLE
Fuente: Ansible Blog
¿QUÉ ES ANSIBLE?
- Es una herramienta de Gestión de la configuración
- Ventajas frente a otras herramientas
- Funciona sobre SSH y no requiere agentes en los hosts
- Python 2.X en los hosts
- Configuración muy legible (YAML)
- Funciona sobre SSH y no requiere agentes en los hosts
ARQUITECTURA DE ANSIBLE
Fuente: sysadmincasts
FICHEROS DE HOSTS (INVENTARIO)
[web]
10.0.15.21
10.0.15.22
[db]
10.0.15.23
[db:vars]
db_user=guest
[infrastructure:children]
web
db
COMANDOS AD-HOC
- Acciones que no hay que realizar continuamente, de usar y tirar
ansible grupo -m módulo -a opciones
- Ejecución de un módulo en cada host de un grupo
Hello World! : Ping
(ansible)alex ~ $ ansible all -m ping
10.0.15.22 | success >> {
"changed": false,
"ping": "pong"
}
10.0.15.21 | success >> {
"changed": false,
"ping": "pong"
}
10.0.15.23 | success >> {
"changed": false,
"ping": "pong"
}
ansible all -m ping
PLAYBOOKS
- Los playbooks son ficheros YAML que especifican una lista de plays
- Cada play es una lista de tareas (tasks) relacionadas aplicadas a un conjunto de hosts
- name: Ensure that nginx is started
service: name=nginx state=started
- Cada tarea consiste en la ejecución de un módulo con ciertos parámetros y un nombre legible:
UN PLAYBOOK PARA DESPLEGAR nginx
---
- hosts: web
sudo: yes
vars:
- external_port: 80
- internal_port: 8000
tasks:
- name: Add the apt repository for nginx
apt_repository: repo="ppa:nginx/stable" update_cache=yes
- name: Install nginx
apt: name=nginx state=present
- name: Ensure that nginx is started
service: name=nginx state=started
- name: Remove default site
file: path=/etc/nginx/sites-enabled/default state=absent
notify:
- Restart nginx
- name: Configure a site
template: src=templates/site.j2 dest=/etc/nginx/sites-available/site
- name: Enable a site
file: src=/etc/nginx/sites-available/site dest=/etc/nginx/sites-enabled/site state=link
notify:
- Restart nginx
handlers:
- name: Restart nginx
service: name=nginx state=restarted
intro/deploy-nginx.yml
EJECUTEMOS EL playbook...
$ ansible-playbook deploy-nginx.yml
PLAY [web] ********************************************************************
GATHERING FACTS ***************************************************************
ok: [10.0.15.21]
ok: [10.0.15.22]
TASK: [Add the apt repository for nginx] **************************************
changed: [10.0.15.21]
changed: [10.0.15.22]
TASK: [Install nginx] *********************************************************
changed: [10.0.15.22]
changed: [10.0.15.21]
TASK: [Ensure that nginx is started] ******************************************
ok: [10.0.15.21]
ok: [10.0.15.22]
TASK: [Remove default site] ***************************************************
changed: [10.0.15.21]
changed: [10.0.15.22]
TASK: [Configure a site] ******************************************************
changed: [10.0.15.21]
changed: [10.0.15.22]
TASK: [Enable a site] *********************************************************
changed: [10.0.15.21]
changed: [10.0.15.22]
TASK: [Remove default site] ***************************************************
ok: [10.0.15.21]
ok: [10.0.15.22]
NOTIFIED: [Restart nginx] *****************************************************
changed: [10.0.15.21]
changed: [10.0.15.22]
PLAY RECAP ********************************************************************
10.0.15.21 : ok=9 changed=6 unreachable=0 failed=0
10.0.15.22 : ok=9 changed=6 unreachable=0 failed=0
ENCAPSULACIÓN: roles
Los roles encapsulan:
- Variables
- Tasks
- Handlers
- Files y templates
- Dependencias de otros roles
EMBEBIENDO Ansible
¿embebiendo?
- Embeber: invocar a los módulos de Ansible y ejecutar playbooks desde tu código Python
- Es posible porque Ansible tiene un API Python
- Aunque no muy bien documentada
- ... pero muy sencilla de usar
DISCLAIMERS...
- Estos ejemplos son válidos Ansible v 1.9.X.
- ¿v2?
- Sólo funciona con Python 2.X :-(
- El código fuente de los ejemplos está en:
EJECUTANDO UNA TAREA DE ANSIBLE
FUNDAMENTOS
- Importa estas clases:
from ansible.runner import Runner
from ansible.inventory import Inventory
- Contruye tu inventario:
inventory = Inventory(["localhost"])
- Invoca a Runner.run():
runner = Runner(module_name='setup', module_args='',
pattern='localhost', inventory=inventory,
transport="local")
result = runner.run()
¿QUÉ SON LOS "FACTS"?
ansible all -m setup
- Hostnames
- Direcciones IP
- Información del HW
- Versiones del SW instalado...
Obtienes:
EJEMPLO: FLASK FACTER
from ansible.runner import Runner
from ansible.inventory import Inventory
from flask import Flask
from flask_restful import Resource, Api
class AnsibleFactResource(Resource):
"""
A read-only local ansible fact
"""
def __init__(self):
self.facts = None
Resource.__init__(self)
def get_local_facts(self):
"""
Loads the Ansible local facts in self.facts
Calls the Ansible Python API v1 'setup' module
"""
inventory = Inventory(["localhost"])
runner = Runner(module_name='setup', module_args='',
pattern='localhost', inventory=inventory,
transport="local")
result = runner.run()
self.facts = result["contacted"]["localhost"]["ansible_facts"]
def get(self, fact_name):
"""
Returns a top-level fact (not nested)
"""
self.get_local_facts()
try:
return self.facts[fact_name]
except KeyError:
return "Not a valid fact: %s" % fact_name
app = Flask(__name__)
api = Api(app)
api.add_resource(AnsibleFactResource, '/facts/<string:fact_name>')
if __name__ == '__main__':
app.run(debug=True, port=8000)
embedding/task/flask_facter.py
EJECUTANDO UN PLAYBOOK DE ANSIBLE
FUNDAMENTOS
- Importa estas clases :
from ansible.playbook import PlayBook
from ansible.inventory import Host, Inventory
from ansible import callbacks
from ansible import utils
- Construye tu inventario
# Create the inventory
controller = Host(name = "localhost")
controller.set_variable('users', user_list)
controller.set_variable('apt_packages', package_list)
local_inventory = Inventory([])
local_inventory.get_group('all').add_host(controller)
FUNDAMENTOS
- Configura los callback
# Boilerplate for callbacks setup
utils.VERBOSITY = 0
# Output callbacks setup
output_callbacks = callbacks.PlaybookCallbacks(verbose=utils.VERBOSITY)
# API callbacks setup
stats = callbacks.AggregateStats()
api_callbacks = callbacks.PlaybookRunnerCallbacks(stats, verbose=utils.VERBOSITY)
- Invoca a PlayBook.run()
provision_playbook = PlayBook(playbook = "installer.yml",
stats = stats,
callbacks = output_callbacks,
runner_callbacks = api_callbacks,
inventory = local_inventory,
transport = "local",
become_pass = sudo_password
)
playbook_result = provision_playbook.run()
EJEMPLO: INSTALLER
import sys
from getpass import getpass
from ansible.playbook import PlayBook
from ansible.inventory import Host, Inventory
from ansible import callbacks
from ansible import utils
def run_installer(user_list, package_list, sudo_password):
"""
Runs the playbook `installer.yml` with the supplied parameters
"""
# Create the inventory
controller = Host(name = "localhost")
controller.set_variable('users', user_list)
controller.set_variable('apt_packages', package_list)
local_inventory = Inventory([])
local_inventory.get_group('all').add_host(controller)
# Boilerplate for callbacks setup
utils.VERBOSITY = 0
# Output callbacks setup
output_callbacks = callbacks.PlaybookCallbacks(verbose=utils.VERBOSITY)
# API callbacks setup
stats = callbacks.AggregateStats()
api_callbacks = callbacks.PlaybookRunnerCallbacks(stats, verbose=utils.VERBOSITY)
provision_playbook = PlayBook(playbook = "installer.yml",
stats = stats,
callbacks = output_callbacks,
runner_callbacks = api_callbacks,
inventory = local_inventory,
transport = "local",
become_pass = sudo_password
)
playbook_result = provision_playbook.run()
return playbook_result
def get_selection_list(initial_prompt, input_prompt, continue_prompt):
"""
Return a selection list chosen according to a flow described by:
- initial_prompt: To enter the selection menu
- input_prompt: To enter an item
- continue_prompt: To continue entering another item
"""
results = []
enter_selection = raw_input(initial_prompt)
enter_selection = True if enter_selection in ["y", "Y", "yes"] else False
if enter_selection:
while True:
current_result = raw_input(input_prompt)
if not current_result:
break
results.append(current_result)
continue_selection = raw_input(continue_prompt)
continue_selection = True if continue_selection in ["y","Y","yes"] else False
if not continue_selection:
break
return results
if __name__ == '__main__':
packages = []
print("")
print("Automated installation script")
print("=============================")
print("")
sudo_password = getpass("Enter sudo password:")
users = get_selection_list(
"Create users? (y/N)",
"Enter username:",
"Add more users? (y/N)"
)
print("")
packages = get_selection_list(
"Install packages? (y/N)",
"Enter package name:",
"Add more packages? (y/N)"
)
run_installer(user_list=users, package_list=packages, sudo_password=sudo_password)
embedding/playbook/installer.py
EXTENDIENDO ANSIBLE
Fuente: Ansible Blog
¿EXTENDIENDO?
- Extender: añadir más funcionalidad a Ansible or personalizar su comportamiento con código Python
- Módulos
- Plugins
- Scripts de inventario dinámico
CREANDO UN MÓDULO DE ANSIBLE
un módulo de ansible ...
- ... es un fichero ejecutable
- En ./library o en ANSIBLE_LIBRARY_PATH
- ... tiene una interfaz JSON, por lo que es agnóstico respecto al lenguaje de programación
- Auque es más sencillo hacerlos en Python
#!/usr/bin/python
# -*- coding: utf-8 -*-
DOCUMENTATION = '''
'''
EXAMPLES = '''
'''
# Your custom Python code / external libraries
from mypackage import mypythonicfunction
def mypythonicfunction(...):
"""
Here you do the hard work. Use external libraries or your own code
And return a meaningful value and message to main()
"""
return status, msg
def main():
module = AnsibleModule(
argument_spec=dict(
# Here you parse the arguments
)
# and set the options and behaviour of Ansible
)
# Here you assign your arguments to Python variables
variable_a = module.params['var_a']
# and call your mypythonicfunction() with the variables
status, msg = mypythonicfunction(variable_a, ...)
# Parse the results and return valid JSONs and meaningful messages
if status == "ok":
module.exit_json(changed=changed, msg=msg)
else:
module.fail_json(msg=msg)
# Boilerplate but IMPORTANT code
from ansible.module_utils.basic import *
if __name__ == "__main__":
main()
ESTRUCTURA DEL MÓDULO
DOCUMENTACIÓN Y EJEMPLOS
- Se emplean por el comando make webdocs para generar el HTML
Puntos clave:
- Mantener la parte de options sincronizada con el argument_spec
- Especificar los requirements
- Usar la sección de notes
- Probar los ejemplos que pones :-)
LA PARTE PYTHÓNICA
- Aunque puedes aprovechar funciones muy cómodas de Ansible
- run_command
- Intenta usar Python puro
- Tu lógica de negocio
- Consejos:
- Devuelve un value y un msg
- No imprimas a stdout o stderr
LA FUNCIÓN MAIN()
- Crea una instancia de la clase AnsibleModule
- No te olvides de:
- Especificar las posibles exclusiones mutuas
- Especificar si se soporta el check mode
- El diccionario argument_spec define los argumentos del módulo
- Required vs optional
- Default values
- Possible choices
- Aliases
- Life is easy: el diccionario params recoge los parámetros con los que ha sido invocado
- Invoca a la parte pythónica y parsea los resultados
- Funciones auxiliares super útiles:
exit_json(changed, ...) fail_json(msg, ...)
LA FUNCIÓN MAIN()
BOILERPLATE CODE
- Coloca estas líneas al final del fichero:
- ¡Resiste la tentación de hacer imports explícitos!
from ansible.module_utils.basic import *
if __name__ == "__main__":
main()
CONSEJOS PARA CREACIÓN DE MÓDULOS
- Crea un módulo que te gustaría usar:
- Idempotente
- Que soporte el check mode
- Prueba tu módulo con el script de test-module:
ansible/hacking/test-module
-m /path/to/module
-a module_args
- Sigue la module creation checklist
EJEMPLO DE MÓDULO: TAIGA_ISSUE
- Usa la biblioteca python-taiga para comunicarse con Taiga mediante su REST API
extending/module/library/taiga_issue.py
CREANDO UN SCRIPT DE INVENTARIO DINÁMICO
SCRIPT DE INVENTARIO DINÁMICO
- Se trata de un fichero ejecutable que soporta estos switches por líneas de comandos:
- --host=<hostname> para mostrar detalles del host
- --list para listar grupos
- La salida es JSON
EJEMPLO: SHELVE_INVENTORY
#!/usr/bin/env python
import argparse
import json
import sys
import shelve
import ConfigParser
def parse_args():
parser = argparse.ArgumentParser(description="Shelve inventory script")
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('--list', action='store_true')
group.add_argument('--host')
return parser.parse_args()
def get_shelve_groups(shelvefile):
d = shelve.open(shelvefile)
g = d["groups"]
d.close()
return g
def get_shelve_host_details(shelvefile, host):
d = shelve.open(shelvefile)
h = d["hostvars"][host]
d.close()
return h
def main():
config = ConfigParser.RawConfigParser()
config.read('shelve_inventory.ini')
shelvefile = config.get('defaults', 'shelvefile')
args = parse_args()
if args.list:
groups = get_shelve_groups(shelvefile)
json.dump(groups, sys.stdout)
else:
details = get_shelve_host_details(shelvefile, args.host)
json.dump(details, sys.stdout)
if __name__ == '__main__':
main()
extending/dynamic_inventory/shelve_inventory.py
CREANDO PLUGINS
PLUGINS
- ¡Se ejecutan en el nodo controller!
- Los hay de diferentes tipos:
- Callback plugins
- Connection plugins
- Lookup plugins
- Filter plugins
- Action plugins
- Vars plugins
- Cache plugins
CÓMO añadir plugins
- O puedes cambiar tu fichero ansible.cfg:
[defaults]
callback_plugins = ./plugins
- Ubicación por defecto: /usr/share/ansible/plugins/
action_plugins/
lookup_plugins/
callback_plugins/
connection_plugins/
filter_plugins/
vars_plugins/
CREANDO UN CALLBAck PLUGIN
CALLBACK PLUGINS
- Reaccionan ante eventos del Playbook y/o el Runner
- En el repositorio de Ansible hay un ejemplo con una lista completa de los métodos disponibles
- Basta con que definas la clase CallbackModule y hagas un override los métodos que quieras
EJEMPLO: NOTIFY_SEND
from subprocess import call
def notify_send(title, message, icon):
call(["notify-send", title, message,"-i", icon])
class CallbackModule(object):
def runner_on_failed(self, host, res, ignore_errors=False):
template = "Host: {}\nModule: {}\nMessage: {}"
notification = template.format(
host,
res.get('invocation').get('module_name'),
res.get('msg')
)
notify_send("ANSIBLE FAILURE", notification, "dialog-warning")
extending/plugin/callbacks/plugins/notify_send.py
CREAndo un CONNECTION PLUGIN
CONNECTION PLUGINS
- Un connection plugin permite al nodo controlador conectarse a los hosts usando varios protocolos:
- SSH
- paramiko_ssh
- local
- winrm (para Win$ hosts)
- chroot
- jails (para FreeBSD)
- zones (Solaris)
- libvirt_lxc
- ...
- La lista de connection plugins oficiales disponibles la puedes encontrar aquí
fundamentos de CONNECTION PLUGINs
- Define una clase Connection y haz un override de los siguientes métodos:
class Connection(object):
def __init__(self, runner, host, port, user,
password, private_key_file, *args, **kwargs):
[...]
def connect(self):
[...]
def exec_command(self, cmd, tmp_path, become_user=None,
sudoable=False, executable='/bin/sh', in_data=None):
[...]
def put_file(self, in_path, out_path):
[...]
def fetch_file(self, in_path, out_path):
[...]
def close(self):
[...]
creando un LOOKUP PLUGIN
LOOKUP PLUGINS
- Acceso a datos
- Filesystem
- Databases
- Servicios externos
- Sintaxis:
contents: "{{ lookup('file', '/etc/foo.txt') }}"
- En ellos se basa la construcción with_
fundamentos de LOOKUP PLUGINS
from ansible import utils, errors
class LookupModule(object):
def __init__(self, basedir=None, **kwargs):
self.basedir = basedir
def run(self, terms, inject=None, **kwargs):
# terms holds the parameters of the lookup call
- Implementa un método run() de la clase LookupModule que devuelva una lista de resultados
- Cada elemento se corresponde con cada elemento de la lista terms
EJEMPLO: SHELVEFILE
import shelve
import os
from ansible import utils, errors
class LookupModule(object):
def __init__(self, basedir=None, **kwargs):
self.basedir = basedir
def read_shelve(self, shelve_filename, key):
"""
Read the value of "key" from a shelve file
"""
d = shelve.open(shelve_filename)
res = d.get(key, None)
d.close()
return res
def run(self, terms, inject=None, **kwargs):
terms = utils.listify_lookup_plugin_terms(terms, self.basedir, inject)
ret = []
if not isinstance(terms, list):
terms = [ terms ]
for term in terms:
playbook_path = None
relative_path = None
paramvals = {"file": None, "key": None}
params = term.split()
try:
for param in params:
name, value = param.split('=')
assert(name in paramvals)
paramvals[name] = value
except (ValueError, AssertionError), e:
# In case "file" or "key" are not present
raise errors.AnsibleError(e)
file = paramvals['file']
key = paramvals['key']
basedir_path = utils.path_dwim(self.basedir, file)
# Search also in the role/files directory and in the playbook directory
if '_original_file' in inject:
relative_path = utils.path_dwim_relative(inject['_original_file'], 'files', file, self.basedir, check=False)
if 'playbook_dir' in inject:
playbook_path = os.path.join(inject['playbook_dir'], file)
for path in (basedir_path, relative_path, playbook_path):
if path and os.path.exists(path):
res = self.read_shelve(path, key)
if res is None:
raise errors.AnsibleError("Key %s not found in shelve file %s" % (key, file))
# Convert the value read to string
ret.append(str(res))
break
else:
raise errors.AnsibleError("Could not locate shelve file in lookup: %s" % file)
return ret
extending/plugin/lookup/plugins/shelvefile.py
Uso:
{{ lookup('shelvefile', 'file=book.db key=current_book_author') }}
CREANDO UN FILTER PLUGIN
FILTER PLUGINS
- Jinja2 filters
- Ansible trae de fábrica unos cuantos muy útiles
{{ some_variable | default(5) }}
{{ some_list | min }}
{{ some_register_variable | changed }}
- Sintaxis: variable | filter
fundamentos de FILTER PLUGINs
class FilterModule(object):
def filters(self):
return {
'filter1': filter1functionname,
'filter2': filter2functionname,
[...]
}
- Crea una clase FilterModule con un método filters que devuelva un diccionario que mapee los filters a funciones
EJEMPLO: ROT13
def rot13(s):
def lookup(v):
o, c = ord(v), v.lower()
if 'a' <= c <= 'm':
return chr(o + 13)
if 'n' <= c <= 'z':
return chr(o - 13)
return v
return ''.join(map(lookup, s))
class FilterModule(object):
def filters(self):
return {
'rot13': rot13
}
extending/plugin/filter/plugins/rot13.py
Uso:
{{ variable | rot13 }}
OTROS PLUGINS
ACTION PLUGINS
- Acciones en el nodo controlador durante la ejecución de un módulo
- Para desarrollar uno, implementa una clase ActionModule con un método run()
- self.runner._execute_module() para invocar al módulo real
VARS PLUGINS
- Las construcciones como host_vars y group_vars de los playbooks funcionan mediante vars plugins
- Es una funcionalidad bastante oscura y poco documentada de Ansible, sólo he podido encontrar este código de ejemplo en el repositorio de Ansible
VARS PLUGIN TEMPLATE CODE
class VarsModule(object):
"""
Loads variables for groups and/or hosts
"""
def __init__(self, inventory):
""" constructor """
self.inventory = inventory
self.inventory_basedir = inventory.basedir()
def run(self, host, vault_password=None):
""" For backwards compatibility, when only vars per host were retrieved
This method should return both host specific vars as well as vars
calculated from groups it is a member of """
return {}
def get_host_vars(self, host, vault_password=None):
""" Get host specific variables. """
return {}
def get_group_vars(self, group, vault_password=None):
""" Get group specific variables. """
return {}
CACHE PLUGINS
Fact caching:
-
hostvars de hosts no contactados en la ejecución del playbook
-
v1.8+
-
Para usarlo, pon esto en tu ansible.cfg:
[defaults]
gathering = smart
fact_caching = redis # Name of the plugin
fact_caching_timeout = 86400
CACHE PLUGINS
- Tipos de cache plugins:
- Memory
- JSONfile
- Memcached
- Redis
CACHE PLUGIN TEMPLATE CODE
import exceptions
class BaseCacheModule(object):
def get(self, key):
raise exceptions.NotImplementedError
def set(self, key, value):
raise exceptions.NotImplementedError
def keys(self):
raise exceptions.NotImplementedError
def contains(self, key):
raise exceptions.NotImplementedError
def delete(self, key):
raise exceptions.NotImplementedError
def flush(self):
raise exceptions.NotImplementedError
def copy(self):
raise exceptions.NotImplementedError
REFERencias
- Ansible Up & Running por Lorin Hochstein
-
Learning Ansible by M.Mohan and R.Raithatha
Libros:
Artículos:
- Ansible for DevOps por Jeff Geerling
- Using Ansible like library programming in Python por Oriol Rius
- Running Ansible Programatically en Servers for Hackers
Happy hacking!
Extendiendo y embebiendo Ansible con Python
By Alejandro Guirao Rodríguez
Extendiendo y embebiendo Ansible con Python
- 4,705