blob: f7d074c7eebba553d1b9806fff26a9e31de9b6a8 [file] [log] [blame]
#!/usr/bin/python
from __future__ import print_function
import sys
import json
import ipaddress
import requests
from optparse import OptionParser
from maasclient.auth import MaasAuth
from maasclient import MaasClient
# For some reason the maasclient doesn't provide a put method. So
# we will add it here
def put(client, url, params=None):
return requests.put(url=client.auth.api_url + url,
auth=client._oauth(),
data=params)
def add_or_update_node_group_interface(client, ng, gw, foundIfc, ifcName, subnet, low, high):
ip = ipaddress.IPv4Network(unicode(subnet, 'utf-8'))
hosts = list(ip.hosts())
# if the caller specified the default gateway then honor that, else used the default
gw = gw or str(hosts[0])
ifc = {
'ip_range_low': low if low != "" else str(hosts[2]),
'ip_range_high': high if high != "" else str(hosts[-1]),
'static_ip_range_high' : None,
'static_ip_range_low' : None,
'management': 2,
'name': ifcName,
#'router_ip' : gw,
#'gateway_ip' : gw,
'ip': str(hosts[0]),
'subnet_mask': str(ip.netmask),
'broadcast_ip': str(ip.broadcast_address),
'interface': ifcName,
}
if foundIfc is not None:
print("INFO: network for specified interface, '%s', already exists" % (ifcName))
resp = client.get('/nodegroups/' + ng['uuid'] + '/interfaces/' + ifcName + '/', dict())
if int(resp.status_code / 100) != 2:
print("ERROR: unable to read specified interface, '%s', '%d : %s'"
% (ifcName, resp.status_code, resp.text), file=sys.stderr)
sys.exit(1)
# A bit of a hack here. Turns out MAAS won't return the router_ip / gateway_ip value
# so we can't really tell if that value is set correctly. So we will compare the
# values we can and use that as the "CHANGED" value, but always set all values.
# Save the compare value
same = ifc == json.loads(resp.text)
# Add router_ip and gateway_ip to the desired state so that those will be set
ifc['router_ip'] = gw
ifc['gateway_ip'] = gw
# If the network already exists, update it with the information we want
resp = put(client, '/nodegroups/' + ng['uuid'] + '/interfaces/' + ifcName + '/', ifc)
if int(resp.status_code / 100) != 2:
print("ERROR: unable to update specified network, '%s', on specified interface '%s', '%d : %s'"
% (subnet, ifcName, resp.status_code, resp.text), file=sys.stderr)
sys.exit(1)
if not same:
print("CHANGED: updated network, '%s', for interface '%s'" % (subnet, ifcName))
else:
print("INFO: Network settings for interface '%s' unchanged" % ifcName)
else:
# Add the operation
ifc['op'] = 'new'
ifc['router_ip'] = gw
ifc['gateway_ip'] = gw
resp = client.post('/nodegroups/' + ng['uuid'] + '/interfaces/', ifc)
if int(resp.status_code / 100) != 2:
print("ERROR: unable to create specified network, '%s', on specified interface '%s', '%d : %s'"
% (subnet, ifcName, resp.status_code, resp.text), file=sys.stderr)
sys.exit(1)
else:
print("CHANGED: created network, '%s', for interface '%s'" % (subnet, ifcName))
# Add the first host to the subnet as the dns_server
subnets = None
resp = client.get('/subnets/', dict())
if int(resp.status_code / 100) != 2:
print("ERROR: unable to query subnets: '%d : %s'" % (resp.status_code, resp.text))
sys.exit(1)
else:
subnets = json.loads(resp.text)
id = None
for sn in subnets:
if sn['name'] == subnet:
id = str(sn['id'])
break
if id == None:
print("ERROR: unable to find subnet entry for network '%s'" % (subnet))
sys.exit(1)
resp = client.get('/subnets/' + id + '/')
if int(resp.status_code / 100) != 2:
print("ERROR: unable to query subnet '%s': '%d : %s'" % (subnet, resp.status_code, resp.text))
sys.exit(1)
data = json.loads(resp.text)
found = False
for ns in data['dns_servers']:
if unicode(ns) == unicode(hosts[0]):
found = True
if not found:
resp = put(client, '/subnets/' + id + '/', dict(dns_servers=[hosts[0]]))
if int(resp.status_code / 100) != 2:
print("ERROR: unable to query subnet '%s': '%d : %s'" % (subnet, resp.status_code, resp.text))
sys.exit(1)
else:
print("CHANGED: updated DNS server information")
else:
print("INFO: DNS already set correctly")
def main():
parser = OptionParser()
parser.add_option('-c', '--config', dest='config_file',
help="specifies file from which configuration should be read", metavar='FILE')
parser.add_option('-a', '--apikey', dest='apikey',
help="specifies the API key to use when accessing MAAS")
parser.add_option('-u', '--url', dest='url', default='http://localhost/MAAS/api/1.0',
help="specifies the URL on which to contact MAAS")
parser.add_option('-z', '--zone', dest='zone', default='administrative',
help="specifies the zone to create for manually managed hosts")
parser.add_option('-i', '--interface', dest='interface', default='eth0:1',
help="the interface on which to set up DHCP for POD local hosts")
parser.add_option('-n', '--network', dest='network', default='10.0.0.0/16',
help="subnet to use for POD local DHCP")
parser.add_option('-l', '--network-low', dest='network_low', default='',
help="low address in network to lease via DHCP")
parser.add_option('-H', '--network-high', dest='network_high', default='',
help="high address in network to lease via DHCP")
parser.add_option('-b', '--bridge', dest='bridge', default='mgmtbr',
help="bridge to use for host local VM allocation")
parser.add_option('-t', '--bridge-subnet', dest='bridge_subnet', default='172.18.0.0/16',
help="subnet to assign from for bridged hosts")
parser.add_option('-r', '--cluster', dest='cluster', default='Cluster master',
help="name of cluster to user for POD / DHCP")
parser.add_option('-s', '--sshkey', dest='sshkey', default=None,
help="specifies public ssh key")
parser.add_option('-d', '--domain', dest='domain', default='cord.lab',
help="specifies the domain to configure in maas")
parser.add_option('-g', '--gateway', dest='gw', default=None,
help="specifies the gateway to configure for servers")
(options, args) = parser.parse_args()
if len(args) > 0:
print("unknown command line arguments specified", file=sys.stderr)
parser.print_help()
sys.exit(1)
# If a config file was specified then read the config from that
config = {}
if options.config_file != None:
with open(options.config_file) as config_file:
config = json.load(config_file)
# Override the config with any command line options
if options.apikey == None:
print("must specify a MAAS API key", file=sys.stderr)
sys.exit(1)
else:
config['key'] = options.apikey
if options.url != None:
config['url'] = options.url
if options.zone != None:
config['zone'] = options.zone
if options.interface != None:
config['interface'] = options.interface
if options.network != None:
config['network'] = options.network
if options.network_low != None:
config['network_low'] = options.network_low
if options.network_high != None:
config['network_high'] = options.network_high
if options.bridge != None:
config['bridge'] = options.bridge
if options.bridge_subnet != None:
config['bridge-subnet'] = options.bridge_subnet
if options.cluster != None:
config['cluster'] = options.cluster
if options.domain != None:
config['domain'] = options.domain
if options.gw != None:
config['gw'] = options.gw
if not 'gw' in config.keys():
config['gw'] = None
if options.sshkey == None:
print("must specify a SSH key to use for cord user", file=sys.stderr)
sys.exit(1)
else:
config['sshkey'] = options.sshkey
auth = MaasAuth(config['url'], config['key'])
client = MaasClient(auth)
resp = client.get('/account/prefs/sshkeys/', dict(op='list'))
if int(resp.status_code / 100) != 2:
print("ERROR: unable to query SSH keys from server '%d : %s'"
% (resp.status_code, resp.text), file=sys.stderr)
sys.exit(1)
found_key = False
keys = json.loads(resp.text)
for key in keys:
if key['key'] == config['sshkey']:
print("INFO: specified SSH key already exists")
found_key = True
# Add the SSH key to the user
# POST /api/2.0/account/prefs/sshkeys/ op=new
if not found_key:
resp = client.post('/account/prefs/sshkeys/', dict(op='new', key=config['sshkey']))
if int(resp.status_code / 100) != 2:
print("ERROR: unable to add sshkey for user: '%d : %s'"
% (resp.status_code, resp.text), file=sys.stderr)
sys.exit(1)
else:
print("CHANGED: updated ssh key")
# Check to see if an "administrative" zone exists and if not
# create one
found = None
zones = client.zones
for zone in zones:
if zone['name'] == config['zone']:
found=zone
if found is not None:
print("INFO: administrative zone, '%s', already exists" % config['zone'], file=sys.stderr)
else:
if not client.zone_new(config['zone'], "Zone for manually administrated nodes"):
print("ERROR: unable to create administrative zone '%s'" % config['zone'], file=sys.stderr)
sys.exit(1)
else:
print("CHANGED: Zone '%s' created" % config['zone'])
# If the interface doesn't already exist in the cluster then
# create it. Look for the "Cluster Master" node group, but
# if it is not found used the first one in the list, if the
# list is empty, error out
found = None
ngs = client.nodegroups
for ng in ngs:
if ng['cluster_name'] == config['cluster']:
found = ng
break
if found is None:
print("ERROR: unable to find cluster with specified name, '%s'" % config['cluster'], file=sys.stderr)
sys.exit(1)
resp = client.get('/nodegroups/' + ng['uuid'] + '/', dict())
if int(resp.status_code / 100) != 2:
print("ERROR: unable to get node group information for cluster '%s': '%d : %s'"
% (config['cluster'], resp.status_code, resp.text), file=sys.stderr)
sys.exit(1)
data = json.loads(resp.text)
# Set the DNS domain name (zone) for the cluster
if data['name'] != config['domain']:
resp = put(client, '/nodegroups/' + ng['uuid'] + '/', dict(name=config['domain']))
if int(resp.status_code / 100) != 2:
print("ERROR: unable to set the DNS domain name for the cluster with specified name, '%s': '%d : %s'"
% (config['cluster'], resp.status_code, resp.text), file=sys.stderr)
sys.exit(1)
else:
print("CHANGE: updated name of cluster to '%s' : %s" % (config['domain'], resp))
else:
print("INFO: domain name already set")
found = None
resp = client.get('/nodegroups/' + ng['uuid'] + '/interfaces/', dict(op='list'))
if int(resp.status_code / 100) != 2:
print("ERROR: unable to fetch interfaces for cluster with specified name, '%s': '%d : %s'"
% (config['cluster'], resp.status_code, resp.text), file=sys.stderr)
sys.exit(1)
ifcs = json.loads(resp.text)
localIfc = hostIfc = None
for ifc in ifcs:
localIfc = ifc if ifc['name'] == config['interface'] else localIfc
hostIfc = ifc if ifc['name'] == config['bridge'] else hostIfc
add_or_update_node_group_interface(client, ng, config['gw'], localIfc, config['interface'], config['network'],
config['network_low'], config['network_high'])
#add_or_update_node_group_interface(client, ng, config['gw'], hostIfc, config['bridge'], config['bridge-subnet'])
# Update the server settings to upstream DNS request to Google
# POST /api/2.0/maas/ op=set_config
resp = client.get('/maas/', dict(op='get_config', name='upstream_dns'))
if int(resp.status_code / 100) != 2:
print("ERROR: unable to get the upstream DNS servers: '%d : %s'"
% (resp.status_code, resp.text), file=sys.stderr)
sys.exit(1)
if unicode(json.loads(resp.text)) != u'8.8.8.8 8.8.8.4':
resp = client.post('/maas/', dict(op='set_config', name='upstream_dns', value='8.8.8.8 8.8.8.4'))
if int(resp.status_code / 100) != 2:
print("ERROR: unable to set the upstream DNS servers: '%d : %s'"
% (resp.status_code, resp.text), file=sys.stderr)
else:
print("CHANGED: updated up stream DNS servers")
else:
print("INFO: Upstream DNS servers correct")
# Start the download of boot images
resp = client.get('/boot-resources/', None)
if int(resp.status_code / 100) != 2:
print("ERROR: unable to read existing images download: '%d : %s'" % (resp.status_code, resp.text), file=sys.stderr)
sys.exit(1)
imgs = json.loads(resp.text)
found = False
for img in imgs:
if img['name'] == u'ubuntu/trusty' and img['architecture'] == u'amd64/hwe-t':
found = True
if not found:
resp = client.post('/boot-resources/', dict(op='import'))
if int(resp.status_code / 100) != 2:
print("ERROR: unable to start image download: '%d : %s'" % (resp.status_code, resp.text), file=sys.stderr)
sys.exit(1)
else:
print("CHANGED: Image download started")
else:
print("INFO: required images already available")
if __name__ == '__main__':
#try:
main()
#except:
# e = sys.exc_info()[0]
# print("ERROR: Unexpected exception: '%s'" % e, file=sys.stderr)