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,
172 chmod=None, remove_if_empty=False):
174 Replace a target file with a new contents
175 checks for changes: does not do anything if previous state was already right
176 can handle chmod if requested
177 can also remove resulting file if contents are void, if requested
179 writes in a tmp file, which is then renamed (from sliverauth originally)
180 returns True if a change occurred, or the file is deleted
183 with open(target) as feed:
184 current = feed.read()
187 if current == new_contents:
188 # if turns out to be an empty string, and remove_if_empty is set,
189 # then make sure to trash the file if it exists
190 if remove_if_empty and not new_contents and os.path.isfile(target):
192 "tools.replace_file_with_string: removing file {}".format(target))
198 # overwrite target file: create a temp in the same directory
199 path = os.path.dirname(target) or '.'
200 fd, name = tempfile.mkstemp('', 'repl', path)
201 os.write(fd, new_contents.encode())
203 if os.path.exists(target):
205 shutil.move(name, target)
207 os.chmod(target, chmod)
211 # utilities functions to get (cached) information from the node
214 # get node_id from /etc/planetlab/node_id and cache it
222 with open("/etc/planetlab/node_id") as f:
223 _node_id = int(f.read())
229 _root_context_arch = None
232 def root_context_arch():
233 global _root_context_arch
234 if not _root_context_arch:
235 sp = subprocess.Popen(["uname", "-i"], stdout=subprocess.PIPE)
236 (_root_context_arch, _) = sp.communicate()
237 _root_context_arch = _root_context_arch.strip()
238 return _root_context_arch
243 def __init__(self, file):
244 logger.log("tools: Lock {} initialized.".format(file), 2)
245 self.fd = os.open(file, os.O_RDWR | os.O_CREAT, 0o600)
246 flags = fcntl.fcntl(self.fd, fcntl.F_GETFD)
247 flags |= fcntl.FD_CLOEXEC
248 fcntl.fcntl(self.fd, fcntl.F_SETFD, flags)
254 logger.log("tools: Lock acquired.", 2)
255 fcntl.lockf(self.fd, fcntl.LOCK_SH)
258 logger.log("tools: Lock released.", 2)
259 fcntl.lockf(self.fd, fcntl.LOCK_UN)
262 # Utilities for getting the IP address of a LXC/Openvswitch slice. Do this by
263 # running ifconfig inside of the slice's context.
266 def get_sliver_process(slice_name, process_cmdline):
268 Utility function to find a process inside of an LXC sliver. Returns
269 (cgroup_fn, pid). cgroup_fn is the filename of the cgroup file for
270 the process, for example /proc/2592/cgroup. Pid is the process id of
271 the process. If the process is not found then (None, None) is returned.
274 cmd = 'grep {} /proc/*/cgroup | grep freezer'.format(slice_name)
275 output = os.popen(cmd).readlines()
277 # the slice couldn't be found
279 "get_sliver_process: couldn't find slice {}".format(slice_name))
287 path = l.split(':')[0]
288 comp = l.rsplit(':')[-1]
289 slice_name_check = comp.rsplit('/')[-1]
290 # the lines below were added by Guilherme <gsm@machados.org>
291 # due to the LXC requirements
292 # What we have to consider here is that libervirt on Fedora 18
293 # uses the following line:
294 # /proc/1253/cgroup:6:freezer:/machine.slice/auto_sirius.libvirt-lxc
295 # While the libvirt on Fedora 20 and 21 uses the following line:
296 # /proc/1253/cgroup:6:freezer:/machine.slice/machine-lxc\x2del_sirius.scope
297 # Further documentation on:
298 # https://libvirt.org/cgroups.html#systemdScope
299 virt = get_node_virt()
301 # This is for Fedora 20 or later
302 regexf20orlater = re.compile(r'machine-lxc\\x2d(.+).scope')
303 isf20orlater = regexf20orlater.search(slice_name_check)
305 slice_name_check = isf20orlater.group(1)
307 # This is for Fedora 18
308 slice_name_check = slice_name_check.rsplit('.')[0]
310 if (slice_name_check == slice_name):
312 pid = slice_path.split('/')[2]
313 with open('/proc/{}/cmdline'.format(pid)) as cmdfile:
314 cmdline = cmdfile.read().rstrip('\n\x00')
315 if (cmdline == process_cmdline):
316 cgroup_fn = slice_path
321 if (not cgroup_fn) or (not pid):
322 logger.log("get_sliver_process: process {} not running in slice {}"
323 .format(process_cmdline, slice_name))
326 return (cgroup_fn, pid)
328 ###################################################
329 # Added by Guilherme Sperb Machado <gsm@machados.org>
330 ###################################################
338 logger.log("Could not import 're', 'socket', or 'fileinput' python packages.")
340 # TODO: is there anything better to do if the "libvirt", "sliver_libvirt",
341 # and "sliver_lxc" are not in place?
344 from sliver_libvirt import Sliver_Libvirt
347 logger.log("Could not import 'sliver_lxc' or 'libvirt' or 'sliver_libvirt'.")
348 ###################################################
351 def get_sliver_ifconfig(slice_name, device="eth0"):
353 return the output of "ifconfig" run from inside the sliver.
355 side effects: adds "/usr/sbin" to sys.path
358 # See if setns is installed. If it's not then we're probably not running
360 if not os.path.exists("/usr/sbin/setns.so"):
363 # setns is part of lxcsu and is installed to /usr/sbin
364 if not "/usr/sbin" in sys.path:
365 sys.path.append("/usr/sbin")
368 (cgroup_fn, pid) = get_sliver_process(slice_name, "/sbin/init")
369 if (not cgroup_fn) or (not pid):
372 path = '/proc/{}/ns/net'.format(pid)
376 setns.chcontext(path)
378 args = ["/sbin/ifconfig", device]
379 sub = subprocess.Popen(
380 args, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
383 if (sub.returncode != 0):
384 logger.log("get_slice_ifconfig: error in ifconfig: {}".format(
387 result = sub.stdout.read()
389 setns.chcontext("/proc/1/ns/net")
394 def get_sliver_ip(slice_name):
395 ifconfig = get_sliver_ifconfig(slice_name)
399 for line in ifconfig.split("\n"):
400 if "inet addr:" in line:
401 # example: ' inet addr:192.168.122.189 Bcast:192.168.122.255 Mask:255.255.255.0'
402 parts = line.strip().split()
403 if len(parts) >= 2 and parts[1].startswith("addr:"):
404 return parts[1].split(":")[1]
408 ###################################################
409 # Author: Guilherme Sperb Machado <gsm@machados.org>
410 ###################################################
411 # Get the slice ipv6 address
413 ###################################################
416 def get_sliver_ipv6(slice_name):
417 ifconfig = get_sliver_ifconfig(slice_name)
421 # example: 'inet6 2001:67c:16dc:1302:5054:ff:fea7:7882 prefixlen 64 scopeid 0x0<global>'
423 r'inet6\s+(.*)\s+prefixlen\s+(\d+)\s+scopeid\s+(.+)<global>')
424 for line in ifconfig.split("\n"):
425 search = prog.search(line)
427 ipv6addr = search.group(1)
428 prefixlen = search.group(2)
429 return (ipv6addr, prefixlen)
432 ###################################################
433 # Author: Guilherme Sperb Machado <gsm@machados.org>
434 ###################################################
435 # Check if the address is a AF_INET6 family address
436 ###################################################
439 def is_valid_ipv6(ipv6addr):
441 socket.inet_pton(socket.AF_INET6, ipv6addr)
447 # this returns the kind of virtualization on the node
448 # either 'vs' or 'lxc'
449 # also caches it in /etc/planetlab/virt for next calls
450 # could be promoted to core nm if need be
451 virt_stamp = "/etc/planetlab/virt"
456 with open(virt_stamp) as f:
457 return f.read().strip()
460 logger.log("Computing virt..")
462 virt = 'vs' if subprocess.call(['vserver', '--help']) == 0 else 'lxc'
465 with open(virt_stamp, "w") as f:
470 # this return True or False to indicate that systemctl is present on that box
471 # cache result in memory as _has_systemctl
472 _has_systemctl = None
476 global _has_systemctl
477 if _has_systemctl is None:
478 _has_systemctl = (subprocess.call(['systemctl', '--help']) == 0)
479 return _has_systemctl
481 ###################################################
482 # Author: Guilherme Sperb Machado <gsm@machados.org>
483 ###################################################
484 # This method was developed to support the ipv6 plugin
486 ###################################################
489 def reboot_slivers():
491 # connecting to the libvirtd
492 connLibvirt = Sliver_Libvirt.getConnection(type)
493 domains = connLibvirt.listAllDomains()
494 for domain in domains:
496 # set the flag VIR_DOMAIN_REBOOT_INITCTL, which uses "initctl"
497 result = domain.reboot(0x04)
499 logger.log("tools: REBOOT {}".format(domain.name()))
502 except Exception as e:
503 logger.log("tools: FAILED to reboot {} ({})".format(
506 "tools: Trying to DESTROY/CREATE {} instead...".format(domain.name()))
508 result = domain.destroy()
510 logger.log("tools: DESTROYED {}".format(domain.name()))
513 "tools: FAILED in the DESTROY call of {}".format(domain.name()))
514 result = domain.create()
516 logger.log("tools: CREATED {}".format(domain.name()))
519 "tools: FAILED in the CREATE call of {}".format(domain.name()))
520 except Exception as e:
522 "tools: FAILED to DESTROY/CREATE {} ({})".format(domain.name(), e))
524 ###################################################
525 # Author: Guilherme Sperb Machado <gsm@machados.org>
526 ###################################################
527 # Get the /etc/hosts file path
528 ###################################################
531 def get_hosts_file_path(slicename):
532 containerDir = os.path.join(sliver_lxc.Sliver_LXC.CON_BASE_DIR, slicename)
533 return os.path.join(containerDir, 'etc', 'hosts')
535 ###################################################
536 # Author: Guilherme Sperb Machado <gsm@machados.org>
537 ###################################################
538 # Search if there is a specific ipv6 address in the
539 # /etc/hosts file of a given slice
540 # If the parameter 'ipv6addr' is None, then search
541 # for any ipv6 address
542 ###################################################
545 def search_ipv6addr_hosts(slicename, ipv6addr):
546 hostsFilePath = get_hosts_file_path(slicename)
549 for line in fileinput.input(r'{}'.format(hostsFilePath)):
550 if ipv6addr is not None:
551 if re.search(r'{}'.format(ipv6addr), line):
554 search = re.search(r'^(.*)\s+.*$', line)
556 ipv6candidate = search.group(1)
557 ipv6candidatestrip = ipv6candidate.strip()
558 valid = is_valid_ipv6(ipv6candidatestrip)
564 logger.log("tools: FAILED to search {} in /etc/hosts file of slice={}"
565 .format(ipv6addr, slicename))
567 ###################################################
568 # Author: Guilherme Sperb Machado <gsm@machados.org>
569 ###################################################
570 # Removes all ipv6 addresses from the /etc/hosts
571 # file of a given slice
572 ###################################################
575 def remove_all_ipv6addr_hosts(slicename, node):
576 hostsFilePath = get_hosts_file_path(slicename)
578 for line in fileinput.input(r'{}'.format(hostsFilePath), inplace=True):
580 r'^(.*)\s+({}|{})$'.format(node, 'localhost'), line)
582 ipv6candidate = search.group(1)
583 ipv6candidatestrip = ipv6candidate.strip()
584 valid = is_valid_ipv6(ipv6candidatestrip)
588 logger.log("tools: REMOVED IPv6 address from /etc/hosts file of slice={}"
591 logger.log("tools: FAILED to remove the IPv6 address from /etc/hosts file of slice={}"
594 ###################################################
595 # Author: Guilherme Sperb Machado <gsm@machados.org>
596 ###################################################
597 # Adds an ipv6 address to the /etc/hosts file within a slice
598 ###################################################
601 def add_ipv6addr_hosts_line(slicename, node, ipv6addr):
602 hostsFilePath = get_hosts_file_path(slicename)
603 logger.log("tools: {}".format(hostsFilePath))
604 # debugging purposes:
605 #string = "127.0.0.1\tlocalhost\n192.168.100.179\tmyplc-node1-vm.mgmt.local\n"
606 #string = "127.0.0.1\tlocalhost\n"
608 with open(hostsFilePath, "a") as file:
609 file.write(ipv6addr + " " + node + "\n")
611 logger.log("tools: ADDED IPv6 address to /etc/hosts file of slice={}"
614 logger.log("tools: FAILED to add the IPv6 address to /etc/hosts file of slice={}"
618 # how to run a command in a slice
619 # now this is a painful matter
620 # the problem is with capsh that forces a bash command to be injected in its exec'ed command
621 # so because lxcsu uses capsh, you cannot exec anything else than bash
622 # bottom line is, what actually needs to be called is
623 # vs: vserver exec slicename command and its arguments
624 # lxc: lxcsu slicename "command and its arguments"
625 # which, OK, is no big deal as long as the command is simple enough,
626 # but do not stretch it with arguments that have spaces or need quoting as that will become a nightmare
627 def command_in_slice(slicename, argv):
628 virt = get_node_virt()
630 return ['vserver', slicename, 'exec', ] + argv
632 # wrap up argv in a single string for -c
633 return ['lxcsu', slicename, ] + [" ".join(argv)]
634 logger.log("command_in_slice: WARNING: could not find a valid virt")
641 def handler(signum, frame):
642 logger.log("Received signal {} - exiting".format(signum))
644 signal.signal(signal.SIGHUP, handler)
645 signal.signal(signal.SIGQUIT, handler)
646 signal.signal(signal.SIGINT, handler)
647 signal.signal(signal.SIGTERM, handler)