LaaS Base functionality
[pharos-tools.git] / laas-fog / source / utilities.py
1 """
2 #############################################################################
3 #Copyright 2017 Parker Berberian and others                                 #
4 #                                                                           #
5 #Licensed under the Apache License, Version 2.0 (the "License");            #
6 #you may not use this file except in compliance with the License.           #
7 #You may obtain a copy of the License at                                    #
8 #                                                                           #
9 #    http://www.apache.org/licenses/LICENSE-2.0                             #
10 #                                                                           #
11 #Unless required by applicable law or agreed to in writing, software        #
12 #distributed under the License is distributed on an "AS IS" BASIS,          #
13 #WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.   #
14 #See the License for the specific language governing permissions and        #
15 #limitations under the License.                                             #
16 #############################################################################
17 """
18
19 import os
20 import logging
21 import string
22 import sys
23 import subprocess
24 import xml.dom
25 import xml.dom.minidom
26 import re
27 import random
28 import yaml
29 from database import HostDataBase, BookingDataBase
30 from api.vpn import VPN
31 LOGGING_DIR = ""
32
33
34 class Utilities:
35     """
36     This class defines some useful functions that may be needed
37     throughout the provisioning and deployment stage.
38     The utility object is carried through most of the deployment process.
39     """
40     def __init__(self, host_ip, hostname, conf):
41         """
42         init function
43         host_ip is the ip of the target host
44         hostname is the FOG hostname of the host
45         conf is the parsed config file
46         """
47         self.host = host_ip
48         self.hostname = hostname
49         root_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
50         self.scripts = os.path.join(root_dir, "hostScripts/")
51         self.remoteDir = "/root/hostScripts/"
52         self.conf = conf
53         self.logger = logging.getLogger(hostname)
54
55     def execRemoteScript(self, script, args=[]):
56         """
57         executes the given script on the
58         remote host with the given args.
59         script must be found in laas/hostScripts
60         """
61         cmd = [self.remoteDir+script]
62         for arg in args:
63             cmd.append(arg)
64         self.sshExec(cmd)
65
66     def waitForBoot(self):
67         """
68         Continually pings the host, waiting for it to boot
69         """
70         i = 0
71         while (not self.pingHost()) and i < 30:
72             i += 1
73         if i == 30:
74             self.logger.error("Host %s has not booted", self.host)
75             sys.exit(1)
76
77     def checkHost(self):
78         """
79         returns true if the host responds to two pings.
80         Sometimes, while a host is pxe booting, a host will
81         respond to one ping but quickly go back offline.
82         """
83         if self.pingHost() and self.pingHost():
84             return True
85         return False
86
87     def pingHost(self):
88         """
89         returns true if the host responds to a ping
90         """
91         i = 0
92         response = 1
93         cmd = "ping -c 1 "+self.host
94         cmd = cmd.split(' ')
95         nul = open(os.devnull, 'w')
96         while i < 10 and response != 0:
97             response = subprocess.call(cmd, stdout=nul, stderr=nul)
98             i = i + 1
99         if response == 0:
100             return True
101         return False
102
103     def copyDir(self, localDir, remoteDir):
104         """
105         uses scp to copy localDir to remoteDir on the
106         remote host
107         """
108         cmd = "mkdir -p "+remoteDir
109         self.sshExec(cmd.split(" "))
110         cmd = "scp -o StrictHostKeyChecking=no -r "
111         cmd += localDir+" root@"+self.host+":/root"
112         cmd = cmd.split()
113         nul = open(os.devnull, 'w')
114         subprocess.call(cmd, stdout=nul, stderr=nul)
115
116     def copyScripts(self):
117         """
118         Copies the hostScrpts dir to the remote host.
119         """
120         self.copyDir(self.scripts, self.remoteDir)
121
122     def sshExec(self, args):
123         """
124         executes args as an ssh
125         command on the remote host.
126         """
127         cmd = ['ssh', 'root@'+self.host]
128         for arg in args:
129             cmd.append(arg)
130         nul = open(os.devnull, 'w')
131         return subprocess.call(cmd, stdout=nul, stderr=nul)
132
133     def resetKnownHosts(self):
134         """
135         edits your known hosts file to remove the previous entry of host
136         Sometimes, the flashing process gives the remote host a new
137         signature, and ssh complains about it.
138         """
139         lines = []
140         sshFile = open('/root/.ssh/known_hosts', 'r')
141         lines = sshFile.read()
142         sshFile.close()
143         lines = lines.split('\n')
144         sshFile = open('/root/.ssh/known_hosts', 'w')
145         for line in lines:
146             if self.host not in line:
147                 sshFile.write(line+'\n')
148         sshFile.close()
149
150     def restartHost(self):
151         """
152         restarts the remote host
153         """
154         cmd = ['shutdown', '-r', 'now']
155         self.sshExec(cmd)
156
157     @staticmethod
158     def randoString(length):
159         """
160         this is an adapted version of the code found here:
161         https://stackoverflow.com/questions/2257441/
162         random-string-generation-with-upper-case-letters-and-digits-in-python
163         generates a random alphanumeric string of length length.
164         """
165         randStr = ''
166         chars = string.ascii_uppercase + string.digits
167         for x in range(length):
168             randStr += random.SystemRandom().choice(chars)
169         return randStr
170
171     def changePassword(self):
172         """
173         Sets the root password to a random string and returns it
174         """
175         paswd = self.randoString(15)
176         command = "printf "+paswd+" | passwd --stdin root"
177         self.sshExec(command.split(' '))
178         return paswd
179
180     def markHostDeployed(self):
181         """
182         Tells the database that this host has finished its deployment
183         """
184         db = HostDataBase(self.conf['database'])
185         db.makeHostDeployed(self.hostname)
186         db.close()
187
188     def make_vpn_user(self):
189         """
190         Creates a vpn user and associates it with this booking
191         """
192         config = yaml.safe_load(open(self.conf['vpn_config']))
193         myVpn = VPN(config)
194         # name = dashboard.getUserName()
195         u, p, uid = myVpn.makeNewUser()  # may pass name arg if wanted
196         self.logger.info("%s", "created new vpn user")
197         self.logger.info("username: %s", u)
198         self.logger.info("password: %s", p)
199         self.logger.info("vpn user uid: %s", uid)
200         self.add_vpn_user(uid)
201
202     def add_vpn_user(self, uid):
203         """
204         Adds the dn of the vpn user to the database
205         so that we can clean it once the booking ends
206         """
207         db = BookingDataBase(self.conf['database'])
208         # converts from hostname to pharos resource id
209         inventory = yaml.safe_load(open(self.conf['inventory']))
210         host_id = -1
211         for resource_id in inventory.keys():
212             if inventory[resource_id] == self.hostname:
213                 host_id = resource_id
214                 break
215         db.setVPN(host_id, uid)
216
217     def finishDeployment(self):
218         """
219         Last method call once a host is finished being deployed.
220         It notifies the database and changes the password to
221         a random string
222         """
223         self.markHostDeployed()
224         self.make_vpn_user()
225         passwd = self.changePassword()
226         self.logger.info("host %s provisioning done", self.hostname)
227         self.logger.info("You may access the host at %s", self.host)
228         self.logger.info("The password is %s", passwd)
229         notice = "You should change all passwords for security"
230         self.logger.warning('%s', notice)
231
232     @staticmethod
233     def restartRemoteHost(host_ip):
234         """
235         This method assumes that you already have ssh access to the target
236         """
237         nul = open(os.devnull, 'w')
238         ret_code = subprocess.call([
239             'ssh', '-o', 'StrictHostKeyChecking=no',
240             'root@'+host_ip,
241             'shutdown', '-r', 'now'],
242             stdout=nul, stderr=nul)
243
244         return ret_code
245
246     @staticmethod
247     def getName(xmlString):
248         """
249         Gets the name value from xml. for example:
250         <name>Parker</name> returns Parker
251         """
252         xmlDoc = xml.dom.minidom.parseString(xmlString)
253         nameNode = xmlDoc.documentElement.getElementsByTagName('name')
254         name = str(nameNode[0].firstChild.nodeValue)
255         return name
256
257     @staticmethod
258     def getXMLFiles(directory):
259         """
260         searches directory non-recursively and
261         returns a list of all xml files
262         """
263         contents = os.listdir(directory)
264         fileContents = []
265         for item in contents:
266             if os.path.isfile(os.path.join(directory, item)):
267                 fileContents.append(os.path.join(directory, item))
268         xmlFiles = []
269         for item in fileContents:
270             if 'xml' in os.path.basename(item):
271                 xmlFiles.append(item)
272         return xmlFiles
273
274     @staticmethod
275     def createLogger(name, log_dir=LOGGING_DIR):
276         """
277         Initializes the logger if it does not yet exist, and returns it.
278         Because of how python logging works, calling logging.getLogger()
279         with the same name always returns a reference to the same log file.
280         So we can call this method from anywhere with the hostname as
281         the name arguement and it will return the log file for that host.
282         The formatting includes the level of importance and the time stamp
283         """
284         global LOGGING_DIR
285         if log_dir != LOGGING_DIR:
286             LOGGING_DIR = log_dir
287         log = logging.getLogger(name)
288         if len(log.handlers) > 0:  # if this logger is already initialized
289             return log
290         log.setLevel(10)
291         han = logging.FileHandler(os.path.join(log_dir, name+".log"))
292         han.setLevel(10)
293         log_format = '[%(levelname)s] %(asctime)s [#] %(message)s'
294         formatter = logging.Formatter(fmt=log_format)
295         han.setFormatter(formatter)
296         log.addHandler(han)
297         return log
298
299     @staticmethod
300     def getIPfromMAC(macAddr, logFile, remote=None):
301         """
302         searches through the dhcp logs for the given mac
303         and returns the associated ip. Will retrieve the
304         logFile from a remote host if remote is given.
305         if given, remote should be an ip address or hostname that
306         we can ssh to.
307         """
308         if remote is not None:
309             logFile = Utilities.retrieveFile(remote, logFile)
310         ip = Utilities.getIPfromLog(macAddr, logFile)
311         if remote is not None:
312             os.remove(logFile)
313         return ip
314
315     @staticmethod
316     def retrieveFile(host, remote_loc, local_loc=os.getcwd()):
317         """
318         Retrieves file from host and puts it in the current directory
319         unless local_loc is given.
320         """
321         subprocess.call(['scp', 'root@'+host+':'+remote_loc, local_loc])
322         return os.path.join(local_loc, os.path.basename(remote_loc))
323
324     @staticmethod
325     def getIPfromLog(macAddr, logFile):
326         """
327         Helper method for getIPfromMAC.
328         uses regex to find the ip address in the
329         log
330         """
331         try:
332             messagesFile = open(logFile, "r")
333             allLines = messagesFile.readlines()
334         except Exception:
335             sys.exit(1)
336         importantLines = []
337         for line in allLines:
338             if macAddr in line and "DHCPACK" in line:
339                 importantLines.append(line)
340         ipRegex = r'(\d+\.\d+\.\d+\.\d+)'
341         IPs = []
342         for line in importantLines:
343             IPs.append(re.findall(ipRegex, line))
344         if len(IPs) > 0 and len(IPs[-1]) > 0:
345             return IPs[-1][0]
346         return None