summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorParker Berberian <pberberian@iol.unh.edu>2017-08-21 08:57:59 -0400
committerParker Berberian <pberberian@iol.unh.edu>2017-08-31 13:28:57 -0400
commit3681849c3ab451145e8c79996d54e10a38d5484a (patch)
tree274723d3668064a06933b52007183aa3cef5ae47
parent4a6ee099cb28e575614a9a58da3da9019a3a03d2 (diff)
LaaS Base functionality
JIRA: N/A This is the base of the laas hosting software. the pod_manager can select a host from a pool and will ghost it with a clean image. The deployment_manager will install OPNFV on that host. Utilities defines misc useful functions that are needed throughout the provisioning and dpeloyment. Change-Id: I2fb24f36491ded1284f5ac1659a505bd88baafb4 Signed-off-by: Parker Berberian <pberberian@iol.unh.edu>
-rw-r--r--laas-fog/source/__init__.py17
-rw-r--r--laas-fog/source/deployment_manager.py108
-rwxr-xr-xlaas-fog/source/pod_manager.py144
-rw-r--r--laas-fog/source/utilities.py346
4 files changed, 615 insertions, 0 deletions
diff --git a/laas-fog/source/__init__.py b/laas-fog/source/__init__.py
new file mode 100644
index 0000000..7bb515b
--- /dev/null
+++ b/laas-fog/source/__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/laas-fog/source/deployment_manager.py b/laas-fog/source/deployment_manager.py
new file mode 100644
index 0000000..f680fa5
--- /dev/null
+++ b/laas-fog/source/deployment_manager.py
@@ -0,0 +1,108 @@
+"""
+#############################################################################
+#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 logging
+from api.libvirt_api import Libvirt
+
+
+class Deployment_Manager:
+ """
+ This class manages the deployment of OPNFV on a booked host
+ if it was requested. If no OPNFV installer was requested, this class will
+ create the virtual machines and networks in the config files and exit.
+ """
+ def __init__(self, installerType, scenario, utility):
+ """
+ init function
+ """
+ # installerType will either be the constructor for an installer or None
+ self.installer = installerType
+ self.virt = Libvirt(
+ utility.host,
+ net_conf=utility.conf['hypervisor_config']['networks'],
+ dom_conf=utility.conf['hypervisor_config']['vms']
+ )
+ self.host = utility.host
+ self.util = utility
+
+ def getIso(self):
+ """
+ checks if any of the domains expect an ISO file to exist
+ and retrieves it.
+ """
+ isoDom = None
+ for dom in self.doms:
+ if dom.iso['used']:
+ isoDom = dom
+ break
+ if isoDom:
+ path = isoDom.iso['location']
+ url = isoDom.iso['URL']
+ self.util.sshExec(['wget', '-q', '-O', path, url])
+
+ def getDomMacs(self):
+ """
+ assigns the 'macs' instance variable to the domains
+ so that they know the mac addresses of their interfaces.
+ """
+ for dom in self.doms:
+ dom.macs = self.virt.getMacs(dom.name)
+
+ def makeDisks(self):
+ """
+ Creates the qcow2 disk files the domains expect on the remote host.
+ """
+ disks = []
+ for dom in self.doms:
+ disks.append(dom.disk)
+ self.util.execRemoteScript("mkDisks.sh", disks)
+
+ def go(self):
+ """
+ 'main' function.
+ creates virtual machines/networks and either passes control to the
+ OPNFV installer, or finishes up if an installer was not requested.
+ """
+ log = logging.getLogger(self.util.hostname)
+ self.virt.setLogger(log)
+ log.info("%s", "Connecting to the host hypervisor")
+ self.virt.openConnection()
+ domains, networks = self.virt.go()
+ log.info("%s", "Created all networks and VM's on host")
+ self.doms = domains
+ self.nets = networks
+ if self.installer is None:
+ log.warning("%s", "No installer requested. Finishing deployment")
+ self.util.finishDeployment()
+ return
+ log.info("%s", "retrieving ISO")
+ self.getIso()
+ self.getDomMacs()
+ self.util.copyScripts()
+ self.makeDisks()
+ log.info("%s", "Beginning installation of OPNFV")
+ try:
+ installer = self.installer(
+ self.doms,
+ self.nets,
+ self.virt,
+ self.util
+ )
+ installer.go()
+ except Exception:
+ log.exception('%s', "failed to install OPNFV")
diff --git a/laas-fog/source/pod_manager.py b/laas-fog/source/pod_manager.py
new file mode 100755
index 0000000..3e1caa8
--- /dev/null
+++ b/laas-fog/source/pod_manager.py
@@ -0,0 +1,144 @@
+#!/usr/bin/python
+"""
+#############################################################################
+#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
+import yaml
+import os
+from api.fog import FOG_Handler
+from utilities import Utilities
+from deployment_manager import Deployment_Manager
+from database import HostDataBase
+from installers import fuel
+from installers import joid
+
+
+class Pod_Manager:
+ """
+ This is the 'main' class that chooses a host and provisions & deploys it.
+ this class can be run directly from the command line,
+ or it can be called from the pharos dashboard listener when
+ a deployment is requested.
+ Either way, this file should be called with:
+ ./pod_manager.py --config <CONFIG_FILE>
+ """
+ # This dictionary allows me to map the supported installers to the
+ # respective installer classes, for easier parsing of the config file
+ INSTALLERS = {
+ "fuel": fuel.Fuel_Installer,
+ "joid": joid.Joid_Installer,
+ "none": None
+ }
+
+ def __init__(self, conf, requested_host=None, reset=False):
+ """
+ init function.
+ conf is the read and parsed config file for this deployment
+ requested_host is the optional hostname of the host you request
+ if reset, we just flash the host to a clean state and return.
+ """
+ self.conf = conf
+ if self.conf['installer'] is not None:
+ inst = Pod_Manager.INSTALLERS[self.conf['installer'].lower()]
+ self.conf['installer'] = inst
+ self.fog = FOG_Handler(self.conf['fog']['server'])
+ # Sets the fog keys, either from the config file
+ # or the secrets file the config points to
+ if os.path.isfile(self.conf['fog']['api_key']):
+ self.fog.getFogKeyFromFile(self.conf['fog']['api_key'])
+ else:
+ self.fog.setFogKey(self.conf['fog']['api_key'])
+
+ if os.path.isfile(self.conf['fog']['user_key']):
+ self.fog.getUserKeyFromFile(self.conf['fog']['user_key'])
+ else:
+ self.fog.setUserKey(self.conf['fog']['user_key'])
+ self.database = HostDataBase(self.conf['database'])
+ self.request = requested_host
+ if reset:
+ mac = self.fog.getHostMac(self.request)
+ log = self.conf['dhcp_log']
+ dhcp_serv = self.conf['dhcp_server']
+ ip = Utilities.getIPfromMAC(mac, log, remote=dhcp_serv)
+ self.flash_host(self.request, ip)
+
+ def start_deploy(self):
+ """
+ Ghosts the machine with the proper disk image and hands off
+ control to the deployment manager.
+ """
+ try:
+ host = self.database.getHost(self.request)
+ hostMac = self.fog.getHostMac(host)
+ dhcp_log = self.conf['dhcp_log']
+ dhcp_server = self.conf['dhcp_server']
+ host_ip = Utilities.getIPfromMAC(
+ hostMac, dhcp_log, remote=dhcp_server
+ )
+ util = Utilities(host_ip, host, self.conf)
+ util.resetKnownHosts()
+ log = Utilities.createLogger(host, self.conf['logging_dir'])
+ self.fog.setLogger(log)
+ log.info("Starting booking on host %s", host)
+ log.info("host is reachable at %s", host_ip)
+ log.info('ghosting host %s with clean image', host)
+ self.flash_host(host, host_ip, util)
+ log.info('Host %s imaging complete', host)
+ inst = self.conf['installer']
+ scenario = self.conf['scenario']
+ Deployment_Manager(inst, scenario, util).go()
+ except Exception:
+ log.exception("Encountered an unexpected error")
+
+ def flash_host(self, host, host_ip, util=None):
+ """
+ We do this using a FOG server, but you can use whatever fits into your
+ lab infrastructure. This method should put the host into a state as if
+ centos was just freshly installed, updated,
+ and needed virtualization software installed.
+ This is the 'clean' starting point we work from
+ """
+ self.fog.setImage(host, self.conf['fog']['image_id'])
+ self.fog.imageHost(host)
+ Utilities.restartRemoteHost(host_ip)
+ self.fog.waitForHost(host)
+ # if util is not given, then we are just
+ # flashing to reset after a booking expires
+ if util is not None:
+ time.sleep(30)
+ util.waitForBoot()
+ util.checkHost()
+ time.sleep(15)
+ util.checkHost()
+
+
+if __name__ == "__main__":
+ configFile = ""
+ host = ""
+ for i in range(len(sys.argv) - 1):
+ if "--config" in sys.argv[i]:
+ configFile = sys.argv[i+1]
+ elif "--host" in sys.argv[i]:
+ host = sys.argv[i+1]
+ if len(configFile) < 1:
+ print "No config file specified"
+ sys.exit(1)
+ configFile = yaml.safe_load(open(configFile))
+ manager = Pod_Manager(configFile, requested_host=host)
+ manager.start_deploy()
diff --git a/laas-fog/source/utilities.py b/laas-fog/source/utilities.py
new file mode 100644
index 0000000..bbe0946
--- /dev/null
+++ b/laas-fog/source/utilities.py
@@ -0,0 +1,346 @@
+"""
+#############################################################################
+#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 os
+import logging
+import string
+import sys
+import subprocess
+import xml.dom
+import xml.dom.minidom
+import re
+import random
+import yaml
+from database import HostDataBase, BookingDataBase
+from api.vpn import VPN
+LOGGING_DIR = ""
+
+
+class Utilities:
+ """
+ This class defines some useful functions that may be needed
+ throughout the provisioning and deployment stage.
+ The utility object is carried through most of the deployment process.
+ """
+ def __init__(self, host_ip, hostname, conf):
+ """
+ init function
+ host_ip is the ip of the target host
+ hostname is the FOG hostname of the host
+ conf is the parsed config file
+ """
+ self.host = host_ip
+ self.hostname = hostname
+ root_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
+ self.scripts = os.path.join(root_dir, "hostScripts/")
+ self.remoteDir = "/root/hostScripts/"
+ self.conf = conf
+ self.logger = logging.getLogger(hostname)
+
+ def execRemoteScript(self, script, args=[]):
+ """
+ executes the given script on the
+ remote host with the given args.
+ script must be found in laas/hostScripts
+ """
+ cmd = [self.remoteDir+script]
+ for arg in args:
+ cmd.append(arg)
+ self.sshExec(cmd)
+
+ def waitForBoot(self):
+ """
+ Continually pings the host, waiting for it to boot
+ """
+ i = 0
+ while (not self.pingHost()) and i < 30:
+ i += 1
+ if i == 30:
+ self.logger.error("Host %s has not booted", self.host)
+ sys.exit(1)
+
+ def checkHost(self):
+ """
+ returns true if the host responds to two pings.
+ Sometimes, while a host is pxe booting, a host will
+ respond to one ping but quickly go back offline.
+ """
+ if self.pingHost() and self.pingHost():
+ return True
+ return False
+
+ def pingHost(self):
+ """
+ returns true if the host responds to a ping
+ """
+ i = 0
+ response = 1
+ cmd = "ping -c 1 "+self.host
+ cmd = cmd.split(' ')
+ nul = open(os.devnull, 'w')
+ while i < 10 and response != 0:
+ response = subprocess.call(cmd, stdout=nul, stderr=nul)
+ i = i + 1
+ if response == 0:
+ return True
+ return False
+
+ def copyDir(self, localDir, remoteDir):
+ """
+ uses scp to copy localDir to remoteDir on the
+ remote host
+ """
+ cmd = "mkdir -p "+remoteDir
+ self.sshExec(cmd.split(" "))
+ cmd = "scp -o StrictHostKeyChecking=no -r "
+ cmd += localDir+" root@"+self.host+":/root"
+ cmd = cmd.split()
+ nul = open(os.devnull, 'w')
+ subprocess.call(cmd, stdout=nul, stderr=nul)
+
+ def copyScripts(self):
+ """
+ Copies the hostScrpts dir to the remote host.
+ """
+ self.copyDir(self.scripts, self.remoteDir)
+
+ def sshExec(self, args):
+ """
+ executes args as an ssh
+ command on the remote host.
+ """
+ cmd = ['ssh', 'root@'+self.host]
+ for arg in args:
+ cmd.append(arg)
+ nul = open(os.devnull, 'w')
+ return subprocess.call(cmd, stdout=nul, stderr=nul)
+
+ def resetKnownHosts(self):
+ """
+ edits your known hosts file to remove the previous entry of host
+ Sometimes, the flashing process gives the remote host a new
+ signature, and ssh complains about it.
+ """
+ lines = []
+ sshFile = open('/root/.ssh/known_hosts', 'r')
+ lines = sshFile.read()
+ sshFile.close()
+ lines = lines.split('\n')
+ sshFile = open('/root/.ssh/known_hosts', 'w')
+ for line in lines:
+ if self.host not in line:
+ sshFile.write(line+'\n')
+ sshFile.close()
+
+ def restartHost(self):
+ """
+ restarts the remote host
+ """
+ cmd = ['shutdown', '-r', 'now']
+ self.sshExec(cmd)
+
+ @staticmethod
+ def randoString(length):
+ """
+ this is an adapted version of the code found here:
+ https://stackoverflow.com/questions/2257441/
+ random-string-generation-with-upper-case-letters-and-digits-in-python
+ generates a random alphanumeric string of length length.
+ """
+ randStr = ''
+ chars = string.ascii_uppercase + string.digits
+ for x in range(length):
+ randStr += random.SystemRandom().choice(chars)
+ return randStr
+
+ def changePassword(self):
+ """
+ Sets the root password to a random string and returns it
+ """
+ paswd = self.randoString(15)
+ command = "printf "+paswd+" | passwd --stdin root"
+ self.sshExec(command.split(' '))
+ return paswd
+
+ def markHostDeployed(self):
+ """
+ Tells the database that this host has finished its deployment
+ """
+ db = HostDataBase(self.conf['database'])
+ db.makeHostDeployed(self.hostname)
+ db.close()
+
+ def make_vpn_user(self):
+ """
+ Creates a vpn user and associates it with this booking
+ """
+ config = yaml.safe_load(open(self.conf['vpn_config']))
+ myVpn = VPN(config)
+ # name = dashboard.getUserName()
+ u, p, uid = myVpn.makeNewUser() # may pass name arg if wanted
+ self.logger.info("%s", "created new vpn user")
+ self.logger.info("username: %s", u)
+ self.logger.info("password: %s", p)
+ self.logger.info("vpn user uid: %s", uid)
+ self.add_vpn_user(uid)
+
+ def add_vpn_user(self, uid):
+ """
+ Adds the dn of the vpn user to the database
+ so that we can clean it once the booking ends
+ """
+ db = BookingDataBase(self.conf['database'])
+ # converts from hostname to pharos resource id
+ inventory = yaml.safe_load(open(self.conf['inventory']))
+ host_id = -1
+ for resource_id in inventory.keys():
+ if inventory[resource_id] == self.hostname:
+ host_id = resource_id
+ break
+ db.setVPN(host_id, uid)
+
+ def finishDeployment(self):
+ """
+ Last method call once a host is finished being deployed.
+ It notifies the database and changes the password to
+ a random string
+ """
+ self.markHostDeployed()
+ self.make_vpn_user()
+ passwd = self.changePassword()
+ self.logger.info("host %s provisioning done", self.hostname)
+ self.logger.info("You may access the host at %s", self.host)
+ self.logger.info("The password is %s", passwd)
+ notice = "You should change all passwords for security"
+ self.logger.warning('%s', notice)
+
+ @staticmethod
+ def restartRemoteHost(host_ip):
+ """
+ This method assumes that you already have ssh access to the target
+ """
+ nul = open(os.devnull, 'w')
+ ret_code = subprocess.call([
+ 'ssh', '-o', 'StrictHostKeyChecking=no',
+ 'root@'+host_ip,
+ 'shutdown', '-r', 'now'],
+ stdout=nul, stderr=nul)
+
+ return ret_code
+
+ @staticmethod
+ def getName(xmlString):
+ """
+ Gets the name value from xml. for example:
+ <name>Parker</name> returns Parker
+ """
+ xmlDoc = xml.dom.minidom.parseString(xmlString)
+ nameNode = xmlDoc.documentElement.getElementsByTagName('name')
+ name = str(nameNode[0].firstChild.nodeValue)
+ return name
+
+ @staticmethod
+ def getXMLFiles(directory):
+ """
+ searches directory non-recursively and
+ returns a list of all xml files
+ """
+ contents = os.listdir(directory)
+ fileContents = []
+ for item in contents:
+ if os.path.isfile(os.path.join(directory, item)):
+ fileContents.append(os.path.join(directory, item))
+ xmlFiles = []
+ for item in fileContents:
+ if 'xml' in os.path.basename(item):
+ xmlFiles.append(item)
+ return xmlFiles
+
+ @staticmethod
+ def createLogger(name, log_dir=LOGGING_DIR):
+ """
+ Initializes the logger if it does not yet exist, and returns it.
+ Because of how python logging works, calling logging.getLogger()
+ with the same name always returns a reference to the same log file.
+ So we can call this method from anywhere with the hostname as
+ the name arguement and it will return the log file for that host.
+ The formatting includes the level of importance and the time stamp
+ """
+ global LOGGING_DIR
+ if log_dir != LOGGING_DIR:
+ LOGGING_DIR = log_dir
+ log = logging.getLogger(name)
+ if len(log.handlers) > 0: # if this logger is already initialized
+ return log
+ log.setLevel(10)
+ han = logging.FileHandler(os.path.join(log_dir, name+".log"))
+ han.setLevel(10)
+ log_format = '[%(levelname)s] %(asctime)s [#] %(message)s'
+ formatter = logging.Formatter(fmt=log_format)
+ han.setFormatter(formatter)
+ log.addHandler(han)
+ return log
+
+ @staticmethod
+ def getIPfromMAC(macAddr, logFile, remote=None):
+ """
+ searches through the dhcp logs for the given mac
+ and returns the associated ip. Will retrieve the
+ logFile from a remote host if remote is given.
+ if given, remote should be an ip address or hostname that
+ we can ssh to.
+ """
+ if remote is not None:
+ logFile = Utilities.retrieveFile(remote, logFile)
+ ip = Utilities.getIPfromLog(macAddr, logFile)
+ if remote is not None:
+ os.remove(logFile)
+ return ip
+
+ @staticmethod
+ def retrieveFile(host, remote_loc, local_loc=os.getcwd()):
+ """
+ Retrieves file from host and puts it in the current directory
+ unless local_loc is given.
+ """
+ subprocess.call(['scp', 'root@'+host+':'+remote_loc, local_loc])
+ return os.path.join(local_loc, os.path.basename(remote_loc))
+
+ @staticmethod
+ def getIPfromLog(macAddr, logFile):
+ """
+ Helper method for getIPfromMAC.
+ uses regex to find the ip address in the
+ log
+ """
+ try:
+ messagesFile = open(logFile, "r")
+ allLines = messagesFile.readlines()
+ except Exception:
+ sys.exit(1)
+ importantLines = []
+ for line in allLines:
+ if macAddr in line and "DHCPACK" in line:
+ importantLines.append(line)
+ ipRegex = r'(\d+\.\d+\.\d+\.\d+)'
+ IPs = []
+ for line in importantLines:
+ IPs.append(re.findall(ipRegex, line))
+ if len(IPs) > 0 and len(IPs[-1]) > 0:
+ return IPs[-1][0]
+ return None