| 1 | #!/usr/bin/python3 |
| 2 | |
| 3 | import argparse |
| 4 | import configparser |
| 5 | import sys |
| 6 | import xml.etree.ElementTree |
| 7 | |
| 8 | import jenkins |
| 9 | |
| 10 | def get_argument_parser(): |
| 11 | parser = argparse.ArgumentParser( |
| 12 | prog='update_jenkins_node.py', |
| 13 | description='Create, update, or delete Jenkins nodes' |
| 14 | ) |
| 15 | parser.add_argument( |
| 16 | '-u', '--url', default=None, |
| 17 | help='Jenkins server URL including protocol' |
| 18 | ) |
| 19 | parser.add_argument( |
| 20 | '--user', default=None, |
| 21 | help='Jenkins username' |
| 22 | ) |
| 23 | parser.add_argument( |
| 24 | '--password', default=None, |
| 25 | help='Jenkins password' |
| 26 | ) |
| 27 | parser.add_argument( |
| 28 | '-n', '--node', default=None, required=True, |
| 29 | help='The name of the node to manage in Jenkins' |
| 30 | ) |
| 31 | parser.add_argument( |
| 32 | '-c', '--node-config', default=[], action='append', |
| 33 | help='An equals-separated set path=value[=attrib]. When attrib is not set, text is assumed' |
| 34 | ) |
| 35 | parser.add_argument( |
| 36 | '-f', '--config-file', default=None, type=argparse.FileType('r'), |
| 37 | help='An INI config file as used by jenkins_jobs' |
| 38 | ) |
| 39 | parser.add_argument( |
| 40 | '-s', '--state', default='online', |
| 41 | choices=['online', 'offline', 'absent'], |
| 42 | help='The state of the Jenkins node' |
| 43 | ) |
| 44 | parser.add_argument( |
| 45 | '-m', '--message', default='', |
| 46 | help='A message to set for the offline reason of a node' |
| 47 | ) |
| 48 | return parser |
| 49 | |
| 50 | |
| 51 | def manage_node(url, user, password, node, state, offline_message='', config={}): |
| 52 | server = jenkins.Jenkins(url, username=user, password=password) |
| 53 | exists = server.node_exists(node) |
| 54 | node_info = {} |
| 55 | changed = False |
| 56 | if exists and state == 'absent': |
| 57 | server.delete_node(node) |
| 58 | changed = True |
| 59 | if not exists and state != 'absent': |
| 60 | server.create_node(node, numExecutors=1, remoteFS='/home/jenkins', |
| 61 | launcher=jenkins.LAUNCHER_SSH) |
| 62 | changed = True |
| 63 | if state != 'absent': |
| 64 | # Check configuration |
| 65 | updated = False |
| 66 | node_config = xml.etree.ElementTree.fromstring(server.get_node_config(node)) |
| 67 | for key, value in config.items(): |
| 68 | element = node_config.find(key) |
| 69 | new_element = None |
| 70 | current_key = key |
| 71 | while element is None: |
| 72 | head = key.rsplit('/', 1)[0] if '/' in current_key else None |
| 73 | tail = key.rsplit('/', 1)[1] if '/' in current_key else current_key |
| 74 | e = xml.etree.ElementTree.Element(tail) |
| 75 | if new_element is not None: |
| 76 | e.append(new_element) |
| 77 | new_element = None |
| 78 | if head is None: |
| 79 | node_config.append(e) |
| 80 | element = node_config.find(key) |
| 81 | else: |
| 82 | parent = node_config.find(head) |
| 83 | if parent: |
| 84 | parent.append(e) |
| 85 | element = node_config.find(key) |
| 86 | else: |
| 87 | new_element = e |
| 88 | current_key = head |
| 89 | continue |
| 90 | |
| 91 | if value['attrib'] is None: |
| 92 | if element.text != value['value']: |
| 93 | updated = True |
| 94 | element.text = value['value'] |
| 95 | else: |
| 96 | try: |
| 97 | if element.attrib[value['attrib']] != value['value']: |
| 98 | updated = True |
| 99 | element.attrib[value['attrib']] = value['value'] |
| 100 | except KeyError: |
| 101 | element.attrib[value['attrib']] = value['value'] |
| 102 | updated = True |
| 103 | if updated: |
| 104 | server.reconfig_node( |
| 105 | node, |
| 106 | xml.etree.ElementTree.tostring( |
| 107 | node_config, |
| 108 | xml_declaration=True, |
| 109 | encoding='unicode' |
| 110 | ) |
| 111 | ) |
| 112 | changed = True |
| 113 | # Online/offline |
| 114 | node_info = server.get_node_info(node) |
| 115 | if node_info['offline'] and state == 'online': |
| 116 | server.enable_node(node) |
| 117 | changed = True |
| 118 | if not node_info['offline'] and state == 'offline': |
| 119 | server.disable_node(node, offline_message) |
| 120 | changed = True |
| 121 | return changed |
| 122 | |
| 123 | |
| 124 | if __name__ == '__main__': |
| 125 | parser = get_argument_parser() |
| 126 | args = parser.parse_args() |
| 127 | if args.config_file is not None: |
| 128 | config = configparser.ConfigParser() |
| 129 | config.read_file(args.config_file) |
| 130 | if 'jenkins' not in config.sections(): |
| 131 | print("[jenkins] section not found") |
| 132 | sys.exit(1) |
| 133 | if args.url is None: |
| 134 | args.url = config.get('jenkins', 'url') |
| 135 | if args.user is None: |
| 136 | args.user = config['jenkins']['user'] |
| 137 | if args.password is None: |
| 138 | args.password = config['jenkins']['password'] |
| 139 | assert(args.user is not None) |
| 140 | assert(args.url is not None) |
| 141 | assert(args.password is not None) |
| 142 | node_config = {} |
| 143 | for entry in args.node_config: |
| 144 | key, value = entry.split('=', 1) |
| 145 | node_config[key] = { |
| 146 | 'attrib': value.split('=', 1)[1] if '=' in value else None, |
| 147 | 'value': value.split('=', 1)[0] if '=' in value else value, |
| 148 | } |
| 149 | print(node_config) |
| 150 | manage_node( |
| 151 | args.url, args.user, args.password, args.node, args.state, |
| 152 | args.message, node_config |
| 153 | ) |