├── .gitignore ├── Dockerfile ├── LICENSE ├── README.rst ├── docs ├── Makefile ├── conf.py ├── docker.rst └── index.rst ├── ectou_metadata ├── __init__.py └── service.py ├── examples └── install-metadata-proxy.sh ├── requirements.txt ├── setup.cfg ├── setup.py └── tests ├── __init__.py └── test_metadata_api.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | 59 | #PyCharm 60 | .idea/ 61 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:2.7-slim 2 | 3 | RUN useradd -m -d /home/ec2-user ec2-user 4 | RUN apt-get update && apt-get install -y curl && apt-get clean && rm -rf /var/lib/apt/lists/* 5 | 6 | WORKDIR /tmp/ectou-metadata 7 | ADD . /tmp/ectou-metadata 8 | RUN python ./setup.py install 9 | 10 | # Install Tini 11 | RUN curl -L https://github.com/krallin/tini/releases/download/v0.15.0/tini > tini && \ 12 | echo "5e92b8d11dae337be0a929d0f8a737a84cebe35959503e4c42acbe76c4d69190 *tini" | sha256sum -c - && \ 13 | mv tini /usr/local/bin/tini && \ 14 | chmod +x /usr/local/bin/tini 15 | 16 | ENV MOCK_METADATA_PORT 5000 17 | 18 | EXPOSE ${MOCK_METADATA_PORT} 19 | 20 | USER ec2-user 21 | 22 | ENTRYPOINT ["tini", "--"] 23 | CMD ectou_metadata --host 0.0.0.0 --port ${MOCK_METADATA_PORT} --role-arn ${MOCK_METADATA_ROLE_ARN} 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Monetate Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ectou-metadata 2 | ============== 3 | 4 | Yet another EC2 instance metadata mocking service. 5 | 6 | Goals 7 | ----- 8 | 9 | Mock subset of the `EC2 instance metadata`_ service to enable local virtual machine environments to assume IAM roles. 10 | 11 | 12 | Usage 13 | ----- 14 | 15 | .. code-block:: sh 16 | 17 | ectou_metadata [--host host] [--port port] [--role-arn role_arn] 18 | 19 | Dependencies 20 | ------------ 21 | 22 | - boto3 >= 1.2.0 23 | - bottle >= 0.12.0 24 | 25 | .. _EC2 instance metadata: http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html 26 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help 23 | help: 24 | @echo "Please use \`make ' where is one of" 25 | @echo " html to make standalone HTML files" 26 | @echo " dirhtml to make HTML files named index.html in directories" 27 | @echo " singlehtml to make a single large HTML file" 28 | @echo " pickle to make pickle files" 29 | @echo " json to make JSON files" 30 | @echo " htmlhelp to make HTML files and a HTML help project" 31 | @echo " qthelp to make HTML files and a qthelp project" 32 | @echo " applehelp to make an Apple Help Book" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | @echo " coverage to run coverage check of the documentation (if enabled)" 49 | 50 | .PHONY: clean 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | .PHONY: html 55 | html: 56 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 57 | @echo 58 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 59 | 60 | .PHONY: dirhtml 61 | dirhtml: 62 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 63 | @echo 64 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 65 | 66 | .PHONY: singlehtml 67 | singlehtml: 68 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 69 | @echo 70 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 71 | 72 | .PHONY: pickle 73 | pickle: 74 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 75 | @echo 76 | @echo "Build finished; now you can process the pickle files." 77 | 78 | .PHONY: json 79 | json: 80 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 81 | @echo 82 | @echo "Build finished; now you can process the JSON files." 83 | 84 | .PHONY: htmlhelp 85 | htmlhelp: 86 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 87 | @echo 88 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 89 | ".hhp project file in $(BUILDDIR)/htmlhelp." 90 | 91 | .PHONY: qthelp 92 | qthelp: 93 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 94 | @echo 95 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 96 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 97 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/ectou-metadata.qhcp" 98 | @echo "To view the help file:" 99 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/ectou-metadata.qhc" 100 | 101 | .PHONY: applehelp 102 | applehelp: 103 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 104 | @echo 105 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 106 | @echo "N.B. You won't be able to view it unless you put it in" \ 107 | "~/Library/Documentation/Help or install it in your application" \ 108 | "bundle." 109 | 110 | .PHONY: devhelp 111 | devhelp: 112 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 113 | @echo 114 | @echo "Build finished." 115 | @echo "To view the help file:" 116 | @echo "# mkdir -p $$HOME/.local/share/devhelp/ectou-metadata" 117 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/ectou-metadata" 118 | @echo "# devhelp" 119 | 120 | .PHONY: epub 121 | epub: 122 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 123 | @echo 124 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 125 | 126 | .PHONY: latex 127 | latex: 128 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 129 | @echo 130 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 131 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 132 | "(use \`make latexpdf' here to do that automatically)." 133 | 134 | .PHONY: latexpdf 135 | latexpdf: 136 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 137 | @echo "Running LaTeX files through pdflatex..." 138 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 139 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 140 | 141 | .PHONY: latexpdfja 142 | latexpdfja: 143 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 144 | @echo "Running LaTeX files through platex and dvipdfmx..." 145 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 146 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 147 | 148 | .PHONY: text 149 | text: 150 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 151 | @echo 152 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 153 | 154 | .PHONY: man 155 | man: 156 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 157 | @echo 158 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 159 | 160 | .PHONY: texinfo 161 | texinfo: 162 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 163 | @echo 164 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 165 | @echo "Run \`make' in that directory to run these through makeinfo" \ 166 | "(use \`make info' here to do that automatically)." 167 | 168 | .PHONY: info 169 | info: 170 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 171 | @echo "Running Texinfo files through makeinfo..." 172 | make -C $(BUILDDIR)/texinfo info 173 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 174 | 175 | .PHONY: gettext 176 | gettext: 177 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 178 | @echo 179 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 180 | 181 | .PHONY: changes 182 | changes: 183 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 184 | @echo 185 | @echo "The overview file is in $(BUILDDIR)/changes." 186 | 187 | .PHONY: linkcheck 188 | linkcheck: 189 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 190 | @echo 191 | @echo "Link check complete; look for any errors in the above output " \ 192 | "or in $(BUILDDIR)/linkcheck/output.txt." 193 | 194 | .PHONY: doctest 195 | doctest: 196 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 197 | @echo "Testing of doctests in the sources finished, look at the " \ 198 | "results in $(BUILDDIR)/doctest/output.txt." 199 | 200 | .PHONY: coverage 201 | coverage: 202 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 203 | @echo "Testing of coverage in the sources finished, look at the " \ 204 | "results in $(BUILDDIR)/coverage/python.txt." 205 | 206 | .PHONY: xml 207 | xml: 208 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 209 | @echo 210 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 211 | 212 | .PHONY: pseudoxml 213 | pseudoxml: 214 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 215 | @echo 216 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 217 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # ectou-metadata documentation build configuration file, created by 4 | # sphinx-quickstart on Wed Feb 3 14:37:37 2016. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | #sys.path.insert(0, os.path.abspath('.')) 22 | 23 | # -- General configuration ------------------------------------------------ 24 | 25 | # If your documentation needs a minimal Sphinx version, state it here. 26 | #needs_sphinx = '1.0' 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be 29 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 30 | # ones. 31 | extensions = [] 32 | 33 | # Add any paths that contain templates here, relative to this directory. 34 | templates_path = ['_templates'] 35 | 36 | # The suffix(es) of source filenames. 37 | # You can specify multiple suffix as a list of string: 38 | # source_suffix = ['.rst', '.md'] 39 | source_suffix = '.rst' 40 | 41 | # The encoding of source files. 42 | #source_encoding = 'utf-8-sig' 43 | 44 | # The master toctree document. 45 | master_doc = 'index' 46 | 47 | # General information about the project. 48 | project = u'ectou-metadata' 49 | copyright = u'2016, Monetate' 50 | author = u'Monetate' 51 | 52 | # The version info for the project you're documenting, acts as replacement for 53 | # |version| and |release|, also used in various other places throughout the 54 | # built documents. 55 | # 56 | # The short X.Y version. 57 | version = u'1.0.0' 58 | # The full version, including alpha/beta/rc tags. 59 | release = u'1.0.0' 60 | 61 | # The language for content autogenerated by Sphinx. Refer to documentation 62 | # for a list of supported languages. 63 | # 64 | # This is also used if you do content translation via gettext catalogs. 65 | # Usually you set "language" from the command line for these cases. 66 | language = None 67 | 68 | # There are two options for replacing |today|: either, you set today to some 69 | # non-false value, then it is used: 70 | #today = '' 71 | # Else, today_fmt is used as the format for a strftime call. 72 | #today_fmt = '%B %d, %Y' 73 | 74 | # List of patterns, relative to source directory, that match files and 75 | # directories to ignore when looking for source files. 76 | exclude_patterns = ['_build'] 77 | 78 | # The reST default role (used for this markup: `text`) to use for all 79 | # documents. 80 | #default_role = None 81 | 82 | # If true, '()' will be appended to :func: etc. cross-reference text. 83 | #add_function_parentheses = True 84 | 85 | # If true, the current module name will be prepended to all description 86 | # unit titles (such as .. function::). 87 | #add_module_names = True 88 | 89 | # If true, sectionauthor and moduleauthor directives will be shown in the 90 | # output. They are ignored by default. 91 | #show_authors = False 92 | 93 | # The name of the Pygments (syntax highlighting) style to use. 94 | pygments_style = 'sphinx' 95 | 96 | # A list of ignored prefixes for module index sorting. 97 | #modindex_common_prefix = [] 98 | 99 | # If true, keep warnings as "system message" paragraphs in the built documents. 100 | #keep_warnings = False 101 | 102 | # If true, `todo` and `todoList` produce output, else they produce nothing. 103 | todo_include_todos = False 104 | 105 | 106 | # -- Options for HTML output ---------------------------------------------- 107 | 108 | # The theme to use for HTML and HTML Help pages. See the documentation for 109 | # a list of builtin themes. 110 | html_theme = 'alabaster' 111 | 112 | # Theme options are theme-specific and customize the look and feel of a theme 113 | # further. For a list of options available for each theme, see the 114 | # documentation. 115 | #html_theme_options = {} 116 | 117 | # Add any paths that contain custom themes here, relative to this directory. 118 | #html_theme_path = [] 119 | 120 | # The name for this set of Sphinx documents. If None, it defaults to 121 | # " v documentation". 122 | #html_title = None 123 | 124 | # A shorter title for the navigation bar. Default is the same as html_title. 125 | #html_short_title = None 126 | 127 | # The name of an image file (relative to this directory) to place at the top 128 | # of the sidebar. 129 | #html_logo = None 130 | 131 | # The name of an image file (within the static path) to use as favicon of the 132 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 133 | # pixels large. 134 | #html_favicon = None 135 | 136 | # Add any paths that contain custom static files (such as style sheets) here, 137 | # relative to this directory. They are copied after the builtin static files, 138 | # so a file named "default.css" will overwrite the builtin "default.css". 139 | html_static_path = ['_static'] 140 | 141 | # Add any extra paths that contain custom files (such as robots.txt or 142 | # .htaccess) here, relative to this directory. These files are copied 143 | # directly to the root of the documentation. 144 | #html_extra_path = [] 145 | 146 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 147 | # using the given strftime format. 148 | #html_last_updated_fmt = '%b %d, %Y' 149 | 150 | # If true, SmartyPants will be used to convert quotes and dashes to 151 | # typographically correct entities. 152 | #html_use_smartypants = True 153 | 154 | # Custom sidebar templates, maps document names to template names. 155 | #html_sidebars = {} 156 | 157 | # Additional templates that should be rendered to pages, maps page names to 158 | # template names. 159 | #html_additional_pages = {} 160 | 161 | # If false, no module index is generated. 162 | #html_domain_indices = True 163 | 164 | # If false, no index is generated. 165 | #html_use_index = True 166 | 167 | # If true, the index is split into individual pages for each letter. 168 | #html_split_index = False 169 | 170 | # If true, links to the reST sources are added to the pages. 171 | #html_show_sourcelink = True 172 | 173 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 174 | #html_show_sphinx = True 175 | 176 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 177 | #html_show_copyright = True 178 | 179 | # If true, an OpenSearch description file will be output, and all pages will 180 | # contain a tag referring to it. The value of this option must be the 181 | # base URL from which the finished HTML is served. 182 | #html_use_opensearch = '' 183 | 184 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 185 | #html_file_suffix = None 186 | 187 | # Language to be used for generating the HTML full-text search index. 188 | # Sphinx supports the following languages: 189 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 190 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' 191 | #html_search_language = 'en' 192 | 193 | # A dictionary with options for the search language support, empty by default. 194 | # Now only 'ja' uses this config value 195 | #html_search_options = {'type': 'default'} 196 | 197 | # The name of a javascript file (relative to the configuration directory) that 198 | # implements a search results scorer. If empty, the default will be used. 199 | #html_search_scorer = 'scorer.js' 200 | 201 | # Output file base name for HTML help builder. 202 | htmlhelp_basename = 'ectou-metadatadoc' 203 | 204 | # -- Options for LaTeX output --------------------------------------------- 205 | 206 | latex_elements = { 207 | # The paper size ('letterpaper' or 'a4paper'). 208 | #'papersize': 'letterpaper', 209 | 210 | # The font size ('10pt', '11pt' or '12pt'). 211 | #'pointsize': '10pt', 212 | 213 | # Additional stuff for the LaTeX preamble. 214 | #'preamble': '', 215 | 216 | # Latex figure (float) alignment 217 | #'figure_align': 'htbp', 218 | } 219 | 220 | # Grouping the document tree into LaTeX files. List of tuples 221 | # (source start file, target name, title, 222 | # author, documentclass [howto, manual, or own class]). 223 | latex_documents = [ 224 | (master_doc, 'ectou-metadata.tex', u'ectou-metadata Documentation', 225 | u'Monetate', 'manual'), 226 | ] 227 | 228 | # The name of an image file (relative to this directory) to place at the top of 229 | # the title page. 230 | #latex_logo = None 231 | 232 | # For "manual" documents, if this is true, then toplevel headings are parts, 233 | # not chapters. 234 | #latex_use_parts = False 235 | 236 | # If true, show page references after internal links. 237 | #latex_show_pagerefs = False 238 | 239 | # If true, show URL addresses after external links. 240 | #latex_show_urls = False 241 | 242 | # Documents to append as an appendix to all manuals. 243 | #latex_appendices = [] 244 | 245 | # If false, no module index is generated. 246 | #latex_domain_indices = True 247 | 248 | 249 | # -- Options for manual page output --------------------------------------- 250 | 251 | # One entry per manual page. List of tuples 252 | # (source start file, name, description, authors, manual section). 253 | man_pages = [ 254 | (master_doc, 'ectou-metadata', u'ectou-metadata Documentation', 255 | [author], 1) 256 | ] 257 | 258 | # If true, show URL addresses after external links. 259 | #man_show_urls = False 260 | 261 | 262 | # -- Options for Texinfo output ------------------------------------------- 263 | 264 | # Grouping the document tree into Texinfo files. List of tuples 265 | # (source start file, target name, title, author, 266 | # dir menu entry, description, category) 267 | texinfo_documents = [ 268 | (master_doc, 'ectou-metadata', u'ectou-metadata Documentation', 269 | author, 'ectou-metadata', 'One line description of project.', 270 | 'Miscellaneous'), 271 | ] 272 | 273 | # Documents to append as an appendix to all manuals. 274 | #texinfo_appendices = [] 275 | 276 | # If false, no module index is generated. 277 | #texinfo_domain_indices = True 278 | 279 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 280 | #texinfo_show_urls = 'footnote' 281 | 282 | # If true, do not generate a @detailmenu in the "Top" node's menu. 283 | #texinfo_no_detailmenu = False 284 | -------------------------------------------------------------------------------- /docs/docker.rst: -------------------------------------------------------------------------------- 1 | Docker 2 | ====== 3 | 4 | First, build the docker image: 5 | 6 | .. code-block:: sh 7 | 8 | docker build -t ectou-metadata . 9 | 10 | Now run the container, injecting AWS credentials in $HOME/ectou-metadata-credentials into the container via bind mount: 11 | 12 | .. code-block:: sh 13 | 14 | docker run -e MOCK_METADATA_ROLE_ARN=... -v $HOME/ectou-metadata-credentials:/home/ec2-user/.aws/credentials ectou-metadata:ro 15 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ectou-metadata 2 | ============== 3 | 4 | Yet another EC2 instance metadata mocking service. 5 | 6 | Goals 7 | ----- 8 | 9 | Mock subset of the `EC2 instance metadata`_ service to enable local virtual machine environments to assume IAM roles. 10 | 11 | 12 | Usage 13 | ----- 14 | 15 | .. code-block:: sh 16 | 17 | ectou_metadata [--host host] [--port port] [--role-arn role_arn] 18 | 19 | Dependencies 20 | ------------ 21 | 22 | - boto3 >= 1.2.0 23 | - bottle >= 0.12.0 24 | 25 | 26 | Examples 27 | -------- 28 | .. toctree:: 29 | docker 30 | :maxdepth: 2 31 | 32 | 33 | 34 | .. _EC2 instance metadata: http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html 35 | 36 | -------------------------------------------------------------------------------- /ectou_metadata/__init__.py: -------------------------------------------------------------------------------- 1 | """ectou-metadata is an EC2 instance metadata mocking service.""" 2 | 3 | __version__ = "1.0.2" 4 | -------------------------------------------------------------------------------- /ectou_metadata/service.py: -------------------------------------------------------------------------------- 1 | """ 2 | Mock subset of instance metadata service. 3 | 4 | http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html 5 | """ 6 | 7 | import datetime 8 | import json 9 | import os 10 | 11 | import boto3.session 12 | import botocore.session 13 | import bottle 14 | import dateutil.tz 15 | 16 | 17 | _refresh_timeout = datetime.timedelta(minutes=5) 18 | _role_arn = None 19 | _conf_dir = None 20 | 21 | _credential_map = {} 22 | 23 | 24 | def _lookup_ip_role_arn(source_ip): 25 | try: 26 | if _conf_dir and source_ip: 27 | with open(os.path.join(_conf_dir, source_ip)) as f: 28 | return f.readline().strip() 29 | except IOError: 30 | pass # no such file 31 | 32 | 33 | def _get_role_arn(): 34 | """ 35 | Return role arn from X-Role-ARN header, 36 | lookup role arn from source IP, 37 | or fall back to command line default. 38 | """ 39 | role_arn = bottle.request.headers.get('X-Role-ARN') 40 | if not role_arn: 41 | role_arn = _lookup_ip_role_arn(bottle.request.environ.get('REMOTE_ADDR')) 42 | if not role_arn: 43 | role_arn = _role_arn 44 | return role_arn 45 | 46 | 47 | def _format_iso(dt): 48 | """ 49 | Format UTC datetime as iso8601 to second resolution. 50 | """ 51 | return datetime.datetime.strftime(dt, "%Y-%m-%dT%H:%M:%SZ") 52 | 53 | 54 | def _index(items): 55 | """ 56 | Format index list pages. 57 | """ 58 | bottle.response.content_type = 'text/plain' 59 | return "\n".join(items) 60 | 61 | 62 | @bottle.route("/latest") 63 | @bottle.route("/latest/meta-data") 64 | @bottle.route("/latest/meta-data/iam") 65 | @bottle.route("/latest/meta-data/iam/security-credentials") 66 | @bottle.route("/latest/meta-data/placement") 67 | def slashify(): 68 | bottle.redirect(bottle.request.path + "/", 301) 69 | 70 | 71 | @bottle.route("/") 72 | def root(): 73 | return _index(["latest"]) 74 | 75 | 76 | @bottle.route("/latest/") 77 | def latest(): 78 | return _index(["meta-data"]) 79 | 80 | 81 | @bottle.route("/latest/meta-data/") 82 | def meta_data(): 83 | return _index(["ami-id", 84 | "iam/", 85 | "instance-id", 86 | "instance-type", 87 | "local-ipv4", 88 | "placement/", 89 | "public-hostname", 90 | "public-ipv4"]) 91 | 92 | 93 | @bottle.route("/latest/meta-data/iam/") 94 | def iam(): 95 | return _index(["security-credentials/"]) 96 | 97 | 98 | @bottle.route("/latest/meta-data/iam/security-credentials/") 99 | def security_credentials(): 100 | return _index(["role-name"]) 101 | 102 | 103 | @bottle.route("/latest/meta-data/iam/security-credentials/role-name") 104 | def security_credentials_role_name(): 105 | role_arn = _get_role_arn() 106 | credentials = _credential_map.get(role_arn) 107 | 108 | # Refresh credentials if going to expire soon. 109 | now = datetime.datetime.now(tz=dateutil.tz.tzutc()) 110 | if not credentials or credentials['Expiration'] < now + _refresh_timeout: 111 | try: 112 | # Use any boto3 credential provider except the instance metadata provider. 113 | botocore_session = botocore.session.Session() 114 | botocore_session.get_component('credential_provider').remove('iam-role') 115 | session = boto3.session.Session(botocore_session=botocore_session) 116 | 117 | credentials = session.client('sts').assume_role(RoleArn=role_arn, 118 | RoleSessionName="ectou-metadata")['Credentials'] 119 | credentials['LastUpdated'] = now 120 | 121 | _credential_map[role_arn] = credentials 122 | 123 | except Exception as e: 124 | bottle.response.status = 404 125 | bottle.response.content_type = 'text/plain' # EC2 serves json as text/plain 126 | return json.dumps({ 127 | 'Code': 'Failure', 128 | 'Message': e.message, 129 | }, indent=2) 130 | 131 | # Return current credential. 132 | bottle.response.content_type = 'text/plain' # EC2 serves json as text/plain 133 | return json.dumps({ 134 | 'Code': 'Success', 135 | 'LastUpdated': _format_iso(credentials['LastUpdated']), 136 | "Type": "AWS-HMAC", 137 | 'AccessKeyId': credentials['AccessKeyId'], 138 | 'SecretAccessKey': credentials['SecretAccessKey'], 139 | 'Token': credentials['SessionToken'], 140 | 'Expiration': _format_iso(credentials['Expiration']) 141 | }, indent=2) 142 | 143 | 144 | @bottle.route("/latest/meta-data/instance-id") 145 | def instance_id(): 146 | bottle.response.content_type = 'text/plain' 147 | return "i-deadbeef" 148 | 149 | 150 | @bottle.route("/latest/meta-data/instance-type") 151 | def instance_type(): 152 | bottle.response.content_type = 'text/plain' 153 | return "m1.small" 154 | 155 | 156 | @bottle.route("/latest/meta-data/ami-id") 157 | def ami_id(): 158 | bottle.response.content_type = 'text/plain' 159 | return "ami-deadbeef" 160 | 161 | 162 | @bottle.route("/latest/meta-data/local-ipv4") 163 | def local_ipv4(): 164 | bottle.response.content_type = 'text/plain' 165 | return "127.0.0.1" 166 | 167 | 168 | @bottle.route("/latest/meta-data/placement/") 169 | def placement(): 170 | return _index(["availability-zone"]) 171 | 172 | 173 | @bottle.route("/latest/meta-data/placement/availability-zone") 174 | def availability_zone(): 175 | bottle.response.content_type = 'text/plain' 176 | return "us-east-1x" 177 | 178 | 179 | @bottle.route("/latest/meta-data/public-hostname") 180 | def public_hostname(): 181 | bottle.response.content_type = 'text/plain' 182 | return "localhost" 183 | 184 | 185 | @bottle.route("/latest/meta-data/public-ipv4") 186 | def public_ipv4(): 187 | bottle.response.content_type = 'text/plain' 188 | return "127.0.0.1" 189 | 190 | @bottle.route("/latest/dynamic/instance-identity") 191 | def instance_identity_index(slashes): 192 | bottle.response.content_type = 'text/plain' 193 | return 'document' 194 | 195 | 196 | @bottle.route("/latest/dynamic/instance-identitydocument") 197 | def instance_identity_document(slashes1, slashes2): 198 | bottle.response.content_type = 'text/plain' 199 | return '{"region": "us-east-1"}' 200 | 201 | 202 | def main(): 203 | import argparse 204 | 205 | parser = argparse.ArgumentParser() 206 | parser.add_argument('--host', default="169.254.169.254") 207 | parser.add_argument('--port', default=80) 208 | parser.add_argument('--role-arn', help="Default role ARN.") 209 | parser.add_argument('--conf-dir', help="Directory containing configuration files named by source ip.") 210 | args = parser.parse_args() 211 | 212 | global _role_arn 213 | _role_arn = args.role_arn 214 | 215 | global _conf_dir 216 | _conf_dir = args.conf_dir 217 | 218 | app = bottle.default_app() 219 | app.run(host=args.host, port=args.port) 220 | 221 | 222 | if __name__ == "__main__": 223 | main() 224 | -------------------------------------------------------------------------------- /examples/install-metadata-proxy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Configure local virtual machine to use proxy to reach the metadata service. 4 | # 5 | # This enables keeping all AWS credentials outside of the virtual machine. 6 | # In this environment, the local virtual machine is trusted to provide the desired Role ARN, 7 | # enabling many local virtual machines to share the same metadata service. 8 | # 9 | # 1. Configure local interface to enable binding to 169.254.169.254 10 | # 2. Configure nginx proxy to forward 169.254.169.254:80 requests to 11 | # $metadata_host:$metadata_port with additional X-Role-ARN header. 12 | # 13 | 14 | metadata_host=$1 15 | metadata_port=$2 16 | role_arn=$3 17 | 18 | # Install nginx. 19 | yum install -y nginx 20 | 21 | # Configure local interface alias. 22 | cat >/etc/sysconfig/network-scripts/ifcfg-lo:0 </etc/nginx/conf.d/ectou-metadata.conf <=1.2.0 2 | bottle>=0.12.0 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup 4 | 5 | 6 | setup( 7 | name="ectou-metadata", 8 | version="1.0.2", 9 | description="Yet another EC2 instance metadata mocking service.", 10 | url="https://github.com/monetate/ectou-metadata", 11 | author='Monetate', 12 | author_email='jjpersch@monetate.com', 13 | license="MIT", 14 | classifiers=[ 15 | "Development Status :: 4 - Beta", 16 | "Intended Audience :: Developers", 17 | "License :: OSI Approved :: MIT License", 18 | "Programming Language :: Python :: 2.7", 19 | "Programming Language :: Python :: 3", 20 | ], 21 | keywords="aws instance metadata", 22 | packages=[ 23 | 'ectou_metadata', 24 | ], 25 | install_requires=[ 26 | "boto3", 27 | "bottle", 28 | ], 29 | entry_points={ 30 | 'console_scripts': [ 31 | 'ectou_metadata = ectou_metadata.service:main', 32 | ], 33 | }, 34 | test_suite="tests", 35 | ) 36 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monetate/ectou-metadata/f5d57d086363321e6e4a1206f94c8f980971cb0c/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_metadata_api.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | try: 5 | from urllib2 import urlopen 6 | except ImportError: 7 | from urllib import urlopen 8 | 9 | import os 10 | import subprocess 11 | import time 12 | import unittest 13 | 14 | 15 | class TestMetadataApi(unittest.TestCase): 16 | @classmethod 17 | def setUpClass(cls): 18 | meta_service_path = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), 19 | "..", "ectou_metadata", "service.py")) 20 | test_host = "localhost" 21 | test_port = 4242 22 | 23 | cls.process = subprocess.Popen("python {} --host {} --port {}".format(meta_service_path, test_host, test_port), 24 | stdout=subprocess.PIPE, shell=True) 25 | time.sleep(1) 26 | cls.base_url = "http://{}:{}".format(test_host, test_port) 27 | 28 | @classmethod 29 | def tearDownClass(cls): 30 | cls.process.kill() 31 | 32 | def fetch(self, url): 33 | response = urlopen(url) 34 | return response.read().decode("utf-8") 35 | 36 | def test_root(self): 37 | self.assertEqual(self.fetch(self.base_url), "latest") 38 | 39 | def test_latest(self): 40 | self.assertEqual(self.fetch("{}/latest/".format(self.base_url)), "meta-data") 41 | 42 | def test_metadata(self): 43 | root_metadata = ("iam/\n" 44 | "instance-id\n" 45 | "local-ipv4\n" 46 | "placement/\n" 47 | "public-hostname\n" 48 | "public-ipv4") 49 | self.assertEqual(self.fetch("{}/latest/meta-data/".format(self.base_url)), root_metadata) 50 | 51 | def test_placement(self): 52 | placement_metadata = "availability-zone" 53 | self.assertEqual(self.fetch("{}/latest/meta-data/placement/".format(self.base_url)), placement_metadata) 54 | 55 | 56 | if __name__ == "__main__": 57 | unittest.main(verbosity=2) 58 | --------------------------------------------------------------------------------