#!/usr/bin/env python
# coding: utf-8

from socket import gethostname, setdefaulttimeout
import json
import subprocess
import time
import signal
import sys
import os
import stat
import getopt
import logging
import fcntl
import cPickle

# Required to support threads
from threading import Thread
from threading import active_count
from Queue import Queue
from Queue import Empty

try:
    import yaml
except ImportError:
    print 'yaml module not found - please install pyyaml - exiting'
    sys.exit(1)

try:
    from boto import Version, config
except ImportError:
    print 'Boto module not found - please install boto - exiting'
    sys.exit(1)

# SQS stuff
from boto.sqs.connection import SQSConnection
from boto.sqs.regioninfo import SQSRegionInfo
from boto.sqs import connect_to_region

if int(Version[0:1]) < 2:
    sys.stderr.write('Boto >= 2.0 is required %s found - exiting\n' % (Version))
    sys.exit(1)

# Programatically set boto timeout
if not config.has_section('Boto'):
    config.add_section('Boto') 
    config.set('Boto','http_socket_timeout','30') 
    config.set('Boto','metadata_service_timeout','1.0') 
    config.set('Boto','metadata_service_num_attempts','3') 

# Set a default timeout on all socket requests
setdefaulttimeout(60)

CFILE='/etc/ec2_collective/ec2-cagent.json'

# Create queues
task_queue = Queue()
done_queue = Queue()
yaml_queue = Queue()

def terminate_process(signum, frame):
    logging.info ('Process asked to exit (SIGTERM) - exiting')
    sys.exit(0)

def watchdog_terminator(signum, frame):
    try:
        import traceback
        logging.error("Self-destructing...")
        logging.error(traceback.format_exc())
    finally:
        os.kill(os.getpid(), signal.SIGKILL)

signal.signal(signal.SIGTERM, terminate_process)
signal.signal(signal.SIGALRM, watchdog_terminator)

def usage():
    sys.stderr.write('Usage:')
    sys.stderr.write('\t' + sys.argv[0] + '\n\n -f, --foreground\trun script in foreground\n -h, --help\tthis help\n -l, --logfile\tlogfile path ( /var/log/ec2_collective/ec2-cagent.log )\n -p, --pidfile\tpath to pidfile (/tmp/ec2-cagent.pid)\n')
    sys.exit(1)

def set_logging(logfile, foreground, supervisor):

    log = logging.getLogger()
    logging.getLogger('boto').setLevel(logging.CRITICAL)
    fmt = logging.Formatter("%(asctime)s [%(levelname)s] %(funcName)s: %(message)s")

    if foreground is True and supervisor is not True:
        ch  = logging.StreamHandler(sys.stdout)
        ch.setFormatter(fmt)
        log.addHandler(ch)
    else:
        fh = logging.handlers.WatchedFileHandler(logfile)
        fh.setFormatter(fmt)
        log.addHandler(fh)

    if CFG['general']['log_level'] not in ['INFO', 'WARN', 'ERROR', 'DEBUG', 'CRITICAL' ]:
        sys.stderr.write('Log level: ' + CFG['general']['log_level'] + ' is invalid\n')
        sys.exit(1)

    if CFG['general']['log_level'] == 'INFO':
        logging.getLogger().setLevel(logging.INFO)
    elif CFG['general']['log_level'] == 'WARN':
        logging.getLogger().setLevel(logging.WARNING)
    elif CFG['general']['log_level'] == 'ERROR':
        logging.getLogger().setLevel(logging.ERROR)
    elif CFG['general']['log_level'] == 'DEBUG':
        logging.getLogger().setLevel(logging.DEBUG)
    elif CFG['general']['log_level'] == 'CRITICAL':
        logging.getLogger().setLevel(logging.CRITICAL)

def acquire_lock(pidfile):

    try:
        f = open(pidfile, 'a')
    except IOError, err:
        sys.stderr.write('Unable to open lockfile: ' + str(pidfile) + ' (' + str(err) + ')\n')
        sys.exit(1)

    try:
        fcntl.flock(f, fcntl.LOCK_EX|fcntl.LOCK_NB)
        return f
    except IOError, err:
        sys.stderr.write('Unable to acquire lockfile: ' + str(pidfile) + ' (' + str(err) + ')\n')
        sys.exit(1)

def initialize(logfile='/var/log/ec2_collective/ec2-cagent.log', pidfile='/tmp/ec2-cagent.pid', foreground=False, supervisor=False):

    try:
        opts, args = getopt.getopt(sys.argv[1:], 'hfsl:p:', ['foreground', 'supervisor', 'help', 'logfile', 'pidfile'])

    except getopt.GetoptError, err:
        sys.stderr.write('Failed to parse arguments: (%s)\n' % (err))
        sys.exit(1)

    for o, a in opts:
        if o in ('-h', '--help'):
            usage()
        elif o in ('-f', '--foreground'):
            foreground=True
        elif o in ('-s', '--supervirsor'):
            supervisor=True
            foreground=True
        elif o in ('-l', '--logfile'):
            logfile=a
        elif o in ('-p', '--pidfile'):
            pidfile=a

    get_config()
    set_logging(logfile,foreground, supervisor)

    stdoutlog = os.path.dirname(logfile) + '/ec2-cagent.stdout'
    stderrlog = os.path.dirname(logfile) + '/ec2-cagent.stderr'

    return (foreground, logfile, pidfile, stdoutlog, stderrlog)

def write_yaml_to_queue ( message, sqs_facts_queue ):

    # Make sure the facts message contains a hostname
    message.update({ 'ec2_cagent_hostname' : gethostname() })

    logging.debug('Writing facts to sqs facts queue')
    write_sqs_msg (message, sqs_facts_queue)

def get_yaml_facts_worker ():
    logging.debug('Loading facts')
    sqs_facts_queue = establish_sqs_conn(CFG['aws']['sqs_facts_queue'])

    while True:
        starttime=time.time()
        # Define dict for storage
        facts = {}
        valid_yaml_files = []

        if CFG['facts']['use_facter']:
            output, rc = system_exec(CFG['facts']['facter_cmd'])
            try:
                facts.update(yaml.safe_load(output))
            except:
                logging.error('Error while loading yaml from facter')

        # Handle comma seperated list or a list
        if ',' in CFG['facts']['facts_path']:
            yaml_files = CFG['facts']['facts_path'].split(',')
        else:
            yaml_files = CFG['facts']['facts_path']
        for yaml_file in yaml_files:

            if not os.path.exists(yaml_file):
                logging.error( yaml_file + ' file does not exist')
                continue

            # If we get to here - the file looks good
            valid_yaml_files.append(yaml_file)

        for yaml_file in valid_yaml_files:
            logging.debug('Will load facts from ' + str(yaml_file))

            try:
                f = open(yaml_file, 'r')
                data_from_file = yaml.safe_load(f)
            except:
                logging.error('Error while loading yaml from file: %s' % (yaml_file))
                continue

            if type(data_from_file) is str:
                logging.debug('Facts file looks to be string ' + str(yaml_file))
                fact_string = data_from_file.split()

                for fact in fact_string:
                    newfact = {fact:'None'}
                    facts.update(newfact)
            else:
                logging.debug('We assume facts file is in yaml format ' + str(yaml_file))
                try: 
                    facts.update(data_from_file)
                    f.close()
                except ValueError, yamlerr:
                    logging.error('Failed to load yaml file %s (%s)' % (yaml_file, yamlerr))
                    break
                except:
                    logging.error('Failed to load yaml file')
                    break

        if len(facts) > 0:
            logging.info('Facts loaded from file(s)')
            logging.debug(str(facts))
            yaml_queue.put(facts)

            if CFG['facts']['use_queue'] == True:
                write_yaml_to_queue(facts, sqs_facts_queue)
        else:
            logging.debug('Facts are empty')

        time.sleep(sleeper(starttime, CFG['facts']['refresh']))

def system_exec (cmd):

    try:
        logging.debug ('Executing: %s' % (cmd))
        o = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True)
        output = o.communicate()[0]
        rc = o.poll()

    except OSError, e:
        output = ('Failed to execute: %s (%d) %s' % (cmd, e.errno, e.strerror))
        rc = e.errno

    logging.debug('output: %s, rc: %d' % (output, rc))

    return (output, rc)

def cli_mode (message):

    output, rc = system_exec(message['cmd'])

    response={'mode': message['mode'], 'output': output, 'rc': rc, 'ts':time.time(), 'msg_id':message['orgid'], 'hostname':gethostname()};
    return response

def write_to_file (payload, identifier):
    script_file = '/tmp/' + 'ec2-cagent-' + identifier

    try:
        f = open (script_file, 'w')
        f.write (payload)
        f.close()
    except IOError, e:
        logging.error ('Failed to write payload to ' + script_file + ' (%d) %s \n' % (e.errno, e.strerror))
        return (False, 'Failed to write payload to file ' + script_file)

    os.chmod(script_file, stat.S_IRWXU)

    return (True, script_file)

def script_mode (message):

    (file_written, script_file ) = write_to_file(message['payload'], message['batch_msg_id'])

    if file_written is False:
        # script_file will include error message
        response={'mode': message['mode'], 'output': script_file, 'rc': '255', 'ts':time.time(), 'msg_id':message['orgid'], 'hostname':gethostname()};
        return response

    command=script_file

    # If we are carrying script parameters, please use them
    if message['script_param'] is not None:
        command = str(command) + ' ' + str(message['script_param'])

    output, rc = system_exec(command)

    try:
        logging.debug ('Deleting file with script '  + script_file)
        os.remove(script_file)
    except OSError, e:
        logging.error ('Failed to delete script file ' + script_file + ' (%d) %s \n' % (e.errno, e.strerror))

    response={'mode': message['mode'], 'output': output, 'rc': rc, 'ts':time.time(), 'msg_id':message['orgid'], 'hostname':gethostname()};
    return response

def message_history(initialize=False, read_msgs={}):
    history_file='/tmp/ec2-cagent-message-hist.pickle'

    if initialize is True and os.path.isfile(history_file):
        logging.debug ('Getting messages from history file %s' %  (history_file))
        # Read history pickle file to avoid re-reading messages from queue on start-up
        try:
            history_fh = open(history_file , 'rb')
            read_msgs = cPickle.load(history_fh)
            history_fh.close()
        except:
            logging.error ('Failed to read history file %s' %  (history_file))
            sys.exit(1)

    # Clear references to old messages ( keep messages until we haven't done anything for 2 minutes )
    # AWS SQS queues must have this setting
    # Message Retention Period:     1 minutes
    # That will allow AWS to purge old messages a minute before we might pick it up again.

    logging.debug ('Cleaning old messages if any')
    for id, lastseen in read_msgs.items():
        if int(lastseen) < (time.time() - 120 ):
            logging.info('removed old message %s' % (id))
            del read_msgs[id]

    # Record current known messages ids to a pickle file so we avoid re-reading messages on start-up
    try:
        logging.debug ('Dumping messages to history file %s' %  (history_file))
        history_fh = open(history_file , 'wb')
        cPickle.dump( read_msgs, history_fh )
        history_fh.close()
    except:
        logging.error ('Failed to write history file %s' %  (history_file))
        sys.exit(1)

    return read_msgs

def sleeper(starttime, interval):
    now=time.time()
    diff = (now - starttime)
    logging.debug('Startime is %f, time now is %f, diff is %f, interval is %d' % (starttime, now, diff, interval))
    if diff < interval:
        logging.debug('Should sleep %f' % (interval - diff))
        return (interval - diff )
    else:
        return 0

def receive_sqs_msg_worker ( yaml_facts) :
    sqs_read_queue = establish_sqs_conn(CFG['aws']['sqs_read_queue'])
    read_msgs = message_history(initialize=True)

    while True:

        starttime=time.time()
        logging.debug('Looking for new messages on SQS')

        logging.info ('Attempting to get messages from SQS')
        try:
            msgs = sqs_read_queue.get_messages(num_messages=10, visibility_timeout=0)
        except:
            logging.error('Attempt to receive sqs messages failed')
            # exit thread
            return

        logging.info ('Request messages from SQS success')

        for msg in msgs:
            if msg.id in read_msgs:
                logging.debug('Message already read')
                continue
            else:
                read_msgs[msg.id] = time.time()

            message=json.loads(msg.get_body())
            batch_msg_id = message['batch_msg_id']
            wf = message['wf']
            wof = message['wof']
            message['orgid'] = msg.id

            # We send multiple duplicate messages - lets avoid handling all of them
            if batch_msg_id in read_msgs:
                logging.debug('Message already read')
                continue
            else:
                read_msgs[batch_msg_id] = time.time()

            if fact_lookup(wf, wof, yaml_facts ):
                logging.debug('SQS messages did not pass filter')
                read_msgs[msg.id] = time.time()
                # Record and clean old messages
                read_msgs = message_history(read_msgs=read_msgs)
                continue

            # Record and clean old messages
            read_msgs = message_history(read_msgs=read_msgs)

            logging.debug('New valid SQS message received')

            logging.debug('Putting SQS message on task queue')
            task_queue.put(message)


        time.sleep(sleeper(starttime, CFG['general']['sqs_poll_interval']))

def process_msg ( message ):

    cmd_str = str(message['cmd'])
    mode = str(message['mode'])
    ts = str(message['ts'])

    if mode in ['ping', 'count']:
        logging.info('Performing ' + str(mode) + ' reply')
        response={'mode': mode, 'output': ts, 'rc': '0', 'ts':time.time(), 'msg_id':message['orgid'], 'hostname':gethostname()};
        done_queue.put(response)
        logging.info("Worker process done")
        return
    else:
        logging.info("Performing ping reply - for discovery purpose")
        response={'mode': 'ping', 'output': ts, 'rc': '0', 'ts':time.time(), 'msg_id':message['orgid'], 'hostname':gethostname(), 'discovery':1};
        done_queue.put(response)

    if mode == 'cli':
        logging.info("Performing command execution")
        response = cli_mode(message)
    elif mode in [ 'script' ]:
        logging.info('Performing ' + mode + ' execution')
        response = script_mode (message)
    else:
        logging.error('Unknown mode: ' + mode)
        response =  'Unknown command ' + cmd_str
        response={'mode': mode, 'output': response, 'rc': '0', 'ts':time.time(), 'msg_id':message['orgid'], 'hostname':gethostname()};

    done_queue.put(response)
    logging.info("Worker process done")

    return

def write_sqs_msg (response, sqs_write_queue):

    # response - dict
    # response_in_json - json string
    # message - sqs message object

    try:
        response_in_json=json.dumps(response)
        logging.debug ('Response size in json ' + str(len(response_in_json)))
        message = sqs_write_queue.new_message(response_in_json)
    except:
        logging.error('Failed to create message object - check queue name')
        return False

    if len(message) >= 65536 and 'output' in response:
        logging.warn ('Response message is too big to put on SQS ' + str(len(message)) + ' - sending last 1000 characters')

        # Let's make sure we can actually decrease response....
        if len(response['output']) > 1000:
            response['output'] = '--- OUTPUT CAPPED ---\n' + str(response['output'][-1000:])
        else:
            logging.error ('Message size is too big to put on sqs, but it is not in the actual output!')
            response['output'] = 'Panic response from agent - please check exit code'

        try:
            response_in_json=json.dumps(response)
            logging.debug ('Response size in json ' + str(len(response_in_json)))
            message = sqs_write_queue.new_message(response_in_json)
        except:
            logging.error('Failed to create message object - check queue name')
            return False

    elif len(message) >= 65536:
        logging.error ('Response message is too big to put on SQS ' + str(len(message)))
        return False

    logging.info ('Attempting write to SQS')
    try:
        org = sqs_write_queue.write(message)
    except:
        logging.error('Failed to write to sqs - quitting thread')
        return False
    if org.id is None:
        logging.error ('Failed to write response message to SQS')
        return False
    else:
        logging.info ('Write to SQS success')
        logging.debug('Wrote response to SQS')
        return True

    return False

def get_config():
    # CFILE
    if not os.path.exists(CFILE):
        sys.stderr.write('Config file (%s) does not exist\n' % (CFILE) )
        sys.exit(1)

    try:
        f = open(CFILE, 'r')
    except IOError, e:
        sys.stderr.write('Failed to open config file (%s)\n' % (CFILE) )
        sys.exit(1)

    try:
        global CFG
        CFG=json.load(f)
    except (TypeError, ValueError), e:
        sys.stderr.write('Error in configuration file (%s)\n' % (CFILE) )
        sys.exit(1)

def write_pidfile(pidfile):

    try:
        f = open (pidfile, 'w')
        f.write (str(os.getpid()))
    except IOError, e:
        sys.stderr.write("Failed to write pid to pidfile (%s): (%d) %s\n" % (pidfile, e.errno, e.strerror))
        sys.exit(1)

def daemonize (foreground, pidfile, stdin='/dev/null', stdout='/dev/null', stderr='/dev/null' ):
    lockfile=acquire_lock(pidfile)

    if foreground is True:
        logging.info ('Running in the foreground')
        write_pidfile(pidfile)
        return lockfile

    try:
        pid = os.fork( )
        if pid > 0:
            sys.exit(0) # Exit first parent.
    except OSError, e:
        sys.stderr.write("fork #1 failed: (%d) %s\n" % (e.errno, e.strerror))
        sys.exit(1)
    # Decouple from parent environment.
    os.chdir("/")
    os.umask(022)
    os.setsid( )
    # Perform second fork.
    try:
        pid = os.fork( )
        if pid > 0:
            sys.exit(0) # Exit second parent.
    except OSError, e:
        sys.stderr.write("fork #2 failed: (%d) %s\n" % (e.errno, e.strerror))
        sys.exit(1)
    # The process is now daemonized, redirect standard file descriptors.
    for f in sys.stdout, sys.stderr: f.flush( )
    si = file(stdin, 'r')
    so = file(stdout, 'a+')
    se = file(stderr, 'a+', 0)
    os.dup2(si.fileno( ), sys.stdin.fileno( ))
    os.dup2(so.fileno( ), sys.stdout.fileno( ))
    os.dup2(se.fileno( ), sys.stderr.fileno( ))

    write_pidfile(pidfile)

    sys.stdout.write('Daemon started with pid %d\n' % os.getpid( ) )

    sys.stderr.flush()
    sys.stdout.flush()

    return lockfile

def fact_lookup (wf, wof, yaml_facts ):

    # True - Skip
    # False - Process

    # WOF
    # Return True if we have the fact ( just on should skip message )
    # Return False if we don't have the fact

    # WF
    # Return False if we have all the fact ( all facts must match )
    # Return True if we don't have the fact

   # Execptions where we need to be careful

    # If yaml facts is false
    if CFG['facts']['enabled'] == False:
        if wf is None and wof is None:
            logging.debug('Process message - yaml_facts is False and message contains no facts')
            return False
        else:
            logging.debug('Skip message - yaml_facts is False but message contains facts')
            return True

    # If yaml facts is true
    if CFG['facts']['enabled'] == True:
        # but we have no facts skip everything
        if yaml_facts is None:
            logging.error('Skip message - yaml_facts is True but there are no facts available!')
            return True

        # If nothing is set we process message
        if wf is None and wof is None:
            logging.debug('Process message - yaml_facts is True but message contains no facts')
            return False

    # If wof is in facts we return True ( skip message )
    if wof is not None:
        wof = wof.split(',')
        for f in wof:
            if '=' in f:
                f = f.split('=')

                if (f[0] in yaml_facts) and (yaml_facts[f[0]] == f[1]):
                    logging.debug('Skip message - wof matched')
                    return True
            else:
                if f in yaml_facts:
                    logging.debug('Skip message - wof matched')
                    return True

    # Without is set but we did not find it, if wf is not set we return False ( process message )
    if wf is None:
        logging.debug('Process message - no wof match and no wf')
        return False

    # If all wf is in facts we return False ( process message )
    no_match=0
    wf = wf.split(',')
    for f in wf:
        if '=' in f:
            f = f.split('=')

            if (f[0] not in yaml_facts) or (yaml_facts[f[0]] != f[1]):
                no_match += 1
        else:
            if f not in yaml_facts:
                no_match += 1

    if no_match == 0 :
        # All facts was found ( process message )
        logging.debug('Process message - wf matched')
        return False
    else:
        # Facts set was not found - return True ( skip message )
        logging.debug('Skip message - no wf match')
        return True

def check_task_queue_worker():
    logging.debug('Checking task queue')

    while True:
        try:
            message = task_queue.get(True)
            logging.debug('Task read from task queue')
        except Empty:
            logging.error('Task queue is empty')
        except:
            logging.error('Unknown error in processing task queue')
        else:
            cmd_worker_thread = Thread(target = process_msg, args = (message,))
            cmd_worker_thread.setDaemon(True)
            try:
                cmd_worker_thread.start()
            except:
                logging.error('Failed ot start cmd_worker thread')

def check_done_queue_worker():
    logging.debug('Checking done queue')
    sqs_write_queue = establish_sqs_conn(CFG['aws']['sqs_write_queue'])

    while True:
        try:
            response = done_queue.get(True)
            logging.debug('Response read from done queue')
        except Empty:
            logging.debug('Done queue is empty')
        except:
            logging.debug('Unknown error in processing done queue')
        else:
            write_sqs_msg (response, sqs_write_queue)

def establish_sqs_conn (queue):

    # Connect with key, secret and region
    try:
        logging.info('Connecting to region ' + str(CFG['aws']['region']))
        conn = connect_to_region(CFG['aws']['region'])
    except:
        logging.error('Could not connect to SQS - check your authentication')
        sys.exit(1)

    try:
        logging.info('Getting queue url for ' + str(queue))
        queue_conn = conn.get_queue(queue)
    except:
        logging.error('Could not determine SQS queue url - check your authentication')
        sys.exit(1)

    if queue_conn is None:
        logging.error('Unable to get ' + str(queue) + ' queue by name')
        sys.exit(1)

    return queue_conn

def main (yaml_facts=None, running_threads_last=0):

    # maintain workers
    workers=[]

    # Start 1 threads to handle facts if necessary
    if CFG['facts']['enabled'] == True:
        get_yaml_facts_worker_thread = Thread(target = get_yaml_facts_worker, args = ())
        get_yaml_facts_worker_thread.daemon=True
        get_yaml_facts_worker_thread.setName('get_yaml_facts_worker')
        try:
            get_yaml_facts_worker_thread.start()
        except:
            logging.error('Failed ot start get_yaml_facts_worker thread')
            terminate_process(15, None)
        workers.append(get_yaml_facts_worker_thread)

        logging.info('Waiting for initial facts')
        try:
            yaml_facts = yaml_queue.get(True, 5)
        except Empty:
            logging.error('Failed to get facts within 5 seconds')
            terminate_process(15, None)

        if yaml_facts is None:
            logging.error('Run with yaml facts is set but nothing was found')
            terminate_process(15, None)
        else:
            logging.info('Populated initial facts')

    # Start 3 threads to handle receive sql, done queue, task queue
    receive_sqs_msg_worker_thread = Thread(target = receive_sqs_msg_worker, args = (yaml_facts,))
    receive_sqs_msg_worker_thread.daemon=True
    receive_sqs_msg_worker_thread.setName('receive_sqs_msg_worker')
    try:
        receive_sqs_msg_worker_thread.start()
    except:
        logging.error('Failed ot start receive_sqs_msg_worker thread')
        terminate_process(15, None)
    workers.append(receive_sqs_msg_worker_thread)

    check_done_queue_worker_thread = Thread(target = check_done_queue_worker)
    check_done_queue_worker_thread.setDaemon(True)
    try:
        check_done_queue_worker_thread.start()
    except:
        logging.error('Failed ot start check_done_queue_worker thread')
        terminate_process(15, None)
    workers.append(check_done_queue_worker_thread)

    check_task_queue_worker_thread = Thread(target = check_task_queue_worker)
    check_task_queue_worker_thread.setDaemon(True)
    try:
        check_task_queue_worker_thread.start()
    except:
        logging.error('Failed ot start check_done_queue_worker thread')
        terminate_process(15, None)
    workers.append(check_task_queue_worker_thread)

    try:
        while True:
            running_threads = active_count()
            if running_threads != running_threads_last:
                running_threads_last = running_threads
                logging.debug('%d active threads' % (running_threads))

            # Monitor if a thread exited...
            for worker in workers:
                if not worker.is_alive():
                    workers.remove(worker)
                    logging.error('%s thread just died!' % (worker.name))
                    terminate_process(15, None)

            time.sleep(5)

    except (KeyboardInterrupt, SystemExit):
        logging.info('Main loop caught interrupt...')
        sys.exit(0)

if __name__ == "__main__":

    (foreground, logfile, pidfile, stdoutlog, stderrlog)=initialize()
    lockfile=daemonize(foreground, pidfile,'/dev/null', stdoutlog, stderrlog)
    try:
        sys.exit(main())
    except (KeyboardInterrupt, SystemExit):
        logging.info('ec2-cagent dutifully exiting...')
        sys.exit(0)
