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, binary=False):
158 fd, temporary_filename = tempfile.mkstemp()
160 os.chmod(temporary_filename, mode)
162 os.chown(temporary_filename, *uidgid)
163 open_mode = 'wb' if binary else 'w'
164 f = os.fdopen(fd, open_mode)
169 return temporary_filename
172 def replace_file_with_string(target, new_contents,
173 chmod=None, remove_if_empty=False):
175 Replace a target file with a new contents
176 checks for changes: does not do anything if previous state was already right
177 can handle chmod if requested
178 can also remove resulting file if contents are void, if requested
180 writes in a tmp file, which is then renamed (from sliverauth originally)
181 returns True if a change occurred, or the file is deleted
184 with open(target) as feed:
185 current = feed.read()
188 if current == new_contents:
189 # if turns out to be an empty string, and remove_if_empty is set,
190 # then make sure to trash the file if it exists
191 if remove_if_empty and not new_contents and os.path.isfile(target):
193 "tools.replace_file_with_string: removing file {}".format(target))
199 # overwrite target file: create a temp in the same directory
200 path = os.path.dirname(target) or '.'
201 fd, name = tempfile.mkstemp('', 'repl', path)
202 os.write(fd, new_contents.encode())
204 if os.path.exists(target):
206 shutil.move(name, target)
208 os.chmod(target, chmod)
212 # utilities functions to get (cached) information from the node
215 # get node_id from /etc/planetlab/node_id and cache it
223 with open("/etc/planetlab/node_id") as f:
224 _node_id = int(f.read())
230 _root_context_arch = None
233 def root_context_arch():
234 global _root_context_arch
235 if not _root_context_arch:
236 sp = subprocess.Popen(["uname", "-i"], stdout=subprocess.PIPE)
237 (_root_context_arch, _) = sp.communicate()
238 _root_context_arch = _root_context_arch.strip()
239 return _root_context_arch
244 def __init__(self, file):
245 logger.log("tools: Lock {} initialized.".format(file), 2)
246 self.fd = os.open(file, os.O_RDWR | os.O_CREAT, 0o600)
247 flags = fcntl.fcntl(self.fd, fcntl.F_GETFD)
248 flags |= fcntl.FD_CLOEXEC
249 fcntl.fcntl(self.fd, fcntl.F_SETFD, flags)
255 logger.log("tools: Lock acquired.", 2)
256 fcntl.lockf(self.fd, fcntl.LOCK_SH)
259 logger.log("tools: Lock released.", 2)
260 fcntl.lockf(self.fd, fcntl.LOCK_UN)
263 # Utilities for getting the IP address of a LXC/Openvswitch slice. Do this by
264 # running ifconfig inside of the slice's context.
267 def get_sliver_process(slice_name, process_cmdline):
269 Utility function to find a process inside of an LXC sliver. Returns
270 (cgroup_fn, pid). cgroup_fn is the filename of the cgroup file for
271 the process, for example /proc/2592/cgroup. Pid is the process id of
272 the process. If the process is not found then (None, None) is returned.
275 cmd = 'grep {} /proc/*/cgroup | grep freezer'.format(slice_name)
276 output = os.popen(cmd).readlines()
278 # the slice couldn't be found
280 "get_sliver_process: couldn't find slice {}".format(slice_name))
288 path = l.split(':')[0]
289 comp = l.rsplit(':')[-1]
290 slice_name_check = comp.rsplit('/')[-1]
291 # the lines below were added by Guilherme <gsm@machados.org>
292 # due to the LXC requirements
293 # What we have to consider here is that libervirt on Fedora 18
294 # uses the following line:
295 # /proc/1253/cgroup:6:freezer:/machine.slice/auto_sirius.libvirt-lxc
296 # While the libvirt on Fedora 20 and 21 uses the following line:
297 # /proc/1253/cgroup:6:freezer:/machine.slice/machine-lxc\x2del_sirius.scope
298 # Further documentation on:
299 # https://libvirt.org/cgroups.html#systemdScope
300 virt = get_node_virt()
302 # This is for Fedora 20 or later
303 regexf20orlater = re.compile(r'machine-lxc\\x2d(.+).scope')
304 isf20orlater = regexf20orlater.search(slice_name_check)
306 slice_name_check = isf20orlater.group(1)
308 # This is for Fedora 18
309 slice_name_check = slice_name_check.rsplit('.')[0]
311 if (slice_name_check == slice_name):
313 pid = slice_path.split('/')[2]
314 with open('/proc/{}/cmdline'.format(pid)) as cmdfile:
315 cmdline = cmdfile.read().rstrip('\n\x00')
316 if (cmdline == process_cmdline):
317 cgroup_fn = slice_path
322 if (not cgroup_fn) or (not pid):
323 logger.log("get_sliver_process: process {} not running in slice {}"
324 .format(process_cmdline, slice_name))
327 return (cgroup_fn, pid)
329 ###################################################
330 # Added by Guilherme Sperb Machado <gsm@machados.org>
331 ###################################################
339 logger.log("Could not import 're', 'socket', or 'fileinput' python packages.")
341 # TODO: is there anything better to do if the "libvirt", "sliver_libvirt",
342 # and "sliver_lxc" are not in place?
345 from sliver_libvirt import Sliver_Libvirt
348 logger.log("Could not import 'sliver_lxc' or 'libvirt' or 'sliver_libvirt'.")
349 ###################################################
352 def get_sliver_ifconfig(slice_name, device="eth0"):
354 return the output of "ifconfig" run from inside the sliver.
356 side effects: adds "/usr/sbin" to sys.path
359 # See if setns is installed. If it's not then we're probably not running
361 if not os.path.exists("/usr/sbin/setns.so"):
364 # setns is part of lxcsu and is installed to /usr/sbin
365 if not "/usr/sbin" in sys.path:
366 sys.path.append("/usr/sbin")
369 (cgroup_fn, pid) = get_sliver_process(slice_name, "/sbin/init")
370 if (not cgroup_fn) or (not pid):
373 path = '/proc/{}/ns/net'.format(pid)
377 setns.chcontext(path)
379 args = ["/sbin/ifconfig", device]
380 sub = subprocess.Popen(
381 args, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
384 if (sub.returncode != 0):
385 logger.log("get_slice_ifconfig: error in ifconfig: {}".format(
388 result = sub.stdout.read()
390 setns.chcontext("/proc/1/ns/net")
395 def get_sliver_ip(slice_name):
396 ifconfig = get_sliver_ifconfig(slice_name)
400 for line in ifconfig.split("\n"):
401 if "inet addr:" in line:
402 # example: ' inet addr:192.168.122.189 Bcast:192.168.122.255 Mask:255.255.255.0'
403 parts = line.strip().split()
404 if len(parts) >= 2 and parts[1].startswith("addr:"):
405 return parts[1].split(":")[1]
409 ###################################################
410 # Author: Guilherme Sperb Machado <gsm@machados.org>
411 ###################################################
412 # Get the slice ipv6 address
414 ###################################################
417 def get_sliver_ipv6(slice_name):
418 ifconfig = get_sliver_ifconfig(slice_name)
422 # example: 'inet6 2001:67c:16dc:1302:5054:ff:fea7:7882 prefixlen 64 scopeid 0x0<global>'
424 r'inet6\s+(.*)\s+prefixlen\s+(\d+)\s+scopeid\s+(.+)<global>')
425 for line in ifconfig.split("\n"):
426 search = prog.search(line)
428 ipv6addr = search.group(1)
429 prefixlen = search.group(2)
430 return (ipv6addr, prefixlen)
433 ###################################################
434 # Author: Guilherme Sperb Machado <gsm@machados.org>
435 ###################################################
436 # Check if the address is a AF_INET6 family address
437 ###################################################
440 def is_valid_ipv6(ipv6addr):
442 socket.inet_pton(socket.AF_INET6, ipv6addr)
448 # this returns the kind of virtualization on the node
449 # either 'vs' or 'lxc'
450 # also caches it in /etc/planetlab/virt for next calls
451 # could be promoted to core nm if need be
452 virt_stamp = "/etc/planetlab/virt"
457 with open(virt_stamp) as f:
458 return f.read().strip()
461 logger.log("Computing virt..")
463 virt = 'vs' if subprocess.call(['vserver', '--help']) == 0 else 'lxc'
466 with open(virt_stamp, "w") as f:
471 # this return True or False to indicate that systemctl is present on that box
472 # cache result in memory as _has_systemctl
473 _has_systemctl = None
477 global _has_systemctl
478 if _has_systemctl is None:
479 _has_systemctl = (subprocess.call(['systemctl', '--help']) == 0)
480 return _has_systemctl
482 ###################################################
483 # Author: Guilherme Sperb Machado <gsm@machados.org>
484 ###################################################
485 # This method was developed to support the ipv6 plugin
487 ###################################################
490 def reboot_slivers():
492 # connecting to the libvirtd
493 connLibvirt = Sliver_Libvirt.getConnection(type)
494 domains = connLibvirt.listAllDomains()
495 for domain in domains:
497 # set the flag VIR_DOMAIN_REBOOT_INITCTL, which uses "initctl"
498 result = domain.reboot(0x04)
500 logger.log("tools: REBOOT {}".format(domain.name()))
503 except Exception as e:
504 logger.log("tools: FAILED to reboot {} ({})".format(
507 "tools: Trying to DESTROY/CREATE {} instead...".format(domain.name()))
509 result = domain.destroy()
511 logger.log("tools: DESTROYED {}".format(domain.name()))
514 "tools: FAILED in the DESTROY call of {}".format(domain.name()))
515 result = domain.create()
517 logger.log("tools: CREATED {}".format(domain.name()))
520 "tools: FAILED in the CREATE call of {}".format(domain.name()))
521 except Exception as e:
523 "tools: FAILED to DESTROY/CREATE {} ({})".format(domain.name(), e))
525 ###################################################
526 # Author: Guilherme Sperb Machado <gsm@machados.org>
527 ###################################################
528 # Get the /etc/hosts file path
529 ###################################################
532 def get_hosts_file_path(slicename):
533 containerDir = os.path.join(sliver_lxc.Sliver_LXC.CON_BASE_DIR, slicename)
534 return os.path.join(containerDir, 'etc', 'hosts')
536 ###################################################
537 # Author: Guilherme Sperb Machado <gsm@machados.org>
538 ###################################################
539 # Search if there is a specific ipv6 address in the
540 # /etc/hosts file of a given slice
541 # If the parameter 'ipv6addr' is None, then search
542 # for any ipv6 address
543 ###################################################
546 def search_ipv6addr_hosts(slicename, ipv6addr):
547 hostsFilePath = get_hosts_file_path(slicename)
550 for line in fileinput.input(r'{}'.format(hostsFilePath)):
551 if ipv6addr is not None:
552 if re.search(r'{}'.format(ipv6addr), line):
555 search = re.search(r'^(.*)\s+.*$', line)
557 ipv6candidate = search.group(1)
558 ipv6candidatestrip = ipv6candidate.strip()
559 valid = is_valid_ipv6(ipv6candidatestrip)
565 logger.log("tools: FAILED to search {} in /etc/hosts file of slice={}"
566 .format(ipv6addr, slicename))
568 ###################################################
569 # Author: Guilherme Sperb Machado <gsm@machados.org>
570 ###################################################
571 # Removes all ipv6 addresses from the /etc/hosts
572 # file of a given slice
573 ###################################################
576 def remove_all_ipv6addr_hosts(slicename, node):
577 hostsFilePath = get_hosts_file_path(slicename)
579 for line in fileinput.input(r'{}'.format(hostsFilePath), inplace=True):
581 r'^(.*)\s+({}|{})$'.format(node, 'localhost'), line)
583 ipv6candidate = search.group(1)
584 ipv6candidatestrip = ipv6candidate.strip()
585 valid = is_valid_ipv6(ipv6candidatestrip)
589 logger.log("tools: REMOVED IPv6 address from /etc/hosts file of slice={}"
592 logger.log("tools: FAILED to remove the IPv6 address from /etc/hosts file of slice={}"
595 ###################################################
596 # Author: Guilherme Sperb Machado <gsm@machados.org>
597 ###################################################
598 # Adds an ipv6 address to the /etc/hosts file within a slice
599 ###################################################
602 def add_ipv6addr_hosts_line(slicename, node, ipv6addr):
603 hostsFilePath = get_hosts_file_path(slicename)
604 logger.log("tools: {}".format(hostsFilePath))
605 # debugging purposes:
606 #string = "127.0.0.1\tlocalhost\n192.168.100.179\tmyplc-node1-vm.mgmt.local\n"
607 #string = "127.0.0.1\tlocalhost\n"
609 with open(hostsFilePath, "a") as file:
610 file.write(ipv6addr + " " + node + "\n")
612 logger.log("tools: ADDED IPv6 address to /etc/hosts file of slice={}"
615 logger.log("tools: FAILED to add the IPv6 address to /etc/hosts file of slice={}"
619 # how to run a command in a slice
620 # now this is a painful matter
621 # the problem is with capsh that forces a bash command to be injected in its exec'ed command
622 # so because lxcsu uses capsh, you cannot exec anything else than bash
623 # bottom line is, what actually needs to be called is
624 # vs: vserver exec slicename command and its arguments
625 # lxc: lxcsu slicename "command and its arguments"
626 # which, OK, is no big deal as long as the command is simple enough,
627 # but do not stretch it with arguments that have spaces or need quoting as that will become a nightmare
628 def command_in_slice(slicename, argv):
629 virt = get_node_virt()
631 return ['vserver', slicename, 'exec', ] + argv
633 # wrap up argv in a single string for -c
634 return ['lxcsu', slicename, ] + [" ".join(argv)]
635 logger.log("command_in_slice: WARNING: could not find a valid virt")
642 def handler(signum, frame):
643 logger.log("Received signal {} - exiting".format(signum))
645 signal.signal(signal.SIGHUP, handler)
646 signal.signal(signal.SIGQUIT, handler)
647 signal.signal(signal.SIGINT, handler)
648 signal.signal(signal.SIGTERM, handler)