blob: c7a082637683b3432b7562c1813a2d27ce1c87a1 [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
19def 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
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")
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. Bainbridgee80fd392016-08-19 15:46:19 -0700293 #add_or_update_node_group_interface(client, ng, config['gw'], hostIfc, config['bridge'], config['bridge-subnet'])
David K. Bainbridgeb5415042016-05-13 17:06:10 -0700294
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
335if __name__ == '__main__':
336 #try:
337 main()
338 #except:
339# e = sys.exc_info()[0]
340# print("ERROR: Unexpected exception: '%s'" % e, file=sys.stderr)