diff --git a/rundeck-puppetdb-nodes-plugin/contents/rundeck_puppetdb_nodes.py b/rundeck-puppetdb-nodes-plugin/contents/rundeck_puppetdb_nodes.py index 7d846e4..14fc7f1 100644 --- a/rundeck-puppetdb-nodes-plugin/contents/rundeck_puppetdb_nodes.py +++ b/rundeck-puppetdb-nodes-plugin/contents/rundeck_puppetdb_nodes.py @@ -1,111 +1,152 @@ # Daniel Fernandez Rodriguez from argparse import ArgumentParser +from collections import defaultdict from requests_kerberos import HTTPKerberosAuth import json import requests import subprocess import logging -from collections import defaultdict +import sys + + +class PuppetDBNodes(object): + + def __init__(self, args): + for k, v in args.items(): + setattr(self, k, v) + + + def negociate_krb_ticket(self, keytab_path, username): + kinit = '/usr/bin/kinit' + kinit_args = [kinit, '-kt', keytab_path, username] + kinit = subprocess.Popen(kinit_args) + kinit.wait() + + + def destroy_krb_ticket(self): + subprocess.call(["kdestroy"]) + + + def get_facts_puppetdb(self, apiurl, facts, hostgroup): + url ='%s/facts' % apiurl + query_base = '["and",["or",%s],["in", "certname", ["extract", "certname", ["select-facts", ["and", ["=", "name", "hostgroup"], ["~", "value", "%s"]]]]]]' + query_facts = ','.join(['["=","name","%s"]' % fact for fact in facts]) + query = query_base % (query_facts, hostgroup) + + headers = {'Content-Type': 'application/json','Accept': 'application/json, version=2'} + payload = {'query': query} + + logging.info("Getting facts from '%s', query: '%s'" % (url, query)) + r = requests.get(url, params=payload, headers=headers, auth=HTTPKerberosAuth()) + + if r.status_code == requests.codes.ok: + logging.info("Request code: '%s'" % r.status_code) + return json.loads(r.text) + else: + logging.error("The request failed with code '%s'" % r.status_code) + return None + + + def print_puppetdb_nodes(self, apiurl, hostgroup, factlist): + ''' + Queries PuppetDB and prints out the nodes information in a supported format for Rundeck +. + ''' + logging.info(type(factlist)) + factlist.extend(["operatingsystem", "operatingsystemrelease", "hostgroup"]) + raw_data = self.get_facts_puppetdb(apiurl, factlist, hostgroup) + data = defaultdict(lambda: {}) + + if raw_data != None: + for entry in raw_data: + data[entry['certname']] = dict(data[entry['certname']].items() + [(entry['name'], entry['value'])]) + + logging.info("Printing node list using standard output...") + for node in data.keys(): + print ('%s:'%node) + print (" "*4 + "hostname: " + node) + print (" "*4 + "username: root") + for fact in factlist: + if data[node].has_key(fact): + print (" "*4 + fact + ": " + data[node][fact] ) + logging.info("Node list printed successfully") + + else: + logging.error("Fact list empty. Check PuppetDB connection params") + + + def store_puppetdb_nodes(self, apiurl, hostgroup, factlist, filename): + ''' + Instead of querying PuppetDB every time, saves the list of nodes on a local file + so Rundeck can access it localy. + + ''' + factlist.extend(["operatingsystem", "operatingsystemrelease", "hostgroup"]) + raw_data = self.get_facts_puppetdb(apiurl, factlist, hostgroup) + data = defaultdict(lambda: {}) + + if raw_data != None: + for entry in raw_data: + data[entry['certname']] = dict(data[entry['certname']].items() + [(entry['name'], entry['value'])]) + + logging.info("Saving node list in '%s'..." % filename) + with open(filename, 'w') as file: + for node in data.keys(): + file.write('%s:\n'%node) + file.write(" "*4 + "hostname: " + node + '\n') + file.write(" "*4 + "username: root" + '\n') + for fact in factlist: + if data[node].has_key(fact): + file.write(" "*4 + fact + ": " + data[node][fact] + '\n') + logging.info("Node list saved successfully") + else: + logging.error("Fact list empty. Check PuppetDB connection params") + + + def run(self): + self.negociate_krb_ticket(self.keytab, self.username) + if self.store: + self.store_puppetdb_nodes(self.apiurl, self.hostgroup, self.factlist, self.file) + else: + self.print_puppetdb_nodes(self.apiurl, self.hostgroup, self.factlist) -def negociateKRBticket(keytab, username): - kinit = '/usr/bin/kinit' - kinit_args = [kinit, '-kt', keytab, username] - kinit = subprocess.Popen(kinit_args) - kinit.wait() - -def destroyKRBticket(): - subprocess.call(["kdestroy"]) - -def getFactsPuppetDB(apiurl, facts, hostgroup): - url ='%s/facts' % apiurl - query_base = '["and",["or",%s],["in", "certname", ["extract", "certname", ["select-facts", ["and", ["=", "name", "hostgroup"], ["~", "value", "%s"]]]]]]' - query_facts = ','.join(['["=","name","%s"]' % fact for fact in facts]) - query = query_base % (query_facts, hostgroup) - headers = {'Content-Type': 'application/json','Accept': 'application/json, version=2'} - payload = {'query': query} - logging.info("Getting facts from '%s', query: '%s'" % (url, query)) - r = requests.get(url, params=payload, headers=headers, verify=False, auth=HTTPKerberosAuth()) - if r.status_code == requests.codes.ok: - logging.info("Request code: '%s'" % r.status_code) - return json.loads(r.text) - logging.error("The request failed with code '%s'" % r.status_code) - return None - -def printNodesList(apiurl, hostgroup, factlist): - ''' - Prints the nodes information in a supported format for Rundeck. - ''' - facts = factlist.replace(',','').split() - facts.extend(["operatingsystem", "operatingsystemrelease", "hostgroup"]) - raw_data = getFactsPuppetDB(apiurl, facts, hostgroup) - data = defaultdict(lambda: {}) - if raw_data != None: - for entry in raw_data: - data[entry['certname']] = dict(data[entry['certname']].items() + [(entry['name'], entry['value'])]) - - logging.info("Printing node list using standard output...") - for node in data.keys(): - print ('%s:'%node) - print (" "*4 + "hostname: " + node) - print (" "*4 + "username: root") - for fact in facts: - if data[node].has_key(fact): - print (" "*4 + fact + ": " + data[node][fact] ) - logging.info("Node list printed successfully") - else: - logging.error("Fact list empty. Check PuppetDB connection params") - -def storeNodesList(apiurl, hostgroup, path): - ''' - Saves the node list in a local file so rundeck can access it localy. - ''' - operatingsystemfacts = getFactPuppetDB(apiurl, "operatingsystem", hostgroup) - hostgroupfacts = getFactPuppetDB(apiurl, "hostgroup", hostgroup) - logging.info("Saving node list in '%s'..." % path) - if not (operatingsystemfacts == None or hostgroupfacts == None): - with open("%s/nodes.yaml" % path, 'w') as file: - for operatingsystem in operatingsystemfacts: - global counter - counter = counter + 1 - file.write (operatingsystem['certname'] + ':\n') - file.write (" "*4+"hostname: "+ operatingsystem['certname']+"\n") - file.write (" "*4+"username: root\n") - file.write(" "*4+"tags: ") - for hostgroup in hostgroupfacts: - if (operatingsystem['certname'] == hostgroup['certname']): - file.write(" hostgroup="+ hostgroup['value']+"\n") - file.write (" "*4+"osName: "+ operatingsystem['value']+"\n") - logging.info("Node list saved successfully") - else: - logging.error("Fact list empty. Check PuppetDB connection params") - -def puppetdb_nodes_main(apiurl, hostgroup, keytab, username, factlist): - negociateKRBticket(keytab, username) - #storeNodesList(apiurl, hostgroup, path) - printNodesList(apiurl, hostgroup, factlist) - destroyKRBticket() def main(): - parser = ArgumentParser(description="Get rundeck nodes from PuppetDB") + parser = ArgumentParser(description="Populate Rundeck list of nodes from PuppetDB") parser.add_argument("-v", "--verbose", help="increase output verbosity", action="store_true") parser.add_argument("-d", "--debug", help="increase output to debug messages", action="store_true") + parser.add_argument("--apiurl", help="PuppetDB API url (https://:/)", required=True) parser.add_argument("--hostgroup", help="Foreman hostgroup", required=True) parser.add_argument("--keytab", help="Keytab", required=True) parser.add_argument("--username", help="Username to connect to PuppetDB", required=True) - parser.add_argument("--factlist", help="List of facts to retrieve for every node", required=True) - parser.add_argument("--path", help="Path where the node list will be stored", nargs='?') + parser.add_argument("--factlist", nargs='*', default=[], help="List of facts to retrieve for every node") + parser.add_argument("--file", default="/tmp/nodes.yaml", help="File path where the node list info will be stored") + + behaviour = parser.add_mutually_exclusive_group() + behaviour.add_argument('--store', action='store_true') + behaviour.add_argument('--print', action='store_false') args = parser.parse_args() + #trick to get the factlist as an object list when called it from Rundeck + if len(args.factlist) == 1: + args.factlist = args.factlist[0].split() + if args.verbose: logging.basicConfig(level=logging.INFO) elif args.debug: logging.basicConfig(level=logging.DEBUG) - puppetdb_nodes_main(args.apiurl, args.hostgroup, args.keytab, args.username, args.factlist) + plugin = PuppetDBNodes(args.__dict__) + plugin.run() + if __name__ == "__main__": - main() + try: + main() + except Exception, e: + logging.error(e) + sys.exit(-1) diff --git a/rundeck-puppetdb-nodes-plugin/plugin.yaml b/rundeck-puppetdb-nodes-plugin/plugin.yaml index e9b2c1f..0f96c59 100644 --- a/rundeck-puppetdb-nodes-plugin/plugin.yaml +++ b/rundeck-puppetdb-nodes-plugin/plugin.yaml @@ -1,10 +1,10 @@ #yaml plugin metadata name: PuppetDB source -version: 1.0 +version: 1.1 rundeckPluginVersion: 1.0 author: Daniel Fernandez (danielfr@cern.ch) -date: 2015/02/12 +date: 2015/10/21 providers: - name: rundeck-puppetdb-nodes service: ResourceModelSource @@ -14,12 +14,13 @@ providers: plugin-type: script script-interpreter: /usr/bin/python script-file: rundeck_puppetdb_nodes.py - script-args: "-v --apiurl ${config.apiurl} --hostgroup ${config.hostgroup} --username ${config.username} --keytab ${config.keytab} --factlist ${config.factlist}" + script-args: "-v --apiurl ${config.apiurl} --hostgroup ${config.hostgroup} --username ${config.username} --keytab ${config.keytab} --file ${config.file} --${config.mode} --factlist ${config.factlist}" config: - name: apiurl type: String title: PuppetDB - description: "PuppetDB API URL (https://:/). Ex: https://my.puppet.db:2525/v3" + description: "PuppetDB API URL. Ex: https://my.puppet.db:2525/v3" + default: "https://judy.cern.ch:9081/v3" required: true - name: hostgroup type: String @@ -39,6 +40,18 @@ providers: - name: factlist type: String title: Fact list - description: "List of facts to retrieve for every node" + description: "Space-separated list of facts to retrieve for every node" + default: "hostname ipaddress" + required: false + - name: mode + type: Select + title: Mode + description: "Prints out/stores on file the list of nodes" + default: print + values: print,store required: true - default: "is_virtual" + - name: file + type: String + title: "Output file" + description: "Save list of nodes to file (only if 'store' mode selected)" + default: "/var/rundeck/projects/${project.name}/etc/resources.yaml"