#!/usr/bin/env python # # (c) 2017 Apstra Inc, <community@apstra.com> # # This file is part of Ansible # # Ansible is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Ansible is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see <http://www.gnu.org/licenses/>. # """ Apstra AOS external inventory script ==================================== Ansible has a feature where instead of reading from /etc/ansible/hosts as a text file, it can query external programs to obtain the list of hosts, groups the hosts are in, and even variables to assign to each host. To use this: - copy this file over /etc/ansible/hosts and chmod +x the file. - Copy both files (.py and .ini) in your preferred directory More information about Ansible Dynamic Inventory here http://unix.stackexchange.com/questions/205479/in-ansible-dynamic-inventory-json-can-i-render-hostvars-based-on-the-hostname 2 modes are currently, supported: **device based** or **blueprint based**: - For **Device based**, the list of device is taken from the global device list the serial ID will be used as the inventory_hostname - For **Blueprint based**, the list of device is taken from the given blueprint the Node name will be used as the inventory_hostname Input parameters parameter can be provided using either with the ini file or by using Environment Variables: The following list of Environment Variables are supported: AOS_SERVER, AOS_PORT, AOS_USERNAME, AOS_PASSWORD, AOS_BLUEPRINT The config file takes precedence over the Environment Variables Tested with Apstra AOS 1.1 This script has been inspired by the cobbler.py inventory. thanks Author: Damien Garros (@dgarros) Version: 0.2.0 """ import json import os import re import sys try: import argparse HAS_ARGPARSE = True except ImportError: HAS_ARGPARSE = False try: from apstra.aosom.session import Session HAS_AOS_PYEZ = True except ImportError: HAS_AOS_PYEZ = False from ansible.module_utils.six.moves import configparser """ ## Expected output format in Device mode { "Cumulus": { "hosts": [ "52540073956E", "52540022211A" ], "vars": {} }, "EOS": { "hosts": [ "5254001CAFD8", "525400DDDF72" ], "vars": {} }, "Generic Model": { "hosts": [ "525400E5486D" ], "vars": {} }, "Ubuntu GNU/Linux": { "hosts": [ "525400E5486D" ], "vars": {} }, "VX": { "hosts": [ "52540073956E", "52540022211A" ], "vars": {} }, "_meta": { "hostvars": { "5254001CAFD8": { "agent_start_time": "2017-02-03T00:49:16.000000Z", "ansible_ssh_host": "172.20.52.6", "aos_hcl_model": "Arista_vEOS", "aos_server": "", "aos_version": "AOS_1.1.1_OB.5", "comm_state": "on", "device_start_time": "2017-02-03T00:47:58.454480Z", "domain_name": "", "error_message": "", "fqdn": "localhost", "hostname": "localhost", "hw_model": "vEOS", "hw_version": "", "is_acknowledged": false, "mgmt_ifname": "Management1", "mgmt_ipaddr": "172.20.52.6", "mgmt_macaddr": "52:54:00:1C:AF:D8", "os_arch": "x86_64", "os_family": "EOS", "os_version": "4.16.6M", "os_version_info": { "build": "6M", "major": "4", "minor": "16" }, "serial_number": "5254001CAFD8", "state": "OOS-QUARANTINED", "vendor": "Arista" }, "52540022211A": { "agent_start_time": "2017-02-03T00:45:22.000000Z", "ansible_ssh_host": "172.20.52.7", "aos_hcl_model": "Cumulus_VX", "aos_server": "172.20.52.3", "aos_version": "AOS_1.1.1_OB.5", "comm_state": "on", "device_start_time": "2017-02-03T00:45:11.019189Z", "domain_name": "", "error_message": "", "fqdn": "cumulus", "hostname": "cumulus", "hw_model": "VX", "hw_version": "", "is_acknowledged": false, "mgmt_ifname": "eth0", "mgmt_ipaddr": "172.20.52.7", "mgmt_macaddr": "52:54:00:22:21:1a", "os_arch": "x86_64", "os_family": "Cumulus", "os_version": "3.1.1", "os_version_info": { "build": "1", "major": "3", "minor": "1" }, "serial_number": "52540022211A", "state": "OOS-QUARANTINED", "vendor": "Cumulus" }, "52540073956E": { "agent_start_time": "2017-02-03T00:45:19.000000Z", "ansible_ssh_host": "172.20.52.8", "aos_hcl_model": "Cumulus_VX", "aos_server": "172.20.52.3", "aos_version": "AOS_1.1.1_OB.5", "comm_state": "on", "device_start_time": "2017-02-03T00:45:11.030113Z", "domain_name": "", "error_message": "", "fqdn": "cumulus", "hostname": "cumulus", "hw_model": "VX", "hw_version": "", "is_acknowledged": false, "mgmt_ifname": "eth0", "mgmt_ipaddr": "172.20.52.8", "mgmt_macaddr": "52:54:00:73:95:6e", "os_arch": "x86_64", "os_family": "Cumulus", "os_version": "3.1.1", "os_version_info": { "build": "1", "major": "3", "minor": "1" }, "serial_number": "52540073956E", "state": "OOS-QUARANTINED", "vendor": "Cumulus" }, "525400DDDF72": { "agent_start_time": "2017-02-03T00:49:07.000000Z", "ansible_ssh_host": "172.20.52.5", "aos_hcl_model": "Arista_vEOS", "aos_server": "", "aos_version": "AOS_1.1.1_OB.5", "comm_state": "on", "device_start_time": "2017-02-03T00:47:46.929921Z", "domain_name": "", "error_message": "", "fqdn": "localhost", "hostname": "localhost", "hw_model": "vEOS", "hw_version": "", "is_acknowledged": false, "mgmt_ifname": "Management1", "mgmt_ipaddr": "172.20.52.5", "mgmt_macaddr": "52:54:00:DD:DF:72", "os_arch": "x86_64", "os_family": "EOS", "os_version": "4.16.6M", "os_version_info": { "build": "6M", "major": "4", "minor": "16" }, "serial_number": "525400DDDF72", "state": "OOS-QUARANTINED", "vendor": "Arista" }, "525400E5486D": { "agent_start_time": "2017-02-02T18:44:42.000000Z", "ansible_ssh_host": "172.20.52.4", "aos_hcl_model": "Generic_Server_1RU_1x10G", "aos_server": "172.20.52.3", "aos_version": "AOS_1.1.1_OB.5", "comm_state": "on", "device_start_time": "2017-02-02T21:11:25.188734Z", "domain_name": "", "error_message": "", "fqdn": "localhost", "hostname": "localhost", "hw_model": "Generic Model", "hw_version": "pc-i440fx-trusty", "is_acknowledged": false, "mgmt_ifname": "eth0", "mgmt_ipaddr": "172.20.52.4", "mgmt_macaddr": "52:54:00:e5:48:6d", "os_arch": "x86_64", "os_family": "Ubuntu GNU/Linux", "os_version": "14.04 LTS", "os_version_info": { "build": "", "major": "14", "minor": "04" }, "serial_number": "525400E5486D", "state": "OOS-QUARANTINED", "vendor": "Generic Manufacturer" } } }, "all": { "hosts": [ "5254001CAFD8", "52540073956E", "525400DDDF72", "525400E5486D", "52540022211A" ], "vars": {} }, "vEOS": { "hosts": [ "5254001CAFD8", "525400DDDF72" ], "vars": {} } } """ def fail(msg): sys.stderr.write("%s\n" % msg) sys.exit(1) class AosInventory(object): def __init__(self): """ Main execution path """ if not HAS_AOS_PYEZ: raise Exception('aos-pyez is not installed. Please see details here: https://github.com/Apstra/aos-pyez') if not HAS_ARGPARSE: raise Exception('argparse is not installed. Please install the argparse library or upgrade to python-2.7') # Initialize inventory self.inventory = dict() # A list of groups and the hosts in that group self.inventory['_meta'] = dict() self.inventory['_meta']['hostvars'] = dict() # Read settings and parse CLI arguments self.read_settings() self.parse_cli_args() # ---------------------------------------------------- # Open session to AOS # ---------------------------------------------------- aos = Session(server=self.aos_server, port=self.aos_server_port, user=self.aos_username, passwd=self.aos_password) aos.login() # Save session information in variables of group all self.add_var_to_group('all', 'aos_session', aos.session) # Add the AOS server itself in the inventory self.add_host_to_group("all", 'aos') self.add_var_to_host("aos", "ansible_ssh_host", self.aos_server) self.add_var_to_host("aos", "ansible_ssh_pass", self.aos_password) self.add_var_to_host("aos", "ansible_ssh_user", self.aos_username) # ---------------------------------------------------- # Build the inventory # 2 modes are supported: device based or blueprint based # - For device based, the list of device is taken from the global device list # the serial ID will be used as the inventory_hostname # - For Blueprint based, the list of device is taken from the given blueprint # the Node name will be used as the inventory_hostname # ---------------------------------------------------- if self.aos_blueprint: bp = aos.Blueprints[self.aos_blueprint] if bp.exists is False: fail("Unable to find the Blueprint: %s" % self.aos_blueprint) for dev_name, dev_id in bp.params['devices'].value.items(): self.add_host_to_group('all', dev_name) device = aos.Devices.find(uid=dev_id) if 'facts' in device.value.keys(): self.add_device_facts_to_var(dev_name, device) # Define admin State and Status if 'user_config' in device.value.keys(): if 'admin_state' in device.value['user_config'].keys(): self.add_var_to_host(dev_name, 'admin_state', device.value['user_config']['admin_state']) self.add_device_status_to_var(dev_name, device) # Go over the contents data structure for node in bp.contents['system']['nodes']: if node['display_name'] == dev_name: self.add_host_to_group(node['role'], dev_name) # Check for additional attribute to import attributes_to_import = [ 'loopback_ip', 'asn', 'role', 'position', ] for attr in attributes_to_import: if attr in node.keys(): self.add_var_to_host(dev_name, attr, node[attr]) # if blueprint_interface is enabled in the configuration # Collect links information if self.aos_blueprint_int: interfaces = dict() for link in bp.contents['system']['links']: # each link has 2 sides [0,1], and it's unknown which one match this device # at first we assume, first side match(0) and peer is (1) peer_id = 1 for side in link['endpoints']: if side['display_name'] == dev_name: # import local information first int_name = side['interface'] # init dict interfaces[int_name] = dict() if 'ip' in side.keys(): interfaces[int_name]['ip'] = side['ip'] if 'interface' in side.keys(): interfaces[int_name]['name'] = side['interface'] if 'display_name' in link['endpoints'][peer_id].keys(): interfaces[int_name]['peer'] = link['endpoints'][peer_id]['display_name'] if 'ip' in link['endpoints'][peer_id].keys(): interfaces[int_name]['peer_ip'] = link['endpoints'][peer_id]['ip'] if 'type' in link['endpoints'][peer_id].keys(): interfaces[int_name]['peer_type'] = link['endpoints'][peer_id]['type'] else: # if we haven't match the first time, prepare the peer_id # for the second loop iteration peer_id = 0 self.add_var_to_host(dev_name, 'interfaces', interfaces) else: for device in aos.Devices: # If not reacheable, create by key and # If reacheable, create by hostname self.add_host_to_group('all', device.name) # populate information for this host self.add_device_status_to_var(device.name, device) if 'user_config' in device.value.keys(): for key, value in device.value['user_config'].items(): self.add_var_to_host(device.name, key, value) # Based on device status online|offline, collect facts as well if device.value['status']['comm_state'] == 'on': if 'facts' in device.value.keys(): self.add_device_facts_to_var(device.name, device) # Check if device is associated with a blueprint # if it's create a new group if 'blueprint_active' in device.value['status'].keys(): if 'blueprint_id' in device.value['status'].keys(): bp = aos.Blueprints.find(uid=device.value['status']['blueprint_id']) if bp: self.add_host_to_group(bp.name, device.name) # ---------------------------------------------------- # Convert the inventory and return a JSON String # ---------------------------------------------------- data_to_print = "" data_to_print += self.json_format_dict(self.inventory, True) print(data_to_print) def read_settings(self): """ Reads the settings from the apstra_aos.ini file """ config = configparser.ConfigParser() config.read(os.path.dirname(os.path.realpath(__file__)) + '/apstra_aos.ini') # Default Values self.aos_blueprint = False self.aos_blueprint_int = True self.aos_username = 'admin' self.aos_password = 'admin' self.aos_server_port = 8888 # Try to reach all parameters from File, if not available try from ENV try: self.aos_server = config.get('aos', 'aos_server') except Exception: if 'AOS_SERVER' in os.environ.keys(): self.aos_server = os.environ['AOS_SERVER'] try: self.aos_server_port = config.get('aos', 'port') except Exception: if 'AOS_PORT' in os.environ.keys(): self.aos_server_port = os.environ['AOS_PORT'] try: self.aos_username = config.get('aos', 'username') except Exception: if 'AOS_USERNAME' in os.environ.keys(): self.aos_username = os.environ['AOS_USERNAME'] try: self.aos_password = config.get('aos', 'password') except Exception: if 'AOS_PASSWORD' in os.environ.keys(): self.aos_password = os.environ['AOS_PASSWORD'] try: self.aos_blueprint = config.get('aos', 'blueprint') except Exception: if 'AOS_BLUEPRINT' in os.environ.keys(): self.aos_blueprint = os.environ['AOS_BLUEPRINT'] try: if config.get('aos', 'blueprint_interface') in ['false', 'no']: self.aos_blueprint_int = False except Exception: pass def parse_cli_args(self): """ Command line argument processing """ parser = argparse.ArgumentParser(description='Produce an Ansible Inventory file based on Apstra AOS') parser.add_argument('--list', action='store_true', default=True, help='List instances (default: True)') parser.add_argument('--host', action='store', help='Get all the variables about a specific instance') self.args = parser.parse_args() def json_format_dict(self, data, pretty=False): """ Converts a dict to a JSON object and dumps it as a formatted string """ if pretty: return json.dumps(data, sort_keys=True, indent=2) else: return json.dumps(data) def add_host_to_group(self, group, host): # Cleanup group name first clean_group = self.cleanup_group_name(group) # Check if the group exist, if not initialize it if clean_group not in self.inventory.keys(): self.inventory[clean_group] = {} self.inventory[clean_group]['hosts'] = [] self.inventory[clean_group]['vars'] = {} self.inventory[clean_group]['hosts'].append(host) def add_var_to_host(self, host, var, value): # Check if the host exist, if not initialize it if host not in self.inventory['_meta']['hostvars'].keys(): self.inventory['_meta']['hostvars'][host] = {} self.inventory['_meta']['hostvars'][host][var] = value def add_var_to_group(self, group, var, value): # Cleanup group name first clean_group = self.cleanup_group_name(group) # Check if the group exist, if not initialize it if clean_group not in self.inventory.keys(): self.inventory[clean_group] = {} self.inventory[clean_group]['hosts'] = [] self.inventory[clean_group]['vars'] = {} self.inventory[clean_group]['vars'][var] = value def add_device_facts_to_var(self, device_name, device): # Populate variables for this host self.add_var_to_host(device_name, 'ansible_ssh_host', device.value['facts']['mgmt_ipaddr']) self.add_var_to_host(device_name, 'id', device.id) # self.add_host_to_group('all', device.name) for key, value in device.value['facts'].items(): self.add_var_to_host(device_name, key, value) if key == 'os_family': self.add_host_to_group(value, device_name) elif key == 'hw_model': self.add_host_to_group(value, device_name) def cleanup_group_name(self, group_name): """ Clean up group name by : - Replacing all non-alphanumeric caracter by underscore - Converting to lowercase """ rx = re.compile(r'\W+') clean_group = rx.sub('_', group_name).lower() return clean_group def add_device_status_to_var(self, device_name, device): if 'status' in device.value.keys(): for key, value in device.value['status'].items(): self.add_var_to_host(device.name, key, value) # Run the script if __name__ == '__main__': AosInventory()