1 # -*- python-indent: 4 -*-
3 """A few things that didn't seem to fit anywhere else."""
18 PID_FILE = '/var/run/nodemanager.pid'
22 interface = get_if_from_hwaddr(get_hwaddr_from_plnode())
23 if not interface: interface = "eth0"
26 def get_hwaddr_from_plnode():
28 for line in open("/usr/boot/plnode.txt", 'r').readlines():
29 if line.startswith("NET_DEVICE"):
30 return line.split("=")[1].strip().strip('"')
35 def get_if_from_hwaddr(hwaddr):
39 dev_hwaddr = sioc.gifhwaddr(dev)
40 if dev_hwaddr == hwaddr: return dev
45 def as_daemon_thread(run):
46 """Call function <run> with no arguments in its own thread."""
47 thr = threading.Thread(target=run)
51 def close_nonstandard_fds():
52 """Close all open file descriptors other than 0, 1, and 2."""
54 for fd in range(3, os.sysconf(_SC_OPEN_MAX)):
56 except OSError: pass # most likely an fd that isn't open
58 # after http://www.erlenstar.demon.co.uk/unix/faq_2.html
60 """Daemonize the current process."""
61 if os.fork() != 0: os._exit(0)
63 if os.fork() != 0: os._exit(0)
66 devnull = os.open(os.devnull, os.O_RDWR)
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)
73 def fork_as(su, function, *args):
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.
86 close_nonstandard_fds()
88 pw_ent = pwd.getpwnam(su)
92 if child_pid == 0: function(*args)
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")
98 else: os.waitpid(child_pid, 0)
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.
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())
113 # check for a process with that pid by sending signal 0
114 try: os.kill(other_pid, 0)
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())))
123 def write_file(filename, do_write, **kw_args):
125 Write file <filename> atomically by opening a temporary file,
126 using <do_write> to write that file, and then renaming the temporary file.
128 shutil.move(write_temp_file(do_write, **kw_args), filename)
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')
137 return temporary_filename
139 def replace_file_with_string (target, new_contents, chmod=None, remove_if_empty=False):
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
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
150 with open(target) as f:
154 if current == new_contents:
155 # if turns out to be an empty string, and remove_if_empty is set,
156 # then make sure to trash the file if it exists
157 if remove_if_empty and not new_contents and os.path.isfile(target):
158 logger.verbose("tools.replace_file_with_string: removing file {}".format(target))
159 try: os.unlink(target)
162 # overwrite target file: create a temp in the same directory
163 path = os.path.dirname(target) or '.'
164 fd, name = tempfile.mkstemp('', 'repl', path)
165 os.write(fd, new_contents)
167 if os.path.exists(target):
169 shutil.move(name, target)
170 if chmod: os.chmod(target, chmod)
174 # utilities functions to get (cached) information from the node
176 # get node_id from /etc/planetlab/node_id and cache it
182 with open("/etc/planetlab/node_id") as f:
183 _node_id = int(f.read())
188 _root_context_arch = None
189 def root_context_arch():
190 global _root_context_arch
191 if not _root_context_arch:
192 sp = subprocess.Popen(["uname", "-i"], stdout=subprocess.PIPE)
193 (_root_context_arch, _) = sp.communicate()
194 _root_context_arch = _root_context_arch.strip()
195 return _root_context_arch
200 def __init__(self, file):
201 logger.log("tools: Lock {} initialized.".format(file), 2)
202 self.fd = os.open(file, os.O_RDWR|os.O_CREAT, 0600)
203 flags = fcntl.fcntl(self.fd, fcntl.F_GETFD)
204 flags |= fcntl.FD_CLOEXEC
205 fcntl.fcntl(self.fd, fcntl.F_SETFD, flags)
209 logger.log("tools: Lock acquired.", 2)
210 fcntl.lockf(self.fd, fcntl.LOCK_SH)
212 logger.log("tools: Lock released.", 2)
213 fcntl.lockf(self.fd, fcntl.LOCK_UN)
216 # Utilities for getting the IP address of a LXC/Openvswitch slice. Do this by
217 # running ifconfig inside of the slice's context.
219 def get_sliver_process(slice_name, process_cmdline):
221 Utility function to find a process inside of an LXC sliver. Returns
222 (cgroup_fn, pid). cgroup_fn is the filename of the cgroup file for
223 the process, for example /proc/2592/cgroup. Pid is the process id of
224 the process. If the process is not found then (None, None) is returned.
227 cmd = 'grep {} /proc/*/cgroup | grep freezer'.format(slice_name)
228 output = os.popen(cmd).readlines()
230 # the slice couldn't be found
231 logger.log("get_sliver_process: couldn't find slice {}".format(slice_name))
239 path = l.split(':')[0]
240 comp = l.rsplit(':')[-1]
241 slice_name_check = comp.rsplit('/')[-1]
242 # the lines below were added by Guilherme <gsm@machados.org>
243 # due to the LXC requirements
244 # What we have to consider here is that libervirt on Fedora 18
245 # uses the following line:
246 # /proc/1253/cgroup:6:freezer:/machine.slice/auto_sirius.libvirt-lxc
247 # While the libvirt on Fedora 20 and 21 uses the following line:
248 # /proc/1253/cgroup:6:freezer:/machine.slice/machine-lxc\x2del_sirius.scope
249 # Further documentation on:
250 # https://libvirt.org/cgroups.html#systemdScope
251 virt = get_node_virt()
253 # This is for Fedora 20 or later
254 regexf20orlater = re.compile(r'machine-lxc\\x2d(.+).scope')
255 isf20orlater = regexf20orlater.search(slice_name_check)
257 slice_name_check = isf20orlater.group(1)
259 # This is for Fedora 18
260 slice_name_check = slice_name_check.rsplit('.')[0]
262 if (slice_name_check == slice_name):
264 pid = slice_path.split('/')[2]
265 with open('/proc/{}/cmdline'.format(pid)) as cmdfile:
266 cmdline = cmdfile.read().rstrip('\n\x00')
267 if (cmdline == process_cmdline):
268 cgroup_fn = slice_path
273 if (not cgroup_fn) or (not pid):
274 logger.log("get_sliver_process: process {} not running in slice {}"
275 .format(process_cmdline, slice_name))
278 return (cgroup_fn, pid)
280 ###################################################
281 # Added by Guilherme Sperb Machado <gsm@machados.org>
282 ###################################################
289 logger.log("Could not import 're', 'socket', or 'fileinput' python packages.")
291 # TODO: is there anything better to do if the "libvirt", "sliver_libvirt",
292 # and "sliver_lxc" are not in place?
295 from sliver_libvirt import Sliver_Libvirt
298 logger.log("Could not import 'sliver_lxc' or 'libvirt' or 'sliver_libvirt'.")
299 ###################################################
301 def get_sliver_ifconfig(slice_name, device="eth0"):
303 return the output of "ifconfig" run from inside the sliver.
305 side effects: adds "/usr/sbin" to sys.path
308 # See if setns is installed. If it's not then we're probably not running
310 if not os.path.exists("/usr/sbin/setns.so"):
313 # setns is part of lxcsu and is installed to /usr/sbin
314 if not "/usr/sbin" in sys.path:
315 sys.path.append("/usr/sbin")
318 (cgroup_fn, pid) = get_sliver_process(slice_name, "/sbin/init")
319 if (not cgroup_fn) or (not pid):
322 path = '/proc/{}/ns/net'.format(pid)
326 setns.chcontext(path)
328 args = ["/sbin/ifconfig", device]
329 sub = subprocess.Popen(args, stderr = subprocess.PIPE, stdout = subprocess.PIPE)
332 if (sub.returncode != 0):
333 logger.log("get_slice_ifconfig: error in ifconfig: {}".format(sub.stderr.read()))
335 result = sub.stdout.read()
337 setns.chcontext("/proc/1/ns/net")
341 def get_sliver_ip(slice_name):
342 ifconfig = get_sliver_ifconfig(slice_name)
346 for line in ifconfig.split("\n"):
347 if "inet addr:" in line:
348 # example: ' inet addr:192.168.122.189 Bcast:192.168.122.255 Mask:255.255.255.0'
349 parts = line.strip().split()
350 if len(parts) >= 2 and parts[1].startswith("addr:"):
351 return parts[1].split(":")[1]
355 ###################################################
356 # Author: Guilherme Sperb Machado <gsm@machados.org>
357 ###################################################
358 # Get the slice ipv6 address
360 ###################################################
361 def get_sliver_ipv6(slice_name):
362 ifconfig = get_sliver_ifconfig(slice_name)
366 # example: 'inet6 2001:67c:16dc:1302:5054:ff:fea7:7882 prefixlen 64 scopeid 0x0<global>'
367 prog = re.compile(r'inet6\s+(.*)\s+prefixlen\s+(\d+)\s+scopeid\s+(.+)<global>')
368 for line in ifconfig.split("\n"):
369 search = prog.search(line)
371 ipv6addr = search.group(1)
372 prefixlen = search.group(2)
373 return (ipv6addr, prefixlen)
376 ###################################################
377 # Author: Guilherme Sperb Machado <gsm@machados.org>
378 ###################################################
379 # Check if the address is a AF_INET6 family address
380 ###################################################
381 def is_valid_ipv6(ipv6addr):
383 socket.inet_pton(socket.AF_INET6, ipv6addr)
388 ### this returns the kind of virtualization on the node
389 # either 'vs' or 'lxc'
390 # also caches it in /etc/planetlab/virt for next calls
391 # could be promoted to core nm if need be
392 virt_stamp = "/etc/planetlab/virt"
393 def get_node_virt ():
395 with open(virt_stamp) as f:
396 return f.read().strip()
399 logger.log("Computing virt..")
401 virt = 'vs' if subprocess.call ([ 'vserver', '--help' ]) == 0 else 'lxc'
404 with file(virt_stamp, "w") as f:
408 ### this return True or False to indicate that systemctl is present on that box
409 # cache result in memory as _has_systemctl
410 _has_systemctl = None
411 def has_systemctl ():
412 global _has_systemctl
413 if _has_systemctl is None:
414 _has_systemctl = (subprocess.call([ 'systemctl', '--help' ]) == 0)
415 return _has_systemctl
417 ###################################################
418 # Author: Guilherme Sperb Machado <gsm@machados.org>
419 ###################################################
420 # This method was developed to support the ipv6 plugin
422 ###################################################
423 def reboot_slivers():
425 # connecting to the libvirtd
426 connLibvirt = Sliver_Libvirt.getConnection(type)
427 domains = connLibvirt.listAllDomains()
428 for domain in domains:
430 # set the flag VIR_DOMAIN_REBOOT_INITCTL, which uses "initctl"
431 result = domain.reboot(0x04)
433 logger.log("tools: REBOOT {}".format(domain.name()) )
437 logger.log("tools: FAILED to reboot {} ({})".format(domain.name(), e) )
438 logger.log("tools: Trying to DESTROY/CREATE {} instead...".format(domain.name()) )
440 result = domain.destroy()
442 logger.log("tools: DESTROYED {}".format(domain.name()) )
443 else: logger.log("tools: FAILED in the DESTROY call of {}".format(domain.name()) )
444 result = domain.create()
446 logger.log("tools: CREATED {}".format(domain.name()) )
447 else: logger.log("tools: FAILED in the CREATE call of {}".format(domain.name()) )
449 logger.log("tools: FAILED to DESTROY/CREATE {} ({})".format(domain.name(), e) )
451 ###################################################
452 # Author: Guilherme Sperb Machado <gsm@machados.org>
453 ###################################################
454 # Get the /etc/hosts file path
455 ###################################################
456 def get_hosts_file_path(slicename):
457 containerDir = os.path.join(sliver_lxc.Sliver_LXC.CON_BASE_DIR, slicename)
458 return os.path.join(containerDir, 'etc', 'hosts')
460 ###################################################
461 # Author: Guilherme Sperb Machado <gsm@machados.org>
462 ###################################################
463 # Search if there is a specific ipv6 address in the
464 # /etc/hosts file of a given slice
465 # If the parameter 'ipv6addr' is None, then search
466 # for any ipv6 address
467 ###################################################
468 def search_ipv6addr_hosts(slicename, ipv6addr):
469 hostsFilePath = get_hosts_file_path(slicename)
472 for line in fileinput.input(r'{}'.format(hostsFilePath)):
473 if ipv6addr is not None:
474 if re.search(r'{}'.format(ipv6addr), line):
477 search = re.search(r'^(.*)\s+.*$', line)
479 ipv6candidate = search.group(1)
480 ipv6candidatestrip = ipv6candidate.strip()
481 valid = is_valid_ipv6(ipv6candidatestrip)
487 logger.log("tools: FAILED to search {} in /etc/hosts file of slice={}"
488 .format(ipv6addr, slicename))
490 ###################################################
491 # Author: Guilherme Sperb Machado <gsm@machados.org>
492 ###################################################
493 # Removes all ipv6 addresses from the /etc/hosts
494 # file of a given slice
495 ###################################################
496 def remove_all_ipv6addr_hosts(slicename, node):
497 hostsFilePath = get_hosts_file_path(slicename)
499 for line in fileinput.input(r'{}'.format(hostsFilePath), inplace=True):
500 search = re.search(r'^(.*)\s+({}|{})$'.format(node, 'localhost'), line)
502 ipv6candidate = search.group(1)
503 ipv6candidatestrip = ipv6candidate.strip()
504 valid = is_valid_ipv6(ipv6candidatestrip)
508 logger.log("tools: REMOVED IPv6 address from /etc/hosts file of slice={}"
511 logger.log("tools: FAILED to remove the IPv6 address from /etc/hosts file of slice={}"
514 ###################################################
515 # Author: Guilherme Sperb Machado <gsm@machados.org>
516 ###################################################
517 # Adds an ipv6 address to the /etc/hosts file within a slice
518 ###################################################
519 def add_ipv6addr_hosts_line(slicename, node, ipv6addr):
520 hostsFilePath = get_hosts_file_path(slicename)
521 logger.log("tools: {}".format(hostsFilePath) )
522 # debugging purposes:
523 #string = "127.0.0.1\tlocalhost\n192.168.100.179\tmyplc-node1-vm.mgmt.local\n"
524 #string = "127.0.0.1\tlocalhost\n"
526 with open(hostsFilePath, "a") as file:
527 file.write(ipv6addr + " " + node + "\n")
529 logger.log("tools: ADDED IPv6 address to /etc/hosts file of slice={}"
532 logger.log("tools: FAILED to add the IPv6 address to /etc/hosts file of slice={}"
537 # how to run a command in a slice
538 # now this is a painful matter
539 # the problem is with capsh that forces a bash command to be injected in its exec'ed command
540 # so because lxcsu uses capsh, you cannot exec anything else than bash
541 # bottom line is, what actually needs to be called is
542 # vs: vserver exec slicename command and its arguments
543 # lxc: lxcsu slicename "command and its arguments"
544 # which, OK, is no big deal as long as the command is simple enough,
545 # but do not stretch it with arguments that have spaces or need quoting as that will become a nightmare
546 def command_in_slice (slicename, argv):
547 virt = get_node_virt()
549 return [ 'vserver', slicename, 'exec', ] + argv
551 # wrap up argv in a single string for -c
552 return [ 'lxcsu', slicename, ] + [ " ".join(argv) ]
553 logger.log("command_in_slice: WARNING: could not find a valid virt")
558 def handler (signum, frame):
559 logger.log("Received signal {} - exiting".format(signum))
561 signal.signal(signal.SIGHUP, handler)
562 signal.signal(signal.SIGQUIT, handler)
563 signal.signal(signal.SIGINT, handler)
564 signal.signal(signal.SIGTERM, handler)