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