CORD-654 configure MAAS via ansible module
Change-Id: I9f1b3b83b41d2cf1e87660d4a50fd3267ad1679a
diff --git a/README.md b/README.md
index 0813706..f1836c7 100644
--- a/README.md
+++ b/README.md
@@ -69,7 +69,7 @@
- A 2 port 10G card is installed on the head node
- Head Node has _Internet_ connectivity via the 10G interface named `eth3`
- _Additionally configuration variables, including network IP addressing information can be found in
+ _Additionally configuration variables, including network IP addressing information can be found in
the file `vars/main.yml`._
### Configuration
diff --git a/Vagrantfile b/Vagrantfile
index 8912e0f..5d03d83 100644
--- a/Vagrantfile
+++ b/Vagrantfile
@@ -69,5 +69,5 @@
if Vagrant.has_plugin?("vagrant-cachier")
config.cache.scope = :box
end
-
+
end
diff --git a/automation/sample-filter.json b/automation/sample-filter.json
index 2a81a99..9d424df 100644
--- a/automation/sample-filter.json
+++ b/automation/sample-filter.json
@@ -1,11 +1,11 @@
-{
- "hosts":{
- "include":[
+{
+ "hosts":{
+ "include":[
".*"
]
},
- "zones":{
- "include":[
+ "zones":{
+ "include":[
"default",
"petaluma-lab"
]
diff --git a/bootstrap/Dockerfile b/bootstrap/Dockerfile
deleted file mode 100644
index f6a4598..0000000
--- a/bootstrap/Dockerfile
+++ /dev/null
@@ -1,32 +0,0 @@
-## Copyright 2016 Open Networking Laboratory
-##
-## Licensed under the Apache License, Version 2.0 (the "License");
-## you may not use this file except in compliance with the License.
-## You may obtain a copy of the License at
-##
-## http://www.apache.org/licenses/LICENSE-2.0
-##
-## Unless required by applicable law or agreed to in writing, software
-## distributed under the License is distributed on an "AS IS" BASIS,
-## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-## See the License for the specific language governing permissions and
-## limitations under the License.
-FROM ubuntu:14.04
-MAINTAINER Open Networking Laboratory <info@onlab.us>
-
-RUN apt-get update -y && \
- apt-get install -y python-pip
-
-RUN pip install maasclient==0.3 && \
- pip install requests_oauthlib && \
- pip install ipaddress
-
-ADD bootstrap.py /bootstrap.py
-
-LABEL org.label-schema.name="bootstrap" \
- org.label-schema.description="Provides bootstrap configuration of MAAS for the CORD deployment" \
- org.label-schema.vcs-url="https://gerrit.opencord.org/maas" \
- org.label-schema.vendor="Open Networking Laboratory" \
- org.label-schema.schema-version="1.0"
-
-ENTRYPOINT [ "/bootstrap.py" ]
diff --git a/bootstrap/LICENSE b/bootstrap/LICENSE
deleted file mode 100644
index 8dada3e..0000000
--- a/bootstrap/LICENSE
+++ /dev/null
@@ -1,201 +0,0 @@
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
-
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "{}"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright {yyyy} {name of copyright owner}
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
diff --git a/bootstrap/Makefile b/bootstrap/Makefile
deleted file mode 100644
index 49d7ff2..0000000
--- a/bootstrap/Makefile
+++ /dev/null
@@ -1,9 +0,0 @@
-help:
- -@echo "Available actions"
- -@echo " docker - builds the docker container"
-
-docker: bootstrap.py
- docker build -t cord/maas-bootstrap:0.1-prerelease .
-
-run:
- docker run -ti --rm=true cord/maas-bootstrap:0.1-prerelease --apikey=$(CORD_APIKEY) --sshkey="$(CORD_SSHKEY)" --url=$(CORD_URL)
diff --git a/bootstrap/bootstrap.py b/bootstrap/bootstrap.py
deleted file mode 100755
index f7d074c..0000000
--- a/bootstrap/bootstrap.py
+++ /dev/null
@@ -1,349 +0,0 @@
-#!/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)
diff --git a/build.gradle b/build.gradle
index 08f50ca..d5dac1f 100644
--- a/build.gradle
+++ b/build.gradle
@@ -113,7 +113,7 @@
}
return branchStdOut.toString().trim()
}
-
+
task buildSwitchqImage(type: Exec) {
commandLine "docker", 'build', '--label', 'org.label-schema.build-date=' + getBuildTimestamp(), '--label', 'org.label-schema.vcs-ref=' + getCommitHash(), '--label', 'org.label-schema.vcs-ref-date=' + getCommitDate(), '--label', 'org.label-schema.version=' + getBranchName(), '-t', 'cord-maas-switchq', './switchq'
}
@@ -128,22 +128,6 @@
commandLine "docker", 'push', "$targetReg/cord-maas-switchq:$targetTag"
}
-// Bootstrap Image
-
-task buildBootstrapImage(type: Exec) {
- commandLine "docker", 'build', '--label', 'org.label-schema.build-date=' + getBuildTimestamp(), '--label', 'org.label-schema.vcs-ref=' + getCommitHash(), '--label', 'org.label-schema.vcs-ref-date=' + getCommitDate(), '--label', 'org.label-schema.version=' + getBranchName(), '-t', 'cord-maas-bootstrap', './bootstrap'
-}
-
-task tagBootstrapImage(type: Exec) {
- dependsOn buildBootstrapImage
- commandLine "docker", 'tag', 'cord-maas-bootstrap', "$targetReg/cord-maas-bootstrap:$targetTag"
-}
-
-task publishBootstrapImage(type: Exec) {
- dependsOn tagBootstrapImage
- commandLine "docker", 'push', "$targetReg/cord-maas-bootstrap:$targetTag"
-}
-
// IP Allocator Image
task buildAllocationImage(type: Exec) {
@@ -242,7 +226,6 @@
// To be used to generate all needed binaries that need to be present on the target
// as docker images in the local docker runner.
task buildImages {
- dependsOn buildBootstrapImage
dependsOn buildHarvesterImage
dependsOn buildAutomationImage
dependsOn buildAllocationImage
@@ -252,7 +235,6 @@
}
task tagImages {
- dependsOn tagBootstrapImage
dependsOn tagHarvesterImage
dependsOn tagAutomationImage
dependsOn tagAllocationImage
@@ -264,7 +246,6 @@
task publish {
//FIXME: This works because the upstream project primes the nodes before running this.
comps.each { name, spec -> if (spec.type == 'image') { dependsOn "publish" + name } }
- dependsOn publishBootstrapImage
dependsOn publishHarvesterImage
dependsOn publishAutomationImage
dependsOn publishAllocationImage
@@ -360,8 +341,8 @@
extraVars = extraVars.p("$targetReg", "deploy_docker_registry")
.p("$targetTag", "deploy_docker_tag")
- // the password set on the compute node is skipped because this is being run against the
- // head node and we don't want to change the head node password as this node was manualy
+ // the password set on the compute node is skipped because this is being run against the
+ // head node and we don't want to change the head node password as this node was manualy
// set up.
def skipTags = [].p(config.seedServer.skipTags).p('set_compute_node_password')
diff --git a/config-generator/README.md b/config-generator/README.md
index 7363057..8c9913b 100644
--- a/config-generator/README.md
+++ b/config-generator/README.md
@@ -6,7 +6,7 @@
- Install Go (only for local debugging)
- Make sure ONOS is reachable either locally or via SSH tunnel (either way is fine)
- Make sure devices, hosts are connected and showing up in ONOS
-- Config-gen server listens on port 1337, so make sure it's available, if not change the ENV VAR `CONFIGGEN_ConfigServerPort` to the port you wish config-gen to use
+- Config-gen server listens on port 1337, so make sure it's available, if not change the ENV VAR `CONFIGGEN_ConfigServerPort` to the port you wish config-gen to use
### To run locally from source (without a container) for debugging:
@@ -40,4 +40,4 @@
LogFormat string `default:"text" envconfig:"LOG_FORMAT"`
ConfigServerPort string `default:"1337"` //Config-gen service port default
ConfigServerIP string `default:"127.0.0.1"` //Config-gen service IP
-```
\ No newline at end of file
+```
diff --git a/library/maas.py b/library/maas.py
new file mode 100644
index 0000000..10cdbe7
--- /dev/null
+++ b/library/maas.py
@@ -0,0 +1,259 @@
+#!/usr/bin/python
+
+DOCUMENTATION = '''
+---
+module: maas
+short_description: Manage MAAS server configuration
+options:
+ maas:
+ description:
+ - URL of MAAS server
+ default: http://localhost/MAAS/api/1.0/
+ key:
+ description:
+ - MAAS API key
+ required: yes
+ options:
+ description:
+ - list of config options to query, this is only used for query
+ enable_http_proxy: :
+ description:
+ - Enable the use of an APT and HTTP/HTTPS proxy.
+ upstream_dns: :
+ description:
+ - Upstream DNS used to resolve domains not managed by this MAAS (space-separated IP addresses).
+ default_storage_layout: :
+ description:
+ - Default storage layout.
+ choices:
+ - ['lvm', 'flat', 'bcache']
+ default_osystem: :
+ description:
+ - Default operating system used for deployment.
+ ports_archive: :
+ description:
+ - Ports archive.
+ http_proxy: :
+ description:
+ - Proxy for APT and HTTP/HTTPS.
+ boot_images_auto_import: :
+ description:
+ - Automatically import/refresh the boot images every 60 minutes.
+ enable_third_party_drivers: :
+ description:
+ - Enable the installation of proprietary drivers (i.e. HPVSA).
+ kernel_opts: :
+ description:
+ - Boot parameters to pass to the kernel by default.
+ main_archive: :
+ description:
+ - Main archive
+ maas_name: :
+ description:
+ - MAAS name.
+ curtin_verbose: :
+ description:
+ - Run the fast-path installer with higher verbosity. This provides more detail in the installation logs..
+ dnssec_validation: :
+ description:
+ - Enable DNSSEC validation of upstream zones.
+ commissioning_distro_series: :
+ description:
+ - Default Ubuntu release used for commissioning.
+ windows_kms_host: :
+ description:
+ - Windows KMS activation host.
+ enable_disk_erasing_on_release: :
+ description:
+ - Erase nodes' disks prior to releasing..
+ default_distro_series: :
+ description:
+ - Default OS release used for deployment.
+ ntp_server: :
+ description:
+ - Address of NTP server for nodes.
+ default_min_hwe_kernel: :
+ description:
+ - Default Minimum Kernel Version.
+ state:
+ description:
+ - possible states for the module
+ choices: ['present', 'query']
+ default: present
+
+requirements: [ipaddress, requests_oauthlib, maasclient]
+author: David Bainbridge
+'''
+
+EXAMPLES = '''
+examples:
+ maas:
+ maas: http://my.maas.server.com/MAAS/api/1.0/
+ key: 'xBvr9dx5k7S52myufC:fqBXV7hJgXegNZDw9c:K8hsmL47XjAppfQy2pDVW7G49p6PELgp'
+ options:
+ - upstream_dns
+ - ntp_servers
+ state: query
+
+ maas:
+ maas: http://my.maas.server.com/MAAS/api/1.0/
+ key: 'xBvr9dx5k7S52myufC:fqBXV7hJgXegNZDw9c:K8hsmL47XjAppfQy2pDVW7G49p6PELgp'
+ upstream_dns: 8.8.8.8 8.8.8.4
+ state: present
+'''
+
+import sys
+import json
+import ipaddress
+import requests
+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)
+
+# Attempt to interpret the given value as a JSON object, if that fails
+# just return it as a string
+def string_or_object(val):
+ try:
+ return json.loads(val)
+ except:
+ return val
+
+# Return a copy of the given dictionary with any `null` valued entries
+# removed
+def remove_null(d_in):
+ d = d_in.copy()
+ to_remove = []
+ for k in d.keys():
+ if d[k] == None:
+ to_remove.append(k)
+ for k in to_remove:
+ del d[k]
+ return d
+
+# Deterine if two dictionaries are different
+def different(have, want):
+ have_keys = have.keys()
+ for key in want.keys():
+ if (key in have_keys and want[key] != have[key]) or key not in have_keys:
+ return True
+ return False
+
+# Get configuration options from MAAS
+def get_config(maas, desired):
+ config = {}
+ for o in desired.keys():
+ res = maas.get('/maas/', dict(name=o, op='get_config'))
+ if res.ok:
+ val = json.loads(res.text)
+ config[o] = val if val else ""
+ else:
+ config[o] = {'error': string_or_object(res.text)}
+ return config
+
+# Walk the list of options in the desired state setting those on MAAS
+def update_config(maas, have, want):
+ have_error = False
+ status = {}
+ for o in want.keys():
+ if want[o] != have[o]:
+ res = maas.post('/maas/', {'name': o, 'value': want[o], 'op': 'set_config'})
+ if res.ok:
+ status[o] = { 'error': False, 'status': want[o] }
+ else:
+ have_error = True
+ status[o] = { 'error': True, 'status': string_or_object(res.text) }
+ return {'error': have_error, 'status': status}
+
+def main():
+ module = AnsibleModule(
+ argument_spec = dict(
+ maas=dict(default='http://localhost/MAAS/api/1.0/'),
+ key=dict(required=True),
+ options=dict(required=False, type='list'),
+ enable_http_proxy=dict(required=False),
+ upstream_dns=dict(required=False),
+ default_storage_layout=dict(required=False),
+ default_osystem=dict(required=False),
+ ports_archive=dict(required=False),
+ http_proxy=dict(required=False),
+ boot_images_auto_import=dict(required=False),
+ enable_third_party_drivers=dict(required=False),
+ kernel_opts=dict(required=False),
+ main_archive=dict(required=False),
+ maas_name=dict(required=False),
+ curtin_verbose=dict(required=False),
+ dnssec_validation=dict(required=False),
+ commissioning_distro_series=dict(required=False),
+ windows_kms_host=dict(required=False),
+ enable_disk_erasing_on_release=dict(required=False),
+ default_distro_series=dict(required=False),
+ ntp_server=dict(required=False),
+ default_min_hwe_kernel=dict(required=False),
+ state=dict(default='present', choices=['present', 'query'])
+ ),
+ supports_check_mode = False
+ )
+
+ maas = module.params['maas']
+ key = module.params['key']
+ options = module.params['options']
+ state = module.params['state']
+
+ if state == 'query':
+ desired = {x:None for x in options}
+ else:
+ # Construct a sparsely populate desired state
+ desired = remove_null({
+ 'enable_http_proxy': module.params['enable_http_proxy'],
+ 'upstream_dns': module.params['upstream_dns'],
+ 'default_storage_layout': module.params['default_storage_layout'],
+ 'default_osystem': module.params['default_osystem'],
+ 'ports_archive': module.params['ports_archive'],
+ 'http_proxy': module.params['http_proxy'],
+ 'boot_images_auto_import': module.params['boot_images_auto_import'],
+ 'enable_third_party_drivers': module.params['enable_third_party_drivers'],
+ 'kernel_opts': module.params['kernel_opts'],
+ 'main_archive': module.params['main_archive'],
+ 'maas_name': module.params['maas_name'],
+ 'curtin_verbose': module.params['curtin_verbose'],
+ 'dnssec_validation': module.params['dnssec_validation'],
+ 'commissioning_distro_series': module.params['commissioning_distro_series'],
+ 'windows_kms_host': module.params['windows_kms_host'],
+ 'enable_disk_erasing_on_release': module.params['enable_disk_erasing_on_release'],
+ 'default_distro_series': module.params['default_distro_series'],
+ 'ntp_server': module.params['ntp_server'],
+ 'default_min_hwe_kernel': module.params['default_min_hwe_kernel'],
+ })
+
+ # Authenticate into MAAS
+ auth = MaasAuth(maas, key)
+ maas = MaasClient(auth)
+
+ # Attempt to get the configuration from MAAS
+ config = get_config(maas, desired)
+
+ if state == 'query':
+ # If this is a query, return the options
+ module.exit_json(changed=False, found=True, maas=config)
+ elif state == 'present':
+ # If we want this to exists check to see if this is different and
+ # needs updated
+ if different(config, desired):
+ res = update_config(maas, config, desired)
+ if res['error']:
+ module.fail_json(msg=res['status'])
+ else:
+ module.exit_json(changed=True, maas=res['status'])
+ else:
+ # No differences, to nothing to change
+ module.exit_json(changed=False, maas=config)
+
+# this is magic, see lib/ansible/module_common.py
+#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
+if __name__ == '__main__':
+ main()
diff --git a/library/maas_boot_resource.py b/library/maas_boot_resource.py
new file mode 100644
index 0000000..f81f3e4
--- /dev/null
+++ b/library/maas_boot_resource.py
@@ -0,0 +1,113 @@
+#!/usr/bin/python
+
+DOCUMENTATION = '''
+---
+module: maas_boot_resources
+short_description: Manage MAAS boot resources
+options:
+ maas:
+ description:
+ - URL of MAAS server
+ default: http://localhost/MAAS/api/1.0/
+ key:
+ description:
+ - MAAS API key
+ required: yes
+ state:
+ description:
+ - possible states for this sshkey
+ choices: ['query', 'import']
+ default: query
+
+requirements: [ipaddress, requests_oauthlib, maasclient]
+author: David Bainbridge
+'''
+
+EXAMPLES = '''
+examples:
+ maas_boot_resource:
+ maas: http://my.maas.server.com/MAAS/api/1.0/
+ key: 'xBvr9dx5k7S52myufC:fqBXV7hJgXegNZDw9c:K8hsmL47XjAppfQy2pDVW7G49p6PELgp'
+ state: query
+'''
+
+import sys
+import json
+import ipaddress
+import requests
+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)
+
+# Attempt to interpret the given value as a JSON object, if that fails
+# just return it as a string
+def string_or_object(val):
+ try:
+ return json.loads(val)
+ except:
+ return val
+
+# Return a copy of the given dictionary with any `null` valued entries
+# removed
+def remove_null(d_in):
+ d = d_in.copy()
+ to_remove = []
+ for k in d.keys():
+ if d[k] == None:
+ to_remove.append(k)
+ for k in to_remove:
+ del d[k]
+ return d
+
+def filter(filter_type, d, keys):
+ if filter_type == 'include':
+ for k in d.keys():
+ if k not in keys:
+ d.pop(k, None)
+ else:
+ for k in d.keys():
+ if k in keys:
+ d.pop(k, None)
+
+def main():
+ module = AnsibleModule(
+ argument_spec = dict(
+ maas=dict(default='http://localhost/MAAS/api/1.0/'),
+ key=dict(required=True),
+ state=dict(default='query', choices=['query', 'import'])
+ ),
+ supports_check_mode = False
+ )
+
+ maas = module.params['maas']
+ key = module.params['key']
+ state = module.params['state']
+
+ # Authenticate into MAAS
+ auth = MaasAuth(maas, key)
+ maas = MaasClient(auth)
+
+ if state == 'query':
+ res = maas.get('/boot-resources/')
+ if res.ok:
+ module.exit_json(changed=False, resources=json.loads(res.text))
+ else:
+ module.fail_json(msg=string_or_object(res.text))
+ elif state == 'import':
+ res = maas.post('/boot-resources/', dict(op='import'))
+ if res.ok:
+ module.exit_json(changed=True)
+ else:
+ module.fail_json(msg=string_or_object(res.text))
+ else:
+ module.fail_json(msg='unknown state')
+
+# this is magic, see lib/ansible/module_common.py
+#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
+if __name__ == '__main__':
+ main()
diff --git a/library/maas_cluster.py b/library/maas_cluster.py
new file mode 100644
index 0000000..5a3f4b7
--- /dev/null
+++ b/library/maas_cluster.py
@@ -0,0 +1,188 @@
+#!/usr/bin/python
+
+DOCUMENTATION = '''
+---
+module: maas_cluster
+short_description: Manage MAAS Clusters Interfaces
+options:
+ maas:
+ description:
+ - URL of MAAS server
+ default: http://localhost/MAAS/api/1.0/
+ key:
+ description:
+ - MAAS API key
+ required: yes
+ name:
+ description:
+ - name of the cluster
+ required: yes
+ status:
+ description:
+ - indicates the enabled state of the cluster
+ choices: ['enabled', 'disabled']
+ default: enabled
+ domain:
+ description:
+ - DNS zone name
+ required: no
+ state:
+ description:
+ - possible states for this cluster
+ choices: ['present', 'query']
+ default: present
+
+requirements: [ipaddress, requests_oauthlib, maasclient]
+author: David Bainbridge
+'''
+
+EXAMPLES = '''
+examples:
+ maas_cluster:
+ maas: http://my.maas.server.com/MAAS/api/1.0/
+ key: 'xBvr9dx5k7S52myufC:fqBXV7hJgXegNZDw9c:K8hsmL47XjAppfQy2pDVW7G49p6PELgp'
+ name: MyCluster
+ status: enabled
+ domain: company.com
+ state: present
+
+ maas_cluster:
+ maas: http://my.maas.server.com/MAAS/api/1.0/
+ key: 'xBvr9dx5k7S52myufC:fqBXV7hJgXegNZDw9c:K8hsmL47XjAppfQy2pDVW7G49p6PELgp'
+ name: MyDeadCluster
+ state: query
+'''
+
+import sys
+import json
+import ipaddress
+import requests
+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)
+
+# Attempt to interpret the given value as a JSON object, if that fails
+# just return it as a string
+def string_or_object(val):
+ try:
+ return json.loads(val)
+ except:
+ return val
+
+# Return a copy of the given dictionary with any `null` valued entries
+# removed
+def remove_null(d_in):
+ d = d_in.copy()
+ to_remove = []
+ for k in d.keys():
+ if d[k] == None:
+ to_remove.append(k)
+ for k in to_remove:
+ del d[k]
+ return d
+
+# Deterine if two dictionaries are different
+def different(have, want):
+ have_keys = have.keys()
+ for key in want.keys():
+ if (key in have_keys and want[key] != have[key]) or key not in have_keys:
+ return True
+ return False
+
+# Get an cluster from MAAS using its name, if not found return None
+def get_cluster(maas, name):
+ res = maas.get('/nodegroups/', dict(op='list'))
+ if res.ok:
+ for ng in json.loads(res.text):
+ if ng['cluster_name'] == name:
+ return ng
+ return None
+
+def update_cluster(maas, have, want):
+ merged = have.copy()
+ merged.update(want)
+ res = put(maas, '/nodegroups/%s/' % merged['uuid'], merged)
+ if res.ok:
+ return { 'error': False, 'status': get_cluster(maas, merged['cluster_name']) }
+ return { 'error': True, 'status': string_or_object(res.text) }
+
+def main():
+ module = AnsibleModule(
+ argument_spec = dict(
+ maas=dict(default='http://localhost/MAAS/api/1.0/'),
+ key=dict(required=True),
+ name=dict(required=True),
+ status=dict(default='enabled', choices=['enabled', 'disabled']),
+ domain=dict(required=False),
+ state=dict(default='present', choices=['present', 'query'])
+ ),
+ supports_check_mode = False
+ )
+
+ maas = module.params['maas']
+ key = module.params['key']
+ state = module.params['state']
+
+ status_map = {
+ 'enabled': 1,
+ 'disabled': 2
+ }
+
+ # Construct a sparsely populate desired state
+ desired = remove_null({
+ 'cluster_name': module.params['name'],
+ 'status': status_map[module.params['status']],
+ 'name' : module.params['domain'],
+ })
+
+ # Authenticate into MAAS
+ auth = MaasAuth(maas, key)
+ maas = MaasClient(auth)
+
+ # Attempt to get the cluster from MAAS
+ cluster = get_cluster(maas, desired['cluster_name'])
+
+ # Actions if the cluster does not currently exist
+ if not cluster:
+ if state == 'query':
+ # If this is a query, returne it is not found
+ module.exit_json(changed=False, found=False)
+ elif state == 'present':
+ # Not able to create clusters via the API
+ module.fail_json(msg='Named cluster does not exist and clusters cannot be programatically created')
+ else:
+ # If this should be absent, then we are done and in the desired state
+ module.exit_json(changed=False)
+
+ # Done with clusters does not exists actions
+ return
+
+ # Actions if the cluster does exist
+ if state == 'query':
+ # If this is a query, return the cluster
+ module.exit_json(changed=False, found=True, cluster=cluster)
+ elif state == 'present':
+ # If we want this to exists check to see if this is different and
+ # needs updated
+ if different(cluster, desired):
+ res = update_cluster(maas, cluster, desired)
+ if res['error']:
+ module.fail_json(msg=res['status'])
+ else:
+ module.exit_json(changed=True, cluster=res['status'])
+ else:
+ # No differences, to nothing to change
+ module.exit_json(changed=False, cluster=cluster)
+ else:
+ # Not able to delete clusters via the API
+ module.fail_json(msg='Named cluster exists and clusters cannot be programmatically deleted')
+
+# this is magic, see lib/ansible/module_common.py
+#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
+if __name__ == '__main__':
+ main()
diff --git a/library/maas_cluster_interface.py b/library/maas_cluster_interface.py
new file mode 100644
index 0000000..9998692
--- /dev/null
+++ b/library/maas_cluster_interface.py
@@ -0,0 +1,302 @@
+#!/usr/bin/python
+
+DOCUMENTATION = '''
+---
+module: maas_cluster_interface
+short_description: Manage MAAS Clusters Interfaces
+options:
+ maas:
+ description:
+ - URL of MAAS server
+ default: http://localhost/MAAS/api/1.0/
+ key:
+ description:
+ - MAAS API key
+ required: yes
+ cluster_name:
+ description:
+ - name of the cluster for the interface
+ required: yes
+ name:
+ description:
+ - name of the cluster interface
+ required: yes
+ management:
+ description:
+ - indicates how or if MAAS manages this interface
+ choices: ['unmanaged', 'dhcp', 'dhcpdns']
+ default: unmanaged
+ interface:
+ description:
+ - the physical NIC for the interface
+ required: no
+ ip:
+ description:
+ - IP address assigned for this interface
+ required: no
+ subnet_mask:
+ description:
+ - network subnet mask for this interface
+ required: no
+ broadcast_ip:
+ description:
+ - broadcast IP for this interfaece's network
+ required: no
+ router_ip:
+ description:
+ - gateway router IP for this interface's network
+ required: no
+ ip_range_low:
+ description:
+ - the low range for dynamic IP address assignement
+ required: no
+ ip_range_high:
+ description:
+ - the high range for dynamic IP address assignment
+ required: no
+ static_ip_range_low:
+ description:
+ - the low range for static IP address assignment
+ required: no
+ static_ip_range_high:
+ description:
+ - the high range for static IP address assignment
+ required: no
+ state:
+ description:
+ - possible states for this cluster interface
+ choices: ['present', 'absent', 'query']
+ default: present
+
+requirements: [ipaddress, requests_oauthlib, maasclient]
+author: David Bainbridge
+'''
+
+EXAMPLES = '''
+examples:
+ maas_cluster_interface:
+ maas: http://my.maas.server.com/MAAS/api/1.0/
+ key: 'xBvr9dx5k7S52myufC:fqBXV7hJgXegNZDw9c:K8hsmL47XjAppfQy2pDVW7G49p6PELgp'
+ name: MyClusterInterface
+ interface: eth0
+ cluster_name: MyCluster
+ ip: 172.16.10.2
+ subnet_mask: 255.255.255.0
+ broadcast_ip: 172.16.10.255
+ router_ip: 172.16.10.1
+ ip_range_low: 172.16.10.3
+ ip_range_high: 172.16.10.127
+ static_ip_range_low: 172.16.10.128
+ static_ip_range_high: 172.16.10.253
+ management: dhcpdns
+ status: enabled
+ state: present
+
+ maas_cluster_interface:
+ maas: http://my.maas.server.com/MAAS/api/1.0/
+ key: 'xBvr9dx5k7S52myufC:fqBXV7hJgXegNZDw9c:K8hsmL47XjAppfQy2pDVW7G49p6PELgp'
+ name: MyDeadClusterInterface
+ state: absent
+'''
+
+import sys
+import json
+import ipaddress
+import requests
+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)
+
+# Attempt to interpret the given value as a JSON object, if that fails
+# just return it as a string
+def string_or_object(val):
+ try:
+ return json.loads(val)
+ except:
+ return val
+
+# Return a copy of the given dictionary with any `null` valued entries
+# removed
+def remove_null(d_in):
+ d = d_in.copy()
+ to_remove = []
+ for k in d.keys():
+ if d[k] == None:
+ to_remove.append(k)
+ for k in to_remove:
+ del d[k]
+ return d
+
+# Deterine if two dictionaries are different
+def different(have, want, debug):
+ have_keys = have.keys()
+ for key in want.keys():
+ if (key in have_keys and want[key] != have[key]) or key not in have_keys:
+ diff = {"diff": key, "want": want[key]}
+ if key in have_keys:
+ diff['have'] = have[key]
+ else:
+ diff['have'] = False
+ debug.append(diff)
+ return True
+ return False
+
+# Get an cluster from MAAS using its name, if not found return None
+def get_cluster(maas, name):
+ res = maas.get('/nodegroups/', dict(op='list'))
+ if res.ok:
+ for ng in json.loads(res.text):
+ if ng['cluster_name'] == name:
+ return ng
+ return None
+
+# Get an cluster interface from MAAS using its name, if not found return None
+def get_cluster_interface(maas, cluster, name):
+ res = maas.get('/nodegroups/%s/interfaces/%s/' % (cluster['uuid'], name))
+ if res.ok:
+ return json.loads(res.text)
+ return None
+
+# Create an cluster interface based on the value given
+def create_cluster_interface(maas, cluster, cluster_interface):
+ merged = cluster_interface.copy()
+ merged['op'] = 'new'
+ res = maas.post('/nodegroups/%s/interfaces/' % cluster['uuid'], merged)
+ if res.ok:
+ return { 'error': False, 'status': get_cluster_interface(maas, cluster, merged['name']) }
+ return { 'error': True, 'status': string_or_object(res.text) }
+
+# Delete an cluster interface based on the name
+def delete_cluster_interface(maas, cluster, name):
+ res = maas.delete('/nodegroups/%s/interfaces/%s/' % (cluster['uuid'], name))
+ if res.ok:
+ return { 'error': False }
+ return { 'error': True, 'status': string_or_object(res.text) }
+
+def update_cluster_interface(maas, cluster, have, want):
+ merged = have.copy()
+ merged.update(want)
+ res = put(maas, '/nodegroups/%s/interfaces/%s/' % (cluster['uuid'], merged['name']), merged)
+ if res.ok:
+ return { 'error': False, 'status': get_cluster_interface(maas, cluster, merged['name']) }
+ return { 'error': True, 'status': string_or_object(res.text) }
+
+def main():
+ module = AnsibleModule(
+ argument_spec = dict(
+ maas=dict(default='http://localhost/MAAS/api/1.0/'),
+ key=dict(required=True),
+ base=dict(required=False),
+ cluster_name=dict(required=True),
+ name=dict(required=True),
+ interface=dict(required=False),
+ ip=dict(required=False),
+ subnet_mask=dict(required=False),
+ management=dict(default='unmanaged', choices=['unmanaged', 'dhcp', 'dhcpdns']),
+ ip_range_low=dict(required=False),
+ ip_range_high=dict(required=False),
+ static_ip_range_low=dict(required=False),
+ static_ip_range_high=dict(required=False),
+ broadcast_ip=dict(required=False),
+ router_ip=dict(required=False),
+ state=dict(default='present', choices=['present', 'absent', 'query'])
+ ),
+ supports_check_mode = False
+ )
+
+ maas = module.params['maas']
+ key = module.params['key']
+ state = module.params['state']
+
+ management_map = {
+ 'unmanaged': 0,
+ 'dhcp': 1,
+ 'dhcpdns': 2
+ }
+
+ # Construct a sparsely populate desired state
+ desired = remove_null({
+ 'name': module.params['name'],
+ 'interface': module.params['interface'],
+ 'ip': module.params['ip'],
+ 'subnet_mask': module.params['subnet_mask'],
+ 'management': management_map[module.params['management']],
+ 'ip_range_low': module.params['ip_range_low'],
+ 'ip_range_high': module.params['ip_range_high'],
+ 'static_ip_range_low': module.params['static_ip_range_low'],
+ 'static_ip_range_high': module.params['static_ip_range_high'],
+ 'broadcast_ip': module.params['broadcast_ip'],
+ 'router_ip': module.params['router_ip'],
+ })
+
+ debug = []
+
+ # Authenticate into MAAS
+ auth = MaasAuth(maas, key)
+ maas = MaasClient(auth)
+
+ # Attempt to locate the cluster on which we will be working, error out if it can't be found
+ cluster = get_cluster(maas, module.params['cluster_name'])
+ if not cluster:
+ module.fail_json(msg='Unable to find specified cluster "%s", cannot continue' % module.params['cluster_name'])
+ return
+
+ debug.append({"desired": desired})
+
+ # Attempt to get the cluster interface from MAAS
+ cluster_interface = get_cluster_interface(maas, cluster, desired['name'])
+
+ debug.append({"found": cluster_interface})
+
+ # Actions if the cluster interface does not currently exist
+ if not cluster_interface:
+ if state == 'query':
+ # If this is a query, returne it is not found
+ module.exit_json(changed=False, found=False)
+ elif state == 'present':
+ # If this should be present, then attempt to create it
+ res = create_cluster_interface(maas, cluster, desired)
+ if res['error']:
+ module.fail_json(msg=res['status'])
+ else:
+ module.exit_json(changed=True, cluster_interface=res['status'], debug=debug)
+ else:
+ # If this should be absent, then we are done and in the desired state
+ module.exit_json(changed=False)
+
+ # Done with cluster interfaces does not exists actions
+ return
+
+ # Actions if the cluster interface does exist
+ if state == 'query':
+ # If this is a query, return the cluster interface
+ module.exit_json(changed=False, found=True, cluster_interface=cluster_interface)
+ elif state == 'present':
+ # If we want this to exists check to see if this is different and
+ # needs updated
+ if different(cluster_interface, desired, debug):
+ res = update_cluster_interface(maas, cluster, cluster_interface, desired)
+ if res['error']:
+ module.fail_json(msg=res['status'])
+ else:
+ module.exit_json(changed=True, cluster_interface=res['status'], debug=debug)
+ else:
+ # No differences, to nothing to change
+ module.exit_json(changed=False, cluster_interface=cluster_interface)
+ else:
+ # If we don't want this cluster interface, then delete it
+ res = delete_cluster_interface(maas, cluster, cluster_interface['name'])
+ if res['error']:
+ module.fail_json(msg=res['status'])
+ else:
+ module.exit_json(changed=True, cluster_interface=cluster_interface)
+
+# this is magic, see lib/ansible/module_common.py
+#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
+if __name__ == '__main__':
+ main()
diff --git a/library/maas_item.py b/library/maas_item.py
new file mode 100644
index 0000000..429c167
--- /dev/null
+++ b/library/maas_item.py
@@ -0,0 +1,190 @@
+#!/usr/bin/python
+
+DOCUMENTATION = '''
+---
+module: maas_item
+short_description: Manage MAAS Clusters Interfaces
+options:
+ maas:
+ description:
+ - URL of MAAS server
+ default: http://localhost/MAAS/api/1.0/
+ key:
+ description:
+ - MAAS API key
+ required: yes
+ name:
+ description:
+ - name of the item
+ required: yes
+ state:
+ description:
+ - possible states for this item
+ choices: ['present', 'absent', 'query']
+ default: present
+
+requirements: [ipaddress, requests_oauthlib, maasclient]
+author: David Bainbridge
+'''
+
+EXAMPLES = '''
+examples:
+ maas_item:
+ maas: http://my.maas.server.com/MAAS/api/1.0/
+ key: 'xBvr9dx5k7S52myufC:fqBXV7hJgXegNZDw9c:K8hsmL47XjAppfQy2pDVW7G49p6PELgp'
+ name: MyItem
+ state: present
+
+ maas_item:
+ maas: http://my.maas.server.com/MAAS/api/1.0/
+ key: 'xBvr9dx5k7S52myufC:fqBXV7hJgXegNZDw9c:K8hsmL47XjAppfQy2pDVW7G49p6PELgp'
+ name: MyDeadItem
+ state: absent
+'''
+
+import sys
+import json
+import ipaddress
+import requests
+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)
+
+# Attempt to interpret the given value as a JSON object, if that fails
+# just return it as a string
+def string_or_object(val):
+ try:
+ return json.loads(val)
+ except:
+ return val
+
+# Return a copy of the given dictionary with any `null` valued entries
+# removed
+def remove_null(d_in):
+ d = d_in.copy()
+ to_remove = []
+ for k in d.keys():
+ if d[k] == None:
+ to_remove.append(k)
+ for k in to_remove:
+ del d[k]
+ return d
+
+# Deterine if two dictionaries are different
+def different(have, want):
+ have_keys = have.keys()
+ for key in want.keys():
+ if (key in have_keys and want[key] != have[key]) or key not in have_keys:
+ return True
+ return False
+
+# Get an item from MAAS using its name, if not found return None
+def get_item(maas, name):
+ res = maas.get('/items/%s/' % name)
+ if res.ok:
+ return json.loads(res.text)
+ return None
+
+# Create an item based on the value given
+def create_item(maas, item):
+ merged = item.copy()
+ # merged['op'] = 'new'
+ res = maas.post('/items/', merged)
+ if res.ok:
+ return { 'error': False, 'status': get_item(maas, merged['name']) }
+ return { 'error': True, 'status': string_or_object(res.text) }
+
+# Delete an item based on the name
+def delete_item(maas, name):
+ res = maas.delete('/items/%s/' % name)
+ if res.ok:
+ return { 'error': False }
+ return { 'error': True, 'status': string_or_object(res.text) }
+
+def update_item(maas, have, want):
+ merged = have.copy()
+ merged.update(want)
+ res = put(maas, '/items/%s/' % merged['name'], merged)
+ if res.ok:
+ return { 'error': False, 'status': get_item(maas, merged['name']) }
+ return { 'error': True, 'status': string_or_object(res.text) }
+
+def main():
+ module = AnsibleModule(
+ argument_spec = dict(
+ maas=dict(default='http://localhost/MAAS/api/1.0/'),
+ key=dict(required=True),
+ name=dict(required=True),
+ state=dict(default='present', choices=['present', 'absent', 'query'])
+ ),
+ supports_check_mode = False
+ )
+
+ maas = module.params['maas']
+ key = module.params['key']
+ state = module.params['state']
+
+ # Construct a sparsely populate desired state
+ desired = remove_null({
+ 'name': module.params['name'],
+ })
+
+ # Authenticate into MAAS
+ auth = MaasAuth(maas, key)
+ maas = MaasClient(auth)
+
+ # Attempt to get the item from MAAS
+ item = get_item(maas, desired['name'])
+
+ # Actions if the item does not currently exist
+ if not item:
+ if state == 'query':
+ # If this is a query, returne it is not found
+ module.exit_json(changed=False, found=False)
+ elif state == 'present':
+ # If this should be present, then attempt to create it
+ res = create_item(maas, desired)
+ if res['error']:
+ module.fail_json(msg=res['status'])
+ else:
+ module.exit_json(changed=True, item=res['status'])
+ else:
+ # If this should be absent, then we are done and in the desired state
+ module.exit_json(changed=False)
+
+ # Done with items does not exists actions
+ return
+
+ # Actions if the item does exist
+ if state == 'query':
+ # If this is a query, return the item
+ module.exit_json(changed=False, found=True, item=item)
+ elif state == 'present':
+ # If we want this to exists check to see if this is different and
+ # needs updated
+ if different(item, desired):
+ res = update_item(maas, item, desired)
+ if res['error']:
+ module.fail_json(msg=res['status'])
+ else:
+ module.exit_json(changed=True, item=res['status'])
+ else:
+ # No differences, to nothing to change
+ module.exit_json(changed=False, item=item)
+ else:
+ # If we don't want this item, then delete it
+ res = delete_item(maas, item['name'])
+ if res['error']:
+ module.fail_json(msg=res['status'])
+ else:
+ module.exit_json(changed=True, item=item)
+
+# this is magic, see lib/ansible/module_common.py
+#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
+if __name__ == '__main__':
+ main()
diff --git a/library/maas_sshkey.py b/library/maas_sshkey.py
new file mode 100644
index 0000000..b901d4e
--- /dev/null
+++ b/library/maas_sshkey.py
@@ -0,0 +1,180 @@
+#!/usr/bin/python
+
+DOCUMENTATION = '''
+---
+module: maas_sshkey
+short_description: Manage MAAS Clusters Interfaces
+options:
+ maas:
+ description:
+ - URL of MAAS server
+ default: http://localhost/MAAS/api/1.0/
+ key:
+ description:
+ - MAAS API key
+ required: yes
+ sshkey:
+ description:
+ - sshkey on which to operate
+ required: yes
+ state:
+ description:
+ - possible states for this sshkey
+ choices: ['present', 'absent', 'query']
+ default: present
+
+requirements: [ipaddress, requests_oauthlib, maasclient]
+author: David Bainbridge
+'''
+
+EXAMPLES = '''
+examples:
+ maas_sshkey:
+ maas: http://my.maas.server.com/MAAS/api/1.0/
+ key: 'xBvr9dx5k7S52myufC:fqBXV7hJgXegNZDw9c:K8hsmL47XjAppfQy2pDVW7G49p6PELgp'
+ sshkey: ... foo@company.com
+ state: present
+
+ maas_sshkeys:
+ maas: http://my.maas.server.com/MAAS/api/1.0/
+ key: 'xBvr9dx5k7S52myufC:fqBXV7hJgXegNZDw9c:K8hsmL47XjAppfQy2pDVW7G49p6PELgp'
+ sshkey: ... foo@company.com
+ state: absent
+'''
+
+import sys
+import json
+import ipaddress
+import requests
+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)
+
+# Attempt to interpret the given value as a JSON object, if that fails
+# just return it as a string
+def string_or_object(val):
+ try:
+ return json.loads(val)
+ except:
+ return val
+
+# Return a copy of the given dictionary with any `null` valued entries
+# removed
+def remove_null(d_in):
+ d = d_in.copy()
+ to_remove = []
+ for k in d.keys():
+ if d[k] == None:
+ to_remove.append(k)
+ for k in to_remove:
+ del d[k]
+ return d
+
+def filter(filter_type, d, keys):
+ if filter_type == 'include':
+ for k in d.keys():
+ if k not in keys:
+ d.pop(k, None)
+ else:
+ for k in d.keys():
+ if k in keys:
+ d.pop(k, None)
+
+# Get an item from MAAS using its name, if not found return None
+def get_sshkey(maas, name):
+ res = maas.get('/account/prefs/sshkeys/', dict(op='list'))
+ if res.ok:
+ for sshkey in json.loads(res.text):
+ if sshkey['key'] == name:
+ return sshkey
+ return None
+
+# Create an item based on the value given
+def create_sshkey(maas, sshkey):
+ merged = sshkey.copy()
+ filter('include', merged, ['key'])
+ merged['op'] = 'new'
+ res = maas.post('/account/prefs/sshkeys/', merged)
+ if res.ok:
+ return { 'error': False, 'status': get_sshkey(maas, merged['key']) }
+ return { 'error': True, 'status': string_or_object(res.text) }
+
+# Delete an item based on the name
+def delete_sshkey(maas, id):
+ res = maas.delete('/account/prefs/sshkeys/%s/' % id)
+ if res.ok:
+ return { 'error': False }
+ return { 'error': True, 'status': string_or_object(res.text) }
+
+def main():
+ module = AnsibleModule(
+ argument_spec = dict(
+ maas=dict(default='http://localhost/MAAS/api/1.0/'),
+ key=dict(required=True),
+ sshkey=dict(required=True),
+ state=dict(default='present', choices=['present', 'absent', 'query'])
+ ),
+ supports_check_mode = False
+ )
+
+ maas = module.params['maas']
+ key = module.params['key']
+ state = module.params['state']
+
+ # Construct a sparsely populate desired state
+ desired = remove_null({
+ 'key': module.params['sshkey'],
+ })
+
+ # Authenticate into MAAS
+ auth = MaasAuth(maas, key)
+ maas = MaasClient(auth)
+
+ # Attempt to get the item from MAAS
+ sshkey = get_sshkey(maas, desired['key'])
+
+ # Actions if the item does not currently exist
+ if not sshkey:
+ if state == 'query':
+ # If this is a query, return it is not found
+ module.exit_json(changed=False, found=False)
+ elif state == 'present':
+ # If this should be present, then attempt to create it
+ res = create_sshkey(maas, desired)
+ if res['error']:
+ module.fail_json(msg=res['status'])
+ else:
+ module.exit_json(changed=True, sshkey=res['status'])
+ else:
+ # If this should be absent, then we are done and in the desired state
+ module.exit_json(changed=False)
+
+ # Done with items does not exists actions
+ return
+
+ # Actions if the item does exist
+ if state == 'query':
+ # If this is a query, return the sshkey
+ module.exit_json(changed=False, found=True, sshkey=sshkey)
+ elif state == 'present':
+ # If we want this to exists check to see if this is different and
+ # needs updated
+ # No differences, to nothing to change
+ module.exit_json(changed=False, sshkey=sshkey)
+ else:
+ # If we don't want this item, then delete it
+ res = delete_sshkey(maas, item['id'])
+ if res['error']:
+ module.fail_json(msg=res['status'])
+ else:
+ module.exit_json(changed=True, sshkey=sshkey)
+
+# this is magic, see lib/ansible/module_common.py
+#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
+if __name__ == '__main__':
+ main()
diff --git a/library/maas_subnet.py b/library/maas_subnet.py
new file mode 100644
index 0000000..1bc461f
--- /dev/null
+++ b/library/maas_subnet.py
@@ -0,0 +1,276 @@
+#!/usr/bin/python
+
+DOCUMENTATION = '''
+---
+module: maas_subnet
+short_description: Manage MAAS Clusters Interfaces
+options:
+ maas:
+ description:
+ - URL of MAAS server
+ default: http://localhost/MAAS/api/1.0/
+ key:
+ description:
+ - MAAS API key
+ required: yes
+ name:
+ description:
+ - name of the subnet
+ required: yes
+ space:
+ description:
+ - network space of the subnet
+ dns_servers:
+ description:
+ - dns servers for the subnet
+ gateway_ip:
+ description:
+ - gateway IP for the subnet
+ cidr:
+ description:
+ - cidr for the subnet
+ state:
+ description:
+ - possible states for this subnet
+ choices: ['present', 'absent', 'query']
+ default: present
+
+requirements: [ipaddress, requests_oauthlib, maasclient]
+author: David Bainbridge
+'''
+
+EXAMPLES = '''
+examples:
+ maas_subnet:
+ maas: http://my.maas.server.com/MAAS/api/1.0/
+ key: 'xBvr9dx5k7S52myufC:fqBXV7hJgXegNZDw9c:K8hsmL47XjAppfQy2pDVW7G49p6PELgp'
+ name: MySubnet
+ state: present
+
+ maas_subnet:
+ maas: http://my.maas.server.com/MAAS/api/1.0/
+ key: 'xBvr9dx5k7S52myufC:fqBXV7hJgXegNZDw9c:K8hsmL47XjAppfQy2pDVW7G49p6PELgp'
+ name: MyDeadSubnet
+ state: absent
+'''
+
+import sys
+import json
+import ipaddress
+import requests
+import string
+from maasclient.auth import MaasAuth
+from maasclient import MaasClient
+
+debug = []
+
+# 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)
+
+# Attempt to interpret the given value as a JSON object, if that fails
+# just return it as a string
+def string_or_object(val):
+ try:
+ return json.loads(val)
+ except:
+ return val
+
+# Return a copy of the given dictionary with any `null` valued entries
+# removed
+def remove_null(d_in):
+ d = d_in.copy()
+ to_remove = []
+ for k in d.keys():
+ if d[k] == None:
+ to_remove.append(k)
+ for k in to_remove:
+ del d[k]
+ return d
+
+# Removes keys from a dictionary either using an include or
+# exclude filter This change happens on given dictionary is
+# modified.
+def filter(filter_type, d, keys):
+ if filter_type == 'include':
+ for k in d.keys():
+ if k not in keys:
+ d.pop(k, None)
+ else:
+ for k in d.keys():
+ if k in keys:
+ d.pop(k, None)
+
+# Converts a subnet structure with names for the vlan and space to their
+# ID equivalents that can be used in a REST call to MAAS
+def convert(maas, subnet):
+ copy = subnet.copy()
+ copy['space'] = get_space(maas, subnet['space'])['id']
+ fabric_name, vlan_name = string.split(subnet['vlan'], ':', 1)
+ fabric = get_fabric(maas, fabric_name)
+ copy['vlan'] = get_vlan(maas, fabric, vlan_name)['id']
+ return copy
+
+# replaces the expanded VLAN object with a unique identifier of
+# `fabric`:`name`
+def simplify(subnet):
+ copy = subnet.copy()
+ if 'dns_servers' in copy.keys() and type(copy['dns_servers']) == list:
+ copy['dns_servers'] = ",".join(copy['dns_servers'])
+ if subnet['vlan'] and type(subnet['vlan']) == dict:
+ copy['vlan'] = "%s:%s" % (subnet['vlan']['fabric'], subnet['vlan']['name'])
+ return copy
+
+# Deterine if two dictionaries are different
+def different(have, want):
+ have_keys = have.keys()
+ for key in want.keys():
+ if (key in have_keys and want[key] != have[key]) or key not in have_keys:
+ debug.append({"have": have, "want": want, "key": key})
+ return True
+ return False
+
+# Get a space object form MAAS based on its name
+def get_space(maas, name):
+ res = maas.get('/spaces/')
+ if res.ok:
+ for space in json.loads(res.text):
+ if space['name'] == name:
+ return space
+ return None
+
+# Get a fabric object from MAAS based on its name
+def get_fabric(maas, name):
+ res = maas.get('/fabrics/')
+ if res.ok:
+ for fabric in json.loads(res.text):
+ if fabric['name'] == name:
+ return fabric
+ return None
+
+# Get a VLAN object form MAAS based on its name
+def get_vlan(maas, fabric, name ):
+ res = maas.get('/fabrics/%d/vlans/' % fabric['id'])
+ if res.ok:
+ for vlan in json.loads(res.text):
+ if vlan['name'] == name:
+ return vlan
+ return None
+
+# Get an subnet from MAAS using its name, if not found return None
+def get_subnet(maas, name):
+ res = maas.get('/subnets/')
+ if res.ok:
+ for subnet in json.loads(res.text):
+ if subnet['name'] == name:
+ return simplify(subnet)
+ return None
+
+# Create an subnet based on the value given
+def create_subnet(maas, subnet):
+ merged = subnet.copy()
+ # merged['op'] = 'new'
+ res = maas.post('/subnets/', convert(maas, merged))
+ if res.ok:
+ return { 'error': False, 'status': get_subnet(maas, merged['name']) }
+ return { 'error': True, 'status': string_or_object(res.text) }
+
+# Delete an subnet based on the name
+def delete_subnet(maas, name):
+ res = maas.delete('/subnets/%s/' % name)
+ if res.ok:
+ return { 'error': False }
+ return { 'error': True, 'status': string_or_object(res.text) }
+
+def update_subnet(maas, have, want):
+ merged = have.copy()
+ merged.update(want)
+ res = put(maas, '/subnets/%s/' % merged['id'], convert(maas, merged))
+ if res.ok:
+ return { 'error': False, 'status': get_subnet(maas, merged['name']) }
+ return { 'error': True, 'status': string_or_object(res.text) }
+
+def main():
+ module = AnsibleModule(
+ argument_spec = dict(
+ maas=dict(default='http://localhost/MAAS/api/1.0/'),
+ key=dict(required=True),
+ name=dict(required=True),
+ space=dict(required=False),
+ dns_servers=dict(required=False),
+ gateway_ip=dict(required=False),
+ cidr=dict(required=False),
+ state=dict(default='present', choices=['present', 'absent', 'query'])
+ ),
+ supports_check_mode = False
+ )
+
+ maas = module.params['maas']
+ key = module.params['key']
+ state = module.params['state']
+
+ # Construct a sparsely populate desired state
+ desired = remove_null({
+ 'name': module.params['name'],
+ 'space': module.params['space'],
+ 'dns_servers': module.params['dns_servers'],
+ 'gateway_ip': module.params['gateway_ip'],
+ 'cidr': module.params['cidr'],
+ })
+
+ # Authenticate into MAAS
+ auth = MaasAuth(maas, key)
+ maas = MaasClient(auth)
+
+ # Attempt to get the subnet from MAAS
+ subnet = get_subnet(maas, desired['name'])
+
+ # Actions if the subnet does not currently exist
+ if not subnet:
+ if state == 'query':
+ # If this is a query, returne it is not found
+ module.exit_json(changed=False, found=False)
+ elif state == 'present':
+ # If this should be present, then attempt to create it
+ res = create_subnet(maas, desired)
+ if res['error']:
+ module.fail_json(msg=res['status'])
+ else:
+ module.exit_json(changed=True, subnet=res['status'])
+ else:
+ # If this should be absent, then we are done and in the desired state
+ module.exit_json(changed=False)
+
+ # Done with subnets does not exists actions
+ return
+
+ # Actions if the subnet does exist
+ if state == 'query':
+ # If this is a query, return the subnet
+ module.exit_json(changed=False, found=True, subnet=subnet)
+ elif state == 'present':
+ # If we want this to exists check to see if this is different and
+ # needs updated
+ if different(subnet, desired):
+ res = update_subnet(maas, subnet, desired)
+ if res['error']:
+ module.fail_json(msg=res['status'])
+ else:
+ module.exit_json(changed=True, subnet=res['status'], debug=debug)
+ else:
+ # No differences, to nothing to change
+ module.exit_json(changed=False, subnet=subnet)
+ else:
+ # If we don't want this subnet, then delete it
+ res = delete_subnet(maas, subnet['name'])
+ if res['error']:
+ module.fail_json(msg=res['status'])
+ else:
+ module.exit_json(changed=True, subnet=subnet)
+
+# this is magic, see lib/ansible/module_common.py
+#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
+if __name__ == '__main__':
+ main()
diff --git a/library/maas_user.py b/library/maas_user.py
new file mode 100644
index 0000000..285269f
--- /dev/null
+++ b/library/maas_user.py
@@ -0,0 +1,200 @@
+#!/usr/bin/python
+
+DOCUMENTATION = '''
+---
+module: maas_user
+short_description: Manage MAAS Clusters Interfaces
+options:
+ maas:
+ description:
+ - URL of MAAS server
+ default: http://localhost/MAAS/api/1.0/
+ key:
+ description:
+ - MAAS API key
+ required: yes
+ name:
+ description:
+ - name of the user
+ required: yes
+ email:
+ description:
+ - email address of the user
+ required: no
+ password:
+ description:
+ - password for the user
+ required: no
+ is_superuser:
+ description:
+ - does the user have priviledges
+ default: no
+ state:
+ description:
+ - possible states for this user
+ choices: ['present', 'absent', 'query']
+ default: present
+
+requirements: [ipaddress, requests_oauthlib, maasclient]
+author: David Bainbridge
+'''
+
+EXAMPLES = '''
+examples:
+ maas_user:
+ maas: http://my.maas.server.com/MAAS/api/1.0/
+ key: 'xBvr9dx5k7S52myufC:fqBXV7hJgXegNZDw9c:K8hsmL47XjAppfQy2pDVW7G49p6PELgp'
+ name: MyUser
+ email: user@company.com
+ password: donttell
+ is_superuser: no
+ state: present
+
+ maas_user:
+ maas: http://my.maas.server.com/MAAS/api/1.0/
+ key: 'xBvr9dx5k7S52myufC:fqBXV7hJgXegNZDw9c:K8hsmL47XjAppfQy2pDVW7G49p6PELgp'
+ name: MyDeadUser
+ state: absent
+'''
+
+import sys
+import json
+import ipaddress
+import requests
+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)
+
+# Attempt to interpret the given value as a JSON object, if that fails
+# just return it as a string
+def string_or_object(val):
+ try:
+ return json.loads(val)
+ except:
+ return val
+
+# Return a copy of the given dictionary with any `null` valued entries
+# removed
+def remove_null(d_in):
+ d = d_in.copy()
+ to_remove = []
+ for k in d.keys():
+ if d[k] == None:
+ to_remove.append(k)
+ for k in to_remove:
+ del d[k]
+ return d
+
+# Deterine if two dictionaries are different
+def different(have, want):
+ have_keys = have.keys()
+ for key in want.keys():
+ if (key in have_keys and want[key] != have[key]) or key not in have_keys:
+ return True
+ return False
+
+# Get an user from MAAS using its name, if not found return None
+def get_user(maas, name):
+ res = maas.get('/users/%s/' % name)
+ if res.ok:
+ return json.loads(res.text)
+ return None
+
+# Create an user based on the value given
+def create_user(maas, user):
+ merged = user.copy()
+ # merged['op'] = 'new'
+ res = maas.post('/users/', merged)
+ if res.ok:
+ return { 'error': False, 'status': get_user(maas, merged['username']) }
+ return { 'error': True, 'status': string_or_object(res.text) }
+
+# Delete an user based on the name
+def delete_user(maas, name):
+ res = maas.delete('/users/%s/' % name)
+ if res.ok:
+ return { 'error': False }
+ return { 'error': True, 'status': string_or_object(res.text) }
+
+def main():
+ module = AnsibleModule(
+ argument_spec = dict(
+ maas=dict(default='http://localhost/MAAS/api/1.0/'),
+ key=dict(required=True),
+ name=dict(required=True),
+ email=dict(required=False),
+ password=dict(required=False),
+ is_superuser=dict(default=False, type='bool'),
+ state=dict(default='present', choices=['present', 'absent', 'query'])
+ ),
+ supports_check_mode = False
+ )
+
+ maas = module.params['maas']
+ key = module.params['key']
+ state = module.params['state']
+
+ # Construct a sparsely populate desired state
+ desired = remove_null({
+ 'username': module.params['name'],
+ 'email': module.params['email'],
+ 'password': module.params['password'],
+ 'is_superuser': 0 if not module.params['is_superuser'] else 1
+ })
+
+ # Authenticate into MAAS
+ auth = MaasAuth(maas, key)
+ maas = MaasClient(auth)
+
+ # Attempt to get the user from MAAS
+ user = get_user(maas, desired['username'])
+
+ # Actions if the user does not currently exist
+ if not user:
+ if state == 'query':
+ # If this is a query, returne it is not found
+ module.exit_json(changed=False, found=False)
+ elif state == 'present':
+ # If this should be present, then attempt to create it
+ res = create_user(maas, desired)
+ if res['error']:
+ module.fail_json(msg=res['status'])
+ else:
+ module.exit_json(changed=True, user=res['status'])
+ else:
+ # If this should be absent, then we are done and in the desired state
+ module.exit_json(changed=False)
+
+ # Done with users does not exists actions
+ return
+
+ # Actions if the user does exist
+ if state == 'query':
+ # If this is a query, return the user
+ module.exit_json(changed=False, found=True, user=user)
+ elif state == 'present':
+ # If we want this to exists check to see if this is different and
+ # needs updated
+ if different(user, desired):
+ module.fail_json(msg='Specified user, "%s", exists and MAAS does not allow the user to be modified programatically'
+ % user['username'])
+ else:
+ # No differences, to nothing to change
+ module.exit_json(changed=False, user=user)
+ else:
+ # If we don't want this user, then delete it
+ res = delete_user(maas, user['username'])
+ if res['error']:
+ module.fail_json(msg=res['status'])
+ else:
+ module.exit_json(changed=True, user=user)
+
+# this is magic, see lib/ansible/module_common.py
+#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
+if __name__ == '__main__':
+ main()
diff --git a/library/maas_zone.py b/library/maas_zone.py
new file mode 100644
index 0000000..bf8d684
--- /dev/null
+++ b/library/maas_zone.py
@@ -0,0 +1,199 @@
+#!/usr/bin/python
+
+DOCUMENTATION = '''
+---
+module: maas_zone
+short_description: Manage MAAS Clusters Interfaces
+options:
+ maas:
+ description:
+ - URL of MAAS server
+ default: http://localhost/MAAS/api/1.0/
+ key:
+ description:
+ - MAAS API key
+ required: yes
+ name:
+ description:
+ - name of the zone
+ required: yes
+ description:
+ description:
+ - description text of zone
+ required: no
+ state:
+ description:
+ - possible states for this zone
+ choices: ['present', 'absent', 'query']
+ default: present
+
+requirements: [ipaddress, requests_oauthlib, maasclient]
+author: David Bainbridge
+'''
+
+EXAMPLES = '''
+examples:
+ maas_zone:
+ maas: http://my.maas.server.com/MAAS/api/1.0/
+ key: 'xBvr9dx5k7S52myufC:fqBXV7hJgXegNZDw9c:K8hsmL47XjAppfQy2pDVW7G49p6PELgp'
+ name: MyZone
+ description: This is my zone
+ state: present
+
+ maas_zone:
+ maas: http://my.maas.server.com/MAAS/api/1.0/
+ key: 'xBvr9dx5k7S52myufC:fqBXV7hJgXegNZDw9c:K8hsmL47XjAppfQy2pDVW7G49p6PELgp'
+ name: MyDeadZone
+ description: This was my zone
+ state: absent
+'''
+
+import sys
+import json
+import ipaddress
+import requests
+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)
+
+# Attempt to interpret the given value as a JSON object, if that fails
+# just return it as a string
+def string_or_object(val):
+ try:
+ return json.loads(val)
+ except:
+ return val
+
+# Return a copy of the given dictionary with any `null` valued entries
+# removed
+def remove_null(d_in):
+ d = d_in.copy()
+ to_remove = []
+ for k in d.keys():
+ if d[k] == None:
+ to_remove.append(k)
+ for k in to_remove:
+ del d[k]
+ return d
+
+# Deterine if two dictionaries are different
+def different(have, want):
+ have_keys = have.keys()
+ for key in want.keys():
+ if (key in have_keys and want[key] != have[key]) or key not in have_keys:
+ return True
+ return False
+
+# Get an zone from MAAS using its name, if not found return None
+def get_zone(maas, name):
+ res = maas.get('/zones/%s/' % name)
+ if res.ok:
+ return json.loads(res.text)
+ return None
+
+# Create an zone based on the value given
+def create_zone(maas, zone):
+ merged = zone.copy()
+ # merged['op'] = 'new'
+ res = maas.post('/zones/', merged)
+ if res.ok:
+ return { 'error': False, 'status': get_zone(maas, merged['name']) }
+ return { 'error': True, 'status': string_or_object(res.text) }
+
+# Delete an zone based on the name
+def delete_zone(maas, name):
+ res = maas.delete('/zones/%s/' % name)
+ if res.ok:
+ return { 'error': False }
+ return { 'error': True, 'status': string_or_object(res.text) }
+
+def update_zone(maas, have, want):
+ merged = have.copy()
+ merged.update(want)
+ res = put(maas, '/zones/%s/' % merged['name'], merged)
+ if res.ok:
+ return { 'error': False, 'status': get_zone(maas, merged['name']) }
+ return { 'error': True, 'status': string_or_object(res.text) }
+
+def main():
+ module = AnsibleModule(
+ argument_spec = dict(
+ maas=dict(default='http://localhost/MAAS/api/1.0/'),
+ key=dict(required=True),
+ name=dict(required=True),
+ description=dict(required=False),
+ state=dict(default='present', choices=['present', 'absent', 'query'])
+ ),
+ supports_check_mode = False
+ )
+
+ maas = module.params['maas']
+ key = module.params['key']
+ state = module.params['state']
+
+ # Construct a sparsely populate desired state
+ desired = remove_null({
+ 'name': module.params['name'],
+ 'description': module.params['description'],
+ })
+
+ # Authenticate into MAAS
+ auth = MaasAuth(maas, key)
+ maas = MaasClient(auth)
+
+ # Attempt to get the zone from MAAS
+ zone = get_zone(maas, desired['name'])
+
+ # Actions if the zone does not currently exist
+ if not zone:
+ if state == 'query':
+ # If this is a query, returne it is not found
+ module.exit_json(changed=False, found=False)
+ elif state == 'present':
+ # If this should be present, then attempt to create it
+ res = create_zone(maas, desired)
+ if res['error']:
+ module.fail_json(msg=res['status'])
+ else:
+ module.exit_json(changed=True, zone=res['status'])
+ else:
+ # If this should be absent, then we are done and in the desired state
+ module.exit_json(changed=False)
+
+ # Done with zones does not exists actions
+ return
+
+ # Actions if the zone does exist
+ if state == 'query':
+ # If this is a query, return the zone
+ module.exit_json(changed=False, found=True, zone=zone)
+ return
+ elif state == 'present':
+ # If we want this to exists check to see if this is different and
+ # needs updated
+ if different(zone, desired):
+ res = update_zone(maas, zone, desired)
+ if res['error']:
+ module.fail_json(msg=res['status'])
+ else:
+ module.exit_json(changed=True, zone=res['status'])
+ else:
+ # No differences, to nothing to change
+ module.exit_json(changed=False, zone=zone)
+ else:
+ # If we don't want this zone, then delete it
+ res = delete_zone(maas, zone['name'])
+ if res['error']:
+ module.fail_json(msg=res['status'])
+ else:
+ module.exit_json(changed=True, zone=zone)
+
+# this is magic, see lib/ansible/module_common.py
+#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
+if __name__ == '__main__':
+ main()
diff --git a/library/netfile.py b/library/netfile.py
index 86868bf..ce0b474 100755
--- a/library/netfile.py
+++ b/library/netfile.py
@@ -211,7 +211,7 @@
if name == "":
result = {
"changed": False,
- "failed": True,
+ "failed": True,
"msg": "Name is a mansitory parameter",
}
print json.dumps(result)
diff --git a/roles/compute-node/files/delete-fabric-config b/roles/compute-node/files/delete-fabric-config
index 4ddce2d..fbe2f5c 100755
--- a/roles/compute-node/files/delete-fabric-config
+++ b/roles/compute-node/files/delete-fabric-config
@@ -2,4 +2,4 @@
CFG=${1:-"config-tibit.json"}
-curl -v -XDELETE -H Content-Type: application/json http://karaf:karaf@onos-fabric:8181/onos/v1/network/configuration
+curl -v -XDELETE -H Content-Type: application/json http://karaf:karaf@onos-fabric:8181/onos/v1/network/configuration
diff --git a/roles/head-node/files/commands/cord b/roles/head-node/files/commands/cord
index 0d63dc8..9f675d5 100755
--- a/roles/head-node/files/commands/cord
+++ b/roles/head-node/files/commands/cord
@@ -16,7 +16,7 @@
for CMD in $ALL_FILES; do
test -x "$CMD" && COMMANDS="$COMMANDS $CMD"
done
-
+
# Process comands for usage information
# Output all commands and their help information to file
# so it can be sorted. The format will be:
diff --git a/roles/head-node/files/commands/cord-harvest b/roles/head-node/files/commands/cord-harvest
index fd43182..8cabd11 100755
--- a/roles/head-node/files/commands/cord-harvest
+++ b/roles/head-node/files/commands/cord-harvest
@@ -50,7 +50,7 @@
fi
;;
check)
- RUNNING=$($SSH_OPT docker inspect --format="'{{ .State.Running }}'" harvester)
+ RUNNING=$($SSH_OPT docker inspect --format="'{{ .State.Running }}'" harvester)
if [ $? -ne 0 ]; then
>&2 echo "Unable to execute docker or locate harvester container, if not running on the head node please specify the server address"
exit 1
@@ -112,4 +112,4 @@
exit 1
;;
esac
-
+
diff --git a/roles/maas/tasks/config-maas.yml b/roles/maas/tasks/config-maas.yml
new file mode 100644
index 0000000..d7e0d1c
--- /dev/null
+++ b/roles/maas/tasks/config-maas.yml
@@ -0,0 +1,74 @@
+---
+- name: Ensure PIP
+ become: yes
+ apt:
+ name: python-pip
+ state: present
+
+- name: Ensure Prerequisites
+ become: yes
+ pip:
+ name: "{{item}}"
+ state: present
+ with_items:
+ - maasclient==0.3
+ - requests_oauthlib
+ - ipaddress
+
+- name: Ensure SSH Key
+ maas_sshkey:
+ key: '{{apikey.stdout}}'
+ maas: 'http://{{mgmt_ip_address.stdout}}/MAAS/api/1.0'
+ sshkey: '{{maas.user_sshkey}}'
+ state: present
+
+- name: Ensure Administrative Zone
+ maas_zone:
+ key: '{{apikey.stdout}}'
+ maas: 'http://{{mgmt_ip_address.stdout}}/MAAS/api/1.0'
+ name: administrative
+ state: present
+
+- name: Ensure Cluster
+ maas_cluster:
+ key: '{{apikey.stdout}}'
+ maas: 'http://{{mgmt_ip_address.stdout}}/MAAS/api/1.0'
+ name: 'Cluster master'
+ status: enabled
+ domain: '{{maas.domain}}'
+
+- name: Ensure Management Interface
+ maas_cluster_interface:
+ key: '{{apikey.stdout}}'
+ maas: 'http://{{mgmt_ip_address.stdout}}/MAAS/api/1.0'
+ cluster_name: 'Cluster master'
+ name: '{{interfaces.management}}'
+ interface: '{{interfaces.management}}'
+ management: dhcpdns
+ ip: "{{networks.management | ipaddr(1) | ipaddr('address')}}"
+ subnet_mask: "{{networks.management | ipaddr('netmask')}}"
+ broadcast_ip: "{{networks.management | ipaddr('broadcast')}}"
+ ip_range_low: '{{ranges.management.low}}'
+ ip_range_high: '{{ranges.management.high}}'
+ state: present
+
+- name: Ensure Subnet Configuration
+ maas_subnet:
+ key: '{{apikey.stdout}}'
+ maas: 'http://{{mgmt_ip_address.stdout}}/MAAS/api/1.0'
+ name: '{{networks.management}}'
+ gateway_ip: "{{networks.management | ipaddr(1) | ipaddr('address')}}"
+ dns_servers: "{{networks.management | ipaddr(1) | ipaddr('address')}}"
+
+- name: Ensure Upstream DNS Server
+ maas:
+ key: '{{apikey.stdout}}'
+ maas: 'http://{{mgmt_ip_address.stdout}}/MAAS/api/1.0'
+ upstream_dns: '{{maas.upstream_dns}}'
+ state: present
+
+- name: Ensure Boot Resources
+ maas_boot_resource:
+ key: '{{apikey.stdout}}'
+ maas: 'http://{{mgmt_ip_address.stdout}}/MAAS/api/1.0'
+ state: import
diff --git a/roles/maas/tasks/main.yml b/roles/maas/tasks/main.yml
index fd136b1..98ac070 100644
--- a/roles/maas/tasks/main.yml
+++ b/roles/maas/tasks/main.yml
@@ -182,18 +182,8 @@
tags:
- maas_restart
-#- name: Ensure latest bootstrap image
-# become: yes
-# docker_image:
-# name: localhost:5000/cord-maas-bootstrap:{{ docker.tag }}
-# pull: yes
-
-- name: Configure MAAS
- become: yes
- command: docker run docker-registry:5000/cord-maas-bootstrap:{{ docker.tag }} --apikey='{{apikey.stdout}}' --sshkey='{{maas.user_sshkey}}' --url='http://{{mgmt_ip_address.stdout}}/MAAS/api/1.0' --network='{{networks.management}}' --network-low='{{ranges.management.low}}' --network-high='{{ranges.management.high}}' --interface='{{interfaces.management}}' --zone='administrative' --cluster='Cluster master' --domain='{{maas.domain}}'
- register: maas_config_result
- changed_when: maas_config_result.stdout.find("CHANGED") != -1
- failed_when: "maas_config_result.rc != 0 or 'ERROR' in maas_config_result.stdout"
+- name: Ensure MAAS Configuration
+ include: config-maas.yml
- name: Custom MAAS Configuration Template
become: yes
@@ -264,9 +254,10 @@
- maas_restart
- name: Ensure Boot Resource Import Started
- become: yes
- shell: maas login cord http://localhost/MAAS/api/1.0/ "{{apikey.stdout}}" && maas cord boot-resources import && maas logout cord
- changed_when: true
+ maas_boot_resource:
+ key: '{{apikey.stdout}}'
+ maas: 'http://{{mgmt_ip_address.stdout}}/MAAS/api/1.0'
+ state: import
- name: Ensure VirtualBox Power Management
include: virtualbox.yml
diff --git a/roles/maas/vars/main.yml b/roles/maas/vars/main.yml
index 59bee09..cb3a397 100644
--- a/roles/maas/vars/main.yml
+++ b/roles/maas/vars/main.yml
@@ -21,6 +21,7 @@
# CHANGE:
# 'domain' specifies the domain name configured in to MAAS
domain: "{{ domain | default('cord.lab') }}"
+ upstream_dns: "{{ upstream_dns | default('8.8.8.8 8.8.8.4') }}"
interfaces:
# CHANGE:
diff --git a/roles/onos-fabric/files/bin/ping-test.sh b/roles/onos-fabric/files/bin/ping-test.sh
index d7b894e..4bbcef9 100755
--- a/roles/onos-fabric/files/bin/ping-test.sh
+++ b/roles/onos-fabric/files/bin/ping-test.sh
@@ -2,7 +2,7 @@
HOSTS="10.3.1.1 10.3.1.2 10.3.2.1 10.3.2.2 10.3.1.254 10.3.2.254 192.168.10.1 8.8.8.8"
-ME=$(ifconfig | grep "10\.3\.[0-9]\.[0-9]" | sed -e 's/.*addr:\(10\.3\.[0-9]\.[0-9]\).*/\1/g' 2> /dev/null)
+ME=$(ifconfig | grep "10\.3\.[0-9]\.[0-9]" | sed -e 's/.*addr:\(10\.3\.[0-9]\.[0-9]\).*/\1/g' 2> /dev/null)
echo "FROM: $ME"
for TO in $HOSTS; do
T=$(ping -q -c 1 -W 1 -I eth0 $TO | grep rtt | awk '{print $4}' | sed -e 's|/| |g') #sed -e 's|r| |')
diff --git a/roles/onos-fabric/templates/fabric-network-config.json.j2 b/roles/onos-fabric/templates/fabric-network-config.json.j2
index f39cf60..f29134c 100644
--- a/roles/onos-fabric/templates/fabric-network-config.json.j2
+++ b/roles/onos-fabric/templates/fabric-network-config.json.j2
@@ -212,7 +212,7 @@
"org.onosproject.core" : {
"core" : {
"linkDiscoveryMode" : "STRICT" // enable strict link validation
- }
+ }
}
}
}