David K. Bainbridge | b541504 | 2016-05-13 17:06:10 -0700 | [diff] [blame] | 1 | #!/usr/bin/python |
| 2 | |
| 3 | from __future__ import print_function |
| 4 | import sys |
| 5 | import json |
| 6 | import ipaddress |
| 7 | import requests |
| 8 | from optparse import OptionParser |
| 9 | from maasclient.auth import MaasAuth |
| 10 | from maasclient import MaasClient |
| 11 | |
| 12 | # For some reason the maasclient doesn't provide a put method. So |
| 13 | # we will add it here |
| 14 | def put(client, url, params=None): |
| 15 | return requests.put(url=client.auth.api_url + url, |
| 16 | auth=client._oauth(), |
| 17 | data=params) |
| 18 | |
| 19 | def add_or_update_node_group_interface(client, ng, gw, foundIfc, ifcName, subnet): |
| 20 | ip = ipaddress.IPv4Network(unicode(subnet, 'utf-8')) |
| 21 | hosts = list(ip.hosts()) |
| 22 | |
| 23 | # if the caller specified the default gateway then honor that, else used the default |
| 24 | gw = gw or str(hosts[0]) |
| 25 | |
| 26 | ifc = { |
| 27 | 'ip_range_high': str(hosts[-1]), |
| 28 | 'ip_range_low': str(hosts[2]), |
| 29 | 'static_ip_range_high' : None, |
| 30 | 'static_ip_range_low' : None, |
| 31 | 'management': 2, |
| 32 | 'name': ifcName, |
| 33 | #'router_ip' : gw, |
| 34 | #'gateway_ip' : gw, |
| 35 | 'ip': str(hosts[0]), |
| 36 | 'subnet_mask': str(ip.netmask), |
| 37 | 'broadcast_ip': str(ip.broadcast_address), |
| 38 | 'interface': ifcName, |
| 39 | } |
| 40 | |
| 41 | if foundIfc is not None: |
| 42 | print("INFO: network for specified interface, '%s', already exists" % (ifcName)) |
| 43 | |
| 44 | resp = client.get('/nodegroups/' + ng['uuid'] + '/interfaces/' + ifcName + '/', dict()) |
| 45 | if int(resp.status_code / 100) != 2: |
| 46 | print("ERROR: unable to read specified interface, '%s', '%d : %s'" |
| 47 | % (ifcName, resp.status_code, resp.text), file=sys.stderr) |
| 48 | sys.exit(1) |
| 49 | |
| 50 | # A bit of a hack here. Turns out MAAS won't return the router_ip / gateway_ip value |
| 51 | # so we can't really tell if that value is set correctly. So we will compare the |
| 52 | # values we can and use that as the "CHANGED" value, but always set all values. |
| 53 | |
| 54 | # Save the compare value |
| 55 | same = ifc == json.loads(resp.text) |
| 56 | |
| 57 | # Add router_ip and gateway_ip to the desired state so that those will be set |
| 58 | ifc['router_ip'] = gw |
| 59 | ifc['gateway_ip'] = gw |
| 60 | |
| 61 | # If the network already exists, update it with the information we want |
| 62 | resp = put(client, '/nodegroups/' + ng['uuid'] + '/interfaces/' + ifcName + '/', ifc) |
| 63 | if int(resp.status_code / 100) != 2: |
| 64 | print("ERROR: unable to update specified network, '%s', on specified interface '%s', '%d : %s'" |
| 65 | % (subnet, ifcName, resp.status_code, resp.text), file=sys.stderr) |
| 66 | sys.exit(1) |
| 67 | |
| 68 | if not same: |
| 69 | print("CHANGED: updated network, '%s', for interface '%s'" % (subnet, ifcName)) |
| 70 | else: |
| 71 | print("INFO: Network settings for interface '%s' unchanged" % ifcName) |
| 72 | |
| 73 | else: |
| 74 | # Add the operation |
| 75 | ifc['op'] = 'new' |
| 76 | ifc['router_ip'] = gw |
| 77 | ifc['gateway_ip'] = gw |
| 78 | |
| 79 | resp = client.post('/nodegroups/' + ng['uuid'] + '/interfaces/', ifc) |
| 80 | if int(resp.status_code / 100) != 2: |
| 81 | print("ERROR: unable to create specified network, '%s', on specified interface '%s', '%d : %s'" |
| 82 | % (subnet, ifcName, resp.status_code, resp.text), file=sys.stderr) |
| 83 | sys.exit(1) |
| 84 | else: |
| 85 | print("CHANGED: created network, '%s', for interface '%s'" % (subnet, ifcName)) |
| 86 | |
| 87 | # Add the first host to the subnet as the dns_server |
| 88 | subnets = None |
| 89 | resp = client.get('/subnets/', dict()) |
| 90 | if int(resp.status_code / 100) != 2: |
| 91 | print("ERROR: unable to query subnets: '%d : %s'" % (resp.status_code, resp.text)) |
| 92 | sys.exit(1) |
| 93 | else: |
| 94 | subnets = json.loads(resp.text) |
| 95 | |
| 96 | id = None |
| 97 | for sn in subnets: |
| 98 | if sn['name'] == subnet: |
| 99 | id = str(sn['id']) |
| 100 | break |
| 101 | |
| 102 | if id == None: |
| 103 | print("ERROR: unable to find subnet entry for network '%s'" % (subnet)) |
| 104 | sys.exit(1) |
| 105 | |
| 106 | resp = client.get('/subnets/' + id + '/') |
| 107 | if int(resp.status_code / 100) != 2: |
| 108 | print("ERROR: unable to query subnet '%s': '%d : %s'" % (subnet, resp.status_code, resp.text)) |
| 109 | sys.exit(1) |
| 110 | |
| 111 | data = json.loads(resp.text) |
| 112 | |
| 113 | found = False |
| 114 | for ns in data['dns_servers']: |
| 115 | if unicode(ns) == unicode(hosts[0]): |
| 116 | found = True |
| 117 | |
| 118 | if not found: |
| 119 | resp = put(client, '/subnets/' + id + '/', dict(dns_servers=[hosts[0]])) |
| 120 | if int(resp.status_code / 100) != 2: |
| 121 | print("ERROR: unable to query subnet '%s': '%d : %s'" % (subnet, resp.status_code, resp.text)) |
| 122 | sys.exit(1) |
| 123 | else: |
| 124 | print("CHANGED: updated DNS server information") |
| 125 | else: |
| 126 | print("INFO: DNS already set correctly") |
| 127 | |
| 128 | |
| 129 | def main(): |
| 130 | parser = OptionParser() |
| 131 | parser.add_option('-c', '--config', dest='config_file', |
| 132 | help="specifies file from which configuration should be read", metavar='FILE') |
| 133 | parser.add_option('-a', '--apikey', dest='apikey', |
| 134 | help="specifies the API key to use when accessing MAAS") |
| 135 | parser.add_option('-u', '--url', dest='url', default='http://localhost/MAAS/api/1.0', |
| 136 | help="specifies the URL on which to contact MAAS") |
| 137 | parser.add_option('-z', '--zone', dest='zone', default='administrative', |
| 138 | help="specifies the zone to create for manually managed hosts") |
| 139 | parser.add_option('-i', '--interface', dest='interface', default='eth0:1', |
| 140 | help="the interface on which to set up DHCP for POD local hosts") |
| 141 | parser.add_option('-n', '--network', dest='network', default='10.0.0.0/16', |
| 142 | help="subnet to use for POD local DHCP") |
| 143 | parser.add_option('-b', '--bridge', dest='bridge', default='mgmtbr', |
| 144 | help="bridge to use for host local VM allocation") |
| 145 | parser.add_option('-t', '--bridge-subnet', dest='bridge_subnet', default='172.18.0.0/16', |
| 146 | help="subnet to assign from for bridged hosts") |
| 147 | parser.add_option('-r', '--cluster', dest='cluster', default='Cluster master', |
| 148 | help="name of cluster to user for POD / DHCP") |
| 149 | parser.add_option('-s', '--sshkey', dest='sshkey', default=None, |
| 150 | help="specifies public ssh key") |
| 151 | parser.add_option('-d', '--domain', dest='domain', default='cord.lab', |
| 152 | help="specifies the domain to configure in maas") |
| 153 | parser.add_option('-g', '--gateway', dest='gw', default=None, |
| 154 | help="specifies the gateway to configure for servers") |
| 155 | (options, args) = parser.parse_args() |
| 156 | |
| 157 | if len(args) > 0: |
| 158 | print("unknown command line arguments specified", file=sys.stderr) |
| 159 | parser.print_help() |
| 160 | sys.exit(1) |
| 161 | |
| 162 | # If a config file was specified then read the config from that |
| 163 | config = {} |
| 164 | if options.config_file != None: |
| 165 | with open(options.config_file) as config_file: |
| 166 | config = json.load(config_file) |
| 167 | |
| 168 | # Override the config with any command line options |
| 169 | if options.apikey == None: |
| 170 | print("must specify a MAAS API key", file=sys.stderr) |
| 171 | sys.exit(1) |
| 172 | else: |
| 173 | config['key'] = options.apikey |
| 174 | if options.url != None: |
| 175 | config['url'] = options.url |
| 176 | if options.zone != None: |
| 177 | config['zone'] = options.zone |
| 178 | if options.interface != None: |
| 179 | config['interface'] = options.interface |
| 180 | if options.network != None: |
| 181 | config['network'] = options.network |
| 182 | if options.bridge != None: |
| 183 | config['bridge'] = options.bridge |
| 184 | if options.bridge_subnet != None: |
| 185 | config['bridge-subnet'] = options.bridge_subnet |
| 186 | if options.cluster != None: |
| 187 | config['cluster'] = options.cluster |
| 188 | if options.domain != None: |
| 189 | config['domain'] = options.domain |
| 190 | if options.gw != None: |
| 191 | config['gw'] = options.gw |
| 192 | if not 'gw' in config.keys(): |
| 193 | config['gw'] = None |
| 194 | if options.sshkey == None: |
| 195 | print("must specify a SSH key to use for cord user", file=sys.stderr) |
| 196 | sys.exit(1) |
| 197 | else: |
| 198 | config['sshkey'] = options.sshkey |
| 199 | |
| 200 | auth = MaasAuth(config['url'], config['key']) |
| 201 | client = MaasClient(auth) |
| 202 | |
| 203 | resp = client.get('/account/prefs/sshkeys/', dict(op='list')) |
| 204 | if int(resp.status_code / 100) != 2: |
| 205 | print("ERROR: unable to query SSH keys from server '%d : %s'" |
| 206 | % (resp.status_code, resp.text), file=sys.stderr) |
| 207 | sys.exit(1) |
| 208 | |
| 209 | found_key = False |
| 210 | keys = json.loads(resp.text) |
| 211 | for key in keys: |
| 212 | if key['key'] == config['sshkey']: |
| 213 | print("INFO: specified SSH key already exists") |
| 214 | found_key = True |
| 215 | |
| 216 | # Add the SSH key to the user |
| 217 | # POST /api/2.0/account/prefs/sshkeys/ op=new |
| 218 | if not found_key: |
| 219 | resp = client.post('/account/prefs/sshkeys/', dict(op='new', key=config['sshkey'])) |
| 220 | if int(resp.status_code / 100) != 2: |
| 221 | print("ERROR: unable to add sshkey for user: '%d : %s'" |
| 222 | % (resp.status_code, resp.text), file=sys.stderr) |
| 223 | sys.exit(1) |
| 224 | else: |
| 225 | print("CHANGED: updated ssh key") |
| 226 | |
| 227 | # Check to see if an "administrative" zone exists and if not |
| 228 | # create one |
| 229 | found = None |
| 230 | zones = client.zones |
| 231 | for zone in zones: |
| 232 | if zone['name'] == config['zone']: |
| 233 | found=zone |
| 234 | |
| 235 | if found is not None: |
| 236 | print("INFO: administrative zone, '%s', already exists" % config['zone'], file=sys.stderr) |
| 237 | else: |
| 238 | if not client.zone_new(config['zone'], "Zone for manually administrated nodes"): |
| 239 | print("ERROR: unable to create administrative zone '%s'" % config['zone'], file=sys.stderr) |
| 240 | sys.exit(1) |
| 241 | else: |
| 242 | print("CHANGED: Zone '%s' created" % config['zone']) |
| 243 | |
| 244 | # If the interface doesn't already exist in the cluster then |
| 245 | # create it. Look for the "Cluster Master" node group, but |
| 246 | # if it is not found used the first one in the list, if the |
| 247 | # list is empty, error out |
| 248 | found = None |
| 249 | ngs = client.nodegroups |
| 250 | for ng in ngs: |
| 251 | if ng['cluster_name'] == config['cluster']: |
| 252 | found = ng |
| 253 | break |
| 254 | |
| 255 | if found is None: |
| 256 | print("ERROR: unable to find cluster with specified name, '%s'" % config['cluster'], file=sys.stderr) |
| 257 | sys.exit(1) |
| 258 | |
| 259 | resp = client.get('/nodegroups/' + ng['uuid'] + '/', dict()) |
| 260 | if int(resp.status_code / 100) != 2: |
| 261 | print("ERROR: unable to get node group information for cluster '%s': '%d : %s'" |
| 262 | % (config['cluster'], resp.status_code, resp.text), file=sys.stderr) |
| 263 | sys.exit(1) |
| 264 | |
| 265 | data = json.loads(resp.text) |
| 266 | |
| 267 | # Set the DNS domain name (zone) for the cluster |
| 268 | if data['name'] != config['domain']: |
| 269 | resp = put(client, '/nodegroups/' + ng['uuid'] + '/', dict(name=config['domain'])) |
| 270 | if int(resp.status_code / 100) != 2: |
| 271 | print("ERROR: unable to set the DNS domain name for the cluster with specified name, '%s': '%d : %s'" |
| 272 | % (config['cluster'], resp.status_code, resp.text), file=sys.stderr) |
| 273 | sys.exit(1) |
| 274 | else: |
| 275 | print("CHANGE: updated name of cluster to '%s' : %s" % (config['domain'], resp)) |
| 276 | else: |
| 277 | print("INFO: domain name already set") |
| 278 | |
| 279 | found = None |
| 280 | resp = client.get('/nodegroups/' + ng['uuid'] + '/interfaces/', dict(op='list')) |
| 281 | if int(resp.status_code / 100) != 2: |
| 282 | print("ERROR: unable to fetch interfaces for cluster with specified name, '%s': '%d : %s'" |
| 283 | % (config['cluster'], resp.status_code, resp.text), file=sys.stderr) |
| 284 | sys.exit(1) |
| 285 | ifcs = json.loads(resp.text) |
| 286 | |
| 287 | localIfc = hostIfc = None |
| 288 | for ifc in ifcs: |
| 289 | localIfc = ifc if ifc['name'] == config['interface'] else localIfc |
| 290 | hostIfc = ifc if ifc['name'] == config['bridge'] else hostIfc |
| 291 | |
| 292 | add_or_update_node_group_interface(client, ng, config['gw'], localIfc, config['interface'], config['network']) |
David K. Bainbridge | e80fd39 | 2016-08-19 15:46:19 -0700 | [diff] [blame] | 293 | #add_or_update_node_group_interface(client, ng, config['gw'], hostIfc, config['bridge'], config['bridge-subnet']) |
David K. Bainbridge | b541504 | 2016-05-13 17:06:10 -0700 | [diff] [blame] | 294 | |
| 295 | # Update the server settings to upstream DNS request to Google |
| 296 | # POST /api/2.0/maas/ op=set_config |
| 297 | resp = client.get('/maas/', dict(op='get_config', name='upstream_dns')) |
| 298 | if int(resp.status_code / 100) != 2: |
| 299 | print("ERROR: unable to get the upstream DNS servers: '%d : %s'" |
| 300 | % (resp.status_code, resp.text), file=sys.stderr) |
| 301 | sys.exit(1) |
| 302 | |
| 303 | if unicode(json.loads(resp.text)) != u'8.8.8.8 8.8.8.4': |
| 304 | resp = client.post('/maas/', dict(op='set_config', name='upstream_dns', value='8.8.8.8 8.8.8.4')) |
| 305 | if int(resp.status_code / 100) != 2: |
| 306 | print("ERROR: unable to set the upstream DNS servers: '%d : %s'" |
| 307 | % (resp.status_code, resp.text), file=sys.stderr) |
| 308 | else: |
| 309 | print("CHANGED: updated up stream DNS servers") |
| 310 | else: |
| 311 | print("INFO: Upstream DNS servers correct") |
| 312 | |
| 313 | # Start the download of boot images |
| 314 | resp = client.get('/boot-resources/', None) |
| 315 | if int(resp.status_code / 100) != 2: |
| 316 | print("ERROR: unable to read existing images download: '%d : %s'" % (resp.status_code, resp.text), file=sys.stderr) |
| 317 | sys.exit(1) |
| 318 | |
| 319 | imgs = json.loads(resp.text) |
| 320 | found = False |
| 321 | for img in imgs: |
| 322 | if img['name'] == u'ubuntu/trusty' and img['architecture'] == u'amd64/hwe-t': |
| 323 | found = True |
| 324 | |
| 325 | if not found: |
| 326 | resp = client.post('/boot-resources/', dict(op='import')) |
| 327 | if int(resp.status_code / 100) != 2: |
| 328 | print("ERROR: unable to start image download: '%d : %s'" % (resp.status_code, resp.text), file=sys.stderr) |
| 329 | sys.exit(1) |
| 330 | else: |
| 331 | print("CHANGED: Image download started") |
| 332 | else: |
| 333 | print("INFO: required images already available") |
| 334 | |
| 335 | if __name__ == '__main__': |
| 336 | #try: |
| 337 | main() |
| 338 | #except: |
| 339 | # e = sys.exc_info()[0] |
| 340 | # print("ERROR: Unexpected exception: '%s'" % e, file=sys.stderr) |