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