a0da5d2d0180f02e9d5a24ab774f2de717962da4
[nodemanager.git] / tools.py
1 """A few things that didn't seem to fit anywhere else."""
2
3 import os, os.path
4 import pwd
5 import tempfile
6 import fcntl
7 import errno
8 import threading
9 import subprocess
10 import shutil
11 import sys
12 import signal
13
14 ###################################################
15 # Added by Guilherme Sperb Machado <gsm@machados.org>
16 ###################################################
17
18 import re
19 import socket
20 import fileinput
21
22 # TODO: is there anything better to do if the "libvirt", "sliver_libvirt",
23 # and "sliver_lxc" are not in place?
24 try:
25         import libvirt
26         from sliver_libvirt import Sliver_Libvirt
27         import sliver_lxc
28 except:
29     logger.log("Could not import sliver_lxc or libvirt or sliver_libvirt -- which is required here.")
30 ###################################################
31
32 import logger
33
34 PID_FILE = '/var/run/nodemanager.pid'
35
36 ####################
37 def get_default_if():
38     interface = get_if_from_hwaddr(get_hwaddr_from_plnode())
39     if not interface: interface = "eth0"
40     return interface
41
42 def get_hwaddr_from_plnode():
43     try:
44         for line in open("/usr/boot/plnode.txt", 'r').readlines():
45             if line.startswith("NET_DEVICE"):
46                 return line.split("=")[1].strip().strip('"')
47     except:
48         pass
49     return None
50
51 def get_if_from_hwaddr(hwaddr):
52     import sioc
53     devs = sioc.gifconf()
54     for dev in devs:
55         dev_hwaddr = sioc.gifhwaddr(dev)
56         if dev_hwaddr == hwaddr: return dev
57     return None
58
59 ####################
60 # daemonizing
61 def as_daemon_thread(run):
62     """Call function <run> with no arguments in its own thread."""
63     thr = threading.Thread(target=run)
64     thr.setDaemon(True)
65     thr.start()
66
67 def close_nonstandard_fds():
68     """Close all open file descriptors other than 0, 1, and 2."""
69     _SC_OPEN_MAX = 4
70     for fd in range(3, os.sysconf(_SC_OPEN_MAX)):
71         try: os.close(fd)
72         except OSError: pass  # most likely an fd that isn't open
73
74 # after http://www.erlenstar.demon.co.uk/unix/faq_2.html
75 def daemon():
76     """Daemonize the current process."""
77     if os.fork() != 0: os._exit(0)
78     os.setsid()
79     if os.fork() != 0: os._exit(0)
80     os.chdir('/')
81     os.umask(0022)
82     devnull = os.open(os.devnull, os.O_RDWR)
83     os.dup2(devnull, 0)
84     # xxx fixme - this is just to make sure that nothing gets stupidly lost - should use devnull
85     crashlog = os.open('/var/log/nodemanager.daemon', os.O_RDWR | os.O_APPEND | os.O_CREAT, 0644)
86     os.dup2(crashlog, 1)
87     os.dup2(crashlog, 2)
88
89 def fork_as(su, function, *args):
90     """fork(), cd / to avoid keeping unused directories open, close all nonstandard file descriptors (to avoid capturing open sockets), fork() again (to avoid zombies) and call <function> with arguments <args> in the grandchild process.  If <su> is not None, set our group and user ids appropriately in the child process."""
91     child_pid = os.fork()
92     if child_pid == 0:
93         try:
94             os.chdir('/')
95             close_nonstandard_fds()
96             if su:
97                 pw_ent = pwd.getpwnam(su)
98                 os.setegid(pw_ent[3])
99                 os.seteuid(pw_ent[2])
100             child_pid = os.fork()
101             if child_pid == 0: function(*args)
102         except:
103             os.seteuid(os.getuid())  # undo su so we can write the log file
104             os.setegid(os.getgid())
105             logger.log_exc("tools: fork_as")
106         os._exit(0)
107     else: os.waitpid(child_pid, 0)
108
109 ####################
110 # manage files
111 def pid_file():
112     """We use a pid file to ensure that only one copy of NM is running at a given time.
113 If successful, this function will write a pid file containing the pid of the current process.
114 The return value is the pid of the other running process, or None otherwise."""
115     other_pid = None
116     if os.access(PID_FILE, os.F_OK):  # check for a pid file
117         handle = open(PID_FILE)  # pid file exists, read it
118         other_pid = int(handle.read())
119         handle.close()
120         # check for a process with that pid by sending signal 0
121         try: os.kill(other_pid, 0)
122         except OSError, e:
123             if e.errno == errno.ESRCH: other_pid = None  # doesn't exist
124             else: raise  # who knows
125     if other_pid == None:
126         # write a new pid file
127         write_file(PID_FILE, lambda f: f.write(str(os.getpid())))
128     return other_pid
129
130 def write_file(filename, do_write, **kw_args):
131     """Write file <filename> atomically by opening a temporary file, using <do_write> to write that file, and then renaming the temporary file."""
132     shutil.move(write_temp_file(do_write, **kw_args), filename)
133
134 def write_temp_file(do_write, mode=None, uidgid=None):
135     fd, temporary_filename = tempfile.mkstemp()
136     if mode: os.chmod(temporary_filename, mode)
137     if uidgid: os.chown(temporary_filename, *uidgid)
138     f = os.fdopen(fd, 'w')
139     try: do_write(f)
140     finally: f.close()
141     return temporary_filename
142
143 # replace a target file with a new contents - checks for changes
144 # can handle chmod if requested
145 # can also remove resulting file if contents are void, if requested
146 # performs atomically:
147 #    writes in a tmp file, which is then renamed (from sliverauth originally)
148 # returns True if a change occurred, or the file is deleted
149 def replace_file_with_string (target, new_contents, chmod=None, remove_if_empty=False):
150     try:
151         current=file(target).read()
152     except:
153         current=""
154     if current==new_contents:
155         # if turns out to be an empty string, and remove_if_empty is set,
156         # then make sure to trash the file if it exists
157         if remove_if_empty and not new_contents and os.path.isfile(target):
158             logger.verbose("tools.replace_file_with_string: removing file %s"%target)
159             try: os.unlink(target)
160             finally: return True
161         return False
162     # overwrite target file: create a temp in the same directory
163     path=os.path.dirname(target) or '.'
164     fd, name = tempfile.mkstemp('','repl',path)
165     os.write(fd,new_contents)
166     os.close(fd)
167     if os.path.exists(target):
168         os.unlink(target)
169     shutil.move(name,target)
170     if chmod: os.chmod(target,chmod)
171     return True
172
173
174 ####################
175 # utilities functions to get (cached) information from the node
176
177 # get node_id from /etc/planetlab/node_id and cache it
178 _node_id=None
179 def node_id():
180     global _node_id
181     if _node_id is None:
182         try:
183             _node_id=int(file("/etc/planetlab/node_id").read())
184         except:
185             _node_id=""
186     return _node_id
187
188 _root_context_arch=None
189 def root_context_arch():
190     global _root_context_arch
191     if not _root_context_arch:
192         sp=subprocess.Popen(["uname","-i"],stdout=subprocess.PIPE)
193         (_root_context_arch,_)=sp.communicate()
194         _root_context_arch=_root_context_arch.strip()
195     return _root_context_arch
196
197
198 ####################
199 class NMLock:
200     def __init__(self, file):
201         logger.log("tools: Lock %s initialized." % file, 2)
202         self.fd = os.open(file, os.O_RDWR|os.O_CREAT, 0600)
203         flags = fcntl.fcntl(self.fd, fcntl.F_GETFD)
204         flags |= fcntl.FD_CLOEXEC
205         fcntl.fcntl(self.fd, fcntl.F_SETFD, flags)
206     def __del__(self):
207         os.close(self.fd)
208     def acquire(self):
209         logger.log("tools: Lock acquired.", 2)
210         fcntl.lockf(self.fd, fcntl.LOCK_SH)
211     def release(self):
212         logger.log("tools: Lock released.", 2)
213         fcntl.lockf(self.fd, fcntl.LOCK_UN)
214
215 ####################
216 # Utilities for getting the IP address of a LXC/Openvswitch slice. Do this by
217 # running ifconfig inside of the slice's context.
218
219 def get_sliver_process(slice_name, process_cmdline):
220     """ Utility function to find a process inside of an LXC sliver. Returns
221         (cgroup_fn, pid). cgroup_fn is the filename of the cgroup file for
222         the process, for example /proc/2592/cgroup. Pid is the process id of
223         the process. If the process is not found then (None, None) is returned.
224     """
225     try:
226         cmd = 'grep %s /proc/*/cgroup | grep freezer'%slice_name
227         output = os.popen(cmd).readlines()
228     except:
229         # the slice couldn't be found
230         logger.log("get_sliver_process: couldn't find slice %s" % slice_name)
231         return (None, None)
232
233     cgroup_fn = None
234     pid = None
235     for e in output:
236         try:
237             l = e.rstrip()
238             path = l.split(':')[0]
239             comp = l.rsplit(':')[-1]
240             slice_name_check = comp.rsplit('/')[-1]
241
242             if (slice_name_check == slice_name):
243                 slice_path = path
244                 pid = slice_path.split('/')[2]
245                 cmdline = open('/proc/%s/cmdline'%pid).read().rstrip('\n\x00')
246                 if (cmdline == process_cmdline):
247                     cgroup_fn = slice_path
248                     break
249         except:
250             break
251
252     if (not cgroup_fn) or (not pid):
253         logger.log("get_sliver_process: process %s not running in slice %s" % (process_cmdline, slice_name))
254         return (None, None)
255
256     return (cgroup_fn, pid)
257
258 ###################################################
259 # Author: Guilherme Sperb Machado <gsm@machados.org>
260 ###################################################
261 # Basically this method is just a copy from "get_process()", just
262 # adding one more split() to correctly parse the processes for LXC.
263 # Only for LXC!
264 # TODO: maybe merge both methods, and put the type as an argument, if
265 # it is LXC or vserver
266 ###################################################
267 def get_sliver_process_lxc(slice_name, process_cmdline):
268     """ Utility function to find a process inside of an LXC sliver. Returns
269         (cgroup_fn, pid). cgroup_fn is the filename of the cgroup file for
270         the process, for example /proc/2592/cgroup. Pid is the process id of
271         the process. If the process is not found then (None, None) is returned.
272     """
273     try:
274         cmd = 'grep %s /proc/*/cgroup | grep freezer'%slice_name
275         output = os.popen(cmd).readlines()
276     except:
277         # the slice couldn't be found
278         logger.log("get_sliver_process: couldn't find slice %s" % slice_name)
279         return (None, None)
280
281     cgroup_fn = None
282     pid = None
283     for e in output:
284         try:
285             l = e.rstrip()
286             #logger.log("tools: l=%s" % (l) )
287             path = l.split(':')[0]
288             #logger.log("tools: path=%s" % (path) )
289             comp = l.rsplit(':')[-1]
290             #logger.log("tools: comp=%s" % (comp) )
291             slice_name_check1 = comp.rsplit('/')[-1]
292             #logger.log("tools: slice_name_check1=%s" % (slice_name_check1) )
293             slice_name_check2 = slice_name_check1.rsplit('.')[0]
294             #logger.log("tools: slice_name_check2=%s" % (slice_name_check2) )
295
296             if (slice_name_check2 == slice_name):
297                 slice_path = path
298                 pid = slice_path.split('/')[2]
299                 #logger.log("tools: pid=%s" % (pid) )
300                 cmdline = open('/proc/%s/cmdline'%pid).read().rstrip('\n\x00')
301                 #logger.log("tools: cmdline=%s" % (cmdline) )
302                 #logger.log("tools: process_cmdline=%s" % (process_cmdline) )
303                 if (cmdline == process_cmdline):
304                         cgroup_fn = slice_path
305                         break
306         except:
307             #logger.log("tools: break!")
308             break
309
310     if (not cgroup_fn) or (not pid):
311         logger.log("get_sliver_process: process %s not running in slice %s" % (process_cmdline, slice_name))
312         return (None, None)
313
314     return (cgroup_fn, pid)
315
316
317 def get_sliver_ifconfig(slice_name, device="eth0"):
318     """ return the output of "ifconfig" run from inside the sliver.
319
320         side effects: adds "/usr/sbin" to sys.path
321     """
322
323     # See if setns is installed. If it's not then we're probably not running
324     # LXC.
325     if not os.path.exists("/usr/sbin/setns.so"):
326         return None
327
328     # setns is part of lxcsu and is installed to /usr/sbin
329     if not "/usr/sbin" in sys.path:
330         sys.path.append("/usr/sbin")
331     import setns
332
333     (cgroup_fn, pid) = get_sliver_process(slice_name, "/sbin/init")
334     if (not cgroup_fn) or (not pid):
335         return None
336
337     path = '/proc/%s/ns/net'%pid
338
339     result = None
340     try:
341         setns.chcontext(path)
342
343         args = ["/sbin/ifconfig", device]
344         sub = subprocess.Popen(args, stderr = subprocess.PIPE, stdout = subprocess.PIPE)
345         sub.wait()
346
347         if (sub.returncode != 0):
348             logger.log("get_slice_ifconfig: error in ifconfig: %s" % sub.stderr.read())
349
350         result = sub.stdout.read()
351     finally:
352         setns.chcontext("/proc/1/ns/net")
353
354     return result
355
356 ###################################################
357 # Author: Guilherme Sperb Machado <gsm@machados.org>
358 ###################################################
359 # Basically this method is just a copy from "get_sliver_ifconfig()", but,
360 # instead, calls the "get_sliver_process_lxc()" method.
361 # Only for LXC!
362 # TODO: maybe merge both methods, and put the type as an argument, if
363 # it is LXC or vserver
364 ###################################################
365 def get_sliver_ifconfig_lxc(slice_name, device="eth0"):
366     """ return the output of "ifconfig" run from inside the sliver.
367
368         side effects: adds "/usr/sbin" to sys.path
369     """
370
371     # See if setns is installed. If it's not then we're probably not running
372     # LXC.
373     if not os.path.exists("/usr/sbin/setns.so"):
374         return None
375
376     # setns is part of lxcsu and is installed to /usr/sbin
377     if not "/usr/sbin" in sys.path:
378         sys.path.append("/usr/sbin")
379     import setns
380
381     (cgroup_fn, pid) = get_sliver_process_lxc(slice_name, "/sbin/init")
382     if (not cgroup_fn) or (not pid):
383         return None
384
385     path = '/proc/%s/ns/net'%pid
386
387     result = None
388     try:
389         setns.chcontext(path)
390
391         args = ["/sbin/ifconfig", device]
392         sub = subprocess.Popen(args, stderr = subprocess.PIPE, stdout = subprocess.PIPE)
393         sub.wait()
394
395         if (sub.returncode != 0):
396             logger.log("get_slice_ifconfig: error in ifconfig: %s" % sub.stderr.read())
397
398         result = sub.stdout.read()
399     finally:
400         setns.chcontext("/proc/1/ns/net")
401
402     return result
403
404
405 def get_sliver_ip(slice_name):
406     ifconfig = get_sliver_ifconfig(slice_name)
407     if not ifconfig:
408         return None
409
410     for line in ifconfig.split("\n"):
411         if "inet addr:" in line:
412             # example: '          inet addr:192.168.122.189  Bcast:192.168.122.255  Mask:255.255.255.0'
413             parts = line.strip().split()
414             if len(parts)>=2 and parts[1].startswith("addr:"):
415                 return parts[1].split(":")[1]
416
417     return None
418
419 ###################################################
420 # Author: Guilherme Sperb Machado <gsm@machados.org>
421 ###################################################
422 # Get the slice ipv6 address
423 # Only for LXC!
424 ###################################################
425 def get_sliver_ipv6(slice_name):
426     ifconfig = get_sliver_ifconfig_lxc(slice_name)
427     if not ifconfig:
428         return None,None
429     
430     # example: 'inet6 2001:67c:16dc:1302:5054:ff:fea7:7882  prefixlen 64  scopeid 0x0<global>'
431     prog = re.compile(r'inet6\s+(.*)\s+prefixlen\s+(\d+)\s+scopeid\s+(.+)<global>')
432     for line in ifconfig.split("\n"):
433         search = prog.search(line)
434         if search:
435                 ipv6addr = search.group(1)
436                 prefixlen = search.group(2)
437                 return (ipv6addr,prefixlen)
438     return None,None
439
440 ################################################### 
441 # Author: Guilherme Sperb Machado <gsm@machados.org>
442 ###################################################
443 # Check if the address is a AF_INET6 family address
444 ###################################################
445 def isValidIPv6(ipv6addr):
446         try:
447                 socket.inet_pton(socket.AF_INET6, ipv6addr)
448         except socket.error:
449                 return False
450         return True
451
452 ### this returns the kind of virtualization on the node
453 # either 'vs' or 'lxc'
454 # also caches it in /etc/planetlab/virt for next calls
455 # could be promoted to core nm if need be
456 virt_stamp="/etc/planetlab/virt"
457 def get_node_virt ():
458     try:
459         return file(virt_stamp).read().strip()
460     except:
461         pass
462     logger.log("Computing virt..")
463     try: 
464         if subprocess.call ([ 'vserver', '--help' ]) ==0: virt='vs'
465         else:                                             virt='lxc'      
466     except:
467         virt='lxc'
468     with file(virt_stamp,"w") as f:
469         f.write(virt)
470     return virt
471
472 ### this return True or False to indicate that systemctl is present on that box
473 # cache result in memory as _has_systemctl
474 _has_systemctl=None
475 def has_systemctl ():
476     global _has_systemctl
477     if _has_systemctl is None:
478         _has_systemctl = (subprocess.call([ 'systemctl', '--help' ]) == 0)
479     return _has_systemctl
480
481 ###################################################
482 # Author: Guilherme Sperb Machado <gsm@machados.org>
483 ###################################################
484 # This method was developed to support the ipv6 plugin
485 # Only for LXC!
486 ###################################################
487 def reboot_sliver(name):
488         type = 'sliver.LXC'
489         # connecting to the libvirtd
490         connLibvirt = Sliver_Libvirt.getConnection(type)
491         domains = connLibvirt.listAllDomains()
492         for domain in domains:
493                 #ret = dir(domain)
494                 #for method in ret:
495                 #       logger.log("ipv6: " + repr(method))
496                 #logger.log("tools: " + str(domain.name()) )
497                 try:
498                         domain.destroy()
499                         logger.log("tools: %s destroyed" % (domain.name()) )
500                         domain.create()
501                         logger.log("tools: %s created" % (domain.name()) )
502                 except:
503                         logger.log("tools: %s could not be rebooted" % (domain.name()) )
504
505 ###################################################
506 # Author: Guilherme Sperb Machado <gsm@machados.org>
507 ###################################################
508 # Get the /etc/hosts file path
509 ###################################################
510 def get_hosts_file_path(slicename):
511     containerDir = os.path.join(sliver_lxc.Sliver_LXC.CON_BASE_DIR, slicename)
512     logger.log("tools: %s" % (containerDir) )
513     return os.path.join(containerDir, 'etc', 'hosts')
514
515 ###################################################
516 # Author: Guilherme Sperb Machado <gsm@machados.org>
517 ###################################################
518 # Search if there is a specific ipv6 address in the /etc/hosts file of a given slice
519 ###################################################
520 def search_ipv6addr_hosts(slicename, ipv6addr):
521     hostsFilePath = get_hosts_file_path(slicename)
522     found=False
523     try:
524         for line in fileinput.input(r'%s' % (hostsFilePath)):
525                 if re.search(r'%s' % (ipv6addr), line):
526                         found=True
527         fileinput.close()
528         return found
529     except:
530         logger.log("tools: error when finding ipv6 address %s in the /etc/hosts file of slice=%s" % (ipv6addr, slicename) )
531
532 ###################################################
533 # Author: Guilherme Sperb Machado <gsm@machados.org>
534 ###################################################
535 # Removes all ipv6 addresses from the /etc/hosts file of a given slice
536 ###################################################
537 def remove_all_ipv6addr_hosts(slicename, node):
538     hostsFilePath = get_hosts_file_path(slicename)
539     try:
540         for line in fileinput.input(r'%s' % (hostsFilePath), inplace=True):
541                 logger.log("tools: line=%s" % (line) )
542                 search = re.search(r'^(.*)\s+(%s|%s)$' % (node,'localhost'), line)
543                 if search:
544                         ipv6candidate = search.group(1)
545                         ipv6candidatestrip = ipv6candidate.strip()
546                         logger.log("tools: group1=%s" % (ipv6candidatestrip) )
547                         valid = isValidIPv6(ipv6candidatestrip)
548                         if not valid:
549                                 logger.log("tools: address=%s not valid" % (ipv6candidatestrip) )
550                                 print line,
551         fileinput.close()
552     except:
553         logger.log("tools: could not delete the ipv6 address from the hosts file of slice=%s" % (slicename) )
554
555 ###################################################
556 # Author: Guilherme Sperb Machado <gsm@machados.org>
557 ###################################################
558 # Adds an ipv6 address to the /etc/hosts file within a slice
559 ###################################################
560 def add_ipv6addr_hosts_line(slicename, node, ipv6addr):
561     hostsFilePath = get_hosts_file_path(slicename)
562     logger.log("tools: %s" % (hostsFilePath) )
563     # debugging purposes:
564     #string = "127.0.0.1\tlocalhost\n192.168.100.179\tmyplc-node1-vm.mgmt.local\n"
565     #string = "127.0.0.1\tlocalhost\n"
566     try:
567                 with open(hostsFilePath, "a") as file:
568                         # debugging purposes only:
569                         #file.write(string)
570                         file.write(ipv6addr + " " + node + "\n")
571                         file.close()
572     except:
573                 logger.log("tools: could not add the IPv6 address to the hosts file of slice=%s" % (slicename) )
574
575
576
577 # how to run a command in a slice
578 # now this is a painful matter
579 # the problem is with capsh that forces a bash command to be injected in its exec'ed command
580 # so because lxcsu uses capsh, you cannot exec anything else than bash
581 # bottom line is, what actually needs to be called is
582 # vs:  vserver exec slicename command and its arguments
583 # lxc: lxcsu slicename "command and its arguments"
584 # which, OK, is no big deal as long as the command is simple enough, 
585 # but do not stretch it with arguments that have spaces or need quoting as that will become a nightmare
586 def command_in_slice (slicename, argv):
587     virt=get_node_virt()
588     if virt=='vs':
589         return [ 'vserver', slicename, 'exec', ] + argv
590     elif virt=='lxc':
591         # wrap up argv in a single string for -c
592         return [ 'lxcsu', slicename, ] + [ " ".join(argv) ]
593     logger.log("command_in_slice: WARNING: could not find a valid virt")
594     return argv
595
596 ####################
597 def init_signals ():
598     def handler (signum, frame):
599         logger.log("Received signal %d - exiting"%signum)
600         os._exit(1)
601     signal.signal(signal.SIGHUP,handler)
602     signal.signal(signal.SIGQUIT,handler)
603     signal.signal(signal.SIGINT,handler)
604     signal.signal(signal.SIGTERM,handler)