├── hammer.ini ├── README.md └── satellite-inventory.py /hammer.ini: -------------------------------------------------------------------------------- 1 | # Ansible Satellite 6 external inventory script settings 2 | # 3 | 4 | [hammer] 5 | 6 | host = https://mysat.redhat.com 7 | username = admin 8 | password = changeme 9 | organisation = MyOrg 10 | 11 | # API calls to Satellite6 can be slow. For this reason, we cache the results of an API 12 | # call. Set this to the path you want cache files to be written to. Two files 13 | # will be written to this directory: 14 | # - ansible-satellite6.cache 15 | # - ansible-satellite6.index 16 | cache_path = /tmp 17 | 18 | # The number of seconds a cache file is considered valid. After this many 19 | # seconds, a new API call will be made, and the cache file will be updated. 20 | cache_max_age = 900 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | DO NO USE - use https://github.com/theforeman/foreman_ansible_inventory instead. 2 | 3 | 4 | Ansible Dynamic Inventory for Red Hat Satellite 6 5 | ================================================= 6 | * Author: Nick Strugnell 7 | * Email: nstrug@redhat.com 8 | * Date: 2015-12-02 9 | * Version: 0.1.0 10 | 11 | ### Introduction 12 | This is a dynamic inventory script to drive Ansible from Red Hat Satellite 6. It's based on the Cobbler inventory script [here](https://github.com/ansible/ansible/blob/devel/contrib/inventory/cobbler.py) 13 | It uses the same caching mechanism as that script so you might want to refer to that to understand it a little better. 14 | 15 | Currently, hosts are grouped by hostgroups only. I will add functionality to group by host collection, lifecycle environment, location, organisation etc. 16 | 17 | ### Usage 18 | Copy satellite-inventory.py and hammer.ini to /etc/ansible and ensure that satellite-inventory.py is executable. 19 | Put your satellite credentials in the hammer.ini file. 20 | 21 | You should then be able to run ansible against hostgroups e.g.: 22 | 23 | ansible -i /etc/ansible/satellite-inventory.py -m setup 24 | 25 | 26 | -------------------------------------------------------------------------------- /satellite-inventory.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Red Hat Satellite 6 external inventory script 5 | ============================================= 6 | 7 | Ansible has a feature where instead of reading from /etc/ansible/hosts 8 | as a text file, it can query external programs to obtain the list 9 | of hosts, groups the hosts are in, and even variables to assign to each host. 10 | 11 | To use this, copy this file over /etc/ansible/hosts and chmod +x the file. 12 | This, more or less, allows you to keep one central database containing 13 | info about all of your managed instances. 14 | 15 | This script is an example of sourcing that data from Red Hat Satellite 6. 16 | Each of the following satellite management entitities will correspond to a 17 | group in Ansible: 18 | 19 | * Location 20 | * Lifecycle Environment 21 | * Hostgroup 22 | * Host Collection 23 | 24 | See http://ansible.github.com/api.html for more info 25 | 26 | Tested with Satellite 6.1.6 27 | 28 | Changelog: 29 | - 2016-01-26 mburgerh: Cleanup 30 | - 2015-11-02 nstrug: Initial version, based on cobbler.py 31 | 32 | """ 33 | 34 | # (c) 2015, Nick Strugnell 35 | # 36 | # This file is part of Ansible, 37 | # 38 | # Ansible is free software: you can redistribute it and/or modify 39 | # it under the terms of the GNU General Public License as published by 40 | # the Free Software Foundation, either version 3 of the License, or 41 | # (at your option) any later version. 42 | # 43 | # Ansible is distributed in the hope that it will be useful, 44 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 45 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 46 | # GNU General Public License for more details. 47 | # 48 | # You should have received a copy of the GNU General Public License 49 | # along with Ansible. If not, see . 50 | 51 | ###################################################################### 52 | 53 | import argparse 54 | import ConfigParser 55 | import os 56 | import re 57 | from time import time 58 | import requests 59 | import sys 60 | import json 61 | 62 | 63 | class SatelliteInventory(object): 64 | 65 | def __init__(self): 66 | 67 | """ Main execution path """ 68 | self.conn = None 69 | 70 | self.inventory = dict() # A list of groups and the hosts in that group 71 | self.cache = dict() # Details about hosts in the inventory 72 | 73 | # Read settings and parse CLI arguments 74 | self.read_settings() 75 | self.parse_cli_args() 76 | 77 | self.post_headers = {'content-type': 'application/json'} 78 | self.ssl_verify = True 79 | 80 | self.org = self.get_json(self.sat_api + "organizations?search=" + self._org_name) 81 | if self.org['results'] == []: 82 | sys.exit(1) 83 | 84 | if self.args.refresh_cache: 85 | self.update_cache() 86 | elif not self.is_cache_valid(): 87 | self.update_cache() 88 | else: 89 | self.load_inventory_from_cache() 90 | self.load_cache_from_cache() 91 | 92 | data_to_print = "" 93 | 94 | # Data to print 95 | if self.args.host: 96 | data_to_print += self.get_host_info() 97 | else: 98 | data_to_print += self.json_format_dict(self.inventory, True) 99 | 100 | print(data_to_print) 101 | 102 | def get_json(self, url): 103 | r = requests.get(url, auth=(self._username, self._password), 104 | verify=self.ssl_verify) 105 | return r.json() 106 | 107 | def is_cache_valid(self): 108 | """ Determines if cache files have expired or if it is still valid """ 109 | 110 | if os.path.isfile(self.cache_path_cache): 111 | mod_time = os.path.getmtime(self.cache_path_cache) 112 | current_time = time() 113 | if (mod_time + self.cache_max_age) > current_time: 114 | if os.path.isfile(self.cache_path_inventory): 115 | return True 116 | 117 | return False 118 | 119 | def read_settings(self): 120 | """ Reads the settings from the hammer.ini file """ 121 | 122 | config = ConfigParser.SafeConfigParser() 123 | config.read(os.path.dirname(os.path.realpath(__file__)) 124 | + '/hammer.ini') 125 | 126 | self._host = config.get('hammer', 'host') 127 | self.sat_api = "%s/api/v2/" % self._host 128 | self.katello_api = "%s/katello/api/v2/" % self._host 129 | self._username = config.get('hammer', 'username') 130 | self._password = config.get('hammer', 'password') 131 | self._org_name = config.get('hammer', 'organisation') 132 | 133 | # Cache related 134 | cache_path = config.get('hammer', 'cache_path') 135 | self.cache_path_cache = cache_path + "/ansible-hammer.cache" 136 | self.cache_path_inventory = cache_path + "/ansible-hammer.index" 137 | self.cache_max_age = config.getint('hammer', 'cache_max_age') 138 | 139 | def parse_cli_args(self): 140 | """ Command line argument processing """ 141 | 142 | parser = argparse.ArgumentParser(description='Produce an Ansible Inventory file based on Satellite 6') 143 | parser.add_argument('--list', action='store_true', default=True, help='List instances (default: True)') 144 | parser.add_argument('--host', action='store', help='Get all the variables about a specific instance') 145 | parser.add_argument('--refresh-cache', action='store_true', default=False, 146 | help='Force refresh of cache by making API requests to hammer (default: False - use cache files)') 147 | self.args = parser.parse_args() 148 | 149 | def update_cache(self): 150 | """ Make calls to satellite and save the output in a cache """ 151 | 152 | self.hostgroups = self.get_json(self.sat_api + "hostgroups") 153 | self.systems = self.get_json(self.sat_api + "hosts") 154 | 155 | for system in self.systems['results']: 156 | if system['hostgroup_name'] not in self.inventory: 157 | self.inventory[system['hostgroup_name']] = [] 158 | self.inventory[system['hostgroup_name']].append(system['name']) 159 | 160 | self.write_to_cache(self.cache, self.cache_path_cache) 161 | self.write_to_cache(self.inventory, self.cache_path_inventory) 162 | 163 | def get_host_info(self): 164 | """ Get variables about a specific host """ 165 | 166 | if not self.cache or len(self.cache) == 0: 167 | # Need to load index from cache 168 | self.load_cache_from_cache() 169 | 170 | if self.args.host not in self.cache: 171 | # try updating the cache 172 | self.update_cache() 173 | 174 | if self.args.host not in self.cache: 175 | # host might not exist anymore 176 | return self.json_format_dict({}, True) 177 | 178 | return self.json_format_dict(self.cache[self.args.host], True) 179 | 180 | def push(self, my_dict, key, element): 181 | """ Pushed an element onto an array that may not have been defined in 182 | the dict """ 183 | 184 | if key in my_dict: 185 | my_dict[key].append(element) 186 | else: 187 | my_dict[key] = [element] 188 | 189 | def load_inventory_from_cache(self): 190 | """ Reads the index from the cache file sets self.index """ 191 | 192 | cache = open(self.cache_path_inventory, 'r') 193 | json_inventory = cache.read() 194 | self.inventory = json.loads(json_inventory) 195 | 196 | def load_cache_from_cache(self): 197 | """ Reads the cache from the cache file sets self.cache """ 198 | 199 | cache = open(self.cache_path_cache, 'r') 200 | json_cache = cache.read() 201 | self.cache = json.loads(json_cache) 202 | 203 | def write_to_cache(self, data, filename): 204 | """ Writes data in JSON format to a file """ 205 | json_data = self.json_format_dict(data, True) 206 | cache = open(filename, 'w') 207 | cache.write(json_data) 208 | cache.close() 209 | 210 | def to_safe(self, word): 211 | """ Converts 'bad' characters in a string to underscores so they can be 212 | used as Ansible groups """ 213 | 214 | return re.sub("[^A-Za-z0-9\-]", "_", word) 215 | 216 | def json_format_dict(self, data, pretty=False): 217 | """ Converts a dict to a JSON object and dumps it as a 218 | formatted string """ 219 | 220 | if pretty: 221 | return json.dumps(data, sort_keys=True, indent=2) 222 | else: 223 | return json.dumps(data) 224 | 225 | SatelliteInventory() 226 | --------------------------------------------------------------------------------