--- /dev/null
+#!/bin/bash
+
+#############################################################################
+#Copyright 2017 Parker Berberian and others #
+# #
+#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. #
+#############################################################################
+
+virsh start master
+
+ret=''
+while [ -z "$ret" ]; do
+ echo "Master node is not accepting ssh. Sleeping 15 seconds..."
+ sleep 15
+ ret=$(nmap 10.20.0.2 -PN -p ssh | grep open)
+done
+
+ssh-keygen -f ~/.ssh/id_rsa -t rsa -N ''
+sshpass -p r00tme ssh-copy-id -o stricthostkeychecking=no root@10.20.0.2
+
+ssh root@10.20.0.2 killall fuelmenu
+
+echo "killed fuel menu. Waiting for installation to complete"
+
+ans=''
+while [ -z "$ans" ]; do
+ echo "fuel api unavailable. Sleeping 15 seconds..."
+ sleep 15
+ ans=$(curl http://10.20.0.2:8000 2>/dev/null )
+done
--- /dev/null
+#!/bin/bash
+#############################################################################
+#Copyright 2017 Parker Berberian and others #
+# #
+#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. #
+#############################################################################
+
+MYIP=$1
+DESTINATION=$2
+MYBRIDGE=10.20.1.1
+DESTNETWORK=10.20.1.0/24
+PORT=80
+
+iptables -I INPUT 2 -d "$MYIP" -p tcp --dport "$PORT" -j ACCEPT
+iptables -t nat -I INPUT 1 -d "$MYIP" -p tcp --dport "$PORT" -j ACCEPT
+iptables -I FORWARD -p tcp --dport "$PORT" -j ACCEPT
+
+iptables -t nat -I PREROUTING -p tcp -d "$MYIP" --dport "$PORT" -j DNAT --to-destination "$DESTINATION:$PORT"
+iptables -t nat -I POSTROUTING -p tcp -s "$DESTINATION" ! -d "$DESTNETWORK" -j SNAT --to-source "$MYIP"
+
+iptables -t nat -I POSTROUTING 2 -d "$DESTINATION" -j SNAT --to-source "$MYBRIDGE"
--- /dev/null
+"""
+#############################################################################
+#Copyright 2017 Parker Berberian and others #
+# #
+#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. #
+#############################################################################
+"""
+
+import requests
+import time
+import sys
+
+
+class Fuel_api:
+
+ def __init__(self, url, logger, user="admin", password="admin"):
+ """
+ url is the url of the fog api in the form
+ http://ip.or.host.name:8000/
+ logger is a reference to the logger
+ the default creds for fuel is admin/admin
+ """
+ self.logger = logger
+ self.base = url
+ self.user = user
+ self.password = password
+ self.header = {"Content-Type": "application/json"}
+
+ def getKey(self):
+ """
+ authenticates with the user and password
+ to get a keystone key, used in the headers
+ from here on to talk to fuel.
+ """
+ url = self.base + 'keystone/v2.0/tokens/'
+ reqData = {"auth": {
+ "tenantName": self.user,
+ "passwordCredentials": {
+ "username": self.user,
+ "password": self.password
+ }
+ }}
+ self.logger.info("Retreiving keystone token from %s", url)
+ token = requests.post(url, headers=self.header, json=reqData)
+ self.logger.info("Received response code %d", token.status_code)
+ self.token = token.json()['access']['token']['id']
+ self.header['X-Auth-Token'] = self.token
+
+ def getNotifications(self):
+ """
+ returns the fuel notifications
+ """
+ url = self.base+'/api/notifications'
+ try:
+ req = requests.get(url, headers=self.header)
+ return req.json()
+
+ except Exception:
+ self.logger.exception('%s', "Failed to talk to the Fuel api!")
+ sys.exit(1)
+
+ def waitForBootstrap(self):
+ """
+ Waits for the bootstrap image to build.
+ """
+ while True:
+ time.sleep(30)
+ notes = self.getNotifications()
+ for note in notes:
+ if "bootstrap image building done" in note['message']:
+ return
+
+ def getNodes(self):
+ """
+ returns a list of all nodes booted into fuel
+ """
+ url = self.base+'api/nodes'
+ try:
+ req = requests.get(url, headers=self.header)
+ return req.json()
+ except Exception:
+ self.logger.exception('%s', "Failed to talk to the Fuel api!")
+ sys.exit(1)
+
+ def getID(self, mac):
+ """
+ gets the fuel id of node with given mac
+ """
+ for node in self.getNodes():
+ if node['mac'] == mac:
+ return node['id']
+
+ def getNetID(self, name, osid):
+ """
+ gets the id of the network with name
+ """
+ url = self.base+'api/clusters/'
+ url += str(osid)+'/network_configuration/neutron'
+ try:
+ req = requests.get(url, headers=self.header)
+ nets = req.json()['networks']
+ for net in nets:
+ if net['name'] == name:
+ return net['id']
+ return -1
+
+ except Exception:
+ self.logger.exception('%s', "Failed to talk to the Fuel api!")
+ sys.exit(1)
+
+ def createOpenstack(self):
+ """
+ defines a new openstack environment in fuel.
+ """
+ url = self.base+'api/clusters'
+ data = {
+ "nodes": [],
+ "tasks": [],
+ "name": "OpenStack",
+ "release_id": 2,
+ "net_segment_type": "vlan"
+ }
+ try:
+ req = requests.post(url, json=data, headers=self.header)
+ return req.json()['id']
+ except Exception:
+ self.logger.exception('%s', "Failed to talk to the Fuel api!")
+ sys.exit(1)
+
+ def simpleNetDict(self, osID):
+ """
+ returns a simple dict of network names and id numbers
+ """
+ nets = self.getNetworks(osID)
+ netDict = {}
+ targetNets = ['admin', 'public', 'storage', 'management']
+ for net in nets['networks']:
+ for tarNet in targetNets:
+ if tarNet in net['name']:
+ netDict[tarNet] = net['id']
+ return netDict
+
+ def getNetworks(self, osID):
+ """
+ Returns the pythonizezd json of the openstack networks
+ """
+ url = self.base + 'api/clusters/'
+ url += str(osID)+'/network_configuration/neutron/'
+ try:
+ req = requests.get(url, headers=self.header)
+ return req.json()
+ except Exception:
+ self.logger.exception('%s', "Failed to talk to the Fuel api!")
+ sys.exit(1)
+
+ def uploadNetworks(self, netJson, osID):
+ """
+ configures the networks of the openstack
+ environment with id osID based on netJson
+ """
+ url = self.base+'api/clusters/'
+ url += str(osID)+'/network_configuration/neutron'
+ try:
+ req = requests.put(url, headers=self.header, json=netJson)
+ return req.json()
+ except Exception:
+ self.logger.exception('%s', "Failed to talk to the Fuel api!")
+ sys.exit(1)
+
+ def addNodes(self, clusterID, nodes):
+ """
+ Adds the nodes into this openstack environment.
+ nodes is valid json
+ """
+ url = self.base + 'api/clusters/'+str(clusterID)+'/assignment'
+ try:
+ req = requests.post(url, headers=self.header, json=nodes)
+ return req.json()
+
+ except Exception:
+ self.logger.exception('%s', "Failed to talk to the Fuel api!")
+ sys.exit(1)
+
+ def getIfaces(self, nodeID):
+ """
+ returns the pythonized json describing the
+ interfaces of given node
+ """
+ url = self.base + 'api/nodes/'+str(nodeID)+'/interfaces'
+ try:
+ req = requests.get(url, headers=self.header)
+ return req.json()
+
+ except Exception:
+ self.logger.exception('%s', "Failed to talk to the Fuel api!")
+ sys.exit(1)
+
+ def setIfaces(self, nodeID, ifaceJson):
+ """
+ configures the interfaces of node with id nodeID
+ with ifaceJson
+ ifaceJson is valid json that fits fuel's schema for ifaces
+ """
+ url = self.base+'/api/nodes/'+str(nodeID)+'/interfaces'
+ try:
+ req = requests.put(url, headers=self.header, json=ifaceJson)
+ return req.json()
+
+ except Exception:
+ self.logger.exception('%s', "Failed to talk to the Fuel api!")
+ sys.exit(1)
+
+ def getTasks(self):
+ """
+ returns a list of all tasks
+ """
+ url = self.base+"/api/tasks/"
+ try:
+ req = requests.get(url, headers=self.header)
+ return req.json()
+ except Exception:
+ self.logger.exception('%s', "Failed to talk to the Fuel api!")
+ sys.exit(1)
+
+ def waitForTask(self, uuid):
+ """
+ Tracks the progress of task with uuid and
+ returns once the task finishes
+ """
+ progress = 0
+ while progress < 100:
+ for task in self.getTasks():
+ if task['uuid'] == uuid:
+ progress = task['progress']
+ self.logger.info("Task is %s percent done", str(progress))
+ time.sleep(20)
+ # Task may hang a minute at 100% without finishing
+ while True:
+ for task in self.getTasks():
+ if task['uuid'] == uuid and not task['status'] == "ready":
+ time.sleep(10)
+ elif task['uuid'] == uuid and task['status'] == "ready":
+ return
+
+ def getHorizonIP(self, osid):
+ """
+ returns the ip address of the horizon dashboard.
+ Horizon always takes the first ip after the public router's
+ """
+ url = self.base+'api/clusters/'
+ url += str(osid)+'/network_configuration/neutron/'
+ try:
+ req = requests.get(url, headers=self.header)
+ routerIP = req.json()['vips']['vrouter_pub']['ipaddr'].split('.')
+ routerIP[-1] = str(int(routerIP[-1])+1)
+ return '.'.join(routerIP)
+ except Exception:
+ self.logger.exception('%s', "Failed to talk to the Fuel api!")
+ sys.exit(1)
+
+ def deployOpenstack(self, clusterID):
+ """
+ Once openstack and the nodes are configured,
+ this method actually deploys openstack.
+ It takes a while.
+ """
+ # First, we need to provision the cluster
+ url = self.base+'/api/clusters/'+str(clusterID)+'/provision'
+ req = requests.put(url, headers=self.header)
+ if req.status_code < 300:
+ self.logger.info('%s', "Sent provisioning task")
+ else:
+ err = "failed to provision Openstack Environment"
+ self.logger.error('%s', err)
+ sys.exit(1)
+
+ taskUID = ''
+ tasks = self.getTasks()
+ for task in tasks:
+ if task['name'] == "provision" and task['cluster'] == clusterID:
+ taskUID = task['uuid']
+
+ self.waitForTask(taskUID)
+
+ # Then, we deploy cluster
+ url = self.base + '/api/clusters/'+str(clusterID)+'/deploy'
+ req = requests.put(url, headers=self.header)
+ if req.status_code < 300:
+ self.logger.info('%s', "Sent deployment task")
+ taskUID = ''
+ tasks = self.getTasks()
+ for task in tasks:
+ if 'deploy' in task['name'] and task['cluster'] == clusterID:
+ taskUID = task['uuid']
+ if len(taskUID) > 0:
+ self.waitForTask(taskUID)
--- /dev/null
+"""
+#############################################################################
+#Copyright 2017 Parker Berberian and others #
+# #
+#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. #
+#############################################################################
+"""
--- /dev/null
+"""
+#############################################################################
+#Copyright 2017 Parker Berberian and others #
+# #
+#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. #
+#############################################################################
+"""
+
+import time
+import sys
+from installer import Installer
+from api.fuel_api import Fuel_api
+
+
+class Fuel_Installer(Installer):
+ """
+ This class is the installer for any OPNFV scenarios which use Fuel as the
+ installer. This class uses the libvirt api handler
+ to create all the virtual hosts,
+ then installs fuel and uses the fuel api handler
+ to create and deploy an openstack environment
+
+ This class will get much smarter and have less configuration hardcoded
+ as we grow support for more OPNFV scenarios
+ """
+
+ def __init__(self, doms, nets, libvirt_handler, util):
+ """
+ init function
+ Calls the super constructor
+ """
+ super(Fuel_Installer, self).__init__(doms, nets, libvirt_handler, util)
+ url = 'http://'+self.libvirt.host+':8000/'
+ self.handler = Fuel_api(url, self.log, 'admin', 'admin')
+ self.fuelNets = None
+
+ def bootMaster(self):
+ """
+ Boots the fuel master node and waits
+ for it to come up
+ """
+ self.libvirt.bootMaster()
+ time.sleep(100)
+
+ def bootNodes(self):
+ """
+ Boots all the slave nodes
+ """
+ self.libvirt.bootSlaves()
+
+ def waitForNodes(self, numNodes):
+ """
+ Waits for the nodes to pxe boot and be recognized by Fuel
+ """
+ done = False
+ self.log.info("Waiting for %i nodes to boot into Fuel", numNodes)
+ discoveredNodes = 0
+ while not done:
+ discoveredNodes = len(self.handler.getNodes())
+ nodes = int(discoveredNodes)
+ self.log.info("found %d nodes", nodes)
+
+ done = discoveredNodes == numNodes
+
+ def installMaster(self):
+ """
+ runs the fuelInstall script, which uses the fuel iso to
+ install fuel onto the master node
+ """
+ self.util.execRemoteScript("ipnat.sh", [self.libvirt.host])
+ self.util.execRemoteScript("fuelInstall.sh", [self.util.remoteDir])
+
+ def makeOpenstack(self):
+ """
+ creates an openstack environment and saves
+ the openstack id
+ """
+ self.osid = self.handler.createOpenstack()
+
+ def addNodesToOpenstack(self):
+ """
+ Adds the nodes to the openstack environment with
+ compute / controller + cinder roles
+ """
+ nodesList = [
+ {"id": 1, "roles": ["controller", "cinder"]},
+ {"id": 2, "roles": ["controller", "cinder"]},
+ {"id": 3, "roles": ["controller", "cinder"]},
+ {"id": 4, "roles": ["compute"]},
+ {"id": 5, "roles": ["compute"]}
+ ]
+
+ self.handler.addNodes(self.osid, nodesList)
+
+ def configNetworks(self):
+ """
+ configures the openstack networks by calling the 3 helper
+ methods
+ """
+ self.configPublicNet()
+ self.configStorageNet()
+ self.configManagementNet()
+
+ def configPublicNet(self):
+ """
+ sets the default public network
+ changes the cidr, gateway, and floating ranges
+ """
+ networks = self.handler.getNetworks(self.osid)
+ for net in networks['networks']:
+ if net['name'] == "public":
+ net["ip_ranges"] = [["10.20.1.10", "10.20.1.126"]]
+ net['cidr'] = "10.20.1.0/24"
+ net['gateway'] = "10.20.1.1"
+
+ # updates the floating ranges
+ rng = [["10.20.1.130", "10.20.1.254"]]
+ networks['networking_parameters']['floating_ranges'] = rng
+ self.handler.uploadNetworks(networks, self.osid)
+
+ def configStorageNet(self):
+ """
+ sets the default storage network to have the right
+ cidr and gateway, and no vlan
+ """
+ networks = self.handler.getNetworks(self.osid)
+ for net in networks['networks']:
+ if net['name'] == "storage":
+ net["ip_ranges"] = [["10.20.3.5", "10.20.3.254"]]
+ net["cidr"] = "10.20.3.0/24"
+ net["meta"]["notation"] = "ip_ranges"
+ net["meta"]["use_gateway"] = True
+ net["gateway"] = "10.20.3.1"
+ net["vlan_start"] = None
+ self.handler.uploadNetworks(networks, self.osid)
+
+ def configManagementNet(self):
+ """
+ sets the default management net to have the right
+ cidr and gatewar and no vlan
+ """
+ networks = self.handler.getNetworks(self.osid)
+ for net in networks['networks']:
+ if net['name'] == "management":
+ net["ip_ranges"] = [["10.20.2.5", "10.20.2.254"]]
+ net["cidr"] = "10.20.2.0/24"
+ net["meta"]["notation"] = "ip_ranges"
+ net["meta"]["use_gateway"] = True
+ net["gateway"] = "10.20.2.1"
+ net["vlan_start"] = None
+ self.handler.uploadNetworks(networks, self.osid)
+
+ # TODO: make this method smarter. I am making too many assumptions about
+ # the order of interfaces and networks
+ def configIfaces(self):
+ """
+ assigns the proper networks to each interface of the nodes
+ """
+ for x in range(1, 6):
+ idNum = x
+ ifaceJson = self.handler.getIfaces(idNum)
+
+ ifaceJson[0]['assigned_networks'] = [
+ {"id": 1, "name": "fuelweb_admin"},
+ {"id": 5, "name": "private"}
+ ]
+ ifaceJson[2]['assigned_networks'] = [
+ {"id": 4, "name": "storage"}
+ ]
+ ifaceJson[3]['assigned_networks'] = [
+ {"id": 3, "name": "management"}
+ ]
+ if idNum < 4:
+ ifaceJson[1]['assigned_networks'] = [{
+ "id": 2,
+ "name": "pubic"
+ }]
+
+ self.handler.setIfaces(idNum, ifaceJson)
+
+ def clearAdminIface(self, ifaceJson, node):
+ """
+ makes the admin interface have *only* the admin network
+ assigned to it
+ """
+ for iface in ifaceJson:
+ if iface['mac'] == node.macs['admin']:
+ iface['assigned_networks'] = [{
+ "id": 1,
+ "name": "fuelweb_admin"
+ }]
+
+ def deployOpenstack(self):
+ """
+ Once openstack is properly configured, this method
+ deploy OS and returns when OS is running
+ """
+ self.log.info("%s", "Deploying Openstack environment.")
+ self.log.info("%s", "This may take a while")
+ self.handler.deployOpenstack(self.osid)
+
+ def getKey(self):
+ """
+ Retrieves authentication tokens for the api handler,
+ while allowing the first few attempts to fail to
+ allow Fuel time to "wake up"
+ """
+ i = 0
+ while i < 20:
+ i += 1
+ try:
+ self.handler.getKey()
+ return
+ except Exception:
+ self.log.warning("%s", "Failed to talk to Fuel api")
+ self.log.warning("Exec try %d/20", i)
+ try:
+ self.handler.getKey()
+ except Exception:
+ self.logger.exception("%s", "Fuel api is unavailable")
+ sys.exit(1)
+
+ def go(self):
+ """
+ This method does all the work of this class.
+ It installs the master node, boots the slaves
+ into Fuel, creates and configures OS, and then
+ deploys it and uses NAT to make the horizon dashboard
+ reachable
+ """
+ self.libvirt.openConnection()
+ self.log.info('%s', 'installing the Fuel master node.')
+ self.log.info('%s', 'This will take some time.')
+ self.installMaster()
+ time.sleep(60)
+ self.getKey()
+ self.log.info('%s', 'The master node is installed.')
+ self.log.info('%s', 'Waiting for bootstrap image to build')
+ self.handler.waitForBootstrap()
+ self.bootNodes()
+ self.waitForNodes(5)
+ self.log.info('%s', "Defining an openstack environment")
+ self.makeOpenstack()
+ self.addNodesToOpenstack()
+ self.log.info('%s', "configuring interfaces...")
+ self.configIfaces()
+ self.log.info('%s', "configuring networks...")
+ self.configNetworks()
+ self.deployOpenstack()
+
+ horizon = self.handler.getHorizonIP(self.osid)
+ self.util.execRemoteScript(
+ '/horizonNat.sh', [self.libvirt.host, horizon])
+ notice = "You may access the Openstack dashboard at %s/horizon"
+ self.log.info(notice, self.libvirt.host)
+
+ self.libvirt.close()
+ self.util.finishDeployment()
--- /dev/null
+"""
+#############################################################################
+#Copyright 2017 Parker Berberian and others #
+# #
+#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. #
+#############################################################################
+"""
+
+
+class Installer(object):
+ """
+ This is a simple base class to define a single constructor
+ for all the different installer types.
+ I may move more functionality to this class as we add support for more
+ installers and there becomes common fucntions that would be nice to share
+ between installers.
+ """
+
+ def __init__(self, domList, netList, libvirt_handler, util):
+ self.doms = domList
+ self.nets = netList
+ self.libvirt = libvirt_handler
+ self.osid = 0
+ self.util = util
+ self.log = util.createLogger(util.hostname)