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 current = file(target).read()
153 if current == new_contents:
154 # if turns out to be an empty string, and remove_if_empty is set,
155 # then make sure to trash the file if it exists
156 if remove_if_empty and not new_contents and os.path.isfile(target):
157 logger.verbose("tools.replace_file_with_string: removing file {}".format(target))
158 try: os.unlink(target)
161 # overwrite target file: create a temp in the same directory
162 path = os.path.dirname(target) or '.'
163 fd, name = tempfile.mkstemp('', 'repl', path)
164 os.write(fd, new_contents)
166 if os.path.exists(target):
168 shutil.move(name, target)
169 if chmod: os.chmod(target, chmod)
173 # utilities functions to get (cached) information from the node
175 # get node_id from /etc/planetlab/node_id and cache it
181 _node_id = int(file("/etc/planetlab/node_id").read())
186 _root_context_arch = None
187 def root_context_arch():
188 global _root_context_arch
189 if not _root_context_arch:
190 sp = subprocess.Popen(["uname", "-i"], stdout=subprocess.PIPE)
191 (_root_context_arch, _) = sp.communicate()
192 _root_context_arch = _root_context_arch.strip()
193 return _root_context_arch
198 def __init__(self, file):
199 logger.log("tools: Lock {} initialized.".format(file), 2)
200 self.fd = os.open(file, os.O_RDWR|os.O_CREAT, 0600)
201 flags = fcntl.fcntl(self.fd, fcntl.F_GETFD)
202 flags |= fcntl.FD_CLOEXEC
203 fcntl.fcntl(self.fd, fcntl.F_SETFD, flags)
207 logger.log("tools: Lock acquired.", 2)
208 fcntl.lockf(self.fd, fcntl.LOCK_SH)
210 logger.log("tools: Lock released.", 2)
211 fcntl.lockf(self.fd, fcntl.LOCK_UN)
214 # Utilities for getting the IP address of a LXC/Openvswitch slice. Do this by
215 # running ifconfig inside of the slice's context.
217 def get_sliver_process(slice_name, process_cmdline):
219 Utility function to find a process inside of an LXC sliver. Returns
220 (cgroup_fn, pid). cgroup_fn is the filename of the cgroup file for
221 the process, for example /proc/2592/cgroup. Pid is the process id of
222 the process. If the process is not found then (None, None) is returned.
225 cmd = 'grep {} /proc/*/cgroup | grep freezer'.format(slice_name)
226 output = os.popen(cmd).readlines()
228 # the slice couldn't be found
229 logger.log("get_sliver_process: couldn't find slice {}".format(slice_name))
237 path = l.split(':')[0]
238 comp = l.rsplit(':')[-1]
239 slice_name_check = comp.rsplit('/')[-1]
240 # the lines below were added by Guilherme <gsm@machados.org>
241 # due to the LXC requirements
242 # What we have to consider here is that libervirt on Fedora 18
243 # uses the following line:
244 # /proc/1253/cgroup:6:freezer:/machine.slice/auto_sirius.libvirt-lxc
245 # While the libvirt on Fedora 20 and 21 uses the following line:
246 # /proc/1253/cgroup:6:freezer:/machine.slice/machine-lxc\x2del_sirius.scope
247 # Further documentation on:
248 # https://libvirt.org/cgroups.html#systemdScope
249 virt = get_node_virt()
251 # This is for Fedora 20 or later
252 regexf20orlater = re.compile(r'machine-lxc\\x2d(.+).scope')
253 isf20orlater = regexf20orlater.search(slice_name_check)
255 slice_name_check = isf20orlater.group(1)
257 # This is for Fedora 18
258 slice_name_check = slice_name_check.rsplit('.')[0]
260 if (slice_name_check == slice_name):
262 pid = slice_path.split('/')[2]
263 with open('/proc/{}/cmdline'.format(pid)) as cmdfile:
264 cmdline = cmdfile.read().rstrip('\n\x00')
265 if (cmdline == process_cmdline):
266 cgroup_fn = slice_path
271 if (not cgroup_fn) or (not pid):
272 logger.log("get_sliver_process: process {} not running in slice {}"
273 .format(process_cmdline, slice_name))
276 return (cgroup_fn, pid)
278 ###################################################
279 # Added by Guilherme Sperb Machado <gsm@machados.org>
280 ###################################################
287 logger.log("Could not import 're', 'socket', or 'fileinput' python packages.")
289 # TODO: is there anything better to do if the "libvirt", "sliver_libvirt",
290 # and "sliver_lxc" are not in place?
293 from sliver_libvirt import Sliver_Libvirt
296 logger.log("Could not import 'sliver_lxc' or 'libvirt' or 'sliver_libvirt'.")
297 ###################################################
299 def get_sliver_ifconfig(slice_name, device="eth0"):
301 return the output of "ifconfig" run from inside the sliver.
303 side effects: adds "/usr/sbin" to sys.path
306 # See if setns is installed. If it's not then we're probably not running
308 if not os.path.exists("/usr/sbin/setns.so"):
311 # setns is part of lxcsu and is installed to /usr/sbin
312 if not "/usr/sbin" in sys.path:
313 sys.path.append("/usr/sbin")
316 (cgroup_fn, pid) = get_sliver_process(slice_name, "/sbin/init")
317 if (not cgroup_fn) or (not pid):
320 path = '/proc/{}/ns/net'.format(pid)
324 setns.chcontext(path)
326 args = ["/sbin/ifconfig", device]
327 sub = subprocess.Popen(args, stderr = subprocess.PIPE, stdout = subprocess.PIPE)
330 if (sub.returncode != 0):
331 logger.log("get_slice_ifconfig: error in ifconfig: {}".format(sub.stderr.read()))
333 result = sub.stdout.read()
335 setns.chcontext("/proc/1/ns/net")
339 def get_sliver_ip(slice_name):
340 ifconfig = get_sliver_ifconfig(slice_name)
344 for line in ifconfig.split("\n"):
345 if "inet addr:" in line:
346 # example: ' inet addr:192.168.122.189 Bcast:192.168.122.255 Mask:255.255.255.0'
347 parts = line.strip().split()
348 if len(parts) >= 2 and parts[1].startswith("addr:"):
349 return parts[1].split(":")[1]
353 ###################################################
354 # Author: Guilherme Sperb Machado <gsm@machados.org>
355 ###################################################
356 # Get the slice ipv6 address
358 ###################################################
359 def get_sliver_ipv6(slice_name):
360 ifconfig = get_sliver_ifconfig(slice_name)
364 # example: 'inet6 2001:67c:16dc:1302:5054:ff:fea7:7882 prefixlen 64 scopeid 0x0<global>'
365 prog = re.compile(r'inet6\s+(.*)\s+prefixlen\s+(\d+)\s+scopeid\s+(.+)<global>')
366 for line in ifconfig.split("\n"):
367 search = prog.search(line)
369 ipv6addr = search.group(1)
370 prefixlen = search.group(2)
371 return (ipv6addr, prefixlen)
374 ###################################################
375 # Author: Guilherme Sperb Machado <gsm@machados.org>
376 ###################################################
377 # Check if the address is a AF_INET6 family address
378 ###################################################
379 def is_valid_ipv6(ipv6addr):
381 socket.inet_pton(socket.AF_INET6, ipv6addr)
386 ### this returns the kind of virtualization on the node
387 # either 'vs' or 'lxc'
388 # also caches it in /etc/planetlab/virt for next calls
389 # could be promoted to core nm if need be
390 virt_stamp = "/etc/planetlab/virt"
391 def get_node_virt ():
393 return file(virt_stamp).read().strip()
396 logger.log("Computing virt..")
398 virt = 'vs' if subprocess.call ([ 'vserver', '--help' ]) == 0 else 'lxc'
401 with file(virt_stamp, "w") as f:
405 ### this return True or False to indicate that systemctl is present on that box
406 # cache result in memory as _has_systemctl
407 _has_systemctl = None
408 def has_systemctl ():
409 global _has_systemctl
410 if _has_systemctl is None:
411 _has_systemctl = (subprocess.call([ 'systemctl', '--help' ]) == 0)
412 return _has_systemctl
414 ###################################################
415 # Author: Guilherme Sperb Machado <gsm@machados.org>
416 ###################################################
417 # This method was developed to support the ipv6 plugin
419 ###################################################
420 def reboot_slivers():
422 # connecting to the libvirtd
423 connLibvirt = Sliver_Libvirt.getConnection(type)
424 domains = connLibvirt.listAllDomains()
425 for domain in domains:
427 # set the flag VIR_DOMAIN_REBOOT_INITCTL, which uses "initctl"
428 result = domain.reboot(0x04)
430 logger.log("tools: REBOOT {}".format(domain.name()) )
434 logger.log("tools: FAILED to reboot {} ({})".format(domain.name(), e) )
435 logger.log("tools: Trying to DESTROY/CREATE {} instead...".format(domain.name()) )
437 result = domain.destroy()
439 logger.log("tools: DESTROYED {}".format(domain.name()) )
440 else: logger.log("tools: FAILED in the DESTROY call of {}".format(domain.name()) )
441 result = domain.create()
443 logger.log("tools: CREATED {}".format(domain.name()) )
444 else: logger.log("tools: FAILED in the CREATE call of {}".format(domain.name()) )
446 logger.log("tools: FAILED to DESTROY/CREATE {} ({})".format(domain.name(), e) )
448 ###################################################
449 # Author: Guilherme Sperb Machado <gsm@machados.org>
450 ###################################################
451 # Get the /etc/hosts file path
452 ###################################################
453 def get_hosts_file_path(slicename):
454 containerDir = os.path.join(sliver_lxc.Sliver_LXC.CON_BASE_DIR, slicename)
455 return os.path.join(containerDir, 'etc', 'hosts')
457 ###################################################
458 # Author: Guilherme Sperb Machado <gsm@machados.org>
459 ###################################################
460 # Search if there is a specific ipv6 address in the
461 # /etc/hosts file of a given slice
462 # If the parameter 'ipv6addr' is None, then search
463 # for any ipv6 address
464 ###################################################
465 def search_ipv6addr_hosts(slicename, ipv6addr):
466 hostsFilePath = get_hosts_file_path(slicename)
469 for line in fileinput.input(r'{}'.format(hostsFilePath)):
470 if ipv6addr is not None:
471 if re.search(r'{}'.format(ipv6addr), line):
474 search = re.search(r'^(.*)\s+.*$', line)
476 ipv6candidate = search.group(1)
477 ipv6candidatestrip = ipv6candidate.strip()
478 valid = is_valid_ipv6(ipv6candidatestrip)
484 logger.log("tools: FAILED to search {} in /etc/hosts file of slice={}"
485 .format(ipv6addr, slicename))
487 ###################################################
488 # Author: Guilherme Sperb Machado <gsm@machados.org>
489 ###################################################
490 # Removes all ipv6 addresses from the /etc/hosts
491 # file of a given slice
492 ###################################################
493 def remove_all_ipv6addr_hosts(slicename, node):
494 hostsFilePath = get_hosts_file_path(slicename)
496 for line in fileinput.input(r'{}'.format(hostsFilePath), inplace=True):
497 search = re.search(r'^(.*)\s+({}|{})$'.format(node, 'localhost'), line)
499 ipv6candidate = search.group(1)
500 ipv6candidatestrip = ipv6candidate.strip()
501 valid = is_valid_ipv6(ipv6candidatestrip)
505 logger.log("tools: REMOVED IPv6 address from /etc/hosts file of slice={}"
508 logger.log("tools: FAILED to remove the IPv6 address from /etc/hosts file of slice={}"
511 ###################################################
512 # Author: Guilherme Sperb Machado <gsm@machados.org>
513 ###################################################
514 # Adds an ipv6 address to the /etc/hosts file within a slice
515 ###################################################
516 def add_ipv6addr_hosts_line(slicename, node, ipv6addr):
517 hostsFilePath = get_hosts_file_path(slicename)
518 logger.log("tools: {}".format(hostsFilePath) )
519 # debugging purposes:
520 #string = "127.0.0.1\tlocalhost\n192.168.100.179\tmyplc-node1-vm.mgmt.local\n"
521 #string = "127.0.0.1\tlocalhost\n"
523 with open(hostsFilePath, "a") as file:
524 file.write(ipv6addr + " " + node + "\n")
526 logger.log("tools: ADDED IPv6 address to /etc/hosts file of slice={}"
529 logger.log("tools: FAILED to add the IPv6 address to /etc/hosts file of slice={}"
534 # how to run a command in a slice
535 # now this is a painful matter
536 # the problem is with capsh that forces a bash command to be injected in its exec'ed command
537 # so because lxcsu uses capsh, you cannot exec anything else than bash
538 # bottom line is, what actually needs to be called is
539 # vs: vserver exec slicename command and its arguments
540 # lxc: lxcsu slicename "command and its arguments"
541 # which, OK, is no big deal as long as the command is simple enough,
542 # but do not stretch it with arguments that have spaces or need quoting as that will become a nightmare
543 def command_in_slice (slicename, argv):
544 virt = get_node_virt()
546 return [ 'vserver', slicename, 'exec', ] + argv
548 # wrap up argv in a single string for -c
549 return [ 'lxcsu', slicename, ] + [ " ".join(argv) ]
550 logger.log("command_in_slice: WARNING: could not find a valid virt")
555 def handler (signum, frame):
556 logger.log("Received signal {} - exiting".format(signum))
558 signal.signal(signal.SIGHUP, handler)
559 signal.signal(signal.SIGQUIT, handler)
560 signal.signal(signal.SIGINT, handler)
561 signal.signal(signal.SIGTERM, handler)