1#!/usr/bin/env python
2
3"""
4Spacewalk external inventory script
5=================================
6
7Ansible has a feature where instead of reading from /usr/local/etc/ansible/hosts
8as a text file, it can query external programs to obtain the list
9of hosts, groups the hosts are in, and even variables to assign to each host.
10
11To use this, copy this file over /usr/local/etc/ansible/hosts and chmod +x the file.
12This, more or less, allows you to keep one central database containing
13info about all of your managed instances.
14
15This script is dependent upon the spacealk-reports package being installed
16on the same machine. It is basically a CSV-to-JSON converter from the
17output of "spacewalk-report system-groups-systems|inventory".
18
19Tested with Ansible 1.9.2 and spacewalk 2.3
20"""
21#
22# Author:: Jon Miller <jonEbird@gmail.com>
23# Copyright:: Copyright (c) 2013, Jon Miller
24#
25# Extended for support of multiple organizations and
26# adding the "_meta" dictionary to --list output by
27# Bernhard Lichtinger <bernhard.lichtinger@lrz.de> 2015
28#
29# This program is free software: you can redistribute it and/or modify
30# it under the terms of the GNU General Public License as published by
31# the Free Software Foundation, either version 2 of the License, or (at
32# your option) any later version.
33#
34# This program is distributed in the hope that it will be useful, but
35# WITHOUT ANY WARRANTY; without even the implied warranty of
36# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
37# General Public License for more details.
38#
39# You should have received a copy of the GNU General Public License
40# along with this program.  If not, see <http://www.gnu.org/licenses/>.
41#
42
43from __future__ import print_function
44
45import sys
46import os
47import time
48from optparse import OptionParser
49import subprocess
50import json
51
52from ansible.module_utils.six import iteritems
53from ansible.module_utils.six.moves import configparser as ConfigParser
54
55
56base_dir = os.path.dirname(os.path.realpath(__file__))
57default_ini_file = os.path.join(base_dir, "spacewalk.ini")
58
59SW_REPORT = '/usr/bin/spacewalk-report'
60CACHE_DIR = os.path.join(base_dir, ".spacewalk_reports")
61CACHE_AGE = 300  # 5min
62INI_FILE = os.path.expanduser(os.path.expandvars(os.environ.get("SPACEWALK_INI_PATH", default_ini_file)))
63
64
65# Sanity check
66if not os.path.exists(SW_REPORT):
67    print('Error: %s is required for operation.' % (SW_REPORT), file=sys.stderr)
68    sys.exit(1)
69
70# Pre-startup work
71if not os.path.exists(CACHE_DIR):
72    os.mkdir(CACHE_DIR)
73    os.chmod(CACHE_DIR, 0o2775)
74
75# Helper functions
76# ------------------------------
77
78
79def spacewalk_report(name):
80    """Yield a dictionary form of each CSV output produced by the specified
81    spacewalk-report
82    """
83    cache_filename = os.path.join(CACHE_DIR, name)
84    if not os.path.exists(cache_filename) or \
85            (time.time() - os.stat(cache_filename).st_mtime) > CACHE_AGE:
86        # Update the cache
87        fh = open(cache_filename, 'w')
88        p = subprocess.Popen([SW_REPORT, name], stdout=fh)
89        p.wait()
90        fh.close()
91
92    lines = open(cache_filename, 'r').readlines()
93    keys = lines[0].strip().split(',')
94    # add 'spacewalk_' prefix to the keys
95    keys = ['spacewalk_' + key for key in keys]
96    for line in lines[1:]:
97        values = line.strip().split(',')
98        if len(keys) == len(values):
99            yield dict(zip(keys, values))
100
101
102# Options
103# ------------------------------
104
105parser = OptionParser(usage="%prog [options] --list | --host <machine>")
106parser.add_option('--list', default=False, dest="list", action="store_true",
107                  help="Produce a JSON consumable grouping of servers for Ansible")
108parser.add_option('--host', default=None, dest="host",
109                  help="Generate additional host specific details for given host for Ansible")
110parser.add_option('-H', '--human', dest="human",
111                  default=False, action="store_true",
112                  help="Produce a friendlier version of either server list or host detail")
113parser.add_option('-o', '--org', default=None, dest="org_number",
114                  help="Limit to spacewalk organization number")
115parser.add_option('-p', default=False, dest="prefix_org_name", action="store_true",
116                  help="Prefix the group name with the organization number")
117(options, args) = parser.parse_args()
118
119
120# read spacewalk.ini if present
121# ------------------------------
122if os.path.exists(INI_FILE):
123    config = ConfigParser.SafeConfigParser()
124    config.read(INI_FILE)
125    if config.has_option('spacewalk', 'cache_age'):
126        CACHE_AGE = config.get('spacewalk', 'cache_age')
127    if not options.org_number and config.has_option('spacewalk', 'org_number'):
128        options.org_number = config.get('spacewalk', 'org_number')
129    if not options.prefix_org_name and config.has_option('spacewalk', 'prefix_org_name'):
130        options.prefix_org_name = config.getboolean('spacewalk', 'prefix_org_name')
131
132
133# Generate dictionary for mapping group_id to org_id
134# ------------------------------
135org_groups = {}
136try:
137    for group in spacewalk_report('system-groups'):
138        org_groups[group['spacewalk_group_id']] = group['spacewalk_org_id']
139
140except (OSError) as e:
141    print('Problem executing the command "%s system-groups": %s' %
142          (SW_REPORT, str(e)), file=sys.stderr)
143    sys.exit(2)
144
145
146# List out the known server from Spacewalk
147# ------------------------------
148if options.list:
149
150    # to build the "_meta"-Group with hostvars first create dictionary for later use
151    host_vars = {}
152    try:
153        for item in spacewalk_report('inventory'):
154            host_vars[item['spacewalk_profile_name']] = dict((key, (value.split(';') if ';' in value else value)) for key, value in item.items())
155
156    except (OSError) as e:
157        print('Problem executing the command "%s inventory": %s' %
158              (SW_REPORT, str(e)), file=sys.stderr)
159        sys.exit(2)
160
161    groups = {}
162    meta = {"hostvars": {}}
163    try:
164        for system in spacewalk_report('system-groups-systems'):
165            # first get org_id of system
166            org_id = org_groups[system['spacewalk_group_id']]
167
168            # shall we add the org_id as prefix to the group name:
169            if options.prefix_org_name:
170                prefix = org_id + "-"
171                group_name = prefix + system['spacewalk_group_name']
172            else:
173                group_name = system['spacewalk_group_name']
174
175            # if we are limited to one organization:
176            if options.org_number:
177                if org_id == options.org_number:
178                    if group_name not in groups:
179                        groups[group_name] = set()
180
181                    groups[group_name].add(system['spacewalk_server_name'])
182                    if system['spacewalk_server_name'] in host_vars and not system['spacewalk_server_name'] in meta["hostvars"]:
183                        meta["hostvars"][system['spacewalk_server_name']] = host_vars[system['spacewalk_server_name']]
184            # or we list all groups and systems:
185            else:
186                if group_name not in groups:
187                    groups[group_name] = set()
188
189                groups[group_name].add(system['spacewalk_server_name'])
190                if system['spacewalk_server_name'] in host_vars and not system['spacewalk_server_name'] in meta["hostvars"]:
191                    meta["hostvars"][system['spacewalk_server_name']] = host_vars[system['spacewalk_server_name']]
192
193    except (OSError) as e:
194        print('Problem executing the command "%s system-groups-systems": %s' %
195              (SW_REPORT, str(e)), file=sys.stderr)
196        sys.exit(2)
197
198    if options.human:
199        for group, systems in iteritems(groups):
200            print('[%s]\n%s\n' % (group, '\n'.join(systems)))
201    else:
202        final = dict([(k, list(s)) for k, s in iteritems(groups)])
203        final["_meta"] = meta
204        print(json.dumps(final))
205        # print(json.dumps(groups))
206    sys.exit(0)
207
208
209# Return a details information concerning the spacewalk server
210# ------------------------------
211elif options.host:
212
213    host_details = {}
214    try:
215        for system in spacewalk_report('inventory'):
216            if system['spacewalk_hostname'] == options.host:
217                host_details = system
218                break
219
220    except (OSError) as e:
221        print('Problem executing the command "%s inventory": %s' %
222              (SW_REPORT, str(e)), file=sys.stderr)
223        sys.exit(2)
224
225    if options.human:
226        print('Host: %s' % options.host)
227        for k, v in iteritems(host_details):
228            print('  %s: %s' % (k, '\n    '.join(v.split(';'))))
229    else:
230        print(json.dumps(dict((key, (value.split(';') if ';' in value else value)) for key, value in host_details.items())))
231    sys.exit(0)
232
233else:
234
235    parser.print_help()
236    sys.exit(1)
237