[python]Show version with Ansible

Introduction
These python scripts do the following:

  1. init_vault.py, initialized the vault and create an encryption key. The initialization creates 5 keys and 1 root token for unsealing and login the vault respectively.
  2. get_vault_resuly.py, this script gets the keys and token to unseal and login the vault.
  3. vault_mgmt.py, this script gets the key and value pair stored in kv/cisco, the key and value pair contains the username and password for the routers.
  4. run_ansible.py, this script runs the ansible-playbook -i hosts backup.yaml -u cyruslab -k command.

The fourth point is actually not I wanted to do, what I wanted is to use the ansible python module, however there is no good documentation or examples for running the yaml file with ansible-playbook, if you have a good guide please leave message to the comment.

hosts file
Initially I got below error, my original hosts file only contains the ip addresses however i need to actually put in the network_os.
error1

To solve the above problem you need to put the ansible_network_os in the hosts file.

[ios]
192.168.1.30
192.168.1.31
192.168.1.32

[ios:vars]
ansible_network_os=ios

ansible.cfg
ansible1
You need to uncomment the host_key_checking in the ansible.cfg this is because by default ansible performs strict host key checking you will encounter error in connecting to the routers if this is not uncommented

Yaml file

---
- name: Cisco ios devices
  hosts: ios
  gather_facts: no
  connection: network_cli

  tasks:
    - name: show version for cisco routers
      ios_command:
        commands:
          - show running-config
          - show version

      register: cisco_config


    - name: Save the config
      copy:
        content: "{{ cisco_config.stdout[0] }}"
        dest: "/home/cyruslab/router_config/{{ inventory_hostname }}_backup.txt"

    - name: Get show version
      copy:
        content: "{{ cisco_config.stdout[1] }}"
        dest: "/home/cyruslab/router_config/{{ inventory_hostname }}_show_versions.txt"
...

register is like a variable, for each command executed it will be put into the stdout as a list object, stdout[0]> contains the output of show run, stdout[1] contains the output of show version.

This is I am using the ios_command this is not used for configuration.
--- is the start of the yaml, ... is the end of the yaml file.
For each hyphen is an indication that it is a list, in json it is like this:

[
	{
		"name": "Cisco ios devices",
		"hosts": "ios",
		"gather_facts": "no",
		"connection": "network_cli",
		"tasks": [
			{
				"name": "show version for cisco routers",
				"ios_command": {
					"commands": [
						"show running-config",
						"show version"
					]
				},
				"register": "cisco_config"
			},
			{
				"name": "Save the config",
				"copy": {
					"content": "{{ cisco_config.stdout[0] }}",
					"dest": "/home/cyruslab/router_config/{{ inventory_hostname }}_backup.txt"
				}
			},
			{
				"name": "Get show version",
				"copy": {
					"content": "{{ cisco_config.stdout[1] }}",
					"dest": "/home/cyruslab/router_config/{{ inventory_hostname }}_show_versions.txt"
				}
			}
		]
	}
]

init_vault.py
If the hashicorp vault is newly installed and no keys and root token, run this first.

from hvac import Client
from os.path import exists
from cryptography.fernet import Fernet
import json
'''
hvac is the python module for Hashicorp vault api
Because after initialization the vault returns a dictionary
and dictionary does not have encode method hence i json is used
to convert the dictionary into a string by using the dumps method.
The string then needs to be encoded into utf-8 as bytes,
then encrypt the data in bytes.
'''

# Client class parameter verify=False because the ssl cert cannot
# validate the ip address, there is no hostname vault.cyruslab.local yet.
client = Client(url='https://127.0.0.1:8200', verify=False)

# if the vault has not been initialized, initialize now.
if not client.is_initialized():
    shares = 5 # generate 5 key shares
    threshold = 3 # require 3 keys to unseal the vault.
    result = client.sys.initialize(shares, threshold)

# if symmetric key is not available generate one.
# save the key as a file for future use.
if not exists('enc.key'):
    key = Fernet.generate_key()
    with open('enc.key', 'wb') as file:
        file.write(key)

# use the symmetric key
with open('enc.key', 'rb') as file:
    sym_key = file.read()

fernet = Fernet(sym_key)
# encrypt the result delivered by initialization.
cipher_result = fernet.encrypt(json.dumps(result).encode('utf-8'))
# save the result into a file.
with open('result.enc', 'wb') as file:
    file.write(cipher_result)

get_vault_result.py
The purpose is not to run this directly, but to use by vault_mgmt.py script.

from cryptography.fernet import Fernet
from json import loads


with open('result.enc', 'rb') as file:
    cipher_result = file.read()

with open('enc.key', 'rb') as file:
    key = file.read()

fernet = Fernet(key)
plaintext_result = (fernet.decrypt(cipher_result).decode('utf-8'))
result_in_json = loads(plaintext_result)
# store the first 3 keys
keys = result_in_json.get('keys')[0:3]
root_token = result_in_json.get('root_token')

vault_mgmt.py
This script is also not to be run directly but used by run_ansible.py to retrieve username and password.

from hvac import Client
# The results got from the result.enc
from get_vault_result import root_token, keys

# Create a vault client, dun check the tls cert
# define the vault address.
# put in the root token
client = Client(url='https://127.0.0.1:8200',
                verify=False,
                token=root_token)
# is_sealed() will be deprecated, use sys.is_sealed() instead.
# if the vault is sealed.
if client.sys.is_sealed():
    # submit all stored keys
    # first 3 keys from the 5 are stored in keys.
    client.sys.submit_unseal_keys(keys)

# get the username and password dict
creds = client.secrets.kv.v2.read_secret_version(
    path='cisco',
    # not documented properly in the doc, you need to explicitly define the mount point.
    mount_point='kv'
).get('data').get('data')

# seal the vault
client.sys.seal()

run_ansible.py
The actual script that will execute the backup.yaml file.
This script runs like a human using the command line ansible-playbook though it works, but this is not what i want.

from vault_mgmt import creds
import pexpect

# run the command of ansible-playbook.
# not ideal because i want to use ansible module to run it.
# if you know any examples please let me know in comment.
child = pexpect.spawn('ansible-playbook -i hosts backup.yaml -u {} -k'.format(creds.get('username')))
# this is the expected prompt
child.expect('SSH password:')
# after the expected prompt put in the password.
child.sendline('{}'.format(creds.get('password')))
# wait until the command finished.
child.expect(pexpect.EOF)
# get the output after command finished.
# child.before is in bytes, need to decode into string.
print(child.before.decode('utf-8'))

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s