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):
56 Call function <run> with no arguments in its own thread.
58 thr = threading.Thread(target=run)
63 def close_nonstandard_fds():
65 Close all open file descriptors other than 0, 1, and 2.
68 for fd in range(3, os.sysconf(_SC_OPEN_MAX)):
72 pass # most likely an fd that isn't open
74 # after http://www.erlenstar.demon.co.uk/unix/faq_2.html
79 Daemonize the current process.
88 devnull = os.open(os.devnull, os.O_RDWR)
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)
97 def fork_as(su, function, *args):
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
105 child_pid = os.fork()
109 close_nonstandard_fds()
111 pw_ent = pwd.getpwnam(su)
112 os.setegid(pw_ent[3])
113 os.seteuid(pw_ent[2])
114 child_pid = os.fork()
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")
123 os.waitpid(child_pid, 0)
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.
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())
141 # check for a process with that pid by sending signal 0
143 os.kill(other_pid, 0)
145 if e.errno == errno.ESRCH:
146 other_pid = None # doesn't exist
149 if other_pid == None:
150 # write a new pid file
151 write_file(PID_FILE, lambda f: f.write(str(os.getpid())))
155 def write_file(filename, do_write, **kw_args):
157 Write file <filename> atomically by opening a temporary file,
158 using <do_write> to write that file, and then renaming the temporary file.
160 shutil.move(write_temp_file(do_write, **kw_args), filename)
163 def write_temp_file(do_write, mode=None, uidgid=None, binary=False):
164 fd, temporary_filename = tempfile.mkstemp()
166 os.chmod(temporary_filename, mode)
168 os.chown(temporary_filename, *uidgid)
169 open_mode = 'wb' if binary else 'w'
170 f = os.fdopen(fd, open_mode)
175 return temporary_filename
178 def replace_file_with_string(target, new_contents,
179 chmod=None, remove_if_empty=False):
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
188 with open(target) as feed:
189 current = feed.read()
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):
197 "tools.replace_file_with_string: removing file {}".format(target))
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())
208 if os.path.exists(target):
210 shutil.move(name, target)
212 os.chmod(target, chmod)
216 # utilities functions to get (cached) information from the node
219 # get node_id from /etc/planetlab/node_id and cache it
227 with open("/etc/planetlab/node_id") as f:
228 _node_id = int(f.read())
234 _root_context_arch = None
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
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)
259 logger.log("tools: Lock acquired.", 2)
260 fcntl.lockf(self.fd, fcntl.LOCK_SH)
263 logger.log("tools: Lock released.", 2)
264 fcntl.lockf(self.fd, fcntl.LOCK_UN)
267 # Utilities for getting the IP address of a LXC/Openvswitch slice. Do this by
268 # running ifconfig inside of the slice's context.
271 def get_sliver_process(slice_name, process_cmdline):
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.
279 cmd = 'grep {} /proc/*/cgroup | grep freezer'.format(slice_name)
280 output = os.popen(cmd).readlines()
282 # the slice couldn't be found
284 "get_sliver_process: couldn't find slice {}".format(slice_name))
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()
306 # This is for Fedora 20 or later
307 regexf20orlater = re.compile(r'machine-lxc\\x2d(.+).scope')
308 isf20orlater = regexf20orlater.search(slice_name_check)
310 slice_name_check = isf20orlater.group(1)
312 # This is for Fedora 18
313 slice_name_check = slice_name_check.rsplit('.')[0]
315 if (slice_name_check == slice_name):
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
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))
331 return (cgroup_fn, pid)
333 ###################################################
334 # Added by Guilherme Sperb Machado <gsm@machados.org>
335 ###################################################
343 logger.log("Could not import 're', 'socket', or 'fileinput' python packages.")
345 # TODO: is there anything better to do if the "libvirt", "sliver_libvirt",
346 # and "sliver_lxc" are not in place?
349 from sliver_libvirt import Sliver_Libvirt
352 logger.log("Could not import 'sliver_lxc' or 'libvirt' or 'sliver_libvirt'.")
353 ###################################################
356 def get_sliver_ifconfig(slice_name, device="eth0"):
358 return the output of "ifconfig" run from inside the sliver.
360 side effects: adds "/usr/sbin" to sys.path
363 # See if setns is installed. If it's not then we're probably not running
365 if not os.path.exists("/usr/sbin/setns.so"):
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")
373 (cgroup_fn, pid) = get_sliver_process(slice_name, "/sbin/init")
374 if (not cgroup_fn) or (not pid):
377 path = '/proc/{}/ns/net'.format(pid)
381 setns.chcontext(path)
383 args = ["/sbin/ifconfig", device]
384 sub = subprocess.Popen(
385 args, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
388 if (sub.returncode != 0):
389 logger.log("get_slice_ifconfig: error in ifconfig: {}".format(
392 result = sub.stdout.read()
394 setns.chcontext("/proc/1/ns/net")
399 def get_sliver_ip(slice_name):
400 ifconfig = get_sliver_ifconfig(slice_name)
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]
413 ###################################################
414 # Author: Guilherme Sperb Machado <gsm@machados.org>
415 ###################################################
416 # Get the slice ipv6 address
418 ###################################################
421 def get_sliver_ipv6(slice_name):
422 ifconfig = get_sliver_ifconfig(slice_name)
426 # example: 'inet6 2001:67c:16dc:1302:5054:ff:fea7:7882 prefixlen 64 scopeid 0x0<global>'
428 r'inet6\s+(.*)\s+prefixlen\s+(\d+)\s+scopeid\s+(.+)<global>')
429 for line in ifconfig.split("\n"):
430 search = prog.search(line)
432 ipv6addr = search.group(1)
433 prefixlen = search.group(2)
434 return (ipv6addr, prefixlen)
437 ###################################################
438 # Author: Guilherme Sperb Machado <gsm@machados.org>
439 ###################################################
440 # Check if the address is a AF_INET6 family address
441 ###################################################
444 def is_valid_ipv6(ipv6addr):
446 socket.inet_pton(socket.AF_INET6, ipv6addr)
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"
461 with open(virt_stamp) as f:
462 return f.read().strip()
465 logger.log("Computing virt..")
467 virt = 'vs' if subprocess.call(['vserver', '--help']) == 0 else 'lxc'
470 with open(virt_stamp, "w") as f:
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
481 global _has_systemctl
482 if _has_systemctl is None:
483 _has_systemctl = (subprocess.call(['systemctl', '--help']) == 0)
484 return _has_systemctl
486 ###################################################
487 # Author: Guilherme Sperb Machado <gsm@machados.org>
488 ###################################################
489 # This method was developed to support the ipv6 plugin
491 ###################################################
494 def reboot_slivers():
496 # connecting to the libvirtd
497 connLibvirt = Sliver_Libvirt.getConnection(type)
498 domains = connLibvirt.listAllDomains()
499 for domain in domains:
501 # set the flag VIR_DOMAIN_REBOOT_INITCTL, which uses "initctl"
502 result = domain.reboot(0x04)
504 logger.log("tools: REBOOT {}".format(domain.name()))
507 except Exception as e:
508 logger.log("tools: FAILED to reboot {} ({})".format(
511 "tools: Trying to DESTROY/CREATE {} instead...".format(domain.name()))
513 result = domain.destroy()
515 logger.log("tools: DESTROYED {}".format(domain.name()))
518 "tools: FAILED in the DESTROY call of {}".format(domain.name()))
519 result = domain.create()
521 logger.log("tools: CREATED {}".format(domain.name()))
524 "tools: FAILED in the CREATE call of {}".format(domain.name()))
525 except Exception as e:
527 "tools: FAILED to DESTROY/CREATE {} ({})".format(domain.name(), e))
529 ###################################################
530 # Author: Guilherme Sperb Machado <gsm@machados.org>
531 ###################################################
532 # Get the /etc/hosts file path
533 ###################################################
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')
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 ###################################################
550 def search_ipv6addr_hosts(slicename, ipv6addr):
551 hostsFilePath = get_hosts_file_path(slicename)
554 for line in fileinput.input(r'{}'.format(hostsFilePath)):
555 if ipv6addr is not None:
556 if re.search(r'{}'.format(ipv6addr), line):
559 search = re.search(r'^(.*)\s+.*$', line)
561 ipv6candidate = search.group(1)
562 ipv6candidatestrip = ipv6candidate.strip()
563 valid = is_valid_ipv6(ipv6candidatestrip)
569 logger.log("tools: FAILED to search {} in /etc/hosts file of slice={}"
570 .format(ipv6addr, slicename))
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 ###################################################
580 def remove_all_ipv6addr_hosts(slicename, node):
581 hostsFilePath = get_hosts_file_path(slicename)
583 for line in fileinput.input(r'{}'.format(hostsFilePath), inplace=True):
585 r'^(.*)\s+({}|{})$'.format(node, 'localhost'), line)
587 ipv6candidate = search.group(1)
588 ipv6candidatestrip = ipv6candidate.strip()
589 valid = is_valid_ipv6(ipv6candidatestrip)
593 logger.log("tools: REMOVED IPv6 address from /etc/hosts file of slice={}"
596 logger.log("tools: FAILED to remove the IPv6 address from /etc/hosts file of slice={}"
599 ###################################################
600 # Author: Guilherme Sperb Machado <gsm@machados.org>
601 ###################################################
602 # Adds an ipv6 address to the /etc/hosts file within a slice
603 ###################################################
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"
613 with open(hostsFilePath, "a") as file:
614 file.write(ipv6addr + " " + node + "\n")
616 logger.log("tools: ADDED IPv6 address to /etc/hosts file of slice={}"
619 logger.log("tools: FAILED to add the IPv6 address to /etc/hosts file of slice={}"
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()
635 return ['vserver', slicename, 'exec', ] + argv
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")
646 def handler(signum, frame):
647 logger.log("Received signal {} - exiting".format(signum))
649 signal.signal(signal.SIGHUP, handler)
650 signal.signal(signal.SIGQUIT, handler)
651 signal.signal(signal.SIGINT, handler)
652 signal.signal(signal.SIGTERM, handler)