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