def test_chown_user(self): """Test chown argument handling (user only).""" # Ensure chown handles a user name: with ReloadConf(self.dir, self.file, '/bin/true', chown='nobody') as rc: self.assertIsInstance(rc.chown[0], numbers.Number) self.assertEqual(-1, rc.chown[1]) with ReloadConf(self.dir, self.file, '/bin/true', chown=TEST_UID) as rc: self.assertEqual((TEST_UID, -1), rc.chown)
def test_chmod(self): """Test chmod capability.""" # Ensure chmod must be numeric: with self.assertRaises(AssertionError): ReloadConf(self.dir, [], None, chmod='foo') # Ensure config files are properly chmod()ed: with ReloadConf(self.dir, self.file, '/bin/true', chmod=0o700) as rc: watch = pathjoin(self.dir, basename(self.file)) with open(watch, 'wb') as f: f.write(b'foo') os.chmod(watch, 0o755) rc.poll() self.assertEqual(stat.S_IMODE(os.stat(self.file).st_mode), 0o700)
def test_chown(self): """Test chown capability.""" with ReloadConf(self.dir, self.file, '/bin/true', chown=(TEST_UID, TEST_UID)) as rc: with open(pathjoin(self.dir, basename(self.file)), 'wb') as f: f.write(b'foo') rc.poll() self.assertEqual(TEST_UID, os.stat(self.file).st_uid) self.assertEqual(TEST_UID, os.stat(self.file).st_gid)
def test_nodir(self): """Test that watch directory does not need to exist.""" # Remove the watch directory. os.rmdir(self.dir) # Ensure reloadconf creates the watch directory. with ReloadConf(self.dir, self.file, '/bin/sleep 1', chown=(TEST_UID, TEST_UID), chmod=0o700) as rc: rc.poll() self.assertTrue(rc.check_command()) self.assertTrue(isdir(self.dir))
def test_inotify(self): """Ensure command is reloaded with valid config (inotify).""" command = '%s %s' % (self.prog, self.sig) with ReloadConf(self.dir, self.file, command, inotify=True) as rc: rc.poll() # Command should now be running. self.assertTrue(rc.check_command()) # Write out "config" file. with open(pathjoin(self.dir, basename(self.file)), 'wb') as f: f.write(b'foo') # Command should receive HUP. rc.poll() time.sleep(0.1) self.assertTrue(pathexists(self.sig))
def test_nohup(self): """Ensure that command is not reloaded with invalid config.""" command = '%s %s' % (self.prog, self.sig) with ReloadConf(self.dir, self.file, command, '/bin/true') as rc: rc.poll() # Command should now be running. self.assertTrue(rc.check_command()) # A bit nasty, but we want the check to fail this time... rc.test = '/bin/false' # Write out "config" file. with open(pathjoin(self.dir, basename(self.file)), 'wb') as f: f.write(b'foo') # Command should NOT receive HUP. rc.poll() time.sleep(0.1) self.assertFalse(pathexists(self.sig))
def test_reload(self): """Ensure reload command is run (instead of HUP) when provided.""" reload = '/bin/touch %s' % self.sig with ReloadConf(self.dir, self.file, '/bin/sleep 1', reload=reload) as rc: rc.poll() # Command should now be running. self.assertTrue(rc.check_command()) self.assertFalse(pathexists(self.sig)) # Write out "config" file. with open(pathjoin(self.dir, basename(self.file)), 'wb') as f: f.write(b'foo') # Reload command should be executed. rc.poll() time.sleep(0.1) self.assertTrue(pathexists(self.sig))
def main(argv): """ reloadconf - Monitor config changes and safely restart. Usage: reloadconf --command=<cmd> --watch=<dir> (--config=<file> ...) [--reload=<cmd> --test=<cmd> --debug --chown=<user,group>] [--chmod=<mode> --inotify --wait-for-path=<file>] [--wait-for-sock=<host:port> --wait-timeout=<secs>] Options: --command=<cmd> The program to run when configuration is valid. --watch=<dir> The directory to watch for incoming files. --config=<file> A destination config file path. --reload=<cmd> The command to reload configuration (defaults to HUP signal). --test=<cmd> The command to test configuration. --chown=<user,group> The user and (optionally) group to chown config files to before starting service. --chmod=<mode> Mode to set config files to before starting service. --inotify Use inotify instead of polling. --wait-for-path=<file> Delay start until file or directory appears on disk. --wait-for-sock=<host:port> Delay start until connection succeeds. --wait-timeout=<secs> Timeout for wait-* commands [default: 5]. --debug Verbose output. Assumptions: - The command accepts HUP signal to reload it's configuration. - Config files don't have the same name (if two config files in different directories have the same name, reloadconf will have issues.) Upon startup reloadconf will test the configuration and if valid, will run command. If command dies for any reason, reloadconf re-runs it. If the configuration is invalid (test command fails) then reloadconf waits for new files to appear in it's input directory, merges those and re-tests the config. If --test is omitted, then the configuration test is skipped, but reloadconf still monitors for new config files and reloads command. Command is reloaded by sending a HUP signal. Config files are matched by name. For example, if the input directory is /tmp, and a given config file is /etc/foo.conf, then reloadconf will watch for /tmp/foo.conf to appear, and will overwrite /etc/foo.conf with it and then test the config. Reloadconf can handle multiple config files, but since it uses the file name to determine a file's destination, names must be unique. Reloadconf will wait for 1 second after seeing any configuration file appear to give the configuration generator time to complete all files for a configuration file set. If it takes more than 1 second to generate a full configuration, then the generator program should write them to temporary space before moving them into the input directory. """ opt = docopt(textwrap.dedent(main.__doc__), argv) try: opt = Schema({ '--command': Use(str), '--watch': Use(str), '--chown': Or(None, Use(user_and_group)), '--chmod': Or(None, Use(int)), '--wait-for-sock': Or(None, Use(host_and_port), error='Invalid socket'), '--wait-timeout': Or(None, Use(float), error='Invalid timeout'), object: object, }).validate(opt) except SchemaError as e: raise DocoptExit(e.args[0]) logger = logging.getLogger() # Set up logging so we can see output. logger.addHandler(logging.StreamHandler(sys.stdout)) logger.setLevel(logging.INFO) if opt.pop('--debug', None): logger.setLevel(logging.DEBUG) # Convert from CLI arguments to kwargs. kwargs = {} for k in opt.keys(): kwargs[k.lstrip('-').replace('-', '_')] = opt[k] try: rc = ReloadConf(**kwargs) except AssertionError as e: raise DocoptExit(e.args[0]) with rc: LOGGER.info('Reloadconf monitoring %s for %s', kwargs['watch'], kwargs['command']) while True: try: rc.poll() except Exception: LOGGER.exception('Error polling', exc_info=True) # Check up to 20 times a minute. time.sleep(3.0)
def test_chown_fail(self): """Test chown validation.""" # Ensure chown must have len() == 2: with self.assertRaises(AssertionError): ReloadConf(self.dir, [], None, chown=(1, 2, 3))
def test_no_test(self): """Ensure command is run when test is omitted.""" rc = ReloadConf(self.dir, self.file, '/bin/sleep 1') rc.poll() # Command should have run. self.assertTrue(rc.check_command())
def test_success(self): """Ensure command is run when test succeeds.""" rc = ReloadConf(self.dir, self.file, '/bin/sleep 1', test='/bin/true') rc.poll() # Command should have run. self.assertTrue(rc.check_command())
def test_fail(self): """Ensure command is NOT run when test fails.""" rc = ReloadConf(self.dir, self.file, '/bin/sleep 1', test='/bin/false') rc.poll() # Command should NOT have run. self.assertFalse(rc.check_command())