blob: f7d074c7eebba553d1b9806fff26a9e31de9b6a8 [file] [log] [blame]
David K. Bainbridgeb5415042016-05-13 17:06:10 -07001#!/usr/bin/python
2
3from __future__ import print_function
4import sys
5import json
6import ipaddress
7import requests
8from optparse import OptionParser
9from maasclient.auth import MaasAuth
10from maasclient import MaasClient
11
12# For some reason the maasclient doesn't provide a put method. So
13# we will add it here
14def put(client, url, params=None):
15 return requests.put(url=client.auth.api_url + url,
16 auth=client._oauth(),
17 data=params)
18
David K. Bainbridgefad8cee2016-11-22 12:39:14 -080019def add_or_update_node_group_interface(client, ng, gw, foundIfc, ifcName, subnet, low, high):
David K. Bainbridgeb5415042016-05-13 17:06:10 -070020 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 = {
David K. Bainbridgefad8cee2016-11-22 12:39:14 -080027 'ip_range_low': low if low != "" else str(hosts[2]),
28 'ip_range_high': high if high != "" else str(hosts[-1]),
29 'static_ip_range_high' : None,
30 'static_ip_range_low' : None,
David K. Bainbridgeb5415042016-05-13 17:06:10 -070031 '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
David K. Bainbridgefad8cee2016-11-22 12:39:14 -080044 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)
David K. Bainbridgeb5415042016-05-13 17:06:10 -070049
David K. Bainbridgefad8cee2016-11-22 12:39:14 -080050 # 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.
David K. Bainbridgeb5415042016-05-13 17:06:10 -070053
David K. Bainbridgefad8cee2016-11-22 12:39:14 -080054 # Save the compare value
55 same = ifc == json.loads(resp.text)
David K. Bainbridgeb5415042016-05-13 17:06:10 -070056
David K. Bainbridgefad8cee2016-11-22 12:39:14 -080057 # 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
David K. Bainbridgeb5415042016-05-13 17:06:10 -070060
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)
David K. Bainbridgefad8cee2016-11-22 12:39:14 -080066 sys.exit(1)
David K. Bainbridgeb5415042016-05-13 17:06:10 -070067
68 if not same:
69 print("CHANGED: updated network, '%s', for interface '%s'" % (subnet, ifcName))
David K. Bainbridgefad8cee2016-11-22 12:39:14 -080070 else:
David K. Bainbridgeb5415042016-05-13 17:06:10 -070071 print("INFO: Network settings for interface '%s' unchanged" % ifcName)
72
73 else:
74 # Add the operation
75 ifc['op'] = 'new'
David K. Bainbridgefad8cee2016-11-22 12:39:14 -080076 ifc['router_ip'] = gw
David K. Bainbridgeb5415042016-05-13 17:06:10 -070077 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)
David K. Bainbridgefad8cee2016-11-22 12:39:14 -080083 sys.exit(1)
David K. Bainbridgeb5415042016-05-13 17:06:10 -070084 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))
David K. Bainbridgefad8cee2016-11-22 12:39:14 -080092 sys.exit(1)
David K. Bainbridgeb5415042016-05-13 17:06:10 -070093 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))
David K. Bainbridgefad8cee2016-11-22 12:39:14 -0800104 sys.exit(1)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700105
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))
David K. Bainbridgefad8cee2016-11-22 12:39:14 -0800109 sys.exit(1)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700110
111 data = json.loads(resp.text)
112
113 found = False
114 for ns in data['dns_servers']:
115 if unicode(ns) == unicode(hosts[0]):
David K. Bainbridgefad8cee2016-11-22 12:39:14 -0800116 found = True
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700117
118 if not found:
119 resp = put(client, '/subnets/' + id + '/', dict(dns_servers=[hosts[0]]))
David K. Bainbridgefad8cee2016-11-22 12:39:14 -0800120 if int(resp.status_code / 100) != 2:
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700121 print("ERROR: unable to query subnet '%s': '%d : %s'" % (subnet, resp.status_code, resp.text))
David K. Bainbridgefad8cee2016-11-22 12:39:14 -0800122 sys.exit(1)
123 else:
124 print("CHANGED: updated DNS server information")
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700125 else:
126 print("INFO: DNS already set correctly")
David K. Bainbridgefad8cee2016-11-22 12:39:14 -0800127
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700128
129def 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")
David K. Bainbridgefad8cee2016-11-22 12:39:14 -0800143 parser.add_option('-l', '--network-low', dest='network_low', default='',
144 help="low address in network to lease via DHCP")
145 parser.add_option('-H', '--network-high', dest='network_high', default='',
146 help="high address in network to lease via DHCP")
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700147 parser.add_option('-b', '--bridge', dest='bridge', default='mgmtbr',
148 help="bridge to use for host local VM allocation")
149 parser.add_option('-t', '--bridge-subnet', dest='bridge_subnet', default='172.18.0.0/16',
150 help="subnet to assign from for bridged hosts")
151 parser.add_option('-r', '--cluster', dest='cluster', default='Cluster master',
152 help="name of cluster to user for POD / DHCP")
153 parser.add_option('-s', '--sshkey', dest='sshkey', default=None,
154 help="specifies public ssh key")
155 parser.add_option('-d', '--domain', dest='domain', default='cord.lab',
156 help="specifies the domain to configure in maas")
157 parser.add_option('-g', '--gateway', dest='gw', default=None,
158 help="specifies the gateway to configure for servers")
159 (options, args) = parser.parse_args()
160
161 if len(args) > 0:
162 print("unknown command line arguments specified", file=sys.stderr)
163 parser.print_help()
164 sys.exit(1)
165
166 # If a config file was specified then read the config from that
167 config = {}
168 if options.config_file != None:
169 with open(options.config_file) as config_file:
170 config = json.load(config_file)
171
172 # Override the config with any command line options
173 if options.apikey == None:
174 print("must specify a MAAS API key", file=sys.stderr)
175 sys.exit(1)
176 else:
177 config['key'] = options.apikey
178 if options.url != None:
179 config['url'] = options.url
180 if options.zone != None:
181 config['zone'] = options.zone
182 if options.interface != None:
183 config['interface'] = options.interface
184 if options.network != None:
185 config['network'] = options.network
David K. Bainbridgefad8cee2016-11-22 12:39:14 -0800186 if options.network_low != None:
187 config['network_low'] = options.network_low
188 if options.network_high != None:
189 config['network_high'] = options.network_high
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700190 if options.bridge != None:
191 config['bridge'] = options.bridge
192 if options.bridge_subnet != None:
193 config['bridge-subnet'] = options.bridge_subnet
194 if options.cluster != None:
195 config['cluster'] = options.cluster
196 if options.domain != None:
197 config['domain'] = options.domain
198 if options.gw != None:
199 config['gw'] = options.gw
200 if not 'gw' in config.keys():
201 config['gw'] = None
202 if options.sshkey == None:
203 print("must specify a SSH key to use for cord user", file=sys.stderr)
204 sys.exit(1)
205 else:
206 config['sshkey'] = options.sshkey
David K. Bainbridgefad8cee2016-11-22 12:39:14 -0800207
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700208 auth = MaasAuth(config['url'], config['key'])
209 client = MaasClient(auth)
210
211 resp = client.get('/account/prefs/sshkeys/', dict(op='list'))
212 if int(resp.status_code / 100) != 2:
213 print("ERROR: unable to query SSH keys from server '%d : %s'"
David K. Bainbridgefad8cee2016-11-22 12:39:14 -0800214 % (resp.status_code, resp.text), file=sys.stderr)
215 sys.exit(1)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700216
217 found_key = False
218 keys = json.loads(resp.text)
219 for key in keys:
David K. Bainbridgefad8cee2016-11-22 12:39:14 -0800220 if key['key'] == config['sshkey']:
221 print("INFO: specified SSH key already exists")
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700222 found_key = True
223
224 # Add the SSH key to the user
225 # POST /api/2.0/account/prefs/sshkeys/ op=new
226 if not found_key:
227 resp = client.post('/account/prefs/sshkeys/', dict(op='new', key=config['sshkey']))
228 if int(resp.status_code / 100) != 2:
229 print("ERROR: unable to add sshkey for user: '%d : %s'"
230 % (resp.status_code, resp.text), file=sys.stderr)
231 sys.exit(1)
David K. Bainbridgefad8cee2016-11-22 12:39:14 -0800232 else:
233 print("CHANGED: updated ssh key")
234
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700235 # Check to see if an "administrative" zone exists and if not
236 # create one
237 found = None
238 zones = client.zones
239 for zone in zones:
240 if zone['name'] == config['zone']:
241 found=zone
David K. Bainbridgefad8cee2016-11-22 12:39:14 -0800242
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700243 if found is not None:
244 print("INFO: administrative zone, '%s', already exists" % config['zone'], file=sys.stderr)
245 else:
246 if not client.zone_new(config['zone'], "Zone for manually administrated nodes"):
247 print("ERROR: unable to create administrative zone '%s'" % config['zone'], file=sys.stderr)
248 sys.exit(1)
249 else:
250 print("CHANGED: Zone '%s' created" % config['zone'])
David K. Bainbridgefad8cee2016-11-22 12:39:14 -0800251
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700252 # If the interface doesn't already exist in the cluster then
253 # create it. Look for the "Cluster Master" node group, but
254 # if it is not found used the first one in the list, if the
255 # list is empty, error out
256 found = None
257 ngs = client.nodegroups
258 for ng in ngs:
259 if ng['cluster_name'] == config['cluster']:
260 found = ng
261 break
David K. Bainbridgefad8cee2016-11-22 12:39:14 -0800262
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700263 if found is None:
264 print("ERROR: unable to find cluster with specified name, '%s'" % config['cluster'], file=sys.stderr)
265 sys.exit(1)
266
267 resp = client.get('/nodegroups/' + ng['uuid'] + '/', dict())
268 if int(resp.status_code / 100) != 2:
269 print("ERROR: unable to get node group information for cluster '%s': '%d : %s'"
David K. Bainbridgefad8cee2016-11-22 12:39:14 -0800270 % (config['cluster'], resp.status_code, resp.text), file=sys.stderr)
271 sys.exit(1)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700272
273 data = json.loads(resp.text)
David K. Bainbridgefad8cee2016-11-22 12:39:14 -0800274
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700275 # Set the DNS domain name (zone) for the cluster
276 if data['name'] != config['domain']:
277 resp = put(client, '/nodegroups/' + ng['uuid'] + '/', dict(name=config['domain']))
278 if int(resp.status_code / 100) != 2:
279 print("ERROR: unable to set the DNS domain name for the cluster with specified name, '%s': '%d : %s'"
280 % (config['cluster'], resp.status_code, resp.text), file=sys.stderr)
David K. Bainbridgefad8cee2016-11-22 12:39:14 -0800281 sys.exit(1)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700282 else:
283 print("CHANGE: updated name of cluster to '%s' : %s" % (config['domain'], resp))
284 else:
285 print("INFO: domain name already set")
David K. Bainbridgefad8cee2016-11-22 12:39:14 -0800286
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700287 found = None
288 resp = client.get('/nodegroups/' + ng['uuid'] + '/interfaces/', dict(op='list'))
289 if int(resp.status_code / 100) != 2:
290 print("ERROR: unable to fetch interfaces for cluster with specified name, '%s': '%d : %s'"
291 % (config['cluster'], resp.status_code, resp.text), file=sys.stderr)
292 sys.exit(1)
293 ifcs = json.loads(resp.text)
294
David K. Bainbridgefad8cee2016-11-22 12:39:14 -0800295 localIfc = hostIfc = None
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700296 for ifc in ifcs:
297 localIfc = ifc if ifc['name'] == config['interface'] else localIfc
298 hostIfc = ifc if ifc['name'] == config['bridge'] else hostIfc
299
David K. Bainbridgefad8cee2016-11-22 12:39:14 -0800300 add_or_update_node_group_interface(client, ng, config['gw'], localIfc, config['interface'], config['network'],
301 config['network_low'], config['network_high'])
David K. Bainbridgee80fd392016-08-19 15:46:19 -0700302 #add_or_update_node_group_interface(client, ng, config['gw'], hostIfc, config['bridge'], config['bridge-subnet'])
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700303
304 # Update the server settings to upstream DNS request to Google
305 # POST /api/2.0/maas/ op=set_config
306 resp = client.get('/maas/', dict(op='get_config', name='upstream_dns'))
307 if int(resp.status_code / 100) != 2:
308 print("ERROR: unable to get the upstream DNS servers: '%d : %s'"
309 % (resp.status_code, resp.text), file=sys.stderr)
David K. Bainbridgefad8cee2016-11-22 12:39:14 -0800310 sys.exit(1)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700311
312 if unicode(json.loads(resp.text)) != u'8.8.8.8 8.8.8.4':
313 resp = client.post('/maas/', dict(op='set_config', name='upstream_dns', value='8.8.8.8 8.8.8.4'))
314 if int(resp.status_code / 100) != 2:
315 print("ERROR: unable to set the upstream DNS servers: '%d : %s'"
316 % (resp.status_code, resp.text), file=sys.stderr)
317 else:
318 print("CHANGED: updated up stream DNS servers")
319 else:
320 print("INFO: Upstream DNS servers correct")
321
322 # Start the download of boot images
323 resp = client.get('/boot-resources/', None)
324 if int(resp.status_code / 100) != 2:
325 print("ERROR: unable to read existing images download: '%d : %s'" % (resp.status_code, resp.text), file=sys.stderr)
David K. Bainbridgefad8cee2016-11-22 12:39:14 -0800326 sys.exit(1)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700327
328 imgs = json.loads(resp.text)
329 found = False
330 for img in imgs:
David K. Bainbridgefad8cee2016-11-22 12:39:14 -0800331 if img['name'] == u'ubuntu/trusty' and img['architecture'] == u'amd64/hwe-t':
332 found = True
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700333
334 if not found:
335 resp = client.post('/boot-resources/', dict(op='import'))
336 if int(resp.status_code / 100) != 2:
337 print("ERROR: unable to start image download: '%d : %s'" % (resp.status_code, resp.text), file=sys.stderr)
David K. Bainbridgefad8cee2016-11-22 12:39:14 -0800338 sys.exit(1)
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700339 else:
340 print("CHANGED: Image download started")
341 else:
David K. Bainbridgefad8cee2016-11-22 12:39:14 -0800342 print("INFO: required images already available")
343
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700344if __name__ == '__main__':
345 #try:
346 main()
347 #except:
David K. Bainbridgefad8cee2016-11-22 12:39:14 -0800348# e = sys.exc_info()[0]
349# print("ERROR: Unexpected exception: '%s'" % e, file=sys.stderr)