Commits

zzzeek  committed 0a6a574

initial rev

  • Participants

Comments (0)

Files changed (12)

+syntax:regexp
+^build/
+^doc/build/output
+.pyc$
+.orig$
+.egg-info
+.*,cover
+.un~
+\.coverage
+\.DS_Store
+test.cfg
+Allows control of EC2 instances via email.
+
+Installation is via procmail or any other system that can route 
+email messages to the script.
+
+Copy the file ec2control.ini.SAMPLE to ec2control.ini.
+
+The script is then invoked as ec2control --config /path/to/ec2control.ini, with the email text as 
+standard input.
+
+An email sends the command via the subject line::
+
+    "startup"
+
+or
+
+    "startup <instancename>"

File ec2control.ini.SAMPLE

+# ec2control section, app wide config.
+[ec2control]
+
+# name of response template.   See minecraft.mako for
+# an example.
+response_template=my_response.mako
+
+# SMTP host to send emails.
+smtp_host=localhost
+
+# SMTP port to send emails.
+smtp_port=25
+
+# From address.  this will be the from for all emails sent.
+from_addr=myec2control@mydomain.com
+
+# CC address, comma separated, will receive copies of all emails.
+cc=address1@foo.com, address2@bar.com
+
+# Amazon API key/secret.  
+aws_key=<Amazon API key>
+aws_secret=<Amazon API secret>
+
+# instance names.  These names are local to this config and
+# don't have to match those used on AWS.
+instances=my_instance_one, my_instance_two
+
+# default instance name, optional,
+# used if no name is given with "startup" command
+default_instance=my_instance_one
+
+# instance sections for each of the above names.
+
+[instance_my_instance_one]
+instance_id=i-12345678
+descriptive_name=My Instance
+custom_key_one=Some Custom Value
+
+[instance_my_instance_two]
+instance_id=i-90123456
+descriptive_name=My Second Instance
+custom_key_one=Some Other Custom Value
+
+
+# Logging configuration
+[loggers]
+keys = root,ec2control
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+qualname =
+
+[logger_ec2control]
+level = INFO
+handlers =
+qualname = ec2control
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(levelname)-5.5s [%(name)s] %(message)s
+datefmt = %H:%M:%S

File ec2control/__init__.py

Empty file added.

File ec2control/config.py

+import ConfigParser
+
+class Config(object):
+    def __init__(self, config_file_name):
+        self.config = ConfigParser.ConfigParser()
+        self.config.read([config_file_name])
+
+    def _split_name(self, name):
+        if "." in name:
+            return name.split(".", 1)
+        else:
+            return "ec2control", name
+
+    def __getitem__(self, name):
+        section, key = self._split_name(name)
+        return self.config.get(section, key)
+
+    def get(self, name, default=None):
+        section, key = self._split_name(name)
+        if self.config.has_option(section, key):
+            return self.config.get(section, key)
+        else:
+            return default
+
+    def section_dict(self, section):
+        return dict(self.config.items(section))

File ec2control/ec2.py

+from boto.ec2.connection import EC2Connection
+import time
+
+class CommandError(Exception):
+    pass
+
+class Instance(object):
+    def __init__(self, config, name):
+        self.config_file = config
+        if name is None:
+            if config.get("default_instance"):
+                name = config["default_instance"]
+            else:
+                raise CommandError("No instance name specified")
+        else:
+            self.name = name
+
+        self.config = config.section_dict("instance_%s" % name)
+        self.descriptive_name = self.config['descriptive_name']
+        self._conn = EC2Connection(config['aws_key'], config['aws_secret'])
+
+    @property
+    def ip_address(self):
+        if self.instance:
+            return self.instance.ip_address
+        else:
+            return None
+
+    def start(self):
+        instances = self._conn.start_instances(instance_ids=[self.config['instance_id']])
+
+        inst = instances[0]
+
+        inst.update()
+        while inst.state == 'pending':
+            time.sleep(10)
+            inst.update()
+
+        # grease the wheels, seems to 
+        # say "stopped" for a second here
+        time.sleep(10)
+
+        self.status = inst.state
+

File ec2control/emailer.py

+import smtplib
+from email.mime.text import MIMEText
+import re
+from mako.template import Template
+
+class ParseMail(object):
+    def __init__(self, email):
+        self.email = email
+        self._parse(email)
+
+    def _parse(self, data):
+        lines = data.split("\n")
+        orig_from = lines[0]
+        if orig_from.startswith("From "):
+            orig_from = re.split(r"\s", orig_from)[1]
+        else:
+            orig_from = None
+        subject = None
+        from_ = None
+        for line in lines:
+            if subject is None and line.startswith("Subject: "):
+                tokens = re.split(r"\s+", line)
+                if len(tokens) == 1:
+                    command = None
+                    arguments = []
+                else:
+                    command = tokens[1]
+                    arguments = tokens[2:]
+            elif from_ is None and line.startswith("From: "):
+                from_ = re.split(r"\s", line, 1)[1]
+                m = re.match(r".*<(.*?)>\s*$", from_)
+                if m:
+                    from_ = m.group(1)
+
+        self.effective_from = orig_from or from_
+        self.command = command
+        self.arguments = arguments
+
+class Response(object):
+    def __init__(self, config, parsed_struct):
+        self.config = config
+        self.parsed_struct = parsed_struct
+
+    def send_started(self, instance):
+        self._render(status="started", instance=instance)
+
+    def send_command_error(self, exception, instance):
+        self._render(status="command_error", exception=exception, instance=instance)
+
+    def send_error(self, exception, instance):
+        self._render(status="error", exception=exception, instance=instance)
+
+    def _render(self, **kw):
+        t = Template(filename=self.config['response_template'])
+        body = t.render(**kw)
+        subject = t.get_def("subject").render(**kw)
+        self._send(subject, body)
+
+    def _send(self, subject, message):
+        msg = MIMEText(message)
+
+        to_addr = self.parsed_struct.effective_from
+        cc_addr = self.config['cc']
+
+        msg['Subject'] = subject
+        msg['From'] = self.config['from_addr']
+        msg['To'] = to_addr
+        msg['Cc'] = cc_addr
+        msg = msg.as_string()
+        smtp = smtplib.SMTP(self.config['smtp_host'], int(self.config['smtp_port']))
+
+        for addr in set([to] + cc.split(",")):
+            smtp.sendmail(from_addr, addr, msg)

File ec2control/main.py

+import sys
+from emailer import ParseMail, Response
+from ec2 import Instance, CommandError
+from config import Config
+import argparse
+
+import logging
+log = logging.getLogger(__name__)
+
+def main(argv):
+    parser = argparse.ArgumentParser()
+    parser.add_argument("-c", "--config", 
+                        type=str, 
+                        default="ec2control.ini", 
+                        help="Path to config file")
+    parser.add_argument("-r", "--read", 
+                        type=str, 
+                        help="Read email text from file instead of stdin")
+
+    options = parser.parse_args()
+
+    config = Config(options.config)
+
+    if options.read:
+        email_data = file(options.read).read()
+    else:
+        email_data = sys.stdin.read()
+    struct = ParseMail(email_data)
+    resp = Response(config, struct)
+    instance = None
+    try:
+        if struct.command == "startup":
+            if struct.arguments:
+                name = struct.arguments[0]
+            else:
+                name = None
+            instance = Instance(config, name)
+            instance.start()
+            resp.send_started(instance)
+        else:
+            raise CommandError("Unknown command: '%s'"  % struct.command)
+    except CommandError, ce:
+        resp.send_command_error(ce, instance)
+    except Exception, e:
+        log.error("Error occurred", e)
+        resp.send_error(e, instance)
+    print email_data
+

File sample_templates/minecraft.mako

+
+<%def name="subject()">
+    % if status == 'started':
+        % if instance.status == 'started':
+            ${instance.descriptive_name} is online!  ${instance.ip_address}:${instance.config['minecraft_port']}
+        % else:
+            ${instance.descriptive_name} failed to start!
+        % endif
+    % elif status == 'command_error':
+        ${exception}
+    % elif status == 'error':
+        An error occurred.
+    % endif
+</%def>
+
+Greetings Citizen -
+
+% if status == 'started':
+    % if instance.status == 'started':
+        Your presence is welcome at ${instance.descriptive_name}!  ${instance.ip_address}:${instance.config['minecraft_port']}
+    % else:
+        ${instance.descriptive_name} wasn't able to start !   Status is ${instance.status}
+    % endif
+% elif status == 'command_error':
+    Sorry, I wasn't able to understand your command.
+
+    ${exception}
+% elif status == 'error':
+    % if instance:
+        ${instance.descriptive_name} had a problem.
+    % else:
+        An unknown error occurred.
+    % endif
+
+    ${exception}
+% endif
+
+Regards, 
+
+The Management

File scripts/ec2control

+#!/usr/bin/env python
+
+from ec2control.main import main
+import sys
+
+if __name__ == "__main__":
+    main(sys.argv)
+import os
+import sys
+
+from setuptools import setup, find_packages
+
+extra = {}
+if sys.version_info >= (3, 0):
+    extra.update(
+        use_2to3=True,
+    )
+
+readme = os.path.join(os.path.dirname(__file__), 'README.rst')
+
+setup(name='Ec2Control',
+      version=0.1,
+      description="Control EC2 Instances via Email",
+      long_description=file(readme).read(),
+      classifiers=[
+      'Development Status :: 4 - Beta',
+      'Intended Audience :: Developers',
+      'License :: OSI Approved :: BSD License',
+      'Programming Language :: Python',
+      'Programming Language :: Python :: 3',
+      ],
+      author='Mike Bayer',
+      author_email='mike_mp@zzzcomputing.com',
+      url='http://bitbucket.org/zzzeek/ec2control',
+      license='BSD',
+      packages=find_packages(exclude=['ez_setup', 'tests']),
+      zip_safe=False,
+      scripts=['scripts/ec2control'],
+      install_requires=[
+          'Mako'
+      ],
+
+      test_suite='nose.collector',
+      tests_require=['nose'],
+      **extra
+)

File test_email.txt

+From someone@bar.com  Fri Aug 26 12:08:03 2011
+Return-Path: 
+X-Original-To: 
+Delivered-To: 
+Received: from foo foo bar bar
+MIME-Version: 1.0
+Received: by 1.2.3.4 with HTTP; Fri, 26 Aug 2011 09:08:02 -0700 (PDT)
+Date: Fri, 26 Aug 2011 12:08:02 -0400
+Subject: start instanceone
+From: someone <foo@bar.com>
+To: ec2receiver@mydomain.com
+Content-Type: multipart/alternative; boundary=001517447aeefb778004ab6abfc5
+
+--001517447aeefb778004ab6abfc5
+Content-Type: text/plain; charset=ISO-8859-1
+
+pm7
+
+--001517447aeefb778004ab6abfc5
+Content-Type: text/html; charset=ISO-8859-1
+
+pm7<br>
+
+--001517447aeefb778004ab6abfc5--
+