diff --git a/salt/cli/daemons.py b/salt/cli/daemons.py index c23d26a529b..c63dd9113d9 100644 --- a/salt/cli/daemons.py +++ b/salt/cli/daemons.py @@ -192,6 +192,7 @@ class Minion(parsers.MinionOptionParser): # pylint: disable=no-init confd = os.path.join( os.path.dirname(self.config['conf_file']), 'minion.d' ) + v_dirs = [ self.config['pki_dir'], self.config['cachedir'], @@ -199,11 +200,13 @@ class Minion(parsers.MinionOptionParser): # pylint: disable=no-init self.config['extension_modules'], confd, ] + if self.config.get('transport') == 'raet': v_dirs.append(os.path.join(self.config['pki_dir'], 'accepted')) v_dirs.append(os.path.join(self.config['pki_dir'], 'pending')) v_dirs.append(os.path.join(self.config['pki_dir'], 'rejected')) v_dirs.append(os.path.join(self.config['cachedir'], 'raet')) + verify_env( v_dirs, self.config['user'], @@ -315,7 +318,8 @@ class ProxyMinion(parsers.MinionOptionParser): # pylint: disable=no-init ''' Create a proxy minion server ''' - def prepare(self, proxydetails): + + def prepare(self): ''' Run the preparation sequence required to start a salt minion. @@ -344,14 +348,23 @@ class ProxyMinion(parsers.MinionOptionParser): # pylint: disable=no-init confd = os.path.join( os.path.dirname(self.config['conf_file']), 'minion.d' ) + + v_dirs = [ + self.config['pki_dir'], + self.config['cachedir'], + self.config['sock_dir'], + self.config['extension_modules'], + confd, + ] + + if self.config.get('transport') == 'raet': + v_dirs.append(os.path.join(self.config['pki_dir'], 'accepted')) + v_dirs.append(os.path.join(self.config['pki_dir'], 'pending')) + v_dirs.append(os.path.join(self.config['pki_dir'], 'rejected')) + v_dirs.append(os.path.join(self.config['cachedir'], 'raet')) + verify_env( - [ - self.config['pki_dir'], - self.config['cachedir'], - self.config['sock_dir'], - self.config['extension_modules'], - confd, - ], + v_dirs, self.config['user'], permissive=self.config['permissive_pki_access'], pki_dir=self.config['pki_dir'], @@ -359,24 +372,30 @@ class ProxyMinion(parsers.MinionOptionParser): # pylint: disable=no-init if 'proxy_log' in proxydetails: logfile = proxydetails['proxy_log'] else: - logfile = None + logfile = self.config['log_file'] if logfile is not None and not logfile.startswith(('tcp://', 'udp://', 'file://')): # Logfile is not using Syslog, verify + current_umask = os.umask(0o027) verify_files([logfile], self.config['user']) + os.umask(current_umask) + except OSError as err: logger.exception('Failed to prepare salt environment') sys.exit(err.errno) - self.config['proxy'] = proxydetails - self.setup_logfile_logger() - logger.info( - 'Setting up a Salt Proxy Minion "{0}"'.format( - self.config['id'] - ) + + self.config['proxy'] = proxydetails + self.setup_logfile_logger() + logger.info( + 'Setting up a Salt Proxy Minion "{0}"'.format( + self.config['id'] ) - migrations.migrate_paths(self.config) + ) + migrations.migrate_paths(self.config) + # TODO: AIO core is separate from transport + if self.config['transport'].lower() in ('zeromq', 'tcp'): # Late import so logging works correctly import salt.minion # If the minion key has not been accepted, then Salt enters a loop @@ -385,12 +404,17 @@ class ProxyMinion(parsers.MinionOptionParser): # pylint: disable=no-init # This is the latest safe place to daemonize self.daemonize_if_required() self.set_pidfile() - if isinstance(self.config.get('master'), list): - self.minion = salt.minion.MultiMinion(self.config) - else: - self.minion = salt.minion.ProxyMinion(self.config) + # TODO Proxy minions don't currently support failover + self.minion = salt.minion.ProxyMinion(self.config) + else: + # For proxy minions, this doesn't work yet. + import salt.daemons.flo + self.daemonize_if_required() + self.set_pidfile() + self.minion = salt.daemons.flo.IofloMinion(self.config) - def start(self, proxydetails): + + def start(self): ''' Start the actual minion. @@ -400,10 +424,11 @@ class ProxyMinion(parsers.MinionOptionParser): # pylint: disable=no-init NOTE: Run any required code before calling `super()`. ''' - self.prepare(proxydetails) try: - self.minion.tune_in() - logger.info('The proxy minion is starting up') + self.prepare(proxydetails) + if check_user(self.config['user']): + logger.info('The proxy minion is starting up') + self.minion.tune_in() except (KeyboardInterrupt, SaltSystemExit) as exc: logger.warn('Stopping the Salt Proxy Minion') if isinstance(exc, KeyboardInterrupt): @@ -413,6 +438,7 @@ class ProxyMinion(parsers.MinionOptionParser): # pylint: disable=no-init finally: self.shutdown() + def shutdown(self): ''' If sub-classed, run any shutdown operations on this method. diff --git a/salt/minion.py b/salt/minion.py index 06b1f0f249b..ec28ed6d27e 100644 --- a/salt/minion.py +++ b/salt/minion.py @@ -554,7 +554,6 @@ class Minion(MinionBase): This class instantiates a minion, runs connections for a minion, and loads all of the functions into the minion ''' - def __init__(self, opts, timeout=60, safe=True, loaded_base_name=None, io_loop=None): # pylint: disable=W0231 ''' Pass in the options dict @@ -642,6 +641,11 @@ class Minion(MinionBase): self.beacons = salt.beacons.Beacon(self.opts, self.functions) uid = salt.utils.get_uid(user=self.opts.get('user', None)) self.proc_dir = get_proc_dir(self.opts['cachedir'], uid=uid) + + # For proxy minions + if 'proxymodule' in self.opts: + self.opts['proxymodule']['init'](self.opts) + self.schedule = salt.utils.schedule.Schedule( self.opts, self.functions, @@ -687,7 +691,7 @@ class Minion(MinionBase): continue else: reinit_crypto() - proxyminion = salt.cli.daemons.ProxyMinion(self.opts) + proxyminion = salt.cli.daemons.ProxyMinion() proxyminion.start(self.opts['pillar']['proxy'][p]) self.clean_die(signal.SIGTERM, None) else: @@ -2483,14 +2487,23 @@ class ProxyMinion(Minion): This class instantiates a 'proxy' minion--a minion that does not manipulate the host it runs on, but instead manipulates a device that cannot run a minion. ''' - def __init__(self, opts, timeout=60, safe=True, loaded_base_name=None): # pylint: disable=W0231 + def __init__(self, opts, timeout=60, safe=True, loaded_base_name=None, io_loop=None): # pylint: disable=W0231 ''' Pass in the options dict ''' + # this means that the parent class doesn't know *which* master we connect to + super(ProxyMinion, self).__init__(opts) + self.timeout = timeout + self.safe = safe + self._running = None self.win_proc = [] self.loaded_base_name = loaded_base_name + self.io_loop = io_loop or zmq.eventloop.ioloop.ZMQIOLoop() + if not self.io_loop.initialized(): + self.io_loop.install() + # Warn if ZMQ < 3.2 if HAS_ZMQ: try: @@ -2508,11 +2521,8 @@ class ProxyMinion(Minion): 'may result in loss of contact with minions. Please ' 'upgrade your ZMQ!' ) - # Late setup the of the opts grains, so we can log from the grains - # module - opts['master'] = self.eval_master(opts, - timeout, - safe) + self.opts = opts + fq_proxyname = opts['proxy']['proxytype'] # Need to match the function signature of the other loader fns # which is def proxy(opts, functions, whitelist=None, loaded_base_name=None) @@ -2520,56 +2530,11 @@ class ProxyMinion(Minion): # but since we are not needing to merge functions into another fn dictionary # we will pass 'None' in self.proxymodule = salt.loader.proxy(opts, None, loaded_base_name=fq_proxyname) - opts['proxymodule'] = self.proxymodule - opts['grains'] = salt.loader.grains(opts) - opts['id'] = opts['proxymodule'][fq_proxyname+'.id'](opts) - opts.update(resolve_dns(opts)) - self.opts = opts - self.opts['pillar'] = salt.pillar.get_pillar( - opts, - opts['grains'], - opts['id'], - opts['environment'], - pillarenv=opts.get('pillarenv'), - ).compile_pillar() - opts['proxymodule'][fq_proxyname+'.init'](opts) - self.functions, self.returners, self.function_errors = self._load_modules() - self.serial = salt.payload.Serial(self.opts) - self.mod_opts = self._prep_mod_opts() - self.matcher = Matcher(self.opts, self.functions) - uid = salt.utils.get_uid(user=opts.get('user', None)) - self.proc_dir = get_proc_dir(opts['cachedir'], uid=uid) - self.schedule = salt.utils.schedule.Schedule( - self.opts, - self.functions, - self.returners) + self.opts['proxymodule'] = self.proxymodule + # Late setup the of the opts grains, so we can log from the grains + # module + self.opts['grains'] = salt.loader.grains(opts) + self.opts['id'] = self.opts['proxymodule'][fq_proxyname+'.id'](opts) - # add default scheduling jobs to the minions scheduler - if 'mine.update' in self.functions: - log.info('Added mine.update to scheduler') - self.schedule.add_job({ - '__mine_interval': - { - 'function': 'mine.update', - 'minutes': opts['mine_interval'], - 'jid_include': True, - 'maxrunning': 2 - } - }) - self.grains_cache = self.opts['grains'] - # self._running = True - - def _prep_mod_opts(self): - ''' - Returns a copy of the opts with key bits stripped out - ''' - return super(ProxyMinion, self)._prep_mod_opts() - - def _load_modules(self, force_refresh=False, notify=False): - ''' - Return the functions and the returners loaded up from the loader - module - ''' - return super(ProxyMinion, self)._load_modules(force_refresh=force_refresh, notify=notify) diff --git a/salt/scripts.py b/salt/scripts.py index 537c0dab3d5..248fc947e15 100644 --- a/salt/scripts.py +++ b/salt/scripts.py @@ -154,6 +154,60 @@ def salt_minion(): logging.basicConfig() +def salt_proxy_minion(): + ''' + Start a proxy minion. + ''' + import salt.cli.daemons + import multiprocessing + if '' in sys.path: + sys.path.remove('') + + if salt.utils.is_windows(): + minion = salt.cli.daemons.ProxyMinion() + minion.start() + return + + if '--disable-keepalive' in sys.argv: + sys.argv.remove('--disable-keepalive') + minion = salt.cli.daemons.ProxyMinion() + minion.start() + return + + # keep one minion subprocess running + while True: + try: + queue = multiprocessing.Queue() + except Exception: + # This breaks in containers + minion = salt.cli.daemons.ProxyMinion() + minion.start() + return + process = multiprocessing.Process(target=proxy_minion_process, args=(queue,)) + process.start() + try: + process.join() + try: + restart_delay = queue.get(block=False) + except Exception: + if process.exitcode == 0: + # Minion process ended naturally, Ctrl+C or --version + break + restart_delay = 60 + if restart_delay == 0: + # Minion process ended naturally, Ctrl+C, --version, etc. + break + # delay restart to reduce flooding and allow network resources to close + time.sleep(restart_delay) + except KeyboardInterrupt: + break + # need to reset logging because new minion objects + # cause extra log handlers to accumulate + rlogger = logging.getLogger() + for handler in rlogger.handlers: + rlogger.removeHandler(handler) + logging.basicConfig() + def salt_syndic(): ''' Start the salt syndic. diff --git a/scripts/salt-proxy b/scripts/salt-proxy new file mode 100755 index 00000000000..8b763816da9 --- /dev/null +++ b/scripts/salt-proxy @@ -0,0 +1,26 @@ +#!/usr/bin/env python +''' +This script is used to kick off a salt proxy minion daemon +''' + +from salt.scripts import salt_proxy_minion +from salt.utils import is_windows +from multiprocessing import freeze_support + + +if __name__ == '__main__': + if is_windows(): + # Since this file does not have a '.py' extension, when running on + # Windows, spawning any addional processes will fail due to Python + # not being able to load this 'module' in the new process. + # Work around this by creating a '.pyc' file which will enable the + # spawned process to load this 'module' and proceed. + import os.path + import py_compile + cfile = os.path.splitext(__file__)[0] + '.pyc' + if not os.path.exists(cfile): + py_compile.compile(__file__, cfile) + # This handles the bootstrapping code that is included with frozen + # scripts. It is a no-op on unfrozen code. + freeze_support() + salt_proxy_minion()