refactor of reboot_slivers() to cover f21 -- only ipv6 part is affected.
[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     """
219     Utility function to find a process inside of an LXC sliver. Returns
220     (cgroup_fn, pid). cgroup_fn is the filename of the cgroup file for
221     the process, for example /proc/2592/cgroup. Pid is the process id of
222     the process. If the process is not found then (None, None) is returned.
223     """
224     try:
225         cmd = 'grep %s /proc/*/cgroup | grep freezer'%slice_name
226         output = os.popen(cmd).readlines()
227     except:
228         # the slice couldn't be found
229         logger.log("get_sliver_process: couldn't find slice %s" % slice_name)
230         return (None, None)
231
232     cgroup_fn = None
233     pid = None
234     for e in output:
235         try:
236             l = e.rstrip()
237             path = l.split(':')[0]
238             comp = l.rsplit(':')[-1]
239             slice_name_check = comp.rsplit('/')[-1]
240             # the lines below were added by Guilherme <gsm@machados.org>
241             # due to the LXC requirements
242             # What we have to consider here is that libervirt on Fedora 18
243             # uses the following line:
244             # /proc/1253/cgroup:6:freezer:/machine.slice/auto_sirius.libvirt-lxc
245             # While the libvirt on Fedora 20 and 21 uses the following line:
246             # /proc/1253/cgroup:6:freezer:/machine.slice/machine-lxc\x2del_sirius.scope
247             # Further documentation on:
248             # https://libvirt.org/cgroups.html#systemdScope
249             virt=get_node_virt()
250             if virt=='lxc':
251                 # This is for Fedora 20 or later
252                 regexf20orlater = re.compile(r'machine-lxc\\x2d(.+).scope')
253                 isf20orlater = regexf20orlater.search(slice_name_check)
254                 if isf20orlater:
255                     slice_name_check = isf20orlater.group(1)
256                 else:
257                     # This is for Fedora 18
258                     slice_name_check = slice_name_check.rsplit('.')[0]
259
260             if (slice_name_check == slice_name):
261                 slice_path = path
262                 pid = slice_path.split('/')[2]
263                 cmdline = open('/proc/%s/cmdline'%pid).read().rstrip('\n\x00')
264                 if (cmdline == process_cmdline):
265                     cgroup_fn = slice_path
266                     break
267         except:
268             break
269
270     if (not cgroup_fn) or (not pid):
271         logger.log("get_sliver_process: process %s not running in slice %s" % (process_cmdline, slice_name))
272         return (None, None)
273
274     return (cgroup_fn, pid)
275
276 ###################################################
277 # Added by Guilherme Sperb Machado <gsm@machados.org>
278 ###################################################
279
280 try:
281     import re
282     import socket
283     import fileinput
284 except:
285     logger.log("Could not import 're', 'socket', or 'fileinput' python packages.")
286
287 # TODO: is there anything better to do if the "libvirt", "sliver_libvirt",
288 # and "sliver_lxc" are not in place?
289 try:
290     import libvirt
291     from sliver_libvirt import Sliver_Libvirt
292     import sliver_lxc
293 except:
294     logger.log("Could not import 'sliver_lxc' or 'libvirt' or 'sliver_libvirt'.")
295 ###################################################
296
297 def get_sliver_ifconfig(slice_name, device="eth0"):
298     """
299     return the output of "ifconfig" run from inside the sliver.
300
301     side effects: adds "/usr/sbin" to sys.path
302     """
303
304     # See if setns is installed. If it's not then we're probably not running
305     # LXC.
306     if not os.path.exists("/usr/sbin/setns.so"):
307         return None
308
309     # setns is part of lxcsu and is installed to /usr/sbin
310     if not "/usr/sbin" in sys.path:
311         sys.path.append("/usr/sbin")
312     import setns
313
314     (cgroup_fn, pid) = get_sliver_process(slice_name, "/sbin/init")
315     if (not cgroup_fn) or (not pid):
316         return None
317
318     path = '/proc/%s/ns/net'%pid
319
320     result = None
321     try:
322         setns.chcontext(path)
323
324         args = ["/sbin/ifconfig", device]
325         sub = subprocess.Popen(args, stderr = subprocess.PIPE, stdout = subprocess.PIPE)
326         sub.wait()
327
328         if (sub.returncode != 0):
329             logger.log("get_slice_ifconfig: error in ifconfig: %s" % sub.stderr.read())
330
331         result = sub.stdout.read()
332     finally:
333         setns.chcontext("/proc/1/ns/net")
334
335     return result
336
337 def get_sliver_ip(slice_name):
338     ifconfig = get_sliver_ifconfig(slice_name)
339     if not ifconfig:
340         return None
341
342     for line in ifconfig.split("\n"):
343         if "inet addr:" in line:
344             # example: '          inet addr:192.168.122.189  Bcast:192.168.122.255  Mask:255.255.255.0'
345             parts = line.strip().split()
346             if len(parts)>=2 and parts[1].startswith("addr:"):
347                 return parts[1].split(":")[1]
348
349     return None
350
351 ###################################################
352 # Author: Guilherme Sperb Machado <gsm@machados.org>
353 ###################################################
354 # Get the slice ipv6 address
355 # Only for LXC!
356 ###################################################
357 def get_sliver_ipv6(slice_name):
358     ifconfig = get_sliver_ifconfig(slice_name)
359     if not ifconfig:
360         return None,None
361
362     # example: 'inet6 2001:67c:16dc:1302:5054:ff:fea7:7882  prefixlen 64  scopeid 0x0<global>'
363     prog = re.compile(r'inet6\s+(.*)\s+prefixlen\s+(\d+)\s+scopeid\s+(.+)<global>')
364     for line in ifconfig.split("\n"):
365         search = prog.search(line)
366         if search:
367             ipv6addr = search.group(1)
368             prefixlen = search.group(2)
369             return (ipv6addr,prefixlen)
370     return None,None
371
372 ###################################################
373 # Author: Guilherme Sperb Machado <gsm@machados.org>
374 ###################################################
375 # Check if the address is a AF_INET6 family address
376 ###################################################
377 def is_valid_ipv6(ipv6addr):
378     try:
379         socket.inet_pton(socket.AF_INET6, ipv6addr)
380     except socket.error:
381         return False
382     return True
383
384 ### this returns the kind of virtualization on the node
385 # either 'vs' or 'lxc'
386 # also caches it in /etc/planetlab/virt for next calls
387 # could be promoted to core nm if need be
388 virt_stamp="/etc/planetlab/virt"
389 def get_node_virt ():
390     try:
391         return file(virt_stamp).read().strip()
392     except:
393         pass
394     logger.log("Computing virt..")
395     try:
396         if subprocess.call ([ 'vserver', '--help' ]) ==0: virt='vs'
397         else:                                             virt='lxc'
398     except:
399         virt='lxc'
400     with file(virt_stamp,"w") as f:
401         f.write(virt)
402     return virt
403
404 ### this return True or False to indicate that systemctl is present on that box
405 # cache result in memory as _has_systemctl
406 _has_systemctl=None
407 def has_systemctl ():
408     global _has_systemctl
409     if _has_systemctl is None:
410         _has_systemctl = (subprocess.call([ 'systemctl', '--help' ]) == 0)
411     return _has_systemctl
412
413 ###################################################
414 # Author: Guilherme Sperb Machado <gsm@machados.org>
415 ###################################################
416 # This method was developed to support the ipv6 plugin
417 # Only for LXC!
418 ###################################################
419 def reboot_slivers():
420     type = 'sliver.LXC'
421     # connecting to the libvirtd
422     connLibvirt = Sliver_Libvirt.getConnection(type)
423     domains = connLibvirt.listAllDomains()
424     for domain in domains:
425         try:
426             # set the flag VIR_DOMAIN_REBOOT_INITCTL, which uses "initctl"
427             result = domain.reboot(0x04)
428             if result==0: logger.log("tools: REBOOT %s" % (domain.name()) )
429             else:
430                 raise Exception()
431         except Exception, e:
432             logger.log("tools: FAILED to reboot %s (%s)" % (domain.name(), e) )
433             logger.log("tools: Trying to DESTROY/CREATE %s instead..." % (domain.name()) )
434             try:
435                 result = domain.destroy()
436                 if result==0: logger.log("tools: DESTROYED %s" % (domain.name()) )
437                 else: logger.log("tools: FAILED in the DESTROY call of %s" % (domain.name()) )
438                 result = domain.create()
439                 if result==0: logger.log("tools: CREATED %s" % (domain.name()) )
440                 else: logger.log("tools: FAILED in the CREATE call of %s" % (domain.name()) )
441             except Exception, e:
442                 logger.log("tools: FAILED to DESTROY/CREATE %s (%s)" % (domain.name(), e) )
443
444 ###################################################
445 # Author: Guilherme Sperb Machado <gsm@machados.org>
446 ###################################################
447 # Get the /etc/hosts file path
448 ###################################################
449 def get_hosts_file_path(slicename):
450     containerDir = os.path.join(sliver_lxc.Sliver_LXC.CON_BASE_DIR, slicename)
451     return os.path.join(containerDir, 'etc', 'hosts')
452
453 ###################################################
454 # Author: Guilherme Sperb Machado <gsm@machados.org>
455 ###################################################
456 # Search if there is a specific ipv6 address in the
457 # /etc/hosts file of a given slice
458 # If the parameter 'ipv6addr' is None, then search
459 # for any ipv6 address
460 ###################################################
461 def search_ipv6addr_hosts(slicename, ipv6addr):
462     hostsFilePath = get_hosts_file_path(slicename)
463     found=False
464     try:
465         for line in fileinput.input(r'%s' % (hostsFilePath)):
466             if ipv6addr is not None:
467                 if re.search(r'%s' % (ipv6addr), line):
468                     found=True
469             else:
470                 search = re.search(r'^(.*)\s+.*$', line)
471                 if search:
472                     ipv6candidate = search.group(1)
473                     ipv6candidatestrip = ipv6candidate.strip()
474                     valid = is_valid_ipv6(ipv6candidatestrip)
475                     if valid:
476                         found=True
477         fileinput.close()
478         return found
479     except:
480         logger.log("tools: FAILED to search %s in /etc/hosts file of slice=%s" % \
481                    (ipv6addr, slicename) )
482
483 ###################################################
484 # Author: Guilherme Sperb Machado <gsm@machados.org>
485 ###################################################
486 # Removes all ipv6 addresses from the /etc/hosts
487 # file of a given slice
488 ###################################################
489 def remove_all_ipv6addr_hosts(slicename, node):
490     hostsFilePath = get_hosts_file_path(slicename)
491     try:
492         for line in fileinput.input(r'%s' % (hostsFilePath), inplace=True):
493             search = re.search(r'^(.*)\s+(%s|%s)$' % (node,'localhost'), line)
494             if search:
495                 ipv6candidate = search.group(1)
496                 ipv6candidatestrip = ipv6candidate.strip()
497                 valid = is_valid_ipv6(ipv6candidatestrip)
498                 if not valid:
499                     print line,
500         fileinput.close()
501         logger.log("tools: REMOVED IPv6 address from /etc/hosts file of slice=%s" % \
502                    (slicename) )
503     except:
504         logger.log("tools: FAILED to remove the IPv6 address from /etc/hosts file of slice=%s" % \
505                    (slicename) )
506
507 ###################################################
508 # Author: Guilherme Sperb Machado <gsm@machados.org>
509 ###################################################
510 # Adds an ipv6 address to the /etc/hosts file within a slice
511 ###################################################
512 def add_ipv6addr_hosts_line(slicename, node, ipv6addr):
513     hostsFilePath = get_hosts_file_path(slicename)
514     logger.log("tools: %s" % (hostsFilePath) )
515     # debugging purposes:
516     #string = "127.0.0.1\tlocalhost\n192.168.100.179\tmyplc-node1-vm.mgmt.local\n"
517     #string = "127.0.0.1\tlocalhost\n"
518     try:
519         with open(hostsFilePath, "a") as file:
520             file.write(ipv6addr + " " + node + "\n")
521             file.close()
522         logger.log("tools: ADDED IPv6 address to /etc/hosts file of slice=%s" % \
523                    (slicename) )
524     except:
525         logger.log("tools: FAILED to add the IPv6 address to /etc/hosts file of slice=%s" % \
526                    (slicename) )
527
528
529
530 # how to run a command in a slice
531 # now this is a painful matter
532 # the problem is with capsh that forces a bash command to be injected in its exec'ed command
533 # so because lxcsu uses capsh, you cannot exec anything else than bash
534 # bottom line is, what actually needs to be called is
535 # vs:  vserver exec slicename command and its arguments
536 # lxc: lxcsu slicename "command and its arguments"
537 # which, OK, is no big deal as long as the command is simple enough,
538 # but do not stretch it with arguments that have spaces or need quoting as that will become a nightmare
539 def command_in_slice (slicename, argv):
540     virt=get_node_virt()
541     if virt=='vs':
542         return [ 'vserver', slicename, 'exec', ] + argv
543     elif virt=='lxc':
544         # wrap up argv in a single string for -c
545         return [ 'lxcsu', slicename, ] + [ " ".join(argv) ]
546     logger.log("command_in_slice: WARNING: could not find a valid virt")
547     return argv
548
549 ####################
550 def init_signals ():
551     def handler (signum, frame):
552         logger.log("Received signal %d - exiting"%signum)
553         os._exit(1)
554     signal.signal(signal.SIGHUP,handler)
555     signal.signal(signal.SIGQUIT,handler)
556     signal.signal(signal.SIGINT,handler)
557     signal.signal(signal.SIGTERM,handler)