From d50bc13aa357f6c3fa9d55f89f3e6c006102f559 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Tue, 19 Jun 2012 11:24:21 -0700 Subject: Add a mock ec2 metadata server that can be used for testing with --- tools/mock-meta.py | 327 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 327 insertions(+) create mode 100755 tools/mock-meta.py (limited to 'tools/mock-meta.py') diff --git a/tools/mock-meta.py b/tools/mock-meta.py new file mode 100755 index 00000000..019a24f2 --- /dev/null +++ b/tools/mock-meta.py @@ -0,0 +1,327 @@ +#!/usr/bin/python + +# Provides a somewhat random, somewhat compat, somewhat useful mock version of +# +# http://docs.amazonwebservices.com/AWSEC2/2007-08-29/DeveloperGuide/AESDG-chapter-instancedata.html + +import functools +import httplib +import logging +import sys +import string +import random +import yaml + +from optparse import OptionParser + +from BaseHTTPServer import (HTTPServer, BaseHTTPRequestHandler) + +log = logging.getLogger('meta-server') + +# Constants +EC2_VERSIONS = [ + '1.0', + '2007-01-19', + '2007-03-01', + '2007-08-29', + '2007-10-10', + '2007-12-15', + '2008-02-01', + '2008-09-01', + '2009-04-04', + 'latest', +] + +BLOCK_DEVS = [ + 'ami', + 'root', + 'ephemeral0', +] + +DEV_PREFIX = 'v' +DEV_MAPPINGS = { + 'ephemeral0': '%sda2' % (DEV_PREFIX), + 'root': '/dev/%sda1' % (DEV_PREFIX), + 'ami': '%sda1' % (DEV_PREFIX), + 'swap': '%sda3' % (DEV_PREFIX), +} + +META_CAPABILITIES = [ + 'aki-id', + 'ami-id', + 'ami-launch-index', + 'ami-manifest-path', + 'ari-id', + 'block-device-mapping/', + 'hostname', + 'instance-action', + 'instance-id', + 'instance-type', + 'local-hostname', + 'local-ipv4', + 'placement/', + 'product-codes', + 'public-hostname', + 'public-ipv4', + 'reservation-id', + 'security-groups' +] + +INSTANCE_TYPES = [ + 'm1.small', + 'm1.medium', + 'm1.large', + 'm1.xlarge', +] + +AVAILABILITY_ZONES = [ + "us-east-1a", + "us-east-1b", + "us-east-1c", + 'us-west-1', + "us-east-1d", + 'eu-west-1a', + 'eu-west-1b', +] + +PLACEMENT_CAPABILITIES = { + 'availability-zone': AVAILABILITY_ZONES, +} + + +class WebException(Exception): + def __init__(self, code, msg): + Exception.__init__(self, msg) + self.code = code + + +def yamlify(data): + formatted = yaml.dump(data, + line_break="\n", + indent=4, + explicit_start=True, + explicit_end=True, + default_flow_style=False) + return formatted + + +def format_text(text): + if not len(text): + return "<<" + lines = text.splitlines() + nlines = [] + for line in lines: + nlines.append("<< %s" % line) + return "\n".join(nlines) + + +ID_CHARS = [c for c in (string.ascii_uppercase + string.digits)] +def id_generator(size=6, lower=False): + txt = ''.join(random.choice(ID_CHARS) for x in range(size)) + if lower: + return txt.lower() + else: + return txt + + +class MetaDataHandler(object): + + def __init__(self, opts): + self.opts = opts + self.instances = {} + + def get_data(self, params, who, **kwargs): + if not params: + caps = sorted(META_CAPABILITIES) + return "\n".join(caps) + action = params[0] + action = action.lower() + if action == 'instance-id': + return 'i-%s' % (id_generator(lower=True)) + elif action == 'ami-launch-index': + return "%s" % random.choice([0,1,2,3]) + elif action == 'aki-id': + return 'aki-%s' % (id_generator(lower=True)) + elif action == 'ami-id': + return 'ami-%s' % (id_generator(lower=True)) + elif action == 'ari-id': + return 'ari-%s' % (id_generator(lower=True)) + elif action == 'block-device-mapping': + nparams = params[1:] + if not nparams: + devs = sorted(BLOCK_DEVS) + return "\n".join(devs) + else: + return "%s" % (DEV_MAPPINGS.get(nparams[0].strip(), '')) + elif action in ['hostname', 'local-hostname', 'public-hostname']: + return "%s" % (who) + elif action == 'instance-type': + return random.choice(INSTANCE_TYPES) + elif action == 'ami-manifest-path': + return 'my-amis/spamd-image.manifest.xml' + elif action == 'security-groups': + return 'default' + elif action in ['local-ipv4', 'public-ipv4']: + there_ip = kwargs.get('client_ip', '10.0.0.1') + return "%s" % (there_ip) + elif action == 'reservation-id': + return "r-%s" % (id_generator(lower=True)) + elif action == 'product-codes': + return "%s" % (id_generator(size=8)) + elif action == 'placement': + nparams = params[1:] + if not nparams: + pcaps = sorted(PLACEMENT_CAPABILITIES.keys()) + return "\n".join(pcaps) + else: + pentry = nparams[0].strip().lower() + if pentry == 'availability-zone': + zones = PLACEMENT_CAPABILITIES[pentry] + return "%s" % random.choice(zones) + else: + return "%s" % (PLACEMENT_CAPABILITIES.get(pentry, '')) + else: + return '' + +class UserDataHandler(object): + + def __init__(self, opts): + self.opts = opts + + def _get_user_blob(self, **kwargs): + blob_mp = {} + blob_mp['hostname'] = kwargs.get('who', '') + lines = [] + lines.append("#cloud-config") + lines.append(yamlify(blob_mp)) + blob = "\n".join(lines) + return blob.strip() + + def get_data(self, params, who, **kwargs): + if not params: + return self._get_user_blob(who=who) + return '' + + +# Seem to need to use globals since can't pass +# data into the request handlers instances... +# Puke! +meta_fetcher = None +user_fetcher = None + + +class Ec2Handler(BaseHTTPRequestHandler): + + def _get_versions(self): + versions = [] + for v in EC2_VERSIONS: + if v == 'latest': + continue + else: + versions.append(v) + versions = sorted(versions) + return "\n".join(versions) + + def log_message(self, format, *args): + msg = "%s - %s" % (self.address_string(), format % (args)) + log.info(msg) + + def _find_method(self, path): + # Puke! (globals) + global meta_fetcher + global user_fetcher + func_mapping = { + 'user-data': user_fetcher.get_data, + 'meta-data': meta_fetcher.get_data, + } + segments = [piece for piece in path.split('/') if len(piece)] + if not segments: + return self._get_versions + date = segments[0].strip().lower() + if date not in EC2_VERSIONS: + raise WebException(httplib.BAD_REQUEST, "Unknown date format %r" % date) + if len(segments) < 2: + raise WebException(httplib.BAD_REQUEST, "No action provided") + look_name = segments[1].lower() + if look_name not in func_mapping: + raise WebException(httplib.BAD_REQUEST, "Unknown requested data %r" % look_name) + base_func = func_mapping[look_name] + who = self.address_string() + kwargs = { + 'params': list(segments[2:]), + 'who': self.address_string(), + 'client_ip': self.client_address[0], + } + return functools.partial(base_func, **kwargs) + + def _do_response(self): + who = self.client_address + log.info("Got a call from %s for path %s", who, self.path) + try: + func = self._find_method(self.path) + log.info("Calling into func %s to get your data.", func) + data = func() + if not data: + data = '' + self.send_response(httplib.OK) + self.send_header("Content-Type", "binary/octet-stream") + self.send_header("Content-Length", len(data)) + log.info("Sending data (len=%s):\n%s", len(data), format_text(data)) + self.end_headers() + self.wfile.write(data) + except RuntimeError as e: + log.exception("Error somewhere in the server.") + self.send_error(httplib.INTERNAL_SERVER_ERROR, message=str(e)) + except WebException as e: + code = e.code + log.exception(str(e)) + self.send_error(code, message=str(e)) + + def do_GET(self): + self._do_response() + + def do_POST(self): + self._do_response() + + +def setup_logging(log_level, format='%(levelname)s: @%(name)s : %(message)s'): + root_logger = logging.getLogger() + console_logger = logging.StreamHandler(sys.stdout) + console_logger.setFormatter(logging.Formatter(format)) + root_logger.addHandler(console_logger) + root_logger.setLevel(log_level) + + +def extract_opts(): + parser = OptionParser() + parser.add_option("-p", "--port", dest="port", action="store", type=int, default=80, + help="port from which to serve traffic (default: %default)", metavar="PORT") + (options, args) = parser.parse_args() + out = dict() + out['extra'] = args + out['port'] = options.port + return out + + +def setup_fetchers(opts): + global meta_fetcher + global user_fetcher + meta_fetcher = MetaDataHandler(opts) + user_fetcher = UserDataHandler(opts) + + +def run_server(): + # Using global here since it doesn't seem like we + # can pass opts into a request handler constructor... + opts = extract_opts() + setup_logging(logging.DEBUG) + setup_fetchers(opts) + log.info("CLI opts: %s", opts) + server = HTTPServer(('0.0.0.0', opts['port']), Ec2Handler) + sa = server.socket.getsockname() + log.info("Serving server on %s using port %s ...", sa[0], sa[1]) + server.serve_forever() + + +if __name__ == '__main__': + run_server() -- cgit v1.2.3 From f7e638f6f58188cd4be1921cb045608f3c00d9c4 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Wed, 20 Jun 2012 23:59:24 -0700 Subject: Return a empty json map as default instead of an empty string for unknown fields --- tools/mock-meta.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'tools/mock-meta.py') diff --git a/tools/mock-meta.py b/tools/mock-meta.py index 019a24f2..d4677af6 100755 --- a/tools/mock-meta.py +++ b/tools/mock-meta.py @@ -181,7 +181,7 @@ class MetaDataHandler(object): else: return "%s" % (PLACEMENT_CAPABILITIES.get(pentry, '')) else: - return '' + return '{}' class UserDataHandler(object): -- cgit v1.2.3 From ffe4cde6d3fd95131b4434ca2d88af469dacfd51 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Mon, 25 Jun 2012 12:28:04 -0700 Subject: Add warning when hitting a unknown api. --- tools/mock-meta.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) (limited to 'tools/mock-meta.py') diff --git a/tools/mock-meta.py b/tools/mock-meta.py index d4677af6..0f13acd6 100755 --- a/tools/mock-meta.py +++ b/tools/mock-meta.py @@ -6,10 +6,11 @@ import functools import httplib +import json import logging -import sys -import string import random +import string +import sys import yaml from optparse import OptionParser @@ -88,6 +89,8 @@ PLACEMENT_CAPABILITIES = { 'availability-zone': AVAILABILITY_ZONES, } +NOT_IMPL_RESPONSE = json.dumps({}) + class WebException(Exception): def __init__(self, code, msg): @@ -181,7 +184,11 @@ class MetaDataHandler(object): else: return "%s" % (PLACEMENT_CAPABILITIES.get(pentry, '')) else: - return '{}' + log.warn(("Did not implement action %s, " + "returning empty response: %r"), + action, NOT_IMPL_RESPONSE) + return NOT_IMPL_RESPONSE + class UserDataHandler(object): -- cgit v1.2.3 From f99dd7c2e43f9117c66e06fc31d10f559c912e86 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Mon, 2 Jul 2012 19:02:16 -0700 Subject: Update the mock metadata server to return a file (or the default generated content) for the userdata blob. --- tools/mock-meta.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) (limited to 'tools/mock-meta.py') diff --git a/tools/mock-meta.py b/tools/mock-meta.py index 0f13acd6..8a73b43c 100755 --- a/tools/mock-meta.py +++ b/tools/mock-meta.py @@ -196,18 +196,26 @@ class UserDataHandler(object): self.opts = opts def _get_user_blob(self, **kwargs): - blob_mp = {} - blob_mp['hostname'] = kwargs.get('who', '') - lines = [] - lines.append("#cloud-config") - lines.append(yamlify(blob_mp)) - blob = "\n".join(lines) + blob = None + if self.opts['user_data_file']: + with open(opts['user_data_file'], 'rb') as fh: + blob = fh.read() + blob = blob.strip() + if not blob: + blob_mp = { + 'hostname': kwargs.get('who', 'localhost'), + } + lines = [ + "#cloud-config", + yamlify(blob_mp), + ] + blob = "\n".join(lines) return blob.strip() def get_data(self, params, who, **kwargs): if not params: return self._get_user_blob(who=who) - return '' + return NOT_IMPL_RESPONSE # Seem to need to use globals since can't pass @@ -303,10 +311,15 @@ def extract_opts(): parser = OptionParser() parser.add_option("-p", "--port", dest="port", action="store", type=int, default=80, help="port from which to serve traffic (default: %default)", metavar="PORT") + parser.add_option("-f", '--user-data-file', dest='user_data_file', action='store', + help="user data blob to serve back to incoming requests", metavar='FILE') (options, args) = parser.parse_args() out = dict() out['extra'] = args out['port'] = options.port + out['user_data_file'] = None + if options.user_data_file: + out['user_data_file'] = options.user_data_file return out -- cgit v1.2.3 From 7e1a250d70c39607bc1f3e3134d6331cd372f511 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Mon, 2 Jul 2012 19:11:46 -0700 Subject: Add a check on the filename provided ensuring it actually exists. --- tools/mock-meta.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'tools/mock-meta.py') diff --git a/tools/mock-meta.py b/tools/mock-meta.py index 8a73b43c..5f421c98 100755 --- a/tools/mock-meta.py +++ b/tools/mock-meta.py @@ -8,6 +8,7 @@ import functools import httplib import json import logging +import os import random import string import sys @@ -312,13 +313,15 @@ def extract_opts(): parser.add_option("-p", "--port", dest="port", action="store", type=int, default=80, help="port from which to serve traffic (default: %default)", metavar="PORT") parser.add_option("-f", '--user-data-file', dest='user_data_file', action='store', - help="user data blob to serve back to incoming requests", metavar='FILE') + help="user data filename to serve back to incoming requests", metavar='FILE') (options, args) = parser.parse_args() out = dict() out['extra'] = args out['port'] = options.port out['user_data_file'] = None if options.user_data_file: + if not os.path.isfile(options.user_data_file): + parser.error("Option -f specified a non-existent file") out['user_data_file'] = options.user_data_file return out -- cgit v1.2.3 From 21117bb5c26abcb42a7dcc5f318190e734c849bd Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Tue, 3 Jul 2012 12:46:55 -0700 Subject: 1. Update the mock ec2 data with some of the pubkey code from smosers ec2 metadata server. 2. Allow the setting of the ip addr (not just to 0.0.0.0) 3. Add comment as to how to use this for the 169 'magic' addr --- tools/mock-meta.py | 78 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 70 insertions(+), 8 deletions(-) (limited to 'tools/mock-meta.py') diff --git a/tools/mock-meta.py b/tools/mock-meta.py index 5f421c98..7c38ec48 100755 --- a/tools/mock-meta.py +++ b/tools/mock-meta.py @@ -4,6 +4,19 @@ # # http://docs.amazonwebservices.com/AWSEC2/2007-08-29/DeveloperGuide/AESDG-chapter-instancedata.html +""" +To use this to mimic the EC2 metadata service entirely, run it like: + # Where 'eth0' is *some* interface. + sudo ifconfig eth0:0 169.254.169.254 netmask 255.255.255.255 + + sudo ./mock-meta -a 169.254.169.254 -p 80 + +Then: + wget -q http://169.254.169.254/latest/meta-data/instance-id -O -; echo + curl --silent http://169.254.169.254/latest/meta-data/instance-id ; echo + ec2metadata --instance-id +""" + import functools import httplib import json @@ -20,7 +33,6 @@ from BaseHTTPServer import (HTTPServer, BaseHTTPRequestHandler) log = logging.getLogger('meta-server') -# Constants EC2_VERSIONS = [ '1.0', '2007-01-19', @@ -69,6 +81,14 @@ META_CAPABILITIES = [ 'security-groups' ] +PUB_KEYS = { + 'brickies': [ + 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA3I7VUf2l5gSn5uavROsc5HRDpZdQueUq5ozemNSj8T7enqKHOEaFoU2VoPgGEWC9RyzSQVeyD6s7APMcE82EtmW4skVEgEGSbDc1pvxzxtchBj78hJP6Cf5TCMFSXw+Fz5rF1dR23QDbN1mkHs7adr8GW4kSWqU7Q7NDwfIrJJtO7Hi42GyXtvEONHbiRPOe8stqUly7MvUoN+5kfjBM8Qqpfl2+FNhTYWpMfYdPUnE7u536WqzFmsaqJctz3gBxH9Ex7dFtrxR4qiqEr9Qtlu3xGn7Bw07/+i1D+ey3ONkZLN+LQ714cgj8fRS4Hj29SCmXp5Kt5/82cD/VN3NtHw== brickies', + '', + ], +} + + INSTANCE_TYPES = [ 'm1.small', 'm1.medium', @@ -136,6 +156,8 @@ class MetaDataHandler(object): def get_data(self, params, who, **kwargs): if not params: + # Show the root level capabilities when + # no params are passed... caps = sorted(META_CAPABILITIES) return "\n".join(caps) action = params[0] @@ -172,6 +194,43 @@ class MetaDataHandler(object): return "r-%s" % (id_generator(lower=True)) elif action == 'product-codes': return "%s" % (id_generator(size=8)) + elif action == 'public-keys': + nparams = params[1:] + # public-keys is messed up. a list of /latest/meta-data/public-keys/ + # shows something like: '0=brickies' + # but a GET to /latest/meta-data/public-keys/0=brickies will fail + # you have to know to get '/latest/meta-data/public-keys/0', then + # from there you get a 'openssh-key', which you can get. + # this hunk of code just re-works the object for that. + key_ids = sorted(list(PUB_KEYS.keys())) + if nparams: + mybe_key = nparams[0] + try: + key_id = int(mybe_key) + key_name = key_ids[key_id] + except: + raise WebException(httplib.BAD_REQUEST, "Unknown key id %r" % mybe_key) + # Extract the possible sub-params + key_info = { + "openssh-key": "\n".join(PUB_KEYS[key_name]), + } + result = dict(key_info) + for k in nparams[1:]: + try: + result = result.get(k) + except (AttributeError, TypeError): + result = None + break + if isinstance(result, (dict)): + result = json.dumps(result) + if result is None: + result = '' + return str(result) + else: + contents = [] + for (i, key_id) in enumerate(key_ids): + contents.append("%s=%s" % (i, key_id)) + return "\n".join(contents) elif action == 'placement': nparams = params[1:] if not nparams: @@ -198,10 +257,8 @@ class UserDataHandler(object): def _get_user_blob(self, **kwargs): blob = None - if self.opts['user_data_file']: - with open(opts['user_data_file'], 'rb') as fh: - blob = fh.read() - blob = blob.strip() + if self.opts['user_data_file'] is not None: + blob = self.opts['user_data_file'] if not blob: blob_mp = { 'hostname': kwargs.get('who', 'localhost'), @@ -312,6 +369,8 @@ def extract_opts(): parser = OptionParser() parser.add_option("-p", "--port", dest="port", action="store", type=int, default=80, help="port from which to serve traffic (default: %default)", metavar="PORT") + parser.add_option("-a", "--addr", dest="address", action="store", type=str, default='0.0.0.0', + help="address from which to serve traffic (default: %default)", metavar="ADDRESS") parser.add_option("-f", '--user-data-file', dest='user_data_file', action='store', help="user data filename to serve back to incoming requests", metavar='FILE') (options, args) = parser.parse_args() @@ -319,10 +378,12 @@ def extract_opts(): out['extra'] = args out['port'] = options.port out['user_data_file'] = None + out['address'] = options.address if options.user_data_file: if not os.path.isfile(options.user_data_file): parser.error("Option -f specified a non-existent file") - out['user_data_file'] = options.user_data_file + with open(options.user_data_file, 'rb') as fh: + out['user_data_file'] = fh.read() return out @@ -340,9 +401,10 @@ def run_server(): setup_logging(logging.DEBUG) setup_fetchers(opts) log.info("CLI opts: %s", opts) - server = HTTPServer(('0.0.0.0', opts['port']), Ec2Handler) + server_address = (opts['address'], opts['port']) + server = HTTPServer(server_address, Ec2Handler) sa = server.socket.getsockname() - log.info("Serving server on %s using port %s ...", sa[0], sa[1]) + log.info("Serving ec2 metadata on %s using port %s ...", sa[0], sa[1]) server.serve_forever() -- cgit v1.2.3 From 101d19df1c71b559bf34cfec87894f84ea041810 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Tue, 3 Jul 2012 19:59:04 -0700 Subject: 1. Cleanup of some mock functionality 2. Adding in returning the 'public-keys' to the metadata 'list' response 3. Adding in sending back the running users keys (useful for testing) along with 'brickies' 4. Add in a traverse function that can walk down a dictionary (if possible) --- tools/mock-meta.py | 103 ++++++++++++++++++++++++++++++++++------------------- 1 file changed, 66 insertions(+), 37 deletions(-) (limited to 'tools/mock-meta.py') diff --git a/tools/mock-meta.py b/tools/mock-meta.py index 7c38ec48..5bbe62cc 100755 --- a/tools/mock-meta.py +++ b/tools/mock-meta.py @@ -43,16 +43,15 @@ EC2_VERSIONS = [ '2008-02-01', '2008-09-01', '2009-04-04', - 'latest', ] BLOCK_DEVS = [ 'ami', - 'root', 'ephemeral0', + 'root', ] -DEV_PREFIX = 'v' +DEV_PREFIX = 'v' # This seems to vary alot depending on images... DEV_MAPPINGS = { 'ephemeral0': '%sda2' % (DEV_PREFIX), 'root': '/dev/%sda1' % (DEV_PREFIX), @@ -77,22 +76,28 @@ META_CAPABILITIES = [ 'product-codes', 'public-hostname', 'public-ipv4', + 'public-keys/', 'reservation-id', 'security-groups' ] PUB_KEYS = { 'brickies': [ - 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA3I7VUf2l5gSn5uavROsc5HRDpZdQueUq5ozemNSj8T7enqKHOEaFoU2VoPgGEWC9RyzSQVeyD6s7APMcE82EtmW4skVEgEGSbDc1pvxzxtchBj78hJP6Cf5TCMFSXw+Fz5rF1dR23QDbN1mkHs7adr8GW4kSWqU7Q7NDwfIrJJtO7Hi42GyXtvEONHbiRPOe8stqUly7MvUoN+5kfjBM8Qqpfl2+FNhTYWpMfYdPUnE7u536WqzFmsaqJctz3gBxH9Ex7dFtrxR4qiqEr9Qtlu3xGn7Bw07/+i1D+ey3ONkZLN+LQ714cgj8fRS4Hj29SCmXp5Kt5/82cD/VN3NtHw== brickies', + ('ssh-rsa ' + 'AAAAB3NzaC1yc2EAAAABIwAAAQEA3I7VUf2l5gSn5uavROsc5HRDpZdQueUq5ozemNSj8T' + '7enqKHOEaFoU2VoPgGEWC9RyzSQVeyD6s7APMcE82EtmW4skVEgEGSbDc1pvxzxtchBj78' + 'hJP6Cf5TCMFSXw+Fz5rF1dR23QDbN1mkHs7adr8GW4kSWqU7Q7NDwfIrJJtO7Hi42GyXtv' + 'EONHbiRPOe8stqUly7MvUoN+5kfjBM8Qqpfl2+FNhTYWpMfYdPUnE7u536WqzFmsaqJctz' + '3gBxH9Ex7dFtrxR4qiqEr9Qtlu3xGn7Bw07/+i1D+ey3ONkZLN+LQ714cgj8fRS4Hj29SC' + 'mXp5Kt5/82cD/VN3NtHw== brickies'), '', ], } - INSTANCE_TYPES = [ - 'm1.small', - 'm1.medium', 'm1.large', + 'm1.medium', + 'm1.small', 'm1.xlarge', ] @@ -100,10 +105,10 @@ AVAILABILITY_ZONES = [ "us-east-1a", "us-east-1b", "us-east-1c", - 'us-west-1', "us-east-1d", 'eu-west-1a', 'eu-west-1b', + 'us-west-1', ] PLACEMENT_CAPABILITIES = { @@ -139,6 +144,17 @@ def format_text(text): return "\n".join(nlines) +def traverse(keys, mp): + result = dict(mp) + for k in keys: + try: + result = result.get(k) + except (AttributeError, TypeError): + result = None + break + return result + + ID_CHARS = [c for c in (string.ascii_uppercase + string.digits)] def id_generator(size=6, lower=False): txt = ''.join(random.choice(ID_CHARS) for x in range(size)) @@ -148,6 +164,23 @@ def id_generator(size=6, lower=False): return txt +def get_ssh_keys(): + keys = {} + keys.update(PUB_KEYS) + + # Nice helper to add in the 'running' users key (if they have one) + key_pth = os.path.expanduser('~/.ssh/id_rsa.pub') + if not os.path.isfile(key_pth): + key_pth = os.path.expanduser('~/.ssh/id_dsa.pub') + + if os.path.isfile(key_pth): + with open(key_pth, 'rb') as fh: + contents = fh.read() + keys[os.getlogin()] = [contents, ''] + + return keys + + class MetaDataHandler(object): def __init__(self, opts): @@ -165,7 +198,7 @@ class MetaDataHandler(object): if action == 'instance-id': return 'i-%s' % (id_generator(lower=True)) elif action == 'ami-launch-index': - return "%s" % random.choice([0,1,2,3]) + return "%s" % random.choice([0, 1, 2, 3]) elif action == 'aki-id': return 'aki-%s' % (id_generator(lower=True)) elif action == 'ami-id': @@ -175,11 +208,15 @@ class MetaDataHandler(object): elif action == 'block-device-mapping': nparams = params[1:] if not nparams: - devs = sorted(BLOCK_DEVS) - return "\n".join(devs) + return "\n".join(BLOCK_DEVS) else: - return "%s" % (DEV_MAPPINGS.get(nparams[0].strip(), '')) + subvalue = traverse(nparams, DEV_MAPPINGS) + if not subvalue: + return "\n".join(sorted(list(DEV_MAPPINGS.keys()))) + else: + return str(subvalue) elif action in ['hostname', 'local-hostname', 'public-hostname']: + # Just echo back there own hostname that they called in on.. return "%s" % (who) elif action == 'instance-type': return random.choice(INSTANCE_TYPES) @@ -188,21 +225,23 @@ class MetaDataHandler(object): elif action == 'security-groups': return 'default' elif action in ['local-ipv4', 'public-ipv4']: - there_ip = kwargs.get('client_ip', '10.0.0.1') - return "%s" % (there_ip) + # Just echo back there own ip that they called in on... + return "%s" % (kwargs.get('client_ip', '10.0.0.1')) elif action == 'reservation-id': return "r-%s" % (id_generator(lower=True)) elif action == 'product-codes': return "%s" % (id_generator(size=8)) elif action == 'public-keys': nparams = params[1:] - # public-keys is messed up. a list of /latest/meta-data/public-keys/ + # This is a weird kludge, why amazon why!!! + # public-keys is messed up, a list of /latest/meta-data/public-keys/ # shows something like: '0=brickies' # but a GET to /latest/meta-data/public-keys/0=brickies will fail # you have to know to get '/latest/meta-data/public-keys/0', then # from there you get a 'openssh-key', which you can get. # this hunk of code just re-works the object for that. - key_ids = sorted(list(PUB_KEYS.keys())) + avail_keys = get_ssh_keys() + key_ids = sorted(list(avail_keys.keys())) if nparams: mybe_key = nparams[0] try: @@ -211,21 +250,15 @@ class MetaDataHandler(object): except: raise WebException(httplib.BAD_REQUEST, "Unknown key id %r" % mybe_key) # Extract the possible sub-params - key_info = { - "openssh-key": "\n".join(PUB_KEYS[key_name]), - } - result = dict(key_info) - for k in nparams[1:]: - try: - result = result.get(k) - except (AttributeError, TypeError): - result = None - break + result = traverse(nparams[1:], { + "openssh-key": "\n".join(avail_keys[key_name]), + }) if isinstance(result, (dict)): - result = json.dumps(result) - if result is None: + # TODO: This might not be right?? + result = "\n".join(sorted(result.keys())) + if not result: result = '' - return str(result) + return result else: contents = [] for (i, key_id) in enumerate(key_ids): @@ -286,12 +319,7 @@ user_fetcher = None class Ec2Handler(BaseHTTPRequestHandler): def _get_versions(self): - versions = [] - for v in EC2_VERSIONS: - if v == 'latest': - continue - else: - versions.append(v) + versions = ['latest'] + EC2_VERSIONS versions = sorted(versions) return "\n".join(versions) @@ -308,11 +336,12 @@ class Ec2Handler(BaseHTTPRequestHandler): 'meta-data': meta_fetcher.get_data, } segments = [piece for piece in path.split('/') if len(piece)] + log.info("Received segments %s", segments) if not segments: return self._get_versions date = segments[0].strip().lower() - if date not in EC2_VERSIONS: - raise WebException(httplib.BAD_REQUEST, "Unknown date format %r" % date) + if date not in self._get_versions(): + raise WebException(httplib.BAD_REQUEST, "Unknown version format %r" % date) if len(segments) < 2: raise WebException(httplib.BAD_REQUEST, "No action provided") look_name = segments[1].lower() -- cgit v1.2.3 From 32ea55364e7688110646818dd651ed476b3c57f1 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Tue, 3 Jul 2012 20:14:57 -0700 Subject: When the hostname is also the ip (thus no hostname) just use 'localhost' --- tools/mock-meta.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'tools/mock-meta.py') diff --git a/tools/mock-meta.py b/tools/mock-meta.py index 5bbe62cc..247cff14 100755 --- a/tools/mock-meta.py +++ b/tools/mock-meta.py @@ -349,10 +349,14 @@ class Ec2Handler(BaseHTTPRequestHandler): raise WebException(httplib.BAD_REQUEST, "Unknown requested data %r" % look_name) base_func = func_mapping[look_name] who = self.address_string() + ip_from = self.client_address[0] + if who == ip_from: + # Nothing resolved, so just use 'localhost' + who = 'localhost' kwargs = { 'params': list(segments[2:]), - 'who': self.address_string(), - 'client_ip': self.client_address[0], + 'who': who, + 'client_ip': ip_from, } return functools.partial(base_func, **kwargs) -- cgit v1.2.3 From 3cd17b470b70cf4c15f47bfe4f84844b078cd914 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Tue, 3 Jul 2012 20:29:36 -0700 Subject: Remove the useless function printout. --- tools/mock-meta.py | 1 - 1 file changed, 1 deletion(-) (limited to 'tools/mock-meta.py') diff --git a/tools/mock-meta.py b/tools/mock-meta.py index 247cff14..4548e4ae 100755 --- a/tools/mock-meta.py +++ b/tools/mock-meta.py @@ -365,7 +365,6 @@ class Ec2Handler(BaseHTTPRequestHandler): log.info("Got a call from %s for path %s", who, self.path) try: func = self._find_method(self.path) - log.info("Calling into func %s to get your data.", func) data = func() if not data: data = '' -- cgit v1.2.3