ignore amazon-linux ami release-candidate versions
[awsible] / sqs-action.py
1 #!/usr/bin/env python
2 '''\
3 Check an SQS queue for ASG lifecycle notifications of new instances,
4 and run the appropriate Ansible playbook against the host.
5 '''
6
7 import argparse
8 import logging
9 import boto3
10 import json
11 import sys
12 import os
13 import errno
14 from subprocess import Popen, PIPE
15 from tempfile import gettempdir
16 from hashlib import sha256
17
18
19 ANSIBLE_PLAYBOOK_CMD = '/usr/local/bin/ansible-playbook'
20
21
22 def notify(subj, msg):
23 if topic:
24 u8msg = unicode(msg).encode('utf-8')
25 topic.publish(Subject=subj, Message=u8msg[:262144])
26 else:
27 print(msg)
28
29
30 def handleEvent(message, event, ASGName, InstanceId):
31 notice = [' '.join([ASGName, InstanceId, event])]
32 if os.path.isfile(os.path.join(args.playbooks, ASGName + '.yml')):
33 message.change_visibility(VisibilityTimeout=(60 * 15)) # hope config doesn't take more than 15m
34 cmd = [ ANSIBLE_PLAYBOOK_CMD, '-i', 'inventory', '--limit', InstanceId, ASGName + '.yml']
35 p = Popen(cmd, cwd=args.playbooks, stdout=PIPE, stderr=PIPE)
36 (stdoutdata, stderrdata) = p.communicate()
37 retval = p.returncode
38 message.change_visibility(VisibilityTimeout=60)
39 if retval:
40 notice += ['FAILURE CODE {}'.format(retval), stderrdata, stdoutdata]
41 else:
42 notice += ['SUCCESS']
43 message.delete()
44 else:
45 notice += ['no action taken: no playbook for this ASG']
46 message.delete()
47 notify(notice[0], '\n'.join(notice))
48
49
50 def processMessage(message):
51 '''Unpack the data we want from an SQS message.'''
52 try:
53 data = json.loads(json.loads(message.body)['Message'])
54 event = data['Event']
55 ASGName = data['AutoScalingGroupName']
56 InstanceId = data['EC2InstanceId']
57 except:
58 logging.warning('unparsable message %r', message.body)
59 message.delete()
60 else:
61 if event == 'autoscaling:EC2_INSTANCE_LAUNCH':
62 try:
63 instanceState = ec2r.Instance(InstanceId).state['Name']
64 except:
65 logging.warning('instance %s does not exist', InstanceId)
66 message.change_visibility(VisibilityTimeout=60 * 2)
67 else:
68 if instanceState == 'running':
69 handleEvent(message, event, ASGName, InstanceId)
70 else:
71 logging.warning('instance %s is in state %s, will try again', InstanceId, instanceState)
72 message.change_visibility(VisibilityTimeout=60 * 2)
73 else:
74 logging.warning('nothing to do for event %r', data)
75 message.delete()
76
77
78 class PidFileSingleton:
79 '''Ensure that only one instance of this specific script runs at once.'''
80 def __init__(self):
81 self.pidfile = os.path.join(gettempdir(), sha256(os.path.abspath(sys.argv[0])).hexdigest() + '.pid')
82 try:
83 fd = os.open(self.pidfile, os.O_WRONLY|os.O_CREAT|os.O_EXCL, )
84 except OSError as e:
85 self.pidfile = None
86 if e.errno == errno.EEXIST:
87 logging.debug('An instance of this is already running.')
88 sys.exit(0)
89 raise e
90 with os.fdopen(fd, 'w') as f:
91 f.write(str(os.getpid()))
92 def __del__(self):
93 if self.pidfile:
94 os.unlink(self.pidfile)
95
96
97 parser = argparse.ArgumentParser(description='act on SQS Notifications')
98 parser.add_argument('--profile', metavar='PROFILE', dest='profile_name', help='AWS Profile (default: current IAM Role)')
99 parser.add_argument('--region', metavar='REGION', dest='region_name', help='AWS Region')
100 parser.add_argument('playbooks', metavar='directory', help='path containing playbooks et al')
101 parser.add_argument('queue', help='SQS Queue')
102 parser.add_argument('arn', nargs='?', default=None, help='ARN of SNS topic')
103 args = parser.parse_args()
104
105 pidfile = PidFileSingleton()
106
107 session = boto3.session.Session(**{k:v for k,v in vars(args).items() if k in ('profile_name', 'region_name')})
108 queue = session.resource('sqs').get_queue_by_name(QueueName=args.queue)
109 topic = session.resource('sns').Topic(args.arn) if args.arn else None
110 ec2r = session.resource('ec2')
111
112 while True:
113 # long poll until there are no more messages
114 messages = queue.receive_messages(MaxNumberOfMessages=10, WaitTimeSeconds=20)
115 if not len(messages):
116 break
117 for message in messages:
118 processMessage(message)