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