@ -28,6 +28,11 @@ that those correspond to addresses.
See http : / / ansible . github . com / api . html for more info
See http : / / ansible . github . com / api . html for more info
Tested with Cobbler 2.0 .11 .
Tested with Cobbler 2.0 .11 .
Changelog :
- 2013 - 09 - 01 pgehres : Refactored implementation to make use of caching and to
limit the number of connections to external cobbler server for performance .
Added use of cobbler . ini file to configure settings . Tested with Cobbler 2.4 .0
"""
"""
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
@ -50,86 +55,210 @@ Tested with Cobbler 2.0.11.
######################################################################
######################################################################
import sys
import argparse
import ConfigParser
import os
import re
from time import time
import xmlrpclib
import xmlrpclib
import shlex
try :
try :
import json
import json
except :
except ImportError :
import simplejson as json
import simplejson as json
# NOTE -- this file assumes Ansible is being accessed FROM the cobbler
# NOTE -- this file assumes Ansible is being accessed FROM the cobbler
# server, so it does not attempt to login with a username and password.
# server, so it does not attempt to login with a username and password.
# this will be addressed in a future version of this script.
# this will be addressed in a future version of this script.
conn = xmlrpclib . Server ( " http://127.0.0.1/cobbler_api " , allow_none = True )
###################################################
class CobblerInventory ( object ) :
# executed with no parameters, return the list of
# all groups and hosts
def __init__ ( self ) :
""" Main execution path """
self . conn = None
self . inventory = dict ( ) # A list of groups and the hosts in that group
self . cache = dict ( ) # Details about hosts in the inventory
# Read settings and parse CLI arguments
self . read_settings ( )
self . parse_cli_args ( )
# Cache
if self . args . refresh_cache :
self . update_cache ( )
elif not self . is_cache_valid ( ) :
self . update_cache ( )
else :
self . load_inventory_from_cache ( )
self . load_cache_from_cache ( )
data_to_print = " "
# Data to print
if self . args . host :
data_to_print = self . get_host_info ( )
elif self . args . list :
# Display list of instances for inventory
data_to_print = self . json_format_dict ( self . inventory , True )
else : # default action with no options
data_to_print = self . json_format_dict ( self . inventory , True )
print data_to_print
def _connect ( self ) :
if not self . conn :
self . conn = xmlrpclib . Server ( self . cobbler_host , allow_none = True )
def is_cache_valid ( self ) :
""" Determines if the cache files have expired, or if it is still valid """
if os . path . isfile ( self . cache_path_cache ) :
mod_time = os . path . getmtime ( self . cache_path_cache )
current_time = time ( )
if ( mod_time + self . cache_max_age ) > current_time :
if os . path . isfile ( self . cache_path_inventory ) :
return True
return False
def read_settings ( self ) :
""" Reads the settings from the cobbler.ini file """
config = ConfigParser . SafeConfigParser ( )
config . read ( os . path . dirname ( os . path . realpath ( __file__ ) ) + ' /cobbler.ini ' )
self . cobbler_host = config . get ( ' cobbler ' , ' host ' )
# Cache related
cache_path = config . get ( ' cobbler ' , ' cache_path ' )
self . cache_path_cache = cache_path + " /ansible-cobbler.cache "
self . cache_path_inventory = cache_path + " /ansible-cobbler.index "
self . cache_max_age = config . getint ( ' cobbler ' , ' cache_max_age ' )
def parse_cli_args ( self ) :
""" Command line argument processing """
parser = argparse . ArgumentParser ( description = ' Produce an Ansible Inventory file based on Cobbler ' )
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 ' )
parser . add_argument ( ' --refresh-cache ' , action = ' store_true ' , default = False ,
help = ' Force refresh of cache by making API requests to cobbler (default: False - use cache files) ' )
self . args = parser . parse_args ( )
def update_cache ( self ) :
""" Make calls to cobbler and save the output in a cache """
self . _connect ( )
self . groups = dict ( )
self . hosts = dict ( )
data = self . conn . get_systems ( )
for host in data :
# Get the FQDN for the host and add it to the right groups
dns_name = None
ksmeta = None
interfaces = host [ ' interfaces ' ]
for ( iname , ivalue ) in interfaces . iteritems ( ) :
if ivalue [ ' management ' ] :
this_dns_name = ivalue . get ( ' dns_name ' , None )
if this_dns_name is not None and this_dns_name is not " " :
dns_name = this_dns_name
if dns_name is None :
continue
status = host [ ' status ' ]
profile = host [ ' profile ' ]
classes = host [ ' mgmt_classes ' ]
if status not in self . inventory :
self . inventory [ status ] = [ ]
self . inventory [ status ] . append ( dns_name )
if profile not in self . inventory :
self . inventory [ profile ] = [ ]
self . inventory [ profile ] . append ( dns_name )
for cls in classes :
if cls not in self . inventory :
self . inventory [ cls ] = [ ]
self . inventory [ cls ] . append ( dns_name )
# Since we already have all of the data for the host, update the host details as well
# The old way was ksmeta only -- provide backwards compatibility
self . cache [ dns_name ] = dict ( )
if " ks_meta " in host :
for key , value in host [ " ks_meta " ] . iteritems ( ) :
self . cache [ dns_name ] [ key ] = value
self . write_to_cache ( self . cache , self . cache_path_cache )
self . write_to_cache ( self . inventory , self . cache_path_inventory )
def get_host_info ( self ) :
""" Get variables about a specific host """
if not self . cache or len ( self . cache ) == 0 :
# Need to load index from cache
self . load_cache_from_cache ( )
if not self . args . host in self . cache :
# try updating the cache
self . update_cache ( )
if len ( sys . argv ) == 2 and ( sys . argv [ 1 ] == ' --list ' ) :
if not self . args . host in self . cache :
# host might not exist anymore
return self . json_format_dict ( { } , True )
systems = conn . get_item_names ( ' system ' )
return self . json_format_dict ( self . cache [ self . args . host ] , True )
groups = { ' ungrouped ' : [ ] }
for system in systems :
def push ( self , my_dict , key , element ) :
""" Pushed an element onto an array that may not have been defined in the dict """
data = conn . get_blended_data ( None , system )
if key in my_dict :
my_dict [ key ] . append ( element )
else :
my_dict [ key ] = [ element ]
dns_name = None
def load_inventory_from_cache ( self ) :
interfaces = data [ ' interfaces ' ]
""" Reads the index from the cache file sets self.index """
for ( iname , ivalue ) in interfaces . iteritems ( ) :
this_dns_name = ivalue . get ( ' dns_name ' , None )
if this_dns_name is not None :
dns_name = this_dns_name
if dns_name is None :
cache = open ( self . cache_path_inventory , ' r ' )
continue
json_inventory = cache . read ( )
self . inventory = json . loads ( json_inventory )
classes = data [ ' mgmt_classes ' ]
def load_cache_from_cache ( self ) :
for cls in classes :
""" Reads the cache from the cache file sets self.cache """
if cls not in groups :
groups [ cls ] = [ ]
# hostname is not really what we want to insert, really insert the
# first DNS name but no further DNS names
groups [ cls ] . append ( dns_name )
# handle hosts without mgmt_classes
cache = open ( self . cache_path_cache , ' r ' )
if not classes :
json_cache = cache . read ( )
groups [ ' ungrouped ' ] . append ( dns_name )
self . cache = json . loads ( json_cache )
print json . dumps ( groups )
def write_to_cache ( self , data , filename ) :
sys . exit ( 0 )
""" Writes data in JSON format to a file """
#####################################################
json_data = self . json_format_dict ( data , True )
# executed with a hostname as a parameter, return the
cache = open ( filename , ' w ' )
# variables for that host
cache . write ( json_data )
cache . close ( )
elif len ( sys . argv ) == 3 and ( sys . argv [ 1 ] == ' --host ' ) :
def to_safe ( self , word ) :
""" Converts ' bad ' characters in a string to underscores so they can be used as Ansible groups """
# look up the system record for the given DNS name
return re . sub ( " [^A-Za-z0-9 \ -] " , " _ " , word )
result = conn . find_system_by_dns_name ( sys . argv [ 2 ] )
system = result . get ( ' name ' , None )
data = { }
if system is None :
print json . dumps ( { } )
sys . exit ( 1 )
data = conn . get_system_for_koan ( system )
# return the ksmeta data for that system
def json_format_dict ( self , data , pretty = False ) :
metadata = data [ ' ks_meta ' ]
""" Converts a dict to a JSON object and dumps it as a formatted string """
tokens = shlex . split ( metadata )
results = { }
for t in tokens :
if t . find ( " = " ) != - 1 :
( k , v ) = t . split ( " = " , 1 )
results [ k ] = v
print json . dumps ( results )
sys . exit ( 0 )
else :
if pretty :
return json . dumps ( data , sort_keys = True , indent = 2 )
else :
return json . dumps ( data )
print " usage: --list ..OR.. --host <hostname> "
CobblerInventory ( )
sys . exit ( 1 )