├── .editorconfig ├── .gitignore ├── .travis.yml ├── HISTORY.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── ec2_ssh.py ├── requirements.in ├── requirements.txt ├── setup.cfg ├── setup.py └── tox.ini /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [LICENSE] 14 | insert_final_newline = false 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | .cache 3 | 4 | # C extensions 5 | *.so 6 | 7 | # Packages 8 | *.egg 9 | *.egg-info 10 | dist 11 | build 12 | eggs 13 | parts 14 | var 15 | sdist 16 | develop-eggs 17 | .eggs 18 | .installed.cfg 19 | lib 20 | lib64 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .coverage* 28 | .tox 29 | nosetests.xml 30 | htmlcov 31 | 32 | # Translations 33 | *.mo 34 | 35 | # Mr Developer 36 | .mr.developer.cfg 37 | .project 38 | .pydevproject 39 | 40 | # Complexity 41 | output/*.html 42 | output/*/index.html 43 | 44 | # Sphinx 45 | docs/_build 46 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: true 2 | dist: xenial 3 | 4 | language: python 5 | python: 3.7 6 | cache: pip 7 | 8 | notifications: 9 | email: false 10 | 11 | install: 12 | - pip install tox 13 | 14 | script: tox 15 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | .. :changelog: 2 | 3 | History 4 | ======= 5 | 6 | Pending 7 | ------- 8 | 9 | * Next version release notes here 10 | 11 | 1.9.0 (2017-09-08) 12 | ------------------ 13 | 14 | * Both ``ec2-host`` and ``ec2-ssh`` now only show/use instances in the 15 | ``running`` state. 16 | * Use the Public IP for an instance if EC2 no Public DNS for a public instance. 17 | It turns out EC2 may not return the Public DNS even when a Public IP is 18 | assigned. 19 | 20 | 1.8.0 (2017-07-19) 21 | ------------------ 22 | 23 | * Use private IP addresses for instances that don't have public ones. Such 24 | instances are not guaranteed to be accessible from the current host, 25 | depending on networking setup, but it's better the tool let's you try it. 26 | 27 | 1.7.0 (2017-04-23) 28 | ------------------ 29 | 30 | * Rewrite to use ``setup.py``'s ``entry_points`` feature, rather than 31 | ``scripts``. This makes everything importable from the ``ec2_ssh`` module and 32 | makes ``ec2-ssh`` faster as calling the ``ec2-host`` behaviour no longer 33 | requires ``subprocess``. 34 | 35 | 1.6.0 (2017-04-13) 36 | ------------------ 37 | 38 | * ``ec2-ssh`` supports specifying the username with the ``-u``/``--user`` flag 39 | or the ``EC2_SSH_USER`` environment variable. 40 | 41 | 1.5.3 (2017-03-23) 42 | ------------------ 43 | 44 | * Acquired the PyPI name ``ec2-ssh``, moved fork back there from 45 | ``ec2-ssh-yplan``. 46 | 47 | 1.5.2 (2016-08-17) 48 | ------------------ 49 | 50 | * Fix Python 3 bug with subprocess output type 51 | 52 | 1.5.1 (2016-01-21) 53 | ------------------ 54 | 55 | * Pip failed to receive wheel in version 1.5.0, re-uploading 56 | 57 | 1.5.0 (2016-01-21) 58 | ------------------ 59 | 60 | * Now using ``boto3`` 61 | 62 | 1.4.0 (2016-01-07) 63 | ------------------ 64 | 65 | * ``ec2-ssh`` rewritten in Python. As part of this, the automatic 'pretty 66 | prompt' has been removed. 67 | 68 | 1.3.0 (2016-01-06) 69 | ------------------ 70 | 71 | * Forked by YPlan 72 | * Output from ec2-host is now in random order, allowing ec2-ssh to spread 73 | logins between similar instances 74 | * Python 3 compatibility 75 | 76 | 1.2.1 (2011-11-27) 77 | ------------------ 78 | * Fix issue when ec2-host finds one offline instance with same name as an online instance 79 | 80 | 1.2 (2011-11-27) 81 | ---------------- 82 | 83 | * Merged pull requests to add region and tag support 84 | 85 | 1.1.1 (2011-11-17) 86 | ------------------ 87 | 88 | * Add line echoing host before establishing SSH connection 89 | 90 | 1.1 (2011-11-15) 91 | ---------------- 92 | 93 | * override prompt (PS1) to show tag name 94 | 95 | 1.0 (2011-09-05) 96 | ---------------- 97 | 98 | * initial release 99 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 YPlan 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include HISTORY.rst 2 | include LICENSE 3 | include README.rst 4 | 5 | recursive-include tests * 6 | recursive-exclude * __pycache__ 7 | recursive-exclude * *.py[co] 8 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | ec2-ssh 3 | ======= 4 | 5 | .. image:: https://img.shields.io/pypi/v/ec2-ssh.svg 6 | :target: https://pypi.python.org/pypi/ec2-ssh 7 | 8 | .. image:: https://travis-ci.org/adamchainz/ec2-ssh.svg?branch=master 9 | :target: https://travis-ci.org/adamchainz/ec2-ssh 10 | 11 | 12 | A pair of command line utilities for finding and SSH-ing into your Amazon EC2 13 | instances by tag (such as 'Name'). 14 | 15 | Forked from Instagram original code by YPlan. 16 | 17 | Installation 18 | ------------ 19 | 20 | From pip: 21 | 22 | .. code-block:: sh 23 | 24 | pip install ec2-ssh 25 | 26 | Python 2 and 3 compatible. 27 | 28 | Usage 29 | ----- 30 | 31 | There are two utilities: ``ec2-ssh`` and ``ec2-host``. 32 | 33 | 1. ``ec2-ssh`` 34 | ~~~~~~~~~~~~~~ 35 | 36 | Use the ``ec2-ssh`` command to SSH into your instances. Rather than a hostname 37 | to SSH into, it takes a tag key (default Name) and value, looks up the 38 | instances matching that tag, randomly picks one (if there is >1) and uses its 39 | public hostname, or private IP if it's not public. The username is defaulted to 40 | ``ubuntu``. 41 | 42 | For example: 43 | 44 | .. code-block:: sh 45 | 46 | $ ec2-ssh myapp 47 | # looks up instances where Name=myapp, expands out to something like: 48 | # ssh ubuntu@ec2-123-45-67-89.compute-1.amazonaws.com 49 | 50 | $ ec2-ssh root@myapp 51 | # expands to 52 | # ssh root@ec2-123-45-67-89.compute-1.amazonaws.com 53 | 54 | You can pass the ``-t / --tag`` option to use a different tag key to search on, 55 | for example: 56 | 57 | .. code-block:: sh 58 | 59 | $ ec2-ssh -t role myapp-frontend 60 | 61 | All other options are passed through to SSH so you can do things like: 62 | 63 | .. code-block:: sh 64 | 65 | $ ec2-ssh myapp -- sudo systemctl restart nginx 66 | $ ec2-ssh -vvv myapp # why can't I connect?? 67 | 68 | 69 | 2. ``ec2-host`` 70 | ~~~~~~~~~~~~~~~ 71 | 72 | This tool exposes just the host-lookup logic from ``ec2-ssh`` you can use it 73 | for other purposes. The only difference is that rather than returning just one 74 | matching instance's hostname/IP, it will return all of them. For example: 75 | 76 | .. code-block:: sh 77 | 78 | $ ec2-host # no tag, prints all instance hosts 79 | ec2-123-45-67-89.compute-1.amazonaws.com 80 | ec2-123-45-67-90.compute-1.amazonaws.com 81 | ec2-123-45-67-91.compute-1.amazonaws.com 82 | 83 | % ec2-host myapp # only instances where Name=myapp 84 | ec2-123-45-67-89.compute-1.amazonaws.com 85 | ec2-123-45-67-90.compute-1.amazonaws.com 86 | 87 | % ec2-host -t environment prod # instances where environment=prod 88 | ec2-123-45-67-90.compute-1.amazonaws.com 89 | ec2-111-45-67-90.compute-1.amazonaws.com 90 | -------------------------------------------------------------------------------- /ec2_ssh.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import argparse 4 | import os 5 | import random 6 | import sys 7 | 8 | import boto3 9 | 10 | __version__ = '1.9.0' 11 | 12 | parser = argparse.ArgumentParser( 13 | description=""" 14 | SSH into an ec2 host where a tag matches the given value. In the case there 15 | is more than one such instance, one will be chosen at random. Any extra 16 | arguments are passed to ssh directly. 17 | """ 18 | ) 19 | parser.add_argument('-t', '--tag', type=str, 20 | default=os.getenv('EC2_HOST_TAG', 'Name'), 21 | help="Tag to match, defaults to 'Name'") 22 | parser.add_argument('-u', '--user', type=str, 23 | default=os.getenv('EC2_SSH_USER', 'ubuntu'), 24 | help="Which user to connect with, defaults to 'ubuntu'") 25 | parser.add_argument('value', help="The value for the tag to match") 26 | 27 | 28 | def main(): 29 | args, unparsed = parser.parse_known_args() 30 | 31 | if '@' in args.value: 32 | username, value = args.value.split('@', 1) 33 | else: 34 | username = args.user 35 | value = args.value 36 | 37 | host_name = get_host_name(args.tag, value) 38 | 39 | if not host_name: 40 | print("ec2-ssh: no hosts matched", file=sys.stderr) 41 | sys.exit(1) 42 | 43 | command = [ 44 | 'ssh', 45 | '-t', '-t', 46 | username + '@' + host_name, 47 | ] 48 | if unparsed: 49 | command.extend(unparsed) 50 | 51 | print("ec2-ssh connecting to {}".format(host_name), file=sys.stderr) 52 | sys.stdout.flush() 53 | os.execlp(*command) 54 | 55 | 56 | def get_host_name(tag, value): 57 | for host in get_dns_names(tag, value): 58 | return host 59 | 60 | 61 | def ec2_host_parser(): 62 | parser = argparse.ArgumentParser( 63 | description="Output ec2 public host names for active hosts in random " 64 | "order, optionally match a tag which defaults to 'Name' " 65 | "or environment variable EC2_HOST_TAG." 66 | ) 67 | parser.add_argument('value', type=str, nargs='?', 68 | help='the value the tag should equal') 69 | parser.add_argument('-t', '--tag', type=str, 70 | default=os.getenv('EC2_HOST_TAG', 'Name'), 71 | help='which tag to search') 72 | return parser 73 | 74 | 75 | def host(): 76 | args = ec2_host_parser().parse_args() 77 | instances = get_dns_names(args.tag, args.value) 78 | random.shuffle(instances) 79 | for instance in instances: 80 | print(instance) 81 | 82 | 83 | def get_dns_names(tag, value): 84 | conn = boto3.client('ec2') 85 | 86 | filters = [ 87 | { 88 | 'Name': 'instance-state-name', 89 | 'Values': ['running'], 90 | } 91 | ] 92 | if value: 93 | filters.append({ 94 | 'Name': 'tag:' + tag, 95 | 'Values': [value] 96 | }) 97 | 98 | data = conn.describe_instances(Filters=filters) 99 | 100 | dns_names = [] 101 | for reservation in data['Reservations']: 102 | for instance in reservation['Instances']: 103 | if instance.get('PublicDnsName'): 104 | dns_names.append(instance['PublicDnsName']) 105 | elif instance.get('PublicIpAddress'): 106 | dns_names.append(instance['PublicIpAddress']) 107 | elif instance['PrivateIpAddress']: 108 | dns_names.append(instance['PrivateIpAddress']) 109 | return dns_names 110 | 111 | 112 | if __name__ == '__main__': 113 | main() 114 | -------------------------------------------------------------------------------- /requirements.in: -------------------------------------------------------------------------------- 1 | docutils 2 | flake8 3 | futures<3.2.0 4 | isort 5 | multilint 6 | Pygments 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile 3 | # To update, run: 4 | # 5 | # pip-compile --output-file requirements.txt requirements.in 6 | # 7 | configparser==3.7.1 # via flake8 8 | docutils==0.14 9 | enum34==1.1.6 # via flake8 10 | flake8==3.6.0 11 | futures==3.1.1 12 | isort==4.3.4 13 | mccabe==0.6.1 # via flake8 14 | multilint==2.4.0 15 | pycodestyle==2.4.0 # via flake8 16 | pyflakes==2.0.0 # via flake8 17 | pygments==2.3.1 18 | six==1.12.0 # via multilint 19 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [flake8] 5 | select = E,F,W 6 | 7 | [tool:multilint] 8 | paths = ec2_ssh.py 9 | setup.py 10 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import print_function 4 | 5 | import codecs 6 | import os 7 | import re 8 | 9 | from setuptools import setup 10 | 11 | 12 | def get_version(filename): 13 | with codecs.open(filename, 'r', 'utf-8') as fp: 14 | contents = fp.read() 15 | return re.search(r"__version__ = ['\"]([^'\"]+)['\"]", contents).group(1) 16 | 17 | 18 | version = get_version(os.path.join('ec2_ssh.py')) 19 | 20 | with codecs.open('README.rst', 'r', 'utf-8') as readme_file: 21 | readme = readme_file.read() 22 | 23 | with codecs.open('HISTORY.rst', 'r', 'utf-8') as history_file: 24 | history = history_file.read().replace('.. :changelog:', '') 25 | 26 | 27 | console_scripts = [ 28 | 'ec2-ssh = ec2_ssh:main', 29 | 'ec2-host = ec2_ssh:host', 30 | ] 31 | 32 | 33 | setup( 34 | name="ec2-ssh", 35 | version=version, 36 | author="Adam Johnson", 37 | author_email="me@adamj.eu", 38 | description="SSH into EC2 instances via tag name", 39 | long_description=readme + '\n\n' + history, 40 | license="MIT", 41 | url="https://github.com/adamchainz/ec2-ssh", 42 | keywords=["amazon", "aws", "ec2", "ami", "ssh", "cloud", "boto"], 43 | install_requires=['boto3>=1.1.0'], 44 | py_modules=['ec2_ssh'], 45 | entry_points={'console_scripts': console_scripts}, 46 | classifiers=[ 47 | "Development Status :: 5 - Production/Stable", 48 | "Environment :: Console", 49 | "Environment :: Web Environment", 50 | "Intended Audience :: Developers", 51 | "License :: OSI Approved :: MIT License", 52 | "Natural Language :: English", 53 | "Operating System :: OS Independent", 54 | "Programming Language :: Python", 55 | "Programming Language :: Python :: 2", 56 | 'Programming Language :: Python :: 2.7', 57 | "Programming Language :: Python :: 3", 58 | 'Programming Language :: Python :: 3.4', 59 | 'Programming Language :: Python :: 3.5', 60 | 'Programming Language :: Python :: 3.6', 61 | 'Programming Language :: Python :: 3.7', 62 | "Topic :: Utilities" 63 | ], 64 | ) 65 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py{27,3}-codestyle 3 | 4 | [testenv] 5 | deps = -rrequirements.txt 6 | commands = multilint 7 | 8 | [testenv:py27-codestyle] 9 | # setup.py check broken on travis python 2.7 10 | commands = multilint --skip setup.py 11 | --------------------------------------------------------------------------------