#!/usr/bin/python3 # ============================================================================== # Copyright 2011 Amazon.com, Inc. or its affiliates. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ============================================================================== import sys import cfnbootstrap from cfnbootstrap import update_hooks, util import datetime import threading import os import logging from optparse import OptionParser import signal import json import random import configparser as ConfigParser # throws: ValueError, ConfigParser.NoOptionError def get_umask_from_conf(config): octal_as_string = config.get("main", "umask") # interpret the number as octal even if it's not specified as octal (just like the Linux umask command) safe_octal_as_string = "0" + octal_as_string ret_val = int(safe_octal_as_string, 8) if (ret_val > 0o777) or (ret_val < 0): raise ValueError( "Octal number for umask out of range: %s" % oct(ret_val)) return ret_val if os.name == 'nt': default_confdir = os.path.expandvars('${SystemDrive}\cfn') else: default_confdir = '/etc/cfn' parser = OptionParser() parser.add_option("-c", "--config", help="The configuration directory (default: %s)" % default_confdir, type="string", dest="config_path", default=default_confdir) parser.add_option("", "--no-daemon", help="Do not daemonize", dest="no_daemon", action="store_true") parser.add_option("-v", "--verbose", help="Enables verbose logging", action="store_true", dest="verbose") (options, args) = parser.parse_args() fatal_event = threading.Event() # with configureLogging in this place (that is: outside main) cfn-hup.log won't get the benefit of umask setting, but # we take additional care of disabling group and world writable permissions at the creation place cfnbootstrap.configureLogging("DEBUG", filename='cfn-hup.log') def parse_config_file(): if not options.config_path: logging.error("Error: A configuration path must be specified") parser.print_help(sys.stderr) sys.exit(1) if not os.path.isdir(options.config_path): logging.error("Error: Could not find configuration at %s", options.config_path) sys.exit(1) try: return update_hooks.parse_config(options.config_path) except ValueError as e: logging.error("Error: %s", str(e)) sys.exit(1) def main(main_config, processor, cmd_processor): if options.no_daemon: if processor: processor.process() if cmd_processor: cmd_processor.register() cmd_processor.process() else: interval = 15 if main_config.has_option('main', 'interval'): interval = main_config.getint('main', 'interval') if interval < 1: logging.error( "Invalid interval (must be 1 minute or greater): %s", interval) sys.exit(1) timers = [None, None] timer_lock = threading.Lock() def do_process(last_log=datetime.datetime.utcnow()): if fatal_event.isSet(): return if datetime.datetime.utcnow() - last_log > datetime.timedelta(minutes=5): last_log = datetime.datetime.utcnow() logging.info("cfn-hup processing is alive.") try: processor.process() except update_hooks.FatalUpdateError as e: logging.exception("Fatal exception") fatal_event.set() except Exception as e: logging.exception("Unhandled exception") with timer_lock: if fatal_event.isSet(): timers[0] = None return last_timer = threading.Timer( interval * 60 + random.randint(-30, 30), do_process, (), {'last_log': last_log}) last_timer.start() timers[0] = last_timer def do_cmd_process(last_log=datetime.datetime.utcnow()): if fatal_event.isSet(): return if datetime.datetime.utcnow() - last_log > datetime.timedelta(minutes=5): last_log = datetime.datetime.utcnow() logging.info("command processing is alive.") delay = 0 try: if not cmd_processor.is_registered(): cmd_processor.register() if cmd_processor.creds_expired(): logging.error( "Expired credentials found; skipping process") delay = 20 else: cmd_processor.process() except update_hooks.FatalUpdateError as e: logging.exception("Fatal exception") fatal_event.set() except Exception as e: logging.exception("Unhandled exception") with timer_lock: if fatal_event.isSet(): timers[1] = None return if delay > 0: last_cmd_timer = threading.Timer( delay, do_cmd_process, (), {'last_log': last_log}) last_cmd_timer.start() timers[1] = last_cmd_timer else: threading.Thread(target=do_cmd_process, args=(), kwargs={ 'last_log': last_log}).start() timers[1] = None if processor: do_process() if cmd_processor: do_cmd_process() while not fatal_event.isSet(): fatal_event.wait(1) # Allow signals with timer_lock: if timers[0] is not None: timers[0].cancel() if timers[1] is not None: timers[1].cancel() sys.exit(0) main_config, processor, cmd_processor = parse_config_file() if options.no_daemon: verbose = options.verbose or main_config.has_option( 'main', 'verbose') and main_config.getboolean('main', 'verbose') cfnbootstrap.configureLogging( "DEBUG" if verbose else "INFO", filename='cfn-hup.log') main(main_config, processor, cmd_processor) elif os.name == 'nt': print("Error: cfn-hup cannot be run directly in daemon mode on Windows", file=sys.stderr) sys.exit(1) else: try: import daemon except ImportError: print("Daemon library was not installed; please install python-daemon", file=sys.stderr) sys.exit(1) from daemon import pidfile def kill(signal_num, stack_frame): logging.info("Shutting down cfn-hup") fatal_event.set() # Default umask value (mask out world and group writable) umask_parameter = 0o022 try: umask_parameter = get_umask_from_conf(main_config) logging.info("Setting umask value to: %s", oct(umask_parameter)) except ValueError as e: logging.error( "Invalid octal value specified for umask in config file: %s", str(e)) sys.exit(1) except ConfigParser.NoOptionError: logging.info("No umask value specified in config file. Using the default one: %s", oct( umask_parameter)) # Close root logger before starting up daemon to avoid collision logging.getLogger().handlers[0].close() with daemon.DaemonContext(pidfile=pidfile.TimeoutPIDLockFile('/var/run/cfn-hup.pid', 300), signal_map={signal.SIGTERM: kill}, umask=umask_parameter): try: verbose = options.verbose or main_config.has_option( 'main', 'verbose') and main_config.getboolean('main', 'verbose') cfnbootstrap.configureLogging( "DEBUG" if verbose else "INFO", filename='cfn-hup.log') main(main_config, processor, cmd_processor) except Exception as e: logging.exception("Unhandled exception: %s", str(e)) sys.exit(1)