Mitch Garnaat

Initial version, barely working.

...@@ -52,3 +52,6 @@ docs/_build/ ...@@ -52,3 +52,6 @@ docs/_build/
52 52
53 # PyBuilder 53 # PyBuilder
54 target/ 54 target/
55 +
56 +# Emacs backup files
57 +*~
...\ No newline at end of file ...\ No newline at end of file
......
1 +include README.md
2 +include LICENSE
3 +include requirements.txt
4 +include kappa/_version
1 +#!/usr/bin/env python
2 +# Copyright (c) 2014 Mitch Garnaat http://garnaat.org/
3 +#
4 +# Licensed under the Apache License, Version 2.0 (the "License"). You
5 +# may not use this file except in compliance with the License. A copy of
6 +# the License is located at
7 +#
8 +# http://aws.amazon.com/apache2.0/
9 +#
10 +# or in the "license" file accompanying this file. This file is
11 +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
12 +# ANY KIND, either express or implied. See the License for the specific
13 +# language governing permissions and limitations under the License.
14 +import logging
15 +
16 +import click
17 +import yaml
18 +
19 +from kappa import Kappa
20 +
21 +FmtString = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
22 +
23 +
24 +def set_debug_logger(logger_names=['kappa'], stream=None):
25 + """
26 + Convenience function to quickly configure full debug output
27 + to go to the console.
28 + """
29 + for logger_name in logger_names:
30 + log = logging.getLogger(logger_name)
31 + log.setLevel(logging.DEBUG)
32 +
33 + ch = logging.StreamHandler(stream)
34 + ch.setLevel(logging.DEBUG)
35 +
36 + # create formatter
37 + formatter = logging.Formatter(FmtString)
38 +
39 + # add formatter to ch
40 + ch.setFormatter(formatter)
41 +
42 + # add ch to logger
43 + log.addHandler(ch)
44 +
45 +
46 +@click.command()
47 +@click.option(
48 + '--config',
49 + help="Path to the Kappa config YAML file",
50 + type=click.File('rb'),
51 + envvar='KAPPA_CONFIG',
52 + default=None
53 +)
54 +@click.option(
55 + '--debug/--no-debug',
56 + default=False,
57 + help='Turn on debugging output'
58 +)
59 +@click.option(
60 + '--dryrun/--no-dryrun',
61 + default=False,
62 + help='Do not send the email'
63 +)
64 +@click.argument(
65 + 'command',
66 + required=True,
67 + type=click.Choice(['deploy', 'test', 'tail', 'delete'])
68 +)
69 +def main(config=None, debug=False, dryrun=False, command=None):
70 + if debug:
71 + set_debug_logger()
72 + config = yaml.load(config)
73 + kappa = Kappa(config)
74 + if command == 'deploy':
75 + kappa.deploy()
76 + elif command == 'test':
77 + kappa.test()
78 + elif command == 'tail':
79 + kappa.tail()
80 + elif command == 'delete':
81 + kappa.delete()
82 +
83 +
84 +if __name__ == '__main__':
85 + main()
1 +# Copyright (c) 2014 Mitch Garnaat http://garnaat.org/
2 +#
3 +# Licensed under the Apache License, Version 2.0 (the "License"). You
4 +# may not use this file except in compliance with the License. A copy of
5 +# the License is located at
6 +#
7 +# http://aws.amazon.com/apache2.0/
8 +#
9 +# or in the "license" file accompanying this file. This file is
10 +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11 +# ANY KIND, either express or implied. See the License for the specific
12 +# language governing permissions and limitations under the License.
13 +
14 +import logging
15 +import os
16 +import zipfile
17 +
18 +import botocore.session
19 +from botocore.exceptions import ClientError
20 +
21 +LOG = logging.getLogger(__name__)
22 +
23 +
24 +class Kappa(object):
25 +
26 + def __init__(self, config):
27 + self.config = config
28 + self.session = botocore.session.get_session()
29 + self.session.profile = config['profile']
30 + self.region = config['region']
31 +
32 + def create_update_roles(self, stack_name, roles_path):
33 + LOG.debug('create_update_policies: stack_name=%s', stack_name)
34 + LOG.debug('create_update_policies: roles_path=%s', roles_path)
35 + cfn = self.session.create_client('cloudformation', self.region)
36 + # Does stack already exist?
37 + try:
38 + response = cfn.describe_stacks(StackName=stack_name)
39 + LOG.debug('Stack %s already exists', stack_name)
40 + except ClientError:
41 + LOG.debug('Stack %s does not exist', stack_name)
42 + response = None
43 + template_body = open(roles_path).read()
44 + if response:
45 + try:
46 + cfn.update_stack(
47 + StackName=stack_name, TemplateBody=template_body,
48 + Capabilities=['CAPABILITY_IAM'])
49 + except ClientError, e:
50 + LOG.debug(str(e))
51 + else:
52 + response = cfn.create_stack(
53 + StackName=stack_name, TemplateBody=template_body,
54 + Capabilities=['CAPABILITY_IAM'])
55 +
56 + def get_role_arn(self, role_name):
57 + role_arn = None
58 + cfn = self.session.create_client('cloudformation', self.region)
59 + try:
60 + resources = cfn.list_stack_resources(
61 + StackName=self.config['cloudformation']['stack_name'])
62 + except Exception:
63 + LOG.exception('Unable to find role ARN: %s', role_name)
64 + for resource in resources['StackResourceSummaries']:
65 + if resource['LogicalResourceId'] == role_name:
66 + iam = self.session.create_client('iam')
67 + role = iam.get_role(RoleName=resource['PhysicalResourceId'])
68 + role_arn = role['Role']['Arn']
69 + LOG.debug('role_arn: %s', role_arn)
70 + return role_arn
71 +
72 + def delete_roles(self, stack_name):
73 + LOG.debug('delete_roles: stack_name=%s', stack_name)
74 + cfn = self.session.create_client('cloudformation', self.region)
75 + try:
76 + cfn.delete_stack(StackName=stack_name)
77 + except Exception:
78 + LOG.exception('Unable to delete stack: %s', stack_name)
79 +
80 + def _zip_lambda_dir(self, zipfile_name, lambda_dir):
81 + LOG.debug('_zip_lambda_dir: lambda_dir=%s', lambda_dir)
82 + LOG.debug('zipfile_name=%s', zipfile_name)
83 + relroot = os.path.abspath(os.path.join(lambda_dir, os.pardir))
84 + with zipfile.ZipFile(zipfile_name, 'w') as zf:
85 + for root, dirs, files in os.walk(lambda_dir):
86 + zf.write(root, os.path.relpath(root, relroot))
87 + for file in files:
88 + filename = os.path.join(root, file)
89 + if os.path.isfile(filename):
90 + arcname = os.path.join(
91 + os.path.relpath(root, relroot), file)
92 + zf.write(filename, arcname)
93 +
94 + def _zip_lambda_file(self, zipfile_name, lambda_file):
95 + LOG.debug('_zip_lambda_file: lambda_file=%s', lambda_file)
96 + LOG.debug('zipfile_name=%s', zipfile_name)
97 + with zipfile.ZipFile(zipfile_name, 'w') as zf:
98 + zf.write(lambda_file)
99 +
100 + def zip_lambda_function(self, zipfile_name, lambda_fn):
101 + if os.path.isdir(lambda_fn):
102 + self._zip_lambda_dir(zipfile_name, lambda_fn)
103 + else:
104 + self._zip_lambda_file(zipfile_name, lambda_fn)
105 +
106 + def upload_lambda_function(self, zip_file):
107 + LOG.debug('uploading %s', zip_file)
108 + lambda_svc = self.session.create_client('lambda', self.region)
109 + with open(zip_file, 'rb') as fp:
110 + exec_role = self.get_role_arn(
111 + self.config['cloudformation']['exec_role'])
112 + try:
113 + response = lambda_svc.upload_function(
114 + FunctionName=self.config['lambda']['name'],
115 + FunctionZip=fp,
116 + Runtime=self.config['lambda']['runtime'],
117 + Role=exec_role,
118 + Handler=self.config['lambda']['handler'],
119 + Mode=self.config['lambda']['mode'],
120 + Description=self.config['lambda']['description'],
121 + Timeout=self.config['lambda']['timeout'],
122 + MemorySize=self.config['lambda']['memory_size'])
123 + LOG.debug(response)
124 + except Exception:
125 + LOG.exception('Unable to upload zip file')
126 +
127 + def delete_lambda_function(self, function_name):
128 + LOG.debug('deleting function %s', function_name)
129 + lambda_svc = self.session.create_client('lambda', self.region)
130 + response = lambda_svc.delete_function(FunctionName=function_name)
131 + LOG.debug(response)
132 + return response
133 +
134 + def _invoke_asynch(self, data_file):
135 + LOG.debug('_invoke_async %s', data_file)
136 + with open(data_file) as fp:
137 + lambda_svc = self.session.create_client('lambda', self.region)
138 + response = lambda_svc.invoke_async(
139 + FunctionName=self.config['lambda']['name'],
140 + InvokeArgs=fp)
141 + LOG.debug(response)
142 +
143 + def _tail(self, function_name):
144 + LOG.debug('tailing function: %s', function_name)
145 + log_svc = self.session.create_client('logs', self.region)
146 + log_group_name = '/aws/lambda/%s' % function_name
147 + latest_stream = None
148 + response = log_svc.describe_log_streams(logGroupName=log_group_name)
149 + for stream in response['logStreams']:
150 + if not latest_stream:
151 + latest_stream = stream
152 + elif stream['lastEventTimestamp'] > latest_stream['lastEventTimestamp']:
153 + latest_stream = stream
154 + response = log_svc.get_log_events(
155 + logGroupName=log_group_name,
156 + logStreamName=latest_stream['logStreamName'])
157 + for log_event in response['events']:
158 + print('%s: %s' % (log_event['timestamp'], log_event['message']))
159 +
160 + def deploy(self):
161 + self.create_update_roles(
162 + self.config['cloudformation']['stack_name'],
163 + self.config['cloudformation']['template'])
164 + self.zip_lambda_function(
165 + self.config['lambda']['zipfile_name'],
166 + self.config['lambda']['path'])
167 + self.upload_lambda_function(self.config['lambda']['zipfile_name'])
168 +
169 + def test(self):
170 + self._invoke_asynch(self.config['lambda']['test_data'])
171 +
172 + def tail(self):
173 + self._tail(self.config['lambda']['name'])
174 +
175 + def delete(self):
176 + self.delete_roles(self.config['cloudformation']['stack_name'])
177 + self.delete_lambda_function(self.config['lambda']['name'])
1 +0.1.0
...\ No newline at end of file ...\ No newline at end of file
1 +botocore==0.75.0
2 +click==3.3
3 +PyYAML>=3.11
4 +nose==1.3.1
5 +tox==1.7.1
1 +#!/usr/bin/env python
2 +
3 +from setuptools import setup, find_packages
4 +
5 +import os
6 +
7 +requires = [
8 + 'botocore==0.75.0',
9 + 'click==3.3',
10 + 'PyYAML>=3.11'
11 +]
12 +
13 +
14 +setup(
15 + name='kappa',
16 + version=open(os.path.join('kappa', '_version')).read().strip(),
17 + description='A CLI tool for AWS Lambda developers',
18 + long_description=open('README.md').read(),
19 + author='Mitch Garnaat',
20 + author_email='mitch@garnaat.com',
21 + url='https://github.com/garnaat/kappa',
22 + packages=find_packages(exclude=['tests*']),
23 + package_data={'kappa': ['_version']},
24 + package_dir={'kappa': 'kappa'},
25 + scripts=['bin/kappa'],
26 + install_requires=requires,
27 + license=open("LICENSE").read(),
28 + classifiers=(
29 + 'Development Status :: 3 - Alpha',
30 + 'Intended Audience :: Developers',
31 + 'Intended Audience :: System Administrators',
32 + 'Natural Language :: English',
33 + 'License :: OSI Approved :: Apache Software License',
34 + 'Programming Language :: Python',
35 + 'Programming Language :: Python :: 2.6',
36 + 'Programming Language :: Python :: 2.7',
37 + 'Programming Language :: Python :: 3',
38 + 'Programming Language :: Python :: 3.3',
39 + 'Programming Language :: Python :: 3.4'
40 + ),
41 +)