diff options
Diffstat (limited to 'tools/laas-fog/source/api')
-rw-r--r-- | tools/laas-fog/source/api/__init__.py | 17 | ||||
-rw-r--r-- | tools/laas-fog/source/api/fog.py | 288 | ||||
-rw-r--r-- | tools/laas-fog/source/api/fuel_api.py | 306 | ||||
-rw-r--r-- | tools/laas-fog/source/api/libvirt_api.py | 331 | ||||
-rw-r--r-- | tools/laas-fog/source/api/vpn.py | 235 |
5 files changed, 1177 insertions, 0 deletions
diff --git a/tools/laas-fog/source/api/__init__.py b/tools/laas-fog/source/api/__init__.py new file mode 100644 index 00000000..7bb515b7 --- /dev/null +++ b/tools/laas-fog/source/api/__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/api/fog.py b/tools/laas-fog/source/api/fog.py new file mode 100644 index 00000000..62874039 --- /dev/null +++ b/tools/laas-fog/source/api/fog.py @@ -0,0 +1,288 @@ +""" +############################################################################# +#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 sys +import time + + +class FOG_Handler: + """ + This class talks with the REST web api for the FOG server. + + TODO: convert prints to logs and remove uneeded pass's + """ + + def __init__(self, baseURL, fogKey=None, userKey=None): + """ + init function + baseURL should be http://fog.ip.or.hostname/fog/ + fogKey and userKey can optionally be supplied here or later + They can be found in fog and provide authentication. + """ + self.baseURL = baseURL + self.fogKey = fogKey + self.userKey = userKey + self.header = {} + self.updateHeader() + + def setLogger(self, logger): + """ + saves the refference to the log object as + self.log + """ + self.log = logger + + def getUserKeyFromFile(self, path): + """ + reads the user api key from a file + """ + self.userKey = open(path).read() + self.updateHeader() + + def getFogKeyFromFile(self, path): + """ + reads the api key from a file + """ + self.fogKey = open(path).read() + self.updateHeader() + + def setUserKey(self, key): + """ + sets the user key + """ + self.userKey = key + self.updateHeader() + + def setFogKey(self, key): + """ + sets the fog key + """ + self.fogKey = key + self.updateHeader() + + def updateHeader(self): + """ + recreates the http header used to talk to the fog api + """ + self.header = {} + self.header['fog-api-token'] = self.fogKey + self.header['fog-user-token'] = self.userKey + + def setImage(self, host, imgNum): + """ + Sets the image to be used during ghosting to the image + with id imgNum. host can either be a hostname or number. + """ + try: + host = int(host) + except: + host = self.getHostNumber(host) + url = self.baseURL+"host/"+str(host) + host_conf = requests.get(url, headers=self.header).json() + host_conf['imageID'] = str(imgNum) + requests.put(url+"/edit", headers=self.header, json=host_conf) + + def delTask(self, hostNum): + """ + Tries to delete an existing task for the host + with hostNum as a host number + """ + try: + url = self.baseURL+'fog/host/'+str(hostNum)+'/cancel' + req = requests.delete(url, headers=self.header) + if req.status_code == 200: + self.log.info("%s", "successfully deleted image task") + except Exception: + self.log.exception("Failed to delete the imaging task!") + + def getHostMac(self, hostname): + """ + returns the primary mac address if the given host. + """ + try: + hostNum = int(self.getHostNumber(hostname)) + url = self.baseURL + "host/"+str(hostNum) + req = requests.get(url, headers=self.header) + macAddr = req.json()['primac'] + return macAddr + except Exception: + self.log.exception('%s', "Failed to connect to the FOG server") + + def getHostNumber(self, hostname): + """ + returns the host number of given host + """ + try: + req = requests.get(self.baseURL+"host", headers=self.header) + hostData = req.json() + if hostData is not None: + for hostDict in hostData['hosts']: + if hostname == hostDict['name']: + return hostDict['id'] + return -1 + except Exception: + self.log.exception('%s', "Failed to connect to the FOG server") + + def imageHost(self, hostName, recurse=False): + """ + Schedules an imaging task for the given host. + This automatically uses the "associated" disk image. + To support extra installers, I will need to create + a way to change what that image is before calling + this method. + """ + num = str(self.getHostNumber(hostName)) + url = self.baseURL+'host/'+num+'/task' + + try: + req = requests.post( + url, + headers=self.header, + json={"taskTypeID": 1} + ) + if req.status_code == 200: + self.log.info("%s", "Scheduled image task for host") + except Exception: + if recurse: # prevents infinite loop + self.log.exception("%s", "Failed to schedule task. Exiting") + sys.exit(1) + self.log.warning("%s", "Failed to schedule host imaging") + self.log.warning("%s", "Trying to delete existing image task") + self.delTask(num) + self.imageHost(num, recurse=True) + + def waitForHost(self, host): + """ + tracks the imaging task to completion. + """ + while True: + imageTask = self.getImagingTask(host) + if imageTask is None: + self.log.info("%s", "Imaging complete") + return + state = int(imageTask['stateID']) + if state == 1: + self.log.info("%s", "Waiting for host to check in") + self.waitForTaskToActive(host) + continue + if state == 3: + self.waitForTaskToStart(host) + self.waitForImaging(host) + continue + time.sleep(8) + + def waitForImaging(self, host): + """ + Once the host begins being imaged, this tracks progress. + """ + # print "Host has begun the imaging process\n" + while True: + task = self.getImagingTask(host) + if task is None: + return + per = str(task['percent']) + self.log.info("%s percent done imaging", per) + time.sleep(15) + + def waitForTaskToActive(self, host): + """ + Waits for the host to reboot and pxe boot + into FOG + """ + while True: + try: + task = self.getImagingTask(host) + except: + pass + state = int(task['stateID']) + if state == 1: + time.sleep(4) + else: + return + + def waitForTaskToStart(self, host): + """ + waits for the task to start and imaging to begin. + """ + while True: + try: + per = str(self.getImagingTask(host)['percent']) + except: + pass + if per.strip() == '': + time.sleep(1) + else: + return + + def getImagingTask(self, host): + """ + Sorts through all current tasks to find the image task + associated with the given host. + """ + try: + taskList = requests.get( + self.baseURL+'task/current', + headers=self.header) + taskList = taskList.json()['tasks'] + imageTask = None + for task in taskList: + hostname = str(task['host']['name']) + if hostname == host and int(task['typeID']) == 1: + imageTask = task + return imageTask + except Exception: + self.log.exception("%s", "Failed to talk to FOG server") + sys.exit(1) + + def getHosts(self): + """ + returns a list of all hosts + """ + req = requests.get(self.baseURL+"host", headers=self.header) + return req.json()['hosts'] + + def getHostsinGroup(self, groupName): + """ + returns a list of all hosts in groupName + """ + groupID = None + groups = requests.get(self.baseURL+"group", headers=self.header) + groups = groups.json()['groups'] + for group in groups: + if groupName.lower() in group['name'].lower(): + groupID = group['id'] + if groupID is None: + return + hostIDs = [] + associations = requests.get( + self.baseURL+"groupassociation", + headers=self.header + ) + associations = associations.json()['groupassociations'] + for association in associations: + if association['groupID'] == groupID: + hostIDs.append(association['hostID']) + + hosts = [] + for hostID in hostIDs: + hosts.append(requests.get( + self.baseURL+"host/"+str(hostID), + headers=self.header + ).json()) + return hosts 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/api/libvirt_api.py b/tools/laas-fog/source/api/libvirt_api.py new file mode 100644 index 00000000..4e19736f --- /dev/null +++ b/tools/laas-fog/source/api/libvirt_api.py @@ -0,0 +1,331 @@ +""" +############################################################################# +#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 libvirt +import time +import xml.dom +import xml.dom.minidom +from domain import Domain +from network import Network +from utilities import Utilities + + +class Libvirt: + """ + This class talks to the Libvirt api. + Given a config file, this class should create all networks and + domains. + + TODO: convert prints to logging and remove uneeded pass statements + """ + + def __init__(self, hostAddr, net_conf=None, dom_conf=None): + """ + init function + hostAddr is the ip address of the host + net_conf and dom_conf are the paths + to the config files + """ + self.host = hostAddr + self.URI = "qemu+ssh://root@"+str(hostAddr)+"/system" + self.hypervisor = None + self.domains = [] + self.networks = [] + self.net_conf = net_conf + self.dom_conf = dom_conf + + def setLogger(self, log): + """ + Saves the logger in self.log + """ + self.log = log + + def bootMaster(self): + """ + starts the previously defined master node + """ + for dom in self.domains: + if 'master' in dom.name(): + try: + dom.create() + except Exception: + pass + + def bootSlaves(self): + """ + boots every defined vm with 'slave' in its name + """ + for dom in self.domains: + if 'slave' in dom.name(): + try: + dom.create() + self.log.info("Booting %s", dom.name()) + except Exception: + self.log.exception("%s", "failed to boot domain") + time.sleep(5) + + def getMacs(self, domName): + """ + returns a dictionary with a network name + mapped to the mac address of the domain on that net + """ + try: + dom = self.hypervisor.lookupByName(domName) + xmlDesc = dom.XMLDesc(0) + parsedXML = xml.dom.minidom.parseString(xmlDesc) + interfacesXML = parsedXML.getElementsByTagName('interface') + netDict = {} + for iface in interfacesXML: + src = iface.getElementsByTagName('source')[0] + mac = iface.getElementsByTagName('mac')[0] + netDict[src] = mac + return netDict + except Exception: + self.log.exception("%s", "Domain not found") + + def defineVM(self, xmlConfig): + """ + Generic method to define a persistent vm with the + given config. + Assumes that self.hypervisor is already connected. + """ + if self.checkForVM(xmlConfig): + vm = self.hypervisor.defineXML(xmlConfig) + if vm is None: + name = self.getName(xmlConfig) + self.log.error("Failed to define vm %s. exiting", name) + exit(1) + else: + self.log.info("Successfully created vm %s", vm.name()) + pass + self.domains.append(vm) + + def checkForVM(self, xmlConfig): + """ + Checks if another vm with the same name exists + on the remote host already. If it does, it will + delete that vm + """ + allGood = False + vms = self.hypervisor.listAllDomains(0) + names = [] + for dom in vms: + names.append(dom.name()) + vmName = Utilities.getName(xmlConfig) + if vmName in names: + self.log.warning("domain %s already exists", vmName) + self.log.warning("%s", "Atempting to delete it") + self.deleteVM(vmName) + allGood = True + else: + allGood = True + return allGood + + def deleteVM(self, name): + """ + removes the given vm from the remote host + """ + try: + vm = self.hypervisor.lookupByName(name) + except: + return + active = vm.isActive() + persistent = vm.isPersistent() + if active: + try: + vm.destroy() + except: + self.log.exception("%s", "Failed to destroy vm") + + if persistent: + try: + vm.undefine() + except: + self.log.exception("%s", "Failed to undefine domain") + pass + + def openConnection(self): + """ + opens a connection to the remote host + and stores it in self.hypervisor + """ + self.log.info("Attempting to connect to libvirt at %s", self.host) + try: + hostHypervisor = libvirt.open(self.URI) + except: + self.log.warning( + "Failed to connect to %s. Trying again", self.host + ) + time.sleep(5) + try: + hostHypervisor = libvirt.open(self.URI) + except: + self.log.exception("Cannot connect to %s. Exiting", self.host) + exit(1) + + if hostHypervisor is None: + self.log.error("Failed to connect to %s. Exiting", self.host) + exit(1) + self.hypervisor = hostHypervisor + + def restartVM(self, vm): + """ + causes the given vm to reboot + """ + dom = self.hypervisor.lookupByName(vm) + dom.destroy() + time.sleep(15) + dom.create() + + def close(self): + """ + Closes connection to remote hypervisor + """ + self.log.info("Closing connection to the hypervisor %s", self.host) + self.hypervisor.close() + + def defineAllDomains(self, path): + """ + Defines a domain from all the xml files in a directory + """ + files = Utilities.getXMLFiles(path) + definitions = [] + for xml_desc in files: + definitions.append(xml_desc.read()) + + for definition in definitions: + self.defineVM(definition) + + def createAllNetworks(self, path): + """ + Creates a network from all xml files in a directory + """ + files = Utilities.getXMLFiles(path) + definitions = [] + for xml_desc in files: + definitions.append(Utilities.fileToString(xml_desc)) + + for definition in definitions: + self.createNet(definition) + + def createNet(self, config): + """ + creates the network on the remote host + config is the xml in string representation + that defines the network + """ + if self.checkNet(config): + network = self.hypervisor.networkDefineXML(config) + + if network is None: + name = self.getName(config) + self.log.warning("Failed to define network %s", name) + network.create() + if network.isActive() == 1: + net = network.name() + self.log.info("Successfully defined network %s", net) + self.networks.append(network) + + def checkNet(self, config): + """ + Checks if another net with the same name exists, and + deletes that network if one is found + """ + allGood = False + netName = Utilities.getName(config) + if netName not in self.hypervisor.listNetworks(): + return True + else: # net name is already used + self.log.warning( + "Network %s already exists. Trying to delete it", netName + ) + network = self.hypervisor.networkLookupByName(netName) + self.deleteNet(network) + allGood = True + return allGood + + def deleteNet(self, net): + """ + removes the given network from the host + """ + active = net.isActive() + persistent = net.isPersistent() + if active: + try: + net.destroy() + except: + self.log.warning("%s", "Failed to destroy network") + + if persistent: + try: + net.undefine() + except: + self.log.warning("%s", "Failed to undefine network") + + def go(self): + """ + This method does all the work of this class, + Parsing the net and vm config files and creating + all the requested nets/domains + returns a list of all networks and a list of all domains + as Network and Domain objects + """ + nets = self.makeNetworks(self.net_conf) + doms = self.makeDomains(self.dom_conf) + return doms, nets + + def makeNetworks(self, conf): + """ + Given a path to a config file, this method + parses the config and creates all requested networks, + and returns them in a list of Network objects + """ + networks = [] + definitions = Network.parseConfigFile(conf) + for definition in definitions: + network = Network(definition) + networks.append(network) + self.createNet(network.toXML()) + return networks + + def makeDomains(self, conf): + """ + Given a path to a config file, this method + parses the config and creates all requested vm's, + and returns them in a list of Domain objects + """ + domains = [] + definitions = Domain.parseConfigFile(conf) + for definition in definitions: + domain = Domain(definition) + domains.append(domain) + self.defineVM(domain.toXML()) + return domains + + @staticmethod + def getName(xmlString): + """ + given xml with a name tag, this returns the value of name + eg: + <name>Parker</name> + returns 'Parker' + """ + xmlDoc = xml.dom.minidom.parseString(xmlString) + nameNode = xmlDoc.documentElement.getElementsByTagName('name') + name = str(nameNode[0].firstChild.nodeValue) + return name diff --git a/tools/laas-fog/source/api/vpn.py b/tools/laas-fog/source/api/vpn.py new file mode 100644 index 00000000..336a681d --- /dev/null +++ b/tools/laas-fog/source/api/vpn.py @@ -0,0 +1,235 @@ +from abc import ABCMeta, abstractmethod +import ldap +import os +import random +from base64 import b64encode +from database import BookingDataBase + + +class VPN_BaseClass: + """ + the vpn handler abstract class / interface + + """ + __metaclass__ = ABCMeta + + @abstractmethod + def __init__(self, config): + """ + config is the parsed vpn.yaml file + """ + pass + + @abstractmethod + def makeNewUser(self, user=None): + """ + This method is called when a vpn user is needed. + This method should create a vpn user in whatever + runs the vpn in our infrastructure. returns the + credentials for the vpn user and some uid + that will be associated with the booking in the + database. This uid is used to track the vpn user and + to delete the user when there are no bookings associated + with that uid. + """ + user = "username" + passwd = "password" + uid = "some way for you to identify this user in the database" + return user, passwd, uid + + @abstractmethod + def removeOldUsers(self): + """ + checks the list of all vpn users against a list of + vpn users associated with active bookings and removes + users who dont have an active booking + + If you want your vpn accounts to be persistent, + you can just ignore this + """ + pass + + +names = [ + 'frodo baggins', 'samwise gamgee', 'peregrin took', 'meriadoc brandybuck', + 'bilbo baggins', 'gandalf grey', 'aragorn dunadan', 'arwen evenstar', + 'saruman white', 'pippin took', 'merry brandybuck', 'legolas greenleaf', + 'gimli gloin', 'anakin skywalker', 'padme amidala', 'han solo', + 'jabba hut', 'mace windu', 'sount dooku', 'qui-gon jinn', + 'admiral ackbar', 'emperor palpatine' +] + + +class VPN: + """ + This class communicates with the ldap server to manage vpn users. + This class extends the above ABC, and implements the makeNewUser, + removeOldUser, and __init__ abstract functions you must override to + extend the VPN_BaseClass + """ + + def __init__(self, config): + """ + init takes the parsed vpn config file as an arguement. + automatically connects and authenticates on the ldap server + based on the configuration file + """ + self.config = config + server = config['server'] + self.uri = "ldap://"+server + + self.conn = None + user = config['authentication']['user'] + pswd = config['authentication']['pass'] + if os.path.isfile(pswd): + pswd = open(pswd).read() + self.connect(user, pswd) + + def connect(self, root_dn, root_pass): + """ + Opens a connection to the server in the config file + and authenticates as the given user + """ + self.conn = ldap.initialize(self.uri) + self.conn.simple_bind_s(root_dn, root_pass) + + def addUser(self, full_name, passwd): + """ + Adds a user to the ldap server. Creates the new user with the classes + and in the directory given in the config file. + full_name should be two tokens seperated by a space. The first token + will become the username + private helper function for the makeNewUser() + """ + first = full_name.split(' ')[0] + last = full_name.split(' ')[1] + user_dir = self.config['directory']['user'] + user_dir += ','+self.config['directory']['root'] + dn = "uid=" + first + ',' + user_dir + record = [ + ('objectclass', ['top', 'inetOrgPerson']), + ('uid', first), + ('cn', full_name), + ('sn', last), + ('userpassword', passwd), + ('ou', self.config['directory']['user'].split('=')[1]) + ] + self.conn.add_s(dn, record) + return dn + + def makeNewUser(self, name=None): + """ + creates a new user in the ldap database, with the given name + if supplied. If no name is given, we will try to select from the + pre-written list above, and will resort to generating a random string + as a username if the preconfigured names are all taken. + Returns the username and password the user needs to authenticate, and + the dn that we can use to manage the user. + """ + if name is None: + i = 0 + while not self.checkName(name): + i += 1 + if i == 20: + name = self.randoString(8) + name += ' '+self.randoString(8) + break # generates a random name to prevent infinite loop + name = self.genUserName() + passwd = self.randoString(15) + dn = self.addUser(name, passwd) + return name, passwd, dn + + def checkName(self, name): + """ + returns true if the name is available + """ + if name is None: + return False + uid = name.split(' ')[0] + base = self.config['directory']['user'] + ',' + base += self.config['directory']['root'] + filtr = '(uid=' + uid + ')' + timeout = 5 + ans = self.conn.search_st( + base, + ldap.SCOPE_SUBTREE, + filtr, + timeout=timeout + ) + return len(ans) < 1 + + @staticmethod + def randoString(n): + """ + uses /dev/urandom to generate a random string of length n + """ + n = int(n) + # defines valid characters + alpha = 'abcdefghijklmnopqrstuvwxyz' + alpha_num = alpha + alpha_num += alpha.upper() + alpha_num += "0123456789" + + # generates random string from /dev/urandom + rnd = b64encode(os.urandom(3*n)).decode('utf-8') + random_string = '' + for char in rnd: + if char in alpha_num: + random_string += char + return str(random_string[:n]) + + def genUserName(self): + """ + grabs a random name from the list above + """ + i = random.randint(0, len(names) - 1) + return names[i] + + def deleteUser(self, dn): + self.conn.delete(dn) + + def getAllUsers(self): + """ + returns all the user dn's in the ldap database in a list + """ + base = self.config['directory']['user'] + ',' + base += self.config['directory']['root'] + filtr = '(objectclass='+self.config['user']['objects'][-1]+')' + timeout = 10 + ans = self.conn.search_st( + base, + ldap.SCOPE_SUBTREE, + filtr, + timeout=timeout + ) + users = [] + for user in ans: + users.append(user[0]) # adds the dn of each user + return users + + def removeOldUsers(self): + """ + removes users from the ldap server who dont have any active bookings. + will not delete a user if their uid's are named in the config + file as permanent users. + """ + db = self.config['database'] + # the dn of all users who have an active booking + active_users = BookingDataBase(db).getVPN() + all_users = self.getAllUsers() + for user in all_users: + # checks if they are a permanent user + if self.is_permanent_user(user): + continue + # deletes the user if they dont have an active booking + if user not in active_users: + self.deleteUser(user) + + def is_permanent_user(self, dn): + for user in self.config['permanent_users']: + if (user in dn) or (dn in user): + return True + return False + + +VPN_BaseClass.register(VPN) |