text: #!/usr/bin/env python
  #
  # (c) 2017 Apstra Inc, <community@apstra.com>
  #
  # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
  #
  
  from __future__ import (absolute_import, division, print_function)
  __metaclass__ = type
  
  """
  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()
  