Skip to content

create a version that supports boto3/python3, updated requirements file #26

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/pythonpackage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Python package

on: [push]

jobs:
build:

runs-on: ubuntu-latest
strategy:
max-parallel: 6
matrix:
python-version: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9]

steps:
- uses: actions/checkout@v1
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Lint with flake8
run: |
pip install flake8
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
234 changes: 152 additions & 82 deletions aws-ssh-config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,27 @@
import re
import sys
import time
import boto.ec2
import boto3


AMI_NAMES_TO_USER = {
'amzn' : 'ec2-user',
'ubuntu' : 'ubuntu',
'CentOS' : 'root',
'DataStax' : 'ubuntu',
'CoreOS' : 'core'
'amzn': 'ec2-user',
'ubuntu': 'ubuntu',
'CentOS': 'root',
'DataStax': 'ubuntu',
'CoreOS': 'core'
}

AMI_IDS_TO_USER = {
'ami-ada2b6c4' : 'ubuntu'
'ami-ada2b6c4': 'ubuntu'
}

AMI_IDS_TO_KEY = {
'ami-ada2b6c4' : 'custom_key'
'ami-ada2b6c4': 'custom_key'
}

BLACKLISTED_REGIONS = [
'cn-north-1',
'us-gov-west-1'

]


Expand All @@ -34,46 +33,92 @@ def generate_id(instance, tags_filter, region):

if tags_filter is not None:
for tag in tags_filter.split(','):
value = instance.tags.get(tag, None)
if value:
if not instance_id:
instance_id = value
else:
instance_id += '-' + value
for aws_tag in instance['Instances'][0].get('Tags', []):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like a minor copy/paste error. As it is currently if you pass --tags Name, it will iterate through all instance tags and append with -, regardless of what tags were passed.

I added the following break here, and now works as expected

if aws_tag['Key'] != tag:
    continue

if aws_tag['Key'] != tag:
continue
value = aws_tag['Value']
if value:
if not instance_id:
instance_id = value
else:
instance_id += '-' + value
else:
for tag, value in instance.tags.items():
if not tag.startswith('aws'):
for tag in sorted(instance['Instances'][0].get('Tags', []), key=lambda tag: tag['Key']):
if not (tag['Key']).startswith('aws'):
if not instance_id:
instance_id = value
instance_id = tag['Value']
else:
instance_id += '-' + value
instance_id += '-' + tag['Value']

if not instance_id:
instance_id = instance.id
instance_id = instance['Instances'][0]['InstanceId']

if region:
instance_id += '-' + instance.placement
instance_id += '-' + instance[
'Instances'][0]['Placement']['AvailabilityZone']

return instance_id


def main():
parser = argparse.ArgumentParser()
parser.add_argument('--default-user', help='Default ssh username to use if it can\'t be detected from AMI name')
parser.add_argument('--keydir', default='~/.ssh/', help='Location of private keys')
parser.add_argument('--no-identities-only', action='store_true', help='Do not include IdentitiesOnly=yes in ssh config; may cause connection refused if using ssh-agent')
parser.add_argument('--postfix', default='', help='Specify a postfix to append to all host names')
parser.add_argument('--prefix', default='', help='Specify a prefix to prepend to all host names')
parser.add_argument('--private', action='store_true', help='Use private IP addresses (public are used by default)')
parser.add_argument('--profile', help='Specify AWS credential profile to use')
parser.add_argument('--proxy', default='', help='Specify a bastion host for ProxyCommand')
parser.add_argument('--region', action='store_true', help='Append the region name at the end of the concatenation')
parser.add_argument('--ssh-key-name', default='', help='Override the ssh key to use')
parser.add_argument('--strict-hostkey-checking', action='store_true', help='Do not include StrictHostKeyChecking=no in ssh config')
parser.add_argument('--tags', help='A comma-separated list of tag names to be considered for concatenation. If omitted, all tags will be used')
parser.add_argument('--user', help='Override the ssh username for all hosts')
parser.add_argument('--white-list-region', default='', help='Which regions must be included. If omitted, all regions are considered', nargs='+')

parser.add_argument(
'--default-user',
help='Default ssh username to use'
'if it can\'t be detected from AMI name')
parser.add_argument(
'--keydir',
default='~/.ssh/',
help='Location of private keys')
parser.add_argument(
'--no-identities-only',
action='store_true',
help='Do not include IdentitiesOnly=yes in ssh config; may cause'
' connection refused if using ssh-agent')
parser.add_argument(
'--postfix',
default='',
help='Specify a postfix to append to all host names')
parser.add_argument(
'--prefix',
default='',
help='Specify a prefix to prepend to all host names')
parser.add_argument(
'--private',
action='store_true',
help='Use private IP addresses (public are used by default)')
parser.add_argument(
'--profile',
help='Specify AWS credential profile to use')
parser.add_argument(
'--proxy',
default='',
help='Specify a bastion host for ProxyCommand')
parser.add_argument(
'--region',
action='store_true',
help='Append the region name at the end of the concatenation')
parser.add_argument(
'--ssh-key-name',
default='',
help='Override the ssh key to use')
parser.add_argument(
'--strict-hostkey-checking',
action='store_true',
help='Do not include StrictHostKeyChecking=no in ssh config')
parser.add_argument(
'--tags',
help='A comma-separated list of tag names to be considered for'
' concatenation. If omitted, all tags will be used')
parser.add_argument(
'--user',
help='Override the ssh username for all hosts')
parser.add_argument(
'--white-list-region',
default='',
help='Which regions must be included. If omitted, all regions'
' are considered',
nargs='+')
args = parser.parse_args()

instances = {}
Expand All @@ -85,31 +130,33 @@ def main():
print('# ' + ' '.join(sys.argv))
print('# ')
print('')

for region in boto.ec2.regions():
if args.white_list_region and region.name not in args.white_list_region:
if args.profile:
session = boto3.Session(profile_name=args.profile)
regions = session.client('ec2').describe_regions()['Regions']
else:
regions = boto3.client('ec2').describe_regions()['Regions']
for region in regions:
if (args.white_list_region
and region['RegionName'] not in args.white_list_region):
continue
if region.name in BLACKLISTED_REGIONS:
if region['RegionName'] in BLACKLISTED_REGIONS:
continue
if args.profile:
conn = boto.ec2.connect_to_region(region.name, profile_name=args.profile)
conn = session.client('ec2', region_name=region['RegionName'])
else:
conn = boto.ec2.connect_to_region(region.name)

for instance in conn.get_only_instances():
if instance.state != 'running':
continue
conn = boto3.client('ec2', region_name=region['RegionName'])

if instance.platform == 'windows':
for instance in conn.describe_instances()['Reservations']:
if instance['Instances'][0]['State']['Name'] != 'running':
continue

if instance.key_name is None:
if instance['Instances'][0].get('KeyName', None) is None:
continue

if instance.launch_time not in instances:
instances[instance.launch_time] = []
if instance['Instances'][0]['LaunchTime'] not in instances:
instances[instance['Instances'][0]['LaunchTime']] = []

instances[instance.launch_time].append(instance)
instances[instance['Instances'][0]['LaunchTime']].append(instance)

instance_id = generate_id(instance, args.tags, args.region)

Expand All @@ -120,36 +167,57 @@ def main():
counts_total[instance_id] += 1

if args.user:
amis[instance.image_id] = args.user
amis[instance['Instances'][0]['ImageId']] = args.user
else:
if not instance.image_id in amis:
image = conn.get_image(instance.image_id)
if not instance['Instances'][0]['ImageId'] in amis:
image = conn.describe_images(
Filters=[
{
'Name': 'image-id',
'Values': [instance['Instances'][0]['ImageId']]
}
]
)

for ami, user in AMI_NAMES_TO_USER.items():
regexp = re.compile(ami)
if image and regexp.match(image.name):
amis[instance.image_id] = user
if (len(image['Images']) > 0
and regexp.match(image['Images'][0]['Name'])):
amis[instance['Instances'][0]['ImageId']] = user
break

if instance.image_id not in amis:
amis[instance.image_id] = args.default_user
if instance['Instances'][0]['ImageId'] not in amis:
amis[
instance['Instances'][0]['ImageId']
] = args.default_user
if args.default_user is None:
image_label = image.name if image is not None else instance.image_id
sys.stderr.write('Can\'t lookup user for AMI \'' + image_label + '\', add a rule to the script\n')
image_label = image[
'Images'
][0][
'ImageId'] if len(image['Images']) and image['Images'][0] is not None else instance[
'Instances'][0]['ImageId']
sys.stderr.write(
'Can\'t lookup user for AMI \''
+ image_label + '\', add a rule to '
'the script\n')

for k in sorted(instances):
for instance in instances[k]:
if args.private:
if instance.private_ip_address:
ip_addr = instance.private_ip_address
if instance['Instances'][0]['PrivateIpAddress']:
ip_addr = instance['Instances'][0]['PrivateIpAddress']
else:
if instance.ip_address:
ip_addr = instance.ip_address
elif instance.private_ip_address:
ip_addr = instance.private_ip_address
else:
sys.stderr.write('Cannot lookup ip address for instance %s, skipped it.' % instance.id)
continue
try:
ip_addr = instance['Instances'][0]['PublicIpAddress']
except KeyError:
try:
ip_addr = instance['Instances'][0]['PrivateIpAddress']
except KeyError:
sys.stderr.write(
'Cannot lookup ip address for instance %s,'
' skipped it.'
% instance['Instances'][0]['InstanceId'])
continue

instance_id = generate_id(instance, args.tags, args.region)

Expand All @@ -158,33 +226,35 @@ def main():
instance_id += '-' + str(counts_incremental[instance_id])

hostid = args.prefix + instance_id + args.postfix
hostid = hostid.replace(' ', '_') # get rid of spaces
hostid = hostid.replace(' ', '_') # get rid of spaces

if instance.id:
print('# id: ' + instance.id)
if instance['Instances'][0]['InstanceId']:
print('# id: ' + instance['Instances'][0]['InstanceId'])
print('Host ' + hostid)
print(' HostName ' + ip_addr)

try:
if amis[instance.image_id] is not None:
print(' User ' + amis[instance.image_id])
except:
pass
if amis[instance['Instances'][0]['ImageId']] is not None:
print(' User ' + amis[instance['Instances'][0]['ImageId']])

if args.keydir:
keydir = args.keydir
else:
keydir = '~/.ssh/'

if args.ssh_key_name:
print(' IdentityFile ' + keydir + args.ssh_key_name + '.pem')
print(' IdentityFile '
+ keydir + args.ssh_key_name + '.pem')
else:
key_name = AMI_IDS_TO_KEY.get(instance.image_id, instance.key_name)
key_name = AMI_IDS_TO_KEY.get(
instance['Instances'][0]['ImageId'],
instance['Instances'][0]['KeyName'])

print(' IdentityFile ' + keydir + key_name.replace(' ', '_') + '.pem')
print(' IdentityFile '
+ keydir + key_name.replace(' ', '_') + '.pem')

if not args.no_identities_only:
# ensure ssh-agent keys don't flood when we know the right file to use
# ensure ssh-agent keys don't flood
# when we know the right file to use
print(' IdentitiesOnly yes')
if not args.strict_hostkey_checking:
print(' StrictHostKeyChecking no')
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
boto
boto3