From 136e34eeb584922f6a3fab4ed9e8f5b35d8b0921 Mon Sep 17 00:00:00 2001 From: Parker Berberian Date: Mon, 21 Aug 2017 09:24:57 -0400 Subject: Adds Fuel Installation Capability JIRA: N/A Adds the ability to automatically install and deploy Fuel onto a remote host. the hostScripts/fuelInstall.sh script boots the master machine and uses the fuel iso to install fuel to the machine. the source/installers/fuel.py then uses the source/api/fuel_api handler to configure and deploy fuel. Running: Fuel_Installer(domains,networks,libvirt,utility).go() Will install fuel and deploy Openstack on top of Fuel assuming the host is properly configured. Change-Id: I41aee773b27b893311c945221b93eacf36aa83cc Signed-off-by: Parker Berberian --- tools/laas-fog/hostScripts/fuelInstall.sh | 40 ++++ tools/laas-fog/hostScripts/horizonNat.sh | 31 +++ tools/laas-fog/source/api/fuel_api.py | 306 ++++++++++++++++++++++++++ tools/laas-fog/source/installers/__init__.py | 17 ++ tools/laas-fog/source/installers/fuel.py | 268 ++++++++++++++++++++++ tools/laas-fog/source/installers/installer.py | 35 +++ 6 files changed, 697 insertions(+) create mode 100755 tools/laas-fog/hostScripts/fuelInstall.sh create mode 100755 tools/laas-fog/hostScripts/horizonNat.sh create mode 100644 tools/laas-fog/source/api/fuel_api.py create mode 100644 tools/laas-fog/source/installers/__init__.py create mode 100644 tools/laas-fog/source/installers/fuel.py create mode 100644 tools/laas-fog/source/installers/installer.py diff --git a/tools/laas-fog/hostScripts/fuelInstall.sh b/tools/laas-fog/hostScripts/fuelInstall.sh new file mode 100755 index 00000000..c68907d0 --- /dev/null +++ b/tools/laas-fog/hostScripts/fuelInstall.sh @@ -0,0 +1,40 @@ +#!/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 diff --git a/tools/laas-fog/hostScripts/horizonNat.sh b/tools/laas-fog/hostScripts/horizonNat.sh new file mode 100755 index 00000000..dd6396c6 --- /dev/null +++ b/tools/laas-fog/hostScripts/horizonNat.sh @@ -0,0 +1,31 @@ +#!/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" diff --git a/tools/laas-fog/source/api/fuel_api.py b/tools/laas-fog/source/api/fuel_api.py new file mode 100644 index 00000000..01278000 --- /dev/null +++ b/tools/laas-fog/source/api/fuel_api.py @@ -0,0 +1,306 @@ +""" +############################################################################# +#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) diff --git a/tools/laas-fog/source/installers/__init__.py b/tools/laas-fog/source/installers/__init__.py new file mode 100644 index 00000000..7bb515b7 --- /dev/null +++ b/tools/laas-fog/source/installers/__init__.py @@ -0,0 +1,17 @@ +""" +############################################################################# +#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. # +############################################################################# +""" diff --git a/tools/laas-fog/source/installers/fuel.py b/tools/laas-fog/source/installers/fuel.py new file mode 100644 index 00000000..c5b647cf --- /dev/null +++ b/tools/laas-fog/source/installers/fuel.py @@ -0,0 +1,268 @@ +""" +############################################################################# +#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() diff --git a/tools/laas-fog/source/installers/installer.py b/tools/laas-fog/source/installers/installer.py new file mode 100644 index 00000000..d4c4889f --- /dev/null +++ b/tools/laas-fog/source/installers/installer.py @@ -0,0 +1,35 @@ +""" +############################################################################# +#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) -- cgit 1.2.3-korg