def dhcp_discovery(dhclient_cmd_path, interface, cleandir): """Run dhclient on the interface without scripts or filesystem artifacts. @param dhclient_cmd_path: Full path to the dhclient used. @param interface: Name of the network inteface on which to dhclient. @param cleandir: The directory from which to run dhclient as well as store dhcp leases. @return: A list of dicts of representing the dhcp leases parsed from the dhcp.leases file or empty list. """ LOG.debug('Performing a dhcp discovery on %s', interface) # XXX We copy dhclient out of /sbin/dhclient to avoid dealing with strict # app armor profiles which disallow running dhclient -sf <our-script-file>. # We want to avoid running /sbin/dhclient-script because of side-effects in # /etc/resolv.conf any any other vendor specific scripts in # /etc/dhcp/dhclient*hooks.d. sandbox_dhclient_cmd = os.path.join(cleandir, 'dhclient') util.copy(dhclient_cmd_path, sandbox_dhclient_cmd) pid_file = os.path.join(cleandir, 'dhclient.pid') lease_file = os.path.join(cleandir, 'dhcp.leases') # ISC dhclient needs the interface up to send initial discovery packets. # Generally dhclient relies on dhclient-script PREINIT action to bring the # link up before attempting discovery. Since we are using -sf /bin/true, # we need to do that "link up" ourselves first. util.subp(['ip', 'link', 'set', 'dev', interface, 'up'], capture=True) cmd = [sandbox_dhclient_cmd, '-1', '-v', '-lf', lease_file, '-pf', pid_file, interface, '-sf', '/bin/true'] util.subp(cmd, capture=True) # Wait for pid file and lease file to appear, and for the process # named by the pid file to daemonize (have pid 1 as its parent). If we # try to read the lease file before daemonization happens, we might try # to read it before the dhclient has actually written it. We also have # to wait until the dhclient has become a daemon so we can be sure to # kill the correct process, thus freeing cleandir to be deleted back # up the callstack. missing = util.wait_for_files( [pid_file, lease_file], maxwait=5, naplen=0.01) if missing: LOG.warning("dhclient did not produce expected files: %s", ', '.join(os.path.basename(f) for f in missing)) return [] ppid = 'unknown' for _ in range(0, 1000): pid_content = util.load_file(pid_file).strip() try: pid = int(pid_content) except ValueError: pass else: ppid = util.get_proc_ppid(pid) if ppid == 1: LOG.debug('killing dhclient with pid=%s', pid) os.kill(pid, signal.SIGKILL) return parse_dhcp_lease_file(lease_file) time.sleep(0.01) LOG.error( 'dhclient(pid=%s, parentpid=%s) failed to daemonize after %s seconds', pid_content, ppid, 0.01 * 1000 ) return parse_dhcp_lease_file(lease_file)
def test_get_proc_ppid(self): """get_proc_ppid returns correct parent pid value.""" my_pid = os.getpid() my_ppid = os.getppid() self.assertEqual(my_ppid, util.get_proc_ppid(my_pid))
def dhcp_discovery(dhclient_cmd_path, interface, cleandir, dhcp_log_func=None): """Run dhclient on the interface without scripts or filesystem artifacts. @param dhclient_cmd_path: Full path to the dhclient used. @param interface: Name of the network inteface on which to dhclient. @param cleandir: The directory from which to run dhclient as well as store dhcp leases. @param dhcp_log_func: A callable accepting the dhclient output and error streams. @return: A list of dicts of representing the dhcp leases parsed from the dhcp.leases file or empty list. """ LOG.debug("Performing a dhcp discovery on %s", interface) # XXX We copy dhclient out of /sbin/dhclient to avoid dealing with strict # app armor profiles which disallow running dhclient -sf <our-script-file>. # We want to avoid running /sbin/dhclient-script because of side-effects in # /etc/resolv.conf any any other vendor specific scripts in # /etc/dhcp/dhclient*hooks.d. sandbox_dhclient_cmd = os.path.join(cleandir, "dhclient") util.copy(dhclient_cmd_path, sandbox_dhclient_cmd) pid_file = os.path.join(cleandir, "dhclient.pid") lease_file = os.path.join(cleandir, "dhcp.leases") # In some cases files in /var/tmp may not be executable, launching dhclient # from there will certainly raise 'Permission denied' error. Try launching # the original dhclient instead. if not os.access(sandbox_dhclient_cmd, os.X_OK): sandbox_dhclient_cmd = dhclient_cmd_path # ISC dhclient needs the interface up to send initial discovery packets. # Generally dhclient relies on dhclient-script PREINIT action to bring the # link up before attempting discovery. Since we are using -sf /bin/true, # we need to do that "link up" ourselves first. subp.subp(["ip", "link", "set", "dev", interface, "up"], capture=True) cmd = [ sandbox_dhclient_cmd, "-1", "-v", "-lf", lease_file, "-pf", pid_file, interface, "-sf", "/bin/true", ] out, err = subp.subp(cmd, capture=True) # Wait for pid file and lease file to appear, and for the process # named by the pid file to daemonize (have pid 1 as its parent). If we # try to read the lease file before daemonization happens, we might try # to read it before the dhclient has actually written it. We also have # to wait until the dhclient has become a daemon so we can be sure to # kill the correct process, thus freeing cleandir to be deleted back # up the callstack. missing = util.wait_for_files([pid_file, lease_file], maxwait=5, naplen=0.01) if missing: LOG.warning( "dhclient did not produce expected files: %s", ", ".join(os.path.basename(f) for f in missing), ) return [] ppid = "unknown" daemonized = False for _ in range(0, 1000): pid_content = util.load_file(pid_file).strip() try: pid = int(pid_content) except ValueError: pass else: ppid = util.get_proc_ppid(pid) if ppid == 1: LOG.debug("killing dhclient with pid=%s", pid) os.kill(pid, signal.SIGKILL) daemonized = True break time.sleep(0.01) if not daemonized: LOG.error( "dhclient(pid=%s, parentpid=%s) failed to daemonize after %s " "seconds", pid_content, ppid, 0.01 * 1000, ) if dhcp_log_func is not None: dhcp_log_func(out, err) return parse_dhcp_lease_file(lease_file)