1 # -*- python-indent: 4 -*-
3 """A few things that didn't seem to fit anywhere else."""
19 PID_FILE = '/var/run/nodemanager.pid'
25 interface = get_if_from_hwaddr(get_hwaddr_from_plnode())
31 def get_hwaddr_from_plnode():
33 for line in open("/usr/boot/plnode.txt", 'r').readlines():
34 if line.startswith("NET_DEVICE"):
35 return line.split("=")[1].strip().strip('"')
41 def get_if_from_hwaddr(hwaddr):
45 dev_hwaddr = sioc.gifhwaddr(dev)
46 if dev_hwaddr == hwaddr:
54 def as_daemon_thread(run):
55 """Call function <run> with no arguments in its own thread."""
56 thr = threading.Thread(target=run)
61 def close_nonstandard_fds():
62 """Close all open file descriptors other than 0, 1, and 2."""
64 for fd in range(3, os.sysconf(_SC_OPEN_MAX)):
68 pass # most likely an fd that isn't open
70 # after http://www.erlenstar.demon.co.uk/unix/faq_2.html
74 """Daemonize the current process."""
82 devnull = os.open(os.devnull, os.O_RDWR)
84 # xxx fixme - this is just to make sure that nothing gets stupidly lost - should use devnull
85 crashlog = os.open('/var/log/nodemanager.daemon',
86 os.O_RDWR | os.O_APPEND | os.O_CREAT, 0o644)
91 def fork_as(su, function, *args):
93 fork(), cd / to avoid keeping unused directories open,
94 close all nonstandard file descriptors (to avoid capturing open sockets),
95 fork() again (to avoid zombies) and call <function>
96 with arguments <args> in the grandchild process.
97 If <su> is not None, set our group and user ids
98 appropriately in the child process.
100 child_pid = os.fork()
104 close_nonstandard_fds()
106 pw_ent = pwd.getpwnam(su)
107 os.setegid(pw_ent[3])
108 os.seteuid(pw_ent[2])
109 child_pid = os.fork()
113 os.seteuid(os.getuid()) # undo su so we can write the log file
114 os.setegid(os.getgid())
115 logger.log_exc("tools: fork_as")
118 os.waitpid(child_pid, 0)
126 We use a pid file to ensure that only one copy of NM is running at a given time.
127 If successful, this function will write a pid file containing the pid of the current process.
128 The return value is the pid of the other running process, or None otherwise.
131 if os.access(PID_FILE, os.F_OK): # check for a pid file
132 handle = open(PID_FILE) # pid file exists, read it
133 other_pid = int(handle.read())
135 # check for a process with that pid by sending signal 0
137 os.kill(other_pid, 0)
139 if e.errno == errno.ESRCH:
140 other_pid = None # doesn't exist
143 if other_pid == None:
144 # write a new pid file
145 write_file(PID_FILE, lambda f: f.write(str(os.getpid())))
149 def write_file(filename, do_write, **kw_args):
151 Write file <filename> atomically by opening a temporary file,
152 using <do_write> to write that file, and then renaming the temporary file.
154 shutil.move(write_temp_file(do_write, **kw_args), filename)
157 def write_temp_file(do_write, mode=None, uidgid=None):
158 fd, temporary_filename = tempfile.mkstemp()
160 os.chmod(temporary_filename, mode)
162 os.chown(temporary_filename, *uidgid)
163 f = os.fdopen(fd, 'w')
168 return temporary_filename
171 def replace_file_with_string(target, new_contents, chmod=None, remove_if_empty=False):
173 Replace a target file with a new contents
174 checks for changes: does not do anything if previous state was already right
175 can handle chmod if requested
176 can also remove resulting file if contents are void, if requested
178 writes in a tmp file, which is then renamed (from sliverauth originally)
179 returns True if a change occurred, or the file is deleted
182 with open(target) as f:
186 if current == new_contents:
187 # if turns out to be an empty string, and remove_if_empty is set,
188 # then make sure to trash the file if it exists
189 if remove_if_empty and not new_contents and os.path.isfile(target):
191 "tools.replace_file_with_string: removing file {}".format(target))
197 # overwrite target file: create a temp in the same directory
198 path = os.path.dirname(target) or '.'
199 fd, name = tempfile.mkstemp('', 'repl', path)
200 os.write(fd, new_contents)
202 if os.path.exists(target):
204 shutil.move(name, target)
206 os.chmod(target, chmod)
210 # utilities functions to get (cached) information from the node
213 # get node_id from /etc/planetlab/node_id and cache it
221 with open("/etc/planetlab/node_id") as f:
222 _node_id = int(f.read())
228 _root_context_arch = None
231 def root_context_arch():
232 global _root_context_arch
233 if not _root_context_arch:
234 sp = subprocess.Popen(["uname", "-i"], stdout=subprocess.PIPE)
235 (_root_context_arch, _) = sp.communicate()
236 _root_context_arch = _root_context_arch.strip()
237 return _root_context_arch
242 def __init__(self, file):
243 logger.log("tools: Lock {} initialized.".format(file), 2)
244 self.fd = os.open(file, os.O_RDWR | os.O_CREAT, 0o600)
245 flags = fcntl.fcntl(self.fd, fcntl.F_GETFD)
246 flags |= fcntl.FD_CLOEXEC
247 fcntl.fcntl(self.fd, fcntl.F_SETFD, flags)
253 logger.log("tools: Lock acquired.", 2)
254 fcntl.lockf(self.fd, fcntl.LOCK_SH)
257 logger.log("tools: Lock released.", 2)
258 fcntl.lockf(self.fd, fcntl.LOCK_UN)
261 # Utilities for getting the IP address of a LXC/Openvswitch slice. Do this by
262 # running ifconfig inside of the slice's context.
265 def get_sliver_process(slice_name, process_cmdline):
267 Utility function to find a process inside of an LXC sliver. Returns
268 (cgroup_fn, pid). cgroup_fn is the filename of the cgroup file for
269 the process, for example /proc/2592/cgroup. Pid is the process id of
270 the process. If the process is not found then (None, None) is returned.
273 cmd = 'grep {} /proc/*/cgroup | grep freezer'.format(slice_name)
274 output = os.popen(cmd).readlines()
276 # the slice couldn't be found
278 "get_sliver_process: couldn't find slice {}".format(slice_name))
286 path = l.split(':')[0]
287 comp = l.rsplit(':')[-1]
288 slice_name_check = comp.rsplit('/')[-1]
289 # the lines below were added by Guilherme <gsm@machados.org>
290 # due to the LXC requirements
291 # What we have to consider here is that libervirt on Fedora 18
292 # uses the following line:
293 # /proc/1253/cgroup:6:freezer:/machine.slice/auto_sirius.libvirt-lxc
294 # While the libvirt on Fedora 20 and 21 uses the following line:
295 # /proc/1253/cgroup:6:freezer:/machine.slice/machine-lxc\x2del_sirius.scope
296 # Further documentation on:
297 # https://libvirt.org/cgroups.html#systemdScope
298 virt = get_node_virt()
300 # This is for Fedora 20 or later
301 regexf20orlater = re.compile(r'machine-lxc\\x2d(.+).scope')
302 isf20orlater = regexf20orlater.search(slice_name_check)
304 slice_name_check = isf20orlater.group(1)
306 # This is for Fedora 18
307 slice_name_check = slice_name_check.rsplit('.')[0]
309 if (slice_name_check == slice_name):
311 pid = slice_path.split('/')[2]
312 with open('/proc/{}/cmdline'.format(pid)) as cmdfile:
313 cmdline = cmdfile.read().rstrip('\n\x00')
314 if (cmdline == process_cmdline):
315 cgroup_fn = slice_path
320 if (not cgroup_fn) or (not pid):
321 logger.log("get_sliver_process: process {} not running in slice {}"
322 .format(process_cmdline, slice_name))
325 return (cgroup_fn, pid)
327 ###################################################
328 # Added by Guilherme Sperb Machado <gsm@machados.org>
329 ###################################################
337 logger.log("Could not import 're', 'socket', or 'fileinput' python packages.")
339 # TODO: is there anything better to do if the "libvirt", "sliver_libvirt",
340 # and "sliver_lxc" are not in place?
343 from sliver_libvirt import Sliver_Libvirt
346 logger.log("Could not import 'sliver_lxc' or 'libvirt' or 'sliver_libvirt'.")
347 ###################################################
350 def get_sliver_ifconfig(slice_name, device="eth0"):
352 return the output of "ifconfig" run from inside the sliver.
354 side effects: adds "/usr/sbin" to sys.path
357 # See if setns is installed. If it's not then we're probably not running
359 if not os.path.exists("/usr/sbin/setns.so"):
362 # setns is part of lxcsu and is installed to /usr/sbin
363 if not "/usr/sbin" in sys.path:
364 sys.path.append("/usr/sbin")
367 (cgroup_fn, pid) = get_sliver_process(slice_name, "/sbin/init")
368 if (not cgroup_fn) or (not pid):
371 path = '/proc/{}/ns/net'.format(pid)
375 setns.chcontext(path)
377 args = ["/sbin/ifconfig", device]
378 sub = subprocess.Popen(
379 args, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
382 if (sub.returncode != 0):
383 logger.log("get_slice_ifconfig: error in ifconfig: {}".format(
386 result = sub.stdout.read()
388 setns.chcontext("/proc/1/ns/net")
393 def get_sliver_ip(slice_name):
394 ifconfig = get_sliver_ifconfig(slice_name)
398 for line in ifconfig.split("\n"):
399 if "inet addr:" in line:
400 # example: ' inet addr:192.168.122.189 Bcast:192.168.122.255 Mask:255.255.255.0'
401 parts = line.strip().split()
402 if len(parts) >= 2 and parts[1].startswith("addr:"):
403 return parts[1].split(":")[1]
407 ###################################################
408 # Author: Guilherme Sperb Machado <gsm@machados.org>
409 ###################################################
410 # Get the slice ipv6 address
412 ###################################################
415 def get_sliver_ipv6(slice_name):
416 ifconfig = get_sliver_ifconfig(slice_name)
420 # example: 'inet6 2001:67c:16dc:1302:5054:ff:fea7:7882 prefixlen 64 scopeid 0x0<global>'
422 r'inet6\s+(.*)\s+prefixlen\s+(\d+)\s+scopeid\s+(.+)<global>')
423 for line in ifconfig.split("\n"):
424 search = prog.search(line)
426 ipv6addr = search.group(1)
427 prefixlen = search.group(2)
428 return (ipv6addr, prefixlen)
431 ###################################################
432 # Author: Guilherme Sperb Machado <gsm@machados.org>
433 ###################################################
434 # Check if the address is a AF_INET6 family address
435 ###################################################
438 def is_valid_ipv6(ipv6addr):
440 socket.inet_pton(socket.AF_INET6, ipv6addr)
446 # this returns the kind of virtualization on the node
447 # either 'vs' or 'lxc'
448 # also caches it in /etc/planetlab/virt for next calls
449 # could be promoted to core nm if need be
450 virt_stamp = "/etc/planetlab/virt"
455 with open(virt_stamp) as f:
456 return f.read().strip()
459 logger.log("Computing virt..")
461 virt = 'vs' if subprocess.call(['vserver', '--help']) == 0 else 'lxc'
464 with open(virt_stamp, "w") as f:
469 # this return True or False to indicate that systemctl is present on that box
470 # cache result in memory as _has_systemctl
471 _has_systemctl = None
475 global _has_systemctl
476 if _has_systemctl is None:
477 _has_systemctl = (subprocess.call(['systemctl', '--help']) == 0)
478 return _has_systemctl
480 ###################################################
481 # Author: Guilherme Sperb Machado <gsm@machados.org>
482 ###################################################
483 # This method was developed to support the ipv6 plugin
485 ###################################################
488 def reboot_slivers():
490 # connecting to the libvirtd
491 connLibvirt = Sliver_Libvirt.getConnection(type)
492 domains = connLibvirt.listAllDomains()
493 for domain in domains:
495 # set the flag VIR_DOMAIN_REBOOT_INITCTL, which uses "initctl"
496 result = domain.reboot(0x04)
498 logger.log("tools: REBOOT {}".format(domain.name()))
501 except Exception as e:
502 logger.log("tools: FAILED to reboot {} ({})".format(
505 "tools: Trying to DESTROY/CREATE {} instead...".format(domain.name()))
507 result = domain.destroy()
509 logger.log("tools: DESTROYED {}".format(domain.name()))
512 "tools: FAILED in the DESTROY call of {}".format(domain.name()))
513 result = domain.create()
515 logger.log("tools: CREATED {}".format(domain.name()))
518 "tools: FAILED in the CREATE call of {}".format(domain.name()))
519 except Exception as e:
521 "tools: FAILED to DESTROY/CREATE {} ({})".format(domain.name(), e))
523 ###################################################
524 # Author: Guilherme Sperb Machado <gsm@machados.org>
525 ###################################################
526 # Get the /etc/hosts file path
527 ###################################################
530 def get_hosts_file_path(slicename):
531 containerDir = os.path.join(sliver_lxc.Sliver_LXC.CON_BASE_DIR, slicename)
532 return os.path.join(containerDir, 'etc', 'hosts')
534 ###################################################
535 # Author: Guilherme Sperb Machado <gsm@machados.org>
536 ###################################################
537 # Search if there is a specific ipv6 address in the
538 # /etc/hosts file of a given slice
539 # If the parameter 'ipv6addr' is None, then search
540 # for any ipv6 address
541 ###################################################
544 def search_ipv6addr_hosts(slicename, ipv6addr):
545 hostsFilePath = get_hosts_file_path(slicename)
548 for line in fileinput.input(r'{}'.format(hostsFilePath)):
549 if ipv6addr is not None:
550 if re.search(r'{}'.format(ipv6addr), line):
553 search = re.search(r'^(.*)\s+.*$', line)
555 ipv6candidate = search.group(1)
556 ipv6candidatestrip = ipv6candidate.strip()
557 valid = is_valid_ipv6(ipv6candidatestrip)
563 logger.log("tools: FAILED to search {} in /etc/hosts file of slice={}"
564 .format(ipv6addr, slicename))
566 ###################################################
567 # Author: Guilherme Sperb Machado <gsm@machados.org>
568 ###################################################
569 # Removes all ipv6 addresses from the /etc/hosts
570 # file of a given slice
571 ###################################################
574 def remove_all_ipv6addr_hosts(slicename, node):
575 hostsFilePath = get_hosts_file_path(slicename)
577 for line in fileinput.input(r'{}'.format(hostsFilePath), inplace=True):
579 r'^(.*)\s+({}|{})$'.format(node, 'localhost'), line)
581 ipv6candidate = search.group(1)
582 ipv6candidatestrip = ipv6candidate.strip()
583 valid = is_valid_ipv6(ipv6candidatestrip)
587 logger.log("tools: REMOVED IPv6 address from /etc/hosts file of slice={}"
590 logger.log("tools: FAILED to remove the IPv6 address from /etc/hosts file of slice={}"
593 ###################################################
594 # Author: Guilherme Sperb Machado <gsm@machados.org>
595 ###################################################
596 # Adds an ipv6 address to the /etc/hosts file within a slice
597 ###################################################
600 def add_ipv6addr_hosts_line(slicename, node, ipv6addr):
601 hostsFilePath = get_hosts_file_path(slicename)
602 logger.log("tools: {}".format(hostsFilePath))
603 # debugging purposes:
604 #string = "127.0.0.1\tlocalhost\n192.168.100.179\tmyplc-node1-vm.mgmt.local\n"
605 #string = "127.0.0.1\tlocalhost\n"
607 with open(hostsFilePath, "a") as file:
608 file.write(ipv6addr + " " + node + "\n")
610 logger.log("tools: ADDED IPv6 address to /etc/hosts file of slice={}"
613 logger.log("tools: FAILED to add the IPv6 address to /etc/hosts file of slice={}"
617 # how to run a command in a slice
618 # now this is a painful matter
619 # the problem is with capsh that forces a bash command to be injected in its exec'ed command
620 # so because lxcsu uses capsh, you cannot exec anything else than bash
621 # bottom line is, what actually needs to be called is
622 # vs: vserver exec slicename command and its arguments
623 # lxc: lxcsu slicename "command and its arguments"
624 # which, OK, is no big deal as long as the command is simple enough,
625 # but do not stretch it with arguments that have spaces or need quoting as that will become a nightmare
626 def command_in_slice(slicename, argv):
627 virt = get_node_virt()
629 return ['vserver', slicename, 'exec', ] + argv
631 # wrap up argv in a single string for -c
632 return ['lxcsu', slicename, ] + [" ".join(argv)]
633 logger.log("command_in_slice: WARNING: could not find a valid virt")
640 def handler(signum, frame):
641 logger.log("Received signal {} - exiting".format(signum))
643 signal.signal(signal.SIGHUP, handler)
644 signal.signal(signal.SIGQUIT, handler)
645 signal.signal(signal.SIGINT, handler)
646 signal.signal(signal.SIGTERM, handler)