summaryrefslogblamecommitdiff
path: root/theforeman.py
blob: efcb79ec642411e1371f6e7ab7460778cdd97ef8 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

















                                                                                  

                                                              


























                                                                                   
            








                                                                           


                                               




          
               















                                                     
















































                                                                                   









                                          


                                                                       



                        
 




                                                        

                                                
            
                                                                              
                                  

                                                                              







                                                        














                                                                      


                            
                                                  

                                              






                                                     

                          
                          
                                                                                             

                                                      

                     





























                                                                            
                       







                                                                          




                                                    
                                                         




                                                      

                                

                          
 
#!/usr/bin/env python
#
# Internet Systems Consortium license
#
# Copyright (c) 2014, Franck Cuny (<franckcuny@gmail.com>)
#
# Permission to use, copy, modify, and/or distribute this software for any purpose
# with or without fee is hereby granted, provided that the above copyright notice
# and this permission notice appear in all copies.

# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
# FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
# TORTUOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
# THIS SOFTWARE.

# TODO -- proper exception handling and meaningful error codes


'''
Foreman external inventory script
=================================

Generates inventory that Ansible can understand by making API requests
to Foreman.

Information about the Foreman's instance can be stored in the ``foreman.ini`` file.
A ``base_url``, ``username`` and ``password`` need to be provided. The path to an
alternate configuration file can be provided by exporting the ``FOREMAN_INI_PATH``
variable.

When run against a specific host, this script returns the following variables
based on the data obtained from Foreman:
 - id
 - ip
 - name
 - environment
 - os
 - model
 - compute_resource
 - domain
 - architecture
 - created
 - updated
 - status
 - hostgroup
 - ansible_ssh_host

When run in --list mode, instances are grouped by the following categories:
 - group

Examples:
  Execute uname on all instances in the dev group
  $ ansible -i theforeman.py dev -m shell -a \"/bin/uname -a\"

Authors:  Franck Cuny <franckcuny@gmail.com>
          Andrew Deck <andrew.deck@outlook.com>
Version: 0.0.2
'''

import sys
import os
import re
import optparse
import ConfigParser
import collections

try:
    import json
except ImportError:
    import simplejson as json

try:
    from foreman.client import Foreman
    from requests.exceptions import ConnectionError
except ImportError, e:
    print ('python-foreman required for this module')
    print e
    sys.exit(1)

class CLIMain(object):
  def __init__(self):
    """Program entry point"""
    settings = self.read_settings()
    self.parse_cli_args()
    foreman = ForemanInventory(settings['username']
                              ,settings['password']
                              ,settings['base_url'])
    if self.args.all:
      data_to_print = foreman.get_all()
    elif self.args.host:
      data_to_print = foreman.get_host_info(self.args.host)
    elif self.args.list:
      data_to_print = foreman.get_inventory()
    else:
      data_to_print = {}
    print(json.dumps(data_to_print, sort_keys=True, indent=4))
    
  def read_settings(self):
    """Read the settings from the foreman.ini file"""
    config = ConfigParser.SafeConfigParser()
    scriptdir = os.path.dirname(os.path.realpath(__file__))
    foreman_default_ini_path = os.path.join(scriptdir, 'foreman.ini')
    foreman_ini_path = os.environ.get('FOREMAN_INI_PATH', foreman_default_ini_path)
    config.read(foreman_ini_path)
    settings = {}
    required = ['base_url', 'username', 'password']
    missing = []
    for setting in required:
      val = config.get('foreman', setting)
      if val is None:
        missing.append(setting)
      settings[setting] = val
    if missing != []:
      raise Exception("Could not find values for Foreman " + ', '.join(missing)
                      + ". They must be specified via ini file.")
    return settings

  def parse_cli_args(self):
    """Command line argument processing"""
    description = 'Produce an Ansible Inventory file based on Foreman'
    parser = optparse.OptionParser(description=description)
    parser.add_option('--list', action='store_true', default=True,
          help='List instances (default: True)')
    parser.add_option('--host', action='store',
          help='Get all the variables about a specific instance')
    parser.add_option('--all', action='store_true',
          help= 'Get all the variables for all the instances')
    (self.args, self.options) = parser.parse_args()

class ForemanInventory(object):
    """Foreman Inventory"""

    def _empty_inventory(self):
        """Empty inventory"""
        return {'_meta': {'hostvars': {}}}

    def _empty_cache(self):
        """Empty cache"""
        keys = ['operatingsystem', 'hostgroup', 'environment', 'model',
                'compute_resource', 'domain', 'subnet', 'architecture',
                'host']
        keys_d = {}
        for i in keys:
          keys_d[i] = {}
        return keys_d

    def __init__(self, username, password, foreman_url):
        # initializing vars
        self.base_url = foreman_url
        self.username = username
        self.password = password
        self.inventory = self._empty_inventory()
        self._cache = self._empty_cache()
        try:
          self.client = Foreman(self.base_url, (self.username, self.password))
        except ConnectionError, e:
          raise Exception("It looks like Foreman's API is unreachable. Error "
                          "was: " + str(e))

    def get_host_info(self, host_id):
        """Get information about an host"""
        host_desc = {}

        meta = self._get_object_from_id('host', host_id)
        if meta is None:
            return host_desc
        for k in ['id', 'ip', 'name', 'status']:
          host_desc[k] = meta.get(k)
        for k in ['model', 'compute_resource', 'domain', 'subnet'
                  ,'architecture', 'hostgroup']:
          host_desc[k] = self._get_from_type(k, meta)
        for k in ['created', 'updated']:
          host_desc[k] = meta.get(k + '_at')
        host_desc['os'] = self._get_from_type('operatingsystem', meta)
        try:
          k = 'environment'
          host_desc[k] = meta.get(k).get(k).get('name').lower()
        except Exception:
          pass # do nothing
        # to ssh from ansible
        host_desc['ansible_ssh_host'] = host_desc['ip']
        return host_desc

    def get_inventory(self):
        """Get all the hosts from the inventory"""
        groups = collections.defaultdict(list)
        hosts  = []
        page = 1
        while True:
            resp = self.client.index_hosts(page=page)
            if len(resp) < 1:
                break
            page  += 1
            hosts += resp
        if len(hosts) < 1:
            return groups
        for host in hosts:
            host_group = self._get_from_id('hostgroup', host.get('host').get('hostgroup_id'))
            server_name = host.get('host').get('name')
            groups[host_group].append(server_name)
        return groups

    def get_all(self):
      """Get all the machines and all the variables for all the machines"""
      groups = self.get_inventory()
      hosts = {}
      for group in groups:
        for host in groups[group]:
          hosts[host] = True
      for host in hosts:
        hosts[host] = self.get_host_info(host)
      groups['_meta'] = {'hostvars': hosts}
      return groups

    def _get_from_type(self, param_type, host):
      return self._get_from_id(param_type, host.get(param_type + '_id'))

    def _get_from_id(self, param_type, param_id):
        """Get the object of type param_type associated with the ID param_id
        The following values for param_type are explicitly accounted for:
        - architecture
        - subnet
        - domain
        - compute_resource
        - model
        - environment
        - label
        - hostgroup
        - operatingsystem
        """
        param = self._get_object_from_id(param_type, param_id)
        if param is None:
            return None
        if param_type == "hostgroup":
            return param.get('label')
        elif param_type == 'operatingsystem':
            return "{0}-{1}".format(param.get('name'), param.get('major'))
        elif param_type == 'environment':
            return param.get('name').lower()
        else:
            return param.get('name')

    def _get_object_from_id(self, obj_type, obj_id):
        """Get an object from it's ID"""
        if obj_id is None:
            return None
        obj = self._cache.get(obj_type).get(obj_id, None)
        if obj is None:
            method_name = "show_{0}s".format(obj_type)
            func = getattr(self.client, method_name)
            obj = func(obj_id)
            self._cache[obj_type][obj_id] = obj
        return obj.get(obj_type)

if __name__ == '__main__':
  CLIMain()