284 lines
8.4 KiB
Python
Executable File
284 lines
8.4 KiB
Python
Executable File
#!/usr/bin/python3
|
|
#
|
|
# Query and display EC2 metadata related to the AMI instance
|
|
# Copyright (c) 2009 Canonical Ltd. (Canonical Contributor Agreement 2.5)
|
|
#
|
|
# Author: Alon Swartz <alon@turnkeylinux.org>
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
import sys
|
|
import time
|
|
import getopt
|
|
import socket
|
|
import os
|
|
try:
|
|
from urllib import request as urllib_request
|
|
from urllib import error as urllib_error
|
|
from urllib import parse as urllib_parse
|
|
except ImportError:
|
|
# python2
|
|
import urllib2 as urllib_request
|
|
import urllib2 as urllib_error
|
|
import urlparse as urllib_parse
|
|
|
|
|
|
instdata_host = "169.254.169.254"
|
|
instdata_ver = "2009-04-04"
|
|
instdata_url = "http://%s/%s" % (instdata_host, instdata_ver)
|
|
|
|
TOKEN_TTL_SECONDS = 21600
|
|
TOKEN_HEADER = "X-aws-ec2-metadata-token"
|
|
TOKEN_HEADER_TTL = "X-aws-ec2-metadata-token-ttl-seconds"
|
|
|
|
session_token_url = "http://%s/%s/%s" % (instdata_host, 'latest', 'api/token')
|
|
|
|
__doc__ = """
|
|
Query and display EC2 metadata.
|
|
|
|
If no options are provided, all options will be displayed
|
|
|
|
Options:
|
|
-h --help show this help
|
|
|
|
--kernel-id display the kernel id
|
|
--ramdisk-id display the ramdisk id
|
|
--reservation-id display the reservation id
|
|
|
|
--ami-id display the ami id
|
|
--ami-launch-index display the ami launch index
|
|
--ami-manifest-path display the ami manifest path
|
|
--ancestor-ami-ids display the ami ancestor id
|
|
--product-codes display the ami associated product codes
|
|
--availability-zone display the ami placement zone name
|
|
--availability-zone-id display the ami placement zone id
|
|
--region display the ami placement region name
|
|
--group-name display the ami placement group name
|
|
--host-id display the dedicated host id
|
|
--partition-number display the partition instance was launched from
|
|
|
|
--instance-id display the instance id
|
|
--instance-type display the instance type
|
|
|
|
--local-hostname display the local hostname
|
|
--public-hostname display the public hostname
|
|
|
|
--local-ipv4 display the local ipv4 ip address
|
|
--public-ipv4 display the public ipv4 ip address
|
|
|
|
--block-device-mapping display the block device id
|
|
--security-groups display the security groups
|
|
|
|
--mac display the instance mac address
|
|
--profile display the instance profile
|
|
--instance-action display the instance-action
|
|
|
|
--public-keys display the openssh public keys
|
|
--user-data display the user data (not actually metadata)
|
|
|
|
-u | --url URL use URL (default: %s)
|
|
|
|
""" % instdata_url
|
|
|
|
|
|
METAOPTS = ['ami-id', 'ami-launch-index', 'ami-manifest-path',
|
|
'ancestor-ami-ids', 'availability-zone', 'block-device-mapping',
|
|
'instance-action', 'instance-id', 'instance-type',
|
|
'local-hostname', 'local-ipv4', 'kernel-id', 'mac',
|
|
'profile', 'product-codes', 'public-hostname', 'public-ipv4',
|
|
'public-keys', 'ramdisk-id', 'reservation-id', 'security-groups',
|
|
'user-data', 'availability-zone-id', 'region', 'host-id',
|
|
'group-name', 'partition-number']
|
|
|
|
binstdout = os.fdopen(sys.stdout.fileno(), 'wb')
|
|
|
|
|
|
def print_binary(data):
|
|
if not isinstance(data, bytes):
|
|
data = data.encode()
|
|
binstdout.write(data)
|
|
binstdout.flush()
|
|
|
|
|
|
class Error(Exception):
|
|
pass
|
|
|
|
|
|
class EC2Metadata: # pylint: disable=R0903
|
|
"""Class for querying metadata from EC2"""
|
|
|
|
def __init__(self, burl=instdata_url):
|
|
self.burl = burl
|
|
|
|
s = urllib_parse.urlsplit(burl)
|
|
addr = s.netloc.split(":")[0]
|
|
port = s.port
|
|
if s.port is None:
|
|
port = 80
|
|
if not self._test_connectivity(addr, port):
|
|
raise Error("could not establish connection to: %s:%s" %
|
|
(addr, port))
|
|
self._imdsv2_ensure_token()
|
|
|
|
@staticmethod
|
|
def _test_connectivity(addr, port):
|
|
for _ in range(6):
|
|
s = socket.socket()
|
|
try:
|
|
s.connect((addr, port))
|
|
s.close()
|
|
return True
|
|
except socket.error:
|
|
time.sleep(1)
|
|
|
|
return False
|
|
|
|
def _imdsv2_ensure_token(self):
|
|
# Get IMDSv2 session token
|
|
request = urllib_request.Request(
|
|
session_token_url,
|
|
method='PUT',
|
|
headers={TOKEN_HEADER_TTL: TOKEN_TTL_SECONDS})
|
|
resp = urllib_request.urlopen(request)
|
|
self.session_token = resp.read()
|
|
|
|
def _get(self, uri, decode=True):
|
|
url = "%s/%s" % (self.burl, uri)
|
|
try:
|
|
resp = urllib_request.urlopen(
|
|
urllib_request.Request(
|
|
url,
|
|
headers={TOKEN_HEADER: self.session_token}))
|
|
value = resp.read()
|
|
if decode:
|
|
value = value.decode()
|
|
except urllib_error.HTTPError as e:
|
|
if e.code == 404:
|
|
return None
|
|
# Eucalyptus may raise a 500 (Internal Server Error)
|
|
if e.code == 500:
|
|
return None
|
|
raise
|
|
|
|
return value
|
|
|
|
def get(self, metaopt):
|
|
"""return value of metaopt"""
|
|
|
|
if metaopt not in METAOPTS:
|
|
raise Error('unknown metaopt', metaopt, METAOPTS)
|
|
|
|
if metaopt in [
|
|
'availability-zone',
|
|
'availability-zone-id',
|
|
'region',
|
|
'host-id',
|
|
'group-name',
|
|
'partition-number',
|
|
]:
|
|
return self._get('meta-data/placement/' + metaopt)
|
|
|
|
if metaopt == 'public-keys':
|
|
data = self._get('meta-data/public-keys')
|
|
if data is None:
|
|
return None
|
|
|
|
keyids = [line.split('=')[0] for line in data.splitlines()]
|
|
|
|
public_keys = []
|
|
for keyid in keyids:
|
|
uri = 'meta-data/public-keys/%d/openssh-key' % int(keyid)
|
|
public_keys.append(self._get(uri).rstrip())
|
|
|
|
return public_keys
|
|
|
|
if metaopt == 'user-data':
|
|
return self._get('user-data', decode=False)
|
|
|
|
return self._get('meta-data/' + metaopt)
|
|
|
|
|
|
def get(metaopt):
|
|
"""primitive: return value of metaopt"""
|
|
|
|
m = EC2Metadata()
|
|
return m.get(metaopt)
|
|
|
|
|
|
def display(metaopts, burl, prefix=False):
|
|
"""primitive: display metaopts (list) values with optional prefix"""
|
|
|
|
m = EC2Metadata(burl)
|
|
for metaopt in metaopts:
|
|
value = m.get(metaopt)
|
|
if not value:
|
|
value = "unavailable"
|
|
|
|
if prefix:
|
|
print("%s: %s" % (metaopt, value))
|
|
elif metaopt == "user-data":
|
|
# We want to avoid binary blob corruption while printing as string
|
|
print_binary(value)
|
|
else:
|
|
print(value)
|
|
|
|
|
|
def usage(s=None):
|
|
"""display usage and exit"""
|
|
|
|
msg = ""
|
|
if s:
|
|
msg = "Error: %s\n" % s
|
|
msg += "Syntax: %s [options]\n" % sys.argv[0]
|
|
msg += __doc__
|
|
sys.stderr.write(msg + "\n")
|
|
sys.exit(1)
|
|
|
|
|
|
def main():
|
|
"""handle cli options"""
|
|
|
|
try:
|
|
getopt_metaopts = METAOPTS[:]
|
|
getopt_metaopts.append('help')
|
|
getopt_metaopts.append('url=')
|
|
opts, _ = getopt.gnu_getopt(sys.argv[1:], "hu:", getopt_metaopts)
|
|
except getopt.GetoptError as e:
|
|
usage(e)
|
|
|
|
burl = instdata_url
|
|
|
|
metaopts = []
|
|
prefix = False
|
|
for opt, val in opts:
|
|
if opt in ('-h', '--help'):
|
|
usage()
|
|
if opt in ('-u', '--url'):
|
|
burl = val
|
|
continue
|
|
|
|
metaopts.append(opt.replace('--', ''))
|
|
|
|
if len(metaopts) == 0:
|
|
prefix = True
|
|
metaopts = METAOPTS
|
|
|
|
display(metaopts, burl, prefix)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
|
|
# vi: ts=4 expandtab
|