├── Rakefile ├── Gemfile ├── lib ├── awsam │ ├── version.rb │ ├── key.rb │ ├── accounts.rb │ ├── utils.rb │ ├── account.rb │ └── ec2.rb └── awsam.rb ├── test ├── test_awsem.rb └── helper.rb ├── bin ├── aenv ├── ascp ├── assh └── raem ├── .gitignore ├── awsam.gemspec ├── LICENSE.txt ├── bashrc └── rc.scr └── README.md /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /lib/awsam/version.rb: -------------------------------------------------------------------------------- 1 | module Awsam 2 | VERSION = "0.2.1" 3 | end 4 | -------------------------------------------------------------------------------- /test/test_awsem.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | class TestAwsam < Test::Unit::TestCase 4 | should "probably rename this file and start testing for real" do 5 | flunk "hey buddy, you should probably rename this file and start testing for real" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler' 3 | begin 4 | Bundler.setup(:default, :development) 5 | rescue Bundler::BundlerError => e 6 | $stderr.puts e.message 7 | $stderr.puts "Run `bundle install` to install missing gems" 8 | exit e.status_code 9 | end 10 | require 'test/unit' 11 | require 'shoulda' 12 | 13 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 14 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 15 | require 'awsam' 16 | 17 | class Test::Unit::TestCase 18 | end 19 | -------------------------------------------------------------------------------- /bin/aenv: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $:.unshift File.join(File.dirname(__FILE__), '../lib') 4 | 5 | require 'awsam' 6 | 7 | if ARGV.length < 1 8 | puts "Usage: aenv cmd [arg1 arg2 ...]" 9 | exit 1 10 | end 11 | 12 | Awsam::Accounts::load! 13 | 14 | acct = Awsam::Accounts::active 15 | unless acct 16 | puts "No active account. Use 'aem use ' to select one" 17 | exit 1 18 | end 19 | 20 | env = acct.get_environ 21 | 22 | env.each do |k, v| 23 | ENV[k] = v.to_s 24 | end 25 | 26 | exec *ARGV 27 | 28 | # Local Variables: 29 | # mode: ruby 30 | # End: 31 | 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # rcov generated 2 | coverage 3 | 4 | # rdoc generated 5 | rdoc 6 | 7 | # yard generated 8 | doc 9 | .yardoc 10 | 11 | # bundler 12 | .bundle 13 | 14 | # jeweler generated 15 | pkg 16 | 17 | # Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore: 18 | # 19 | # * Create a file at ~/.gitignore 20 | # * Include files you want ignored 21 | # * Run: git config --global core.excludesfile ~/.gitignore 22 | # 23 | # After doing this, these files will be ignored in all your git projects, 24 | # saving you from having to 'pollute' every project you touch with them 25 | # 26 | # Not sure what to needs to be ignored for particular editors/OSes? Here's some ideas to get you started. (Remember, remove the leading # of the line) 27 | # 28 | # For MacOS: 29 | # 30 | #.DS_Store 31 | # 32 | # For TextMate 33 | #*.tmproj 34 | #tmtags 35 | # 36 | # For emacs: 37 | #*~ 38 | #\#* 39 | #.\#* 40 | # 41 | # For vim: 42 | #*.swp 43 | /Gemfile.lock 44 | -------------------------------------------------------------------------------- /awsam.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'awsam/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "awsam" 8 | spec.version = Awsam::VERSION 9 | spec.authors = ["Mike Heffner"] 10 | spec.email = ["mikeh@fesnel.com"] 11 | spec.summary = %q{Amazon Web Services Account Manager} 12 | spec.description = %q{Amazon Web Services Account Manager (modeled after 'rvm')} 13 | spec.homepage = "" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files -z`.split("\x0") 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_dependency 'aws-sdk', '~> 2.3.22' 22 | spec.add_dependency 'trollop', '2.0' 23 | 24 | spec.add_development_dependency "bundler", "~> 1.7" 25 | spec.add_development_dependency "rake", "~> 10.0" 26 | end 27 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Mike Heffner 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /lib/awsam.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.join(File.dirname(__FILE__), 'awsam') 2 | 3 | require 'fileutils' 4 | 5 | require 'aws-sdk' 6 | 7 | require 'accounts' 8 | require 'ec2' 9 | 10 | module Awsam 11 | CONF_BASE_DIR = ".awsam" 12 | CONF_DIR = File.join(ENV['HOME'], CONF_BASE_DIR) 13 | DEFAULT_LINK_NAME = ".default" 14 | 15 | def self.get_conf_dir 16 | FileUtils.mkdir(CONF_DIR) unless File.exist?(CONF_DIR) 17 | CONF_DIR 18 | end 19 | 20 | def self.get_accts_dir 21 | base = get_conf_dir() 22 | acctsdir = File.join(base, 'accts') 23 | FileUtils.mkdir(acctsdir) unless File.exist?(acctsdir) 24 | acctsdir 25 | end 26 | 27 | def self.init_awsam 28 | dir = get_conf_dir 29 | File.open(File.join(dir, "bash.rc"), "w") do |f| 30 | f << File.read(File.join(File.dirname(__FILE__), '../bashrc/rc.scr')) 31 | end 32 | 33 | puts 34 | puts "Initialized AWS Account Manager" 35 | puts 36 | puts "Add the following to your $HOME/.bashrc:" 37 | puts 38 | puts " if [ -s $HOME/#{CONF_BASE_DIR}/bash.rc ]; then" 39 | puts " source $HOME/#{CONF_BASE_DIR}/bash.rc" 40 | puts " fi" 41 | puts 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/awsam/key.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | 3 | module Awsam 4 | class Key 5 | KEYFILE = "key.pem" 6 | 7 | attr_reader :name 8 | 9 | def initialize(keydir) 10 | @name = File.basename(keydir) 11 | @dir = keydir 12 | if @name == Awsam::DEFAULT_LINK_NAME 13 | # This is required for the default link 14 | raise "Can not name a key: #{Awsam::DEFAULT_LINK_NAME}" 15 | end 16 | end 17 | 18 | def path 19 | File.join(@dir, KEYFILE) 20 | end 21 | 22 | def self.import(acctdir, key_name, key_file) 23 | dir = File.join(Key::keys_dir(acctdir), key_name) 24 | FileUtils.mkdir(dir) unless File.exist?(dir) 25 | 26 | File.open(File.join(dir, KEYFILE), "w", 0400) do |f| 27 | f << File.read(key_file) 28 | end 29 | 30 | Key.new(dir) 31 | end 32 | 33 | def self.keys_dir(base) 34 | dir = File.join(base, "keys") 35 | FileUtils.mkdir(dir) unless File.exist?(dir) 36 | dir 37 | end 38 | 39 | def print_environ 40 | envs = { 41 | "AMAZON_SSH_KEY_NAME" => @name, 42 | "AMAZON_SSH_KEY_FILE" => self.path 43 | } 44 | 45 | Utils::bash_environ(envs) 46 | end 47 | 48 | def remove 49 | FileUtils.rm(self.path) 50 | FileUtils.rmdir(@dir) 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/awsam/accounts.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | 3 | require 'account' 4 | require 'utils' 5 | 6 | module Awsam 7 | module Accounts 8 | 9 | @@accounts = {} 10 | 11 | def self.load! 12 | accts = Hash.new 13 | accts_dir = Awsam::get_accts_dir 14 | Utils::confdir_scan(accts_dir) do |name| 15 | acct = Account::load_from_disk(File.join(accts_dir, name)) 16 | accts[name] = acct if acct 17 | end 18 | 19 | @@accounts = accts 20 | end 21 | 22 | def self.active 23 | active = ENV['AWSAM_ACTIVE_ACCOUNT'] 24 | return nil unless active 25 | 26 | acct = find(active) 27 | unless acct 28 | puts "No account named '#{active}' found." 29 | return nil 30 | end 31 | 32 | acct 33 | end 34 | 35 | def self.get 36 | return @@accounts 37 | end 38 | 39 | def self.find(name) 40 | @@accounts[name] 41 | end 42 | 43 | def self.set_default(name) 44 | unless find(name) 45 | $stderr.puts "Failed to find account #{name}" 46 | return false 47 | end 48 | 49 | Utils::set_default(Awsam::get_accts_dir, name) 50 | end 51 | 52 | def self.get_default 53 | dflt = Utils::get_default(Awsam::get_accts_dir) 54 | dflt ? find(dflt) : nil 55 | end 56 | 57 | def self.remove_default 58 | Utils::remove_default(Awsam::get_accts_dir) 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /bin/ascp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $:.unshift File.join(File.dirname(__FILE__), '../lib') 4 | 5 | require 'awsam' 6 | 7 | # 8 | # Will perform an scp to/from an instance ID. 9 | # 10 | # 11 | # Usage: ascp [user@]:remote-file local-file 12 | # ascp local-file [user@]:remote-file 13 | 14 | if ARGV.length == 0 15 | puts 'Usage: ascp [user@]:remote-file local-file' 16 | puts ' ascp local-file [user@]:remote-file' 17 | exit 1 18 | end 19 | 20 | user = "root" 21 | 22 | args = ARGV.clone 23 | 24 | Awsam::Accounts::load! 25 | acct = Awsam::Accounts::active 26 | unless acct 27 | puts "No active account. Use 'aem use ' to select one" 28 | exit 1 29 | end 30 | 31 | keyfile = nil 32 | 0.upto(args.length - 1) do |i| 33 | m = args[i].match(/([a-zA-Z0-9_]{1,}@){0,1}(i-[0-9a-f]{8,17}):/) 34 | if m 35 | user = m[1] 36 | instance_id = m[2] 37 | 38 | inst = Awsam::Ec2::find_instance(acct, instance_id) 39 | unless inst 40 | puts "Unable to locate instance ID #{instance_id}" 41 | exit 1 42 | end 43 | 44 | if inst.state.name != "running" 45 | puts "Instance #{instance_id} is not running, it's #{inst.state.name}" 46 | exit 1 47 | end 48 | 49 | hostname = Awsam::Ec2::instance_hostname(inst) 50 | args[i] = args[i].gsub(instance_id, hostname) 51 | 52 | key = acct.find_key(inst.key_name) 53 | keyfile = key.path if key 54 | break 55 | end 56 | end 57 | 58 | if keyfile 59 | args.unshift('-i', keyfile, '-o', 'IdentitiesOnly=yes') 60 | end 61 | 62 | exec "scp #{args.join(" ")}" 63 | 64 | # Local Variables: 65 | # mode: ruby 66 | # End: 67 | -------------------------------------------------------------------------------- /lib/awsam/utils.rb: -------------------------------------------------------------------------------- 1 | 2 | module Awsam 3 | module Utils 4 | # Scan a directory yielding for each file 5 | def self.confdir_scan(dir) 6 | Dir.entries(dir).each do |name| 7 | next if name == '.' || name == '..' || name == Awsam::DEFAULT_LINK_NAME 8 | yield(name) 9 | end 10 | end 11 | 12 | # Unset each of the environ settings to clear the environ 13 | def self.bash_unset_environ(envs) 14 | envs.each_pair do |k, v| 15 | puts "unset #{k}" 16 | end 17 | end 18 | 19 | # Print the appropriate environment variables set commands for bash 20 | def self::bash_environ(envs, set_export = true) 21 | envs.each_pair do |k, v| 22 | puts "%s#{k}=\"#{v}\"" % [set_export ? "export " : ""] 23 | end 24 | end 25 | 26 | # Set the default resource with link directory and target 27 | def self.set_default(basedir, target) 28 | link = File.join(basedir, Awsam::DEFAULT_LINK_NAME) 29 | if File.exist?(link) 30 | begin 31 | FileUtils.rm(link) 32 | rescue => err 33 | $stderr.puts "Failed to remove link #{link}: #{err.message}" 34 | return false 35 | end 36 | end 37 | begin 38 | FileUtils.ln_s(target, link) 39 | rescue => err 40 | $stderr.puts "Failed to create symlink: #{err.message}" 41 | return false 42 | end 43 | true 44 | end 45 | 46 | # Get the target of the default link 47 | def self.get_default(basedir) 48 | link = File.join(basedir, Awsam::DEFAULT_LINK_NAME) 49 | File.exist?(link) ? File.readlink(link) : nil 50 | end 51 | 52 | # Remove the default link 53 | def self.remove_default(basedir) 54 | FileUtils.rm File.join(basedir, Awsam::DEFAULT_LINK_NAME) 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /bin/assh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $:.unshift File.join(File.dirname(__FILE__), '../lib') 4 | 5 | require 'awsam' 6 | require 'trollop' 7 | 8 | $opts = Trollop::options do 9 | banner <<-EOS 10 | 11 | NAME 12 | assh -- ssh client for EC2 13 | 14 | SYNOPSIS 15 | assh [-f --first-node] [user@]hostname 16 | 17 | DESCRIPTION 18 | assh is a program for logging into an EC2 node. assh allows you to specify a hostname by either an instance ID or a 19 | tag name. Specifying a hostname by a substring of multiple tags, by default, will allow you to specify the exact node you 20 | want to use. 21 | 22 | The options are as follows: 23 | 24 | -f first-node 25 | Use first_node to specify that you want to override assh's default behavior when multiple tag names result from your 26 | requested hostname. By enabling this flag, you're allowing assh to use any of the resulting hosts. 27 | EOS 28 | 29 | opt :first_node, "Use first node mode", :short => "-f" 30 | end 31 | 32 | user = "root" 33 | instance_id = ARGV[0] 34 | 35 | if ARGV.length < 1 36 | puts "Usage: assh " 37 | exit 1 38 | end 39 | 40 | if instance_id.include?("@") 41 | (user, instance_id) = instance_id.split("@") 42 | elsif ENV['AWS_DEFAULT_USER'].to_s.length > 0 43 | user = ENV['AWS_DEFAULT_USER'] 44 | end 45 | 46 | Awsam::Accounts::load! 47 | 48 | acct = Awsam::Accounts::active 49 | unless acct 50 | puts "No active account. Use 'aem use ' to select one" 51 | exit 1 52 | end 53 | 54 | inst = Awsam::Ec2::find_instance(acct, instance_id) 55 | unless inst 56 | puts "Unable to locate instance ID #{instance_id}" 57 | exit 1 58 | end 59 | 60 | if inst.state.name != "running" 61 | puts "Instance #{instance_id} is not running, it's #{inst[:aws_state]}" 62 | exit 1 63 | end 64 | 65 | hostname = Awsam::Ec2::instance_hostname(inst) 66 | 67 | puts "Logging in as #{user} to #{hostname}" 68 | puts 69 | 70 | key = acct.find_key(inst.key_name) 71 | 72 | if key 73 | exec "ssh -i #{key.path} -o IdentitiesOnly=yes #{user}@#{hostname}" 74 | else 75 | exec "ssh #{user}@#{hostname}" 76 | end 77 | 78 | # Local Variables: 79 | # mode: ruby 80 | # End: 81 | -------------------------------------------------------------------------------- /lib/awsam/account.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | require 'fileutils' 3 | 4 | require 'key' 5 | require 'utils' 6 | 7 | module Awsam 8 | class Account 9 | DEFAULT_REGION = "us-east-1" 10 | 11 | attr_reader :name, :keys 12 | 13 | def initialize(name, params) 14 | if name == Awsam::DEFAULT_LINK_NAME 15 | # We require this for our default account symlink 16 | raise "Can not name an account: #{Awsam::DEFAULT_LINK_NAME}" 17 | end 18 | @name = name 19 | @params = params 20 | 21 | @params[:aws_region] ||= DEFAULT_REGION 22 | 23 | load_keys 24 | end 25 | 26 | def self.load_from_disk(dir) 27 | name = File.basename(dir) 28 | conffile = File.join(dir, 'conf.yml') 29 | 30 | return nil unless File.exist?(conffile) 31 | 32 | File.open(conffile) do |yf| 33 | @conf = YAML::load(yf) 34 | end 35 | 36 | Account.new(name, @conf) 37 | end 38 | 39 | def load_keys 40 | @keys = Hash.new 41 | base = conf_file('keys') 42 | return unless File.exist?(base) 43 | Utils::confdir_scan(base) do |name| 44 | @keys[name] = Key.new(File.join(base, name)) 45 | end 46 | end 47 | 48 | def print_unset_environ 49 | Utils::bash_unset_environ(get_environ) 50 | end 51 | 52 | def print_environ(set_export) 53 | Utils::bash_environ(get_environ, set_export) 54 | end 55 | 56 | def get_environ 57 | envs = { 58 | "AMAZON_ACCESS_KEY_ID" => @params[:access_key], 59 | "AWS_ACCESS_KEY_ID" => @params[:access_key], 60 | "AWS_ACCESS_KEY" => @params[:access_key], 61 | 62 | "AMAZON_SECRET_ACCESS_KEY" => @params[:secret_key], 63 | "AWS_SECRET_ACCESS_KEY" => @params[:secret_key], 64 | "AWS_SECRET_KEY" => @params[:secret_key], 65 | 66 | "AMAZON_AWS_ID" => @params[:aws_id], 67 | "AWS_DEFAULT_REGION" => @params[:aws_region], 68 | 69 | "EC2_URL" => ec2_url 70 | } 71 | end 72 | 73 | def find_key(name) 74 | @keys[name] 75 | end 76 | 77 | def import_key(name, path) 78 | @keys[name] = Key.import(conf_file, name, path) 79 | end 80 | 81 | def remove_key(name) 82 | return false unless @keys.has_key?(name) 83 | 84 | dflt = get_default_key 85 | Utils::remove_default(conf_file('keys')) if dflt && dflt.name == name 86 | @keys[name].remove 87 | @keys.delete(name) 88 | true 89 | end 90 | 91 | def set_default_key(keyname) 92 | key = @keys[keyname] 93 | unless key 94 | $stderr.puts "No key named #{keyname}" 95 | return false 96 | end 97 | 98 | Utils::set_default(conf_file('keys'), keyname) 99 | end 100 | 101 | def get_default_key 102 | dflt = Utils::get_default(conf_file('keys')) 103 | @keys[dflt] 104 | end 105 | 106 | def remove 107 | dir = conf_file 108 | acct = Awsam::Accounts::get_default 109 | if acct && acct.name == @name 110 | # Need to remove default link if we're the default account 111 | Awsam::Accounts::remove_default 112 | end 113 | 114 | FileUtils.rm_rf(dir) 115 | end 116 | 117 | def save 118 | dir = File.join(Awsam::get_accts_dir, @name) 119 | FileUtils.mkdir(dir) unless File.exist?(dir) 120 | 121 | File.open(File.join(dir, 'conf.yml'), "w", 0600) do |out| 122 | YAML.dump(@params, out ) 123 | end 124 | end 125 | 126 | # Export params...need better way to do this 127 | def desc 128 | @params[:description] 129 | end 130 | 131 | def access_key 132 | @params[:access_key] 133 | end 134 | 135 | def secret_key 136 | @params[:secret_key] 137 | end 138 | 139 | def aws_region 140 | @params[:aws_region] 141 | end 142 | 143 | def ec2_url 144 | "https://ec2.#{aws_region}.amazonaws.com" 145 | end 146 | 147 | private 148 | 149 | def conf_file(file = nil) 150 | dir = File.join(Awsam::get_accts_dir(), @name) 151 | 152 | return file.nil? ? dir : File.join(dir, file) 153 | end 154 | end 155 | end 156 | -------------------------------------------------------------------------------- /lib/awsam/ec2.rb: -------------------------------------------------------------------------------- 1 | module Awsam 2 | module Ec2 3 | 4 | LOOKUP_TAGS = ["Name", "aws:autoscaling:groupName", "AlsoKnownAs"].freeze 5 | 6 | def self.instance_hostname(inst) 7 | # Always use private IP address 8 | inst.private_ip_address 9 | end 10 | 11 | def self.find_instance(acct, instance_id) 12 | ec2 = Aws::EC2::Client.new(:access_key_id => acct.access_key, 13 | :secret_access_key => acct.secret_key, 14 | :region => acct.aws_region) 15 | 16 | unless ec2 17 | puts "Unable to connect to EC2" 18 | return nil 19 | end 20 | 21 | find(ec2, instance_id) 22 | end 23 | 24 | def self.find(ec2, instance_id) 25 | if instance_id =~ /^i-[0-9a-f]{8,17}$/ 26 | find_by_instance_id(ec2, instance_id) 27 | else 28 | find_by_tag(ec2, instance_id) 29 | end 30 | end 31 | 32 | def self.find_by_instance_id(ec2, instance_id) 33 | begin 34 | resp = ec2.describe_instances(:instance_ids => [instance_id]) 35 | resp.reservations.length > 0 ? resp.reservations[0].instances.first : nil 36 | rescue => e 37 | puts "error describing instance: #{instance_id}: #{e}" 38 | exit 1 39 | end 40 | end 41 | 42 | def self.find_by_tag(ec2, instance_id) 43 | results = [] 44 | 45 | params = { 46 | :filters => [{ 47 | :name => "resource-type", 48 | :values => ["instance"] 49 | }] 50 | } 51 | resp = ec2.describe_tags(params) 52 | resp.tags.each do |tag| 53 | if LOOKUP_TAGS.include?(tag.key) && 54 | tag.value.downcase.include?(instance_id.downcase) 55 | results << tag 56 | end 57 | end 58 | 59 | if !results || results.length == 0 60 | puts "No tags by this name are available in your account" 61 | exit 1 62 | end 63 | 64 | results.uniq! { |a| a.resource_id } 65 | results.sort! { |a,b| a.value <=> b.value } 66 | 67 | rmap = {} 68 | params = { 69 | :instance_ids => results.map{|a| a.resource_id}, 70 | :filters => [{ 71 | :name => "instance-state-name", 72 | :values => ["running"] 73 | }] 74 | } 75 | resp = ec2.describe_instances(params) 76 | resp.reservations.each do |resv| 77 | resv.instances.each do |inst| 78 | rmap[inst.instance_id] = inst 79 | end 80 | end 81 | 82 | results.reject! { |a| rmap[a.resource_id].nil? } 83 | 84 | if results.length == 0 85 | puts "No running instances by that tag name are available" 86 | exit 1 87 | end 88 | 89 | if $opts[:first_node] || results.length == 1 90 | node = results.first 91 | else 92 | puts "Please select which node you wish to use:" 93 | puts 94 | 95 | namemax = 0 96 | instmax = 0 97 | ipmax = 0 98 | results.each_with_index do |elem, i| 99 | inst = rmap[elem.resource_id] 100 | if elem.value.length > namemax 101 | namemax = elem.value.length 102 | end 103 | if inst.instance_id.length > instmax 104 | instmax = inst.instance_id.length 105 | end 106 | if inst.private_ip_address.length > ipmax 107 | ipmax = inst.private_ip_address.length 108 | end 109 | end 110 | 111 | countmax = results.size.to_s.length 112 | results.each_with_index do |elem, i| 113 | inst = rmap[elem.resource_id] 114 | 115 | launchtime = inst.launch_time 116 | puts "%*d) %-*s (%*s %-*s %-11s %s %s)" % 117 | [countmax, i + 1, 118 | namemax, elem.value, 119 | instmax, inst.instance_id, 120 | ipmax, inst.private_ip_address, 121 | inst.instance_type, 122 | inst.placement.availability_zone, 123 | launchtime.strftime("%Y-%m-%d")] 124 | end 125 | puts "%*s) Quit" % [countmax, "q"] 126 | puts 127 | 128 | print "> " 129 | input = $stdin.gets 130 | puts 131 | exit unless input =~ /^\d+$/ 132 | sel = input.to_i 133 | exit unless sel > 0 && sel <= results.size 134 | 135 | node = results[sel - 1] 136 | end 137 | 138 | return rmap[node.resource_id] 139 | end 140 | end 141 | end 142 | -------------------------------------------------------------------------------- /bin/raem: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $:.unshift File.join(File.dirname(__FILE__), '../lib') 4 | 5 | begin 6 | require 'awsam' 7 | rescue LoadError => err 8 | $stderr.puts "ERROR: Unable to load AWSAM gem dependencies: #{err.message}" 9 | exit 1 10 | end 11 | 12 | require 'optparse' 13 | 14 | def err(*args) 15 | $stderr.puts *args 16 | end 17 | 18 | def read_val(prompt, default = nil) 19 | begin 20 | if default 21 | print "#{prompt} [#{default}]: " 22 | else 23 | print "#{prompt}: " 24 | end 25 | 26 | val = gets.chomp 27 | if val.empty? && default 28 | val = default 29 | end 30 | end while val.empty? 31 | 32 | val 33 | end 34 | 35 | Awsam::Accounts::load! 36 | 37 | $options = {} 38 | $cmd = nil 39 | 40 | def selected_account(default_fallback = false) 41 | if !$options[:account] || $options[:account].empty? 42 | err "Command requires an account" 43 | exit 1 44 | end 45 | 46 | acct = Awsam::Accounts.find($options[:account]) 47 | unless acct 48 | exit 1 49 | end 50 | acct 51 | end 52 | 53 | def usage 54 | puts "Usage: raem --init" 55 | exit 56 | end 57 | 58 | optparse = OptionParser.new do|opts| 59 | opts.banner = "Usage: raem [options]\n" 60 | 61 | # help 62 | opts.on( '-h', '--help', 'Display this screen' ) do 63 | usage 64 | end 65 | 66 | opts.on('--add') do 67 | $cmd = :add_account 68 | end 69 | 70 | opts.on('--remove') do 71 | $cmd = :remove_account 72 | end 73 | 74 | opts.on('--account ') do |acctname| 75 | $options[:account] = acctname 76 | end 77 | 78 | opts.on('--import-key') do 79 | $cmd = :import_key 80 | end 81 | 82 | opts.on('--remove-key') do 83 | $cmd = :remove_key 84 | end 85 | 86 | opts.on('--keyname ') do |keyname| 87 | $options[:keyname] = keyname 88 | end 89 | 90 | opts.on('--keyfile ') do |keyfile| 91 | $options[:keyfile] = keyfile 92 | end 93 | 94 | opts.on('--list') do 95 | $cmd = :list 96 | end 97 | 98 | opts.on('--environ') do 99 | $cmd = :environ 100 | end 101 | 102 | opts.on('--environ-key') do 103 | $cmd = :environ_key 104 | end 105 | 106 | opts.on('--export') do 107 | $options[:set_export] = true 108 | end 109 | 110 | opts.on('--unset') do 111 | $options[:unset_environ] = true 112 | end 113 | 114 | opts.on('--init') do 115 | $cmd = :init 116 | end 117 | 118 | opts.on('--print-default-acct') do 119 | $cmd = :print_default_acct 120 | end 121 | 122 | opts.on('--print-default-key') do 123 | $cmd = :print_default_key 124 | end 125 | 126 | opts.on('--default') do 127 | $options[:set_default] = true 128 | end 129 | end 130 | 131 | optparse.parse!(ARGV) 132 | 133 | usage unless $cmd 134 | 135 | case $cmd 136 | when :init 137 | Awsam::init_awsam 138 | 139 | when :list 140 | inuse = ENV['AWSAM_ACTIVE_ACCOUNT'] 141 | inusekey = ENV['AWSAM_ACTIVE_KEY'] 142 | puts "\nAWS Accounts:\n\n" 143 | 144 | default = Awsam::Accounts::get_default 145 | accts = Awsam::Accounts::get 146 | accts.each_pair do |name, acct| 147 | desc = acct.desc ? " [#{acct.desc}]" : "" 148 | if acct.keys.length > 0 149 | dfltkey = acct.get_default_key 150 | names = acct.keys.keys.collect{ |k| 151 | pfx1 = (inuse == name && inusekey == k) ? ">" : "" 152 | pfx2 = (dfltkey && dfltkey.name == k) ? "*" : "" 153 | "#{pfx1}#{pfx2}#{k}" 154 | }.join(",") 155 | plural = "key" + (acct.keys.length > 1 ? "s" : "") 156 | desc += " [#{acct.keys.length} #{plural}: #{names}]" 157 | end 158 | pfx = inuse == name ? "=>" : " " 159 | dfltmark = (default && default.name == name) ? "*" : " " 160 | puts "#{pfx}#{dfltmark}#{name}#{desc}" 161 | end 162 | 163 | puts 164 | 165 | when :print_default_acct 166 | default = Awsam::Accounts::get_default 167 | exit 1 unless default 168 | puts default.name 169 | 170 | when :print_default_key 171 | acct = selected_account 172 | default = acct.get_default_key 173 | exit 1 unless default 174 | puts default.name 175 | 176 | when :environ 177 | acct = selected_account 178 | 179 | if $options[:set_default] 180 | r = Awsam::Accounts::set_default(acct.name) 181 | unless r 182 | err "Failed to set account #{acct.name} as the default" 183 | exit 1 184 | end 185 | end 186 | 187 | if $options[:unset_environ] 188 | acct.print_unset_environ 189 | else 190 | acct.print_environ(!$options[:set_export].nil?) 191 | end 192 | 193 | when :environ_key 194 | unless $options[:keyname] 195 | err "Option requires a keyname" 196 | exit 1 197 | end 198 | 199 | acct = selected_account 200 | 201 | k = acct.find_key($options[:keyname]) 202 | unless k 203 | err "Could not find key #{$options[:keyname]} in active account" 204 | exit 1 205 | end 206 | 207 | if $options[:set_default] 208 | r = acct.set_default_key(k.name) 209 | unless r 210 | err "Failed to set key #{k.name} as the default" 211 | exit 1 212 | end 213 | end 214 | 215 | k.print_environ 216 | 217 | when :add_account 218 | puts "Creating a new AWS account...\n" 219 | short_name = read_val("Short name") 220 | desc = read_val("Description") 221 | aws_region = read_val("AWS Region", ENV['AWS_DEFAULT_REGION']) 222 | access_key = read_val("Access key", (ENV['AMAZON_ACCESS_KEY_ID'] || ENV['AWS_ACCESS_KEY'])) 223 | secret_key = read_val("Secret key", (ENV['AMAZON_SECRET_ACCESS_KEY'] || ENV['AWS_SECRET_KEY'])) 224 | aws_id = read_val("AWS ID", ENV['AMAZON_AWS_ID']) 225 | 226 | if aws_id.match(/[0-9]+/).nil? 227 | err "AWS ID must be the numerical account ID" 228 | exit 1 229 | end 230 | 231 | acct = Awsam::Account.new(short_name, 232 | { :description => desc, 233 | :access_key => access_key, 234 | :secret_key => secret_key, 235 | :aws_id => aws_id, 236 | :aws_region => aws_region 237 | }) 238 | acct.save 239 | 240 | when :remove_account 241 | selected_account.remove 242 | 243 | when :import_key 244 | unless $options[:keyname] && $options[:keyfile] 245 | err "Requires keyname and keyfile!" 246 | exit 1 247 | end 248 | 249 | acct = selected_account 250 | 251 | display = acct.name + (acct.desc ? " [#{acct.desc}]" : "") 252 | 253 | unless File.exist?($options[:keyfile]) 254 | err "Unable to locate key file: #{$options[:keyfile]}" 255 | exit 1 256 | end 257 | 258 | ret = acct.import_key($options[:keyname], $options[:keyfile]) 259 | err "Imported key pair #{$options[:keyname]} for account #{display}" 260 | 261 | when :remove_key 262 | unless $options[:keyname] 263 | err "Requires keyname!" 264 | exit 1 265 | end 266 | 267 | acct = selected_account 268 | unless acct.remove_key($options[:keyname]) 269 | err "Failed to remove key #{$options[:keyname]}" 270 | exit 1 271 | end 272 | end 273 | 274 | 275 | # Local Variables: 276 | # mode: ruby 277 | # End: 278 | -------------------------------------------------------------------------------- /bashrc/rc.scr: -------------------------------------------------------------------------------- 1 | function __aem_active() 2 | { 3 | echo "$AWSAM_ACTIVE_ACCOUNT" 4 | } 5 | 6 | function __aem_active_key() 7 | { 8 | echo "$AWSAM_ACTIVE_KEY" 9 | } 10 | 11 | function __aem_usage() 12 | { 13 | echo "Usage: aem []" 14 | echo 15 | echo "Possible commands:" 16 | echo 17 | echo " add Add a new AWS account" 18 | echo 19 | echo " remove NAME Remove the NAME account" 20 | echo 21 | echo " key " 22 | echo " +-> add [--acct ] KEYNAME KEYFILE" 23 | echo " | Add key to ACCT or default to active one" 24 | echo " +-> remove [--acct ] KEYNAME" 25 | echo " | Remove key from ACCT or default" 26 | echo " +-> use [--default] KEYNAME" 27 | echo " Use the KEYNAME from the current account" 28 | echo " Will set the key as the default for the" 29 | echo " account if the --default option is set" 30 | echo 31 | echo " list List AWS accounts" 32 | echo 33 | echo " use [--default] NAME" 34 | echo " Use the NAME account. Set as default if" 35 | echo " the option --default is specified" 36 | echo 37 | } 38 | 39 | # Fail with error message and print usage 40 | function __aem_fusage() 41 | { 42 | local MSG="$*" 43 | 44 | if [ -n "$MSG" ]; then 45 | printf "ERROR: $MSG\n" >&2 46 | # Add a blank between error and usage 47 | echo 48 | fi 49 | 50 | __aem_usage 51 | } 52 | 53 | # Just print message to standard output 54 | function __aem_fail() 55 | { 56 | local MSG="$*" 57 | 58 | if [ -n "$MSG" ]; then 59 | printf "ERROR: $MSG\n" >&2 60 | fi 61 | } 62 | 63 | function __aem_add() 64 | { 65 | 66 | raem --add 67 | } 68 | 69 | function __aem_remove() 70 | { 71 | if [ $# -ne 1 ]; then 72 | __aem_fusage "Remove requires an argument" 73 | return 1 74 | fi 75 | 76 | local ACCT="$1" 77 | if [ "$ACCT" = "$(__aem_active)" ]; then 78 | __aem_fail "Can not remove the active account" 79 | return 1 80 | fi 81 | 82 | raem --remove --account "$ACCT" 83 | } 84 | 85 | function __aem_key_add() 86 | { 87 | local KEYNAME="" 88 | local KEYFILE="" 89 | local ACCT="" 90 | 91 | while [ $# -gt 0 ]; do 92 | if [ "$1" = "--acct" ]; then 93 | shift 94 | if [ $# -lt 1 ]; then 95 | __aem_fusage "--acct requires argument" 96 | return 1 97 | fi 98 | 99 | ACCT="$1" 100 | shift 101 | continue 102 | fi 103 | 104 | if [ -z "$KEYNAME" ]; then 105 | KEYNAME="$1" 106 | else 107 | KEYFILE="$1" 108 | fi 109 | shift 110 | done 111 | 112 | if [ -z "$KEYNAME" -o -z "$KEYFILE" ]; then 113 | __aem_fusage "Insufficient arguments" 114 | return 1 115 | fi 116 | 117 | if [ -z "$ACCT" ]; then 118 | ACCT=$(__aem_active) 119 | if [ -z "$ACCT" ]; then 120 | __aem_fail "No account specified and none active." 121 | return 1 122 | fi 123 | fi 124 | 125 | raem --import-key --account "$ACCT" --keyname "$KEYNAME" \ 126 | --keyfile "$KEYFILE" 127 | } 128 | 129 | function __aem_key_remove() 130 | { 131 | local KEYNAME="" 132 | local ACCT="" 133 | 134 | while [ $# -gt 0 ]; do 135 | if [ "$1" = "--acct" ]; then 136 | shift 137 | if [ $# -lt 1 ]; then 138 | __aem_fusage "--acct requires argument" 139 | return 1 140 | fi 141 | 142 | ACCT="$1" 143 | shift 144 | continue 145 | fi 146 | 147 | KEYNAME="$1" 148 | shift 149 | done 150 | 151 | if [ -z "$KEYNAME" ]; then 152 | __aem_fusage "Must specify key to remove" 153 | return 1 154 | fi 155 | 156 | if [ -z "$ACCT" ]; then 157 | ACCT=$(__aem_active) 158 | if [ -z "$ACCT" ]; then 159 | __aem_fail "No account specified and none active." 160 | return 1 161 | fi 162 | fi 163 | 164 | # Unset active key 165 | if [ "$KEYNAME" = "$(__aem_active_key)" ]; then 166 | unset AWSAM_ACTIVE_KEY 167 | fi 168 | 169 | raem --remove-key --account "$ACCT" --keyname "$KEYNAME" 170 | } 171 | 172 | function __aem_key_use() 173 | { 174 | if [ $# -lt 1 ]; then 175 | __aem_fusage "Must specify keyname" 176 | return 1 177 | fi 178 | 179 | local ACCT 180 | local SETDEFAULT=0 181 | local KEYNAME="" 182 | 183 | while [ $# -gt 0 ]; do 184 | if [ "$1" == "--default" ]; then 185 | SETDEFAULT=1 186 | shift 187 | continue 188 | fi 189 | 190 | if [ -n "$KEYNAME" ]; then 191 | __aem_fusage "Invalid argument to key use command" 192 | return 1 193 | fi 194 | 195 | KEYNAME="$1" 196 | shift 197 | done 198 | 199 | if [ -z "$KEYNAME" ]; then 200 | __aem_fusage "Must specify key name" 201 | return 1 202 | fi 203 | 204 | ACCT=$(__aem_active) 205 | if [ -z "$ACCT" ]; then 206 | __aem_fail "Must select an account first" 207 | return 1 208 | fi 209 | 210 | local ENV 211 | if [ $SETDEFAULT -ne 0 ]; then 212 | # Will set the key as the default 213 | ENV=$(raem --environ-key --account $ACCT --default --keyname $KEYNAME) 214 | else 215 | ENV=$(raem --environ-key --account $ACCT --keyname $KEYNAME) 216 | fi 217 | if [ $? -ne 0 ]; then 218 | __aem_fail "Failed to select key $KEYNAME" 219 | return 1 220 | fi 221 | 222 | eval $ENV 223 | export AWSAM_ACTIVE_KEY=$KEYNAME 224 | } 225 | 226 | function __aem_key() 227 | { 228 | if [ $# -lt 1 ]; then 229 | __aem_fusage "Key cmd requires an argument" 230 | return 1 231 | fi 232 | 233 | local key_cmd="$1" 234 | shift 235 | 236 | case "$key_cmd" in 237 | add) 238 | __aem_key_add "$@" 239 | return $? 240 | ;; 241 | remove) 242 | __aem_key_remove "$@" 243 | return $? 244 | ;; 245 | use) 246 | __aem_key_use "$@" 247 | return $? 248 | ;; 249 | *) 250 | echo "Unknown key command: $key_cmd" 251 | return 1; 252 | ;; 253 | esac 254 | } 255 | 256 | function __aem_list() 257 | { 258 | raem --list 259 | } 260 | 261 | function __aem_use() 262 | { 263 | local ACCT 264 | local ENV 265 | local SETDEFAULT=0 266 | 267 | while [ $# -gt 0 ]; do 268 | if [[ "$1" == "--default" ]]; then 269 | SETDEFAULT=1 270 | shift 271 | continue 272 | fi 273 | 274 | if [ -n "$ACCT" ]; then 275 | __aem_fusage "Invalid arguments to use command" 276 | return 1 277 | fi 278 | 279 | ACCT="$1" 280 | shift 281 | done 282 | 283 | if [ -z "$ACCT" ]; then 284 | __aem_fusage "use command requires account name" 285 | return 1 286 | fi 287 | 288 | if [ $SETDEFAULT -eq 0 ]; then 289 | ENV=$(raem --environ --account $ACCT) 290 | else 291 | ENV=$(raem --environ --default --account $ACCT) 292 | fi 293 | if [ $? -ne 0 ]; then 294 | __aem_fail "No such account: $ACCT" 295 | return 1 296 | fi 297 | 298 | eval $ENV 299 | export AWSAM_ACTIVE_ACCOUNT=$ACCT 300 | 301 | # Clear active key 302 | unset AWSAM_ACTIVE_KEY 303 | 304 | # Check if there is a default key 305 | DEFAULT=$(raem --account $ACCT --print-default-key) 306 | if [ $? -eq 0 ]; then 307 | aem key use "$DEFAULT" 308 | fi 309 | 310 | # Create IAM credential file that is required by AWS IAM CLI tools 311 | CREDENTIALS_FILE="${HOME}/.awsam/credentials.txt" 312 | 313 | touch ${CREDENTIALS_FILE} 314 | chmod 0600 ${CREDENTIALS_FILE} 315 | 316 | echo "AWSAccessKeyId=${AWS_ACCESS_KEY_ID}" >| ${CREDENTIALS_FILE} 317 | echo "AWSSecretKey=${AWS_SECRET_ACCESS_KEY}" >> ${CREDENTIALS_FILE} 318 | 319 | # We're done, so clear the environment. This protects against 320 | # leaking AWS creds to other apps. 321 | UNSET_ENV=$(raem --environ --account $ACCT --unset) 322 | eval $UNSET_ENV 323 | 324 | return 0 325 | } 326 | 327 | function aem() 328 | { 329 | 330 | if [ $# -lt 1 ]; then 331 | __aem_usage 332 | return 0 333 | fi 334 | 335 | if ! `which raem > /dev/null 2>&1`; then 336 | __aem_fail "Unable to find 'raem' binary in PATH"; 337 | return 1 338 | fi 339 | 340 | local aem_cmd="$1" 341 | shift 342 | 343 | case "$aem_cmd" in 344 | add) 345 | __aem_add 346 | ;; 347 | remove) 348 | __aem_remove "$@" 349 | ;; 350 | key) 351 | __aem_key "$@" 352 | ;; 353 | list) 354 | __aem_list 355 | ;; 356 | use) 357 | __aem_use "$@" 358 | ;; 359 | *) 360 | __aem_fusage "Unknown command: $aem_cmd" 361 | return 1 362 | esac 363 | 364 | return $? 365 | } 366 | 367 | # Check for default account 368 | DEFAULT=$(raem --print-default-acct) 369 | if [ $? -eq 0 ]; then 370 | __aem_use "$DEFAULT" 371 | fi 372 | 373 | # Local Variables: 374 | # mode: shell-script 375 | # End: 376 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | AWSAM (Amazon Web Services Account Manager) allows you to easily manage multiple sets of AWS credentials. It has support for multiple accounts and multiple key-pairs per account. 2 | 3 | Account switching auto-populates ENV vars used by AWS' command line tools and AWSAM additionally gives you intelligent wrappers for `ssh` and `scp` which can be used like: 4 | 5 | # ssh by AWS instance id 6 | $ assh ubuntu@i-123456 7 | 8 | # ssh by AWS tag name 9 | $ assh ubuntu@web-node-01 10 | 11 | # ssh by AWS tag name to an arbitrary node using a substring 12 | # 13 | # This example assumes you have the following nodes and that 14 | # you're indifferent to which node you connect to: 15 | # web-node-01, web-node-02, web-node-3 16 | $ assh -f ubuntu@web-node- 17 | 18 | # scp by instance id 19 | $ ascp local-file ubuntu@i-123456:remote-file 20 | 21 | AWSAM supports both AWS' legacy [Java-based CLI tools](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/SettingUp_CommandLine.html) and their newer [python-based CLI](http://docs.aws.amazon.com/cli/latest/userguide/cli-chap-welcome.html). 22 | 23 | # Installation 24 | 25 | 1. Install the gem. 26 | 27 | $ gem install awsam 28 | 29 | 2. Install BASH rc file 30 | 31 | $ raem --init 32 | Initialized AWS Account Manager 33 | 34 | Add the following to your .bashrc: 35 | 36 | if [ -s $HOME/.awsam/bash.rc ]; then 37 | source $HOME/.awsam/bash.rc 38 | fi 39 | 40 | 3. Open a new bash environment. 41 | 42 | ### Environment variables 43 | 44 | *AWS Account Manager* will set a variety of environment variables when 45 | you execute the `aenv` shell wrapper: 46 | 47 | $ env | grep AMAZON_ACCESS 48 | Exit 1 49 | $ aenv env | grep AMAZON_ACCESS 50 | AMAZON_ACCESS_KEY_ID=AK.... 51 | 52 | Some of these environment variables match the ones used by the Amazon 53 | EC2 CLI tools and some our unique to AWSAM. It is often convenient to 54 | use these environment variables in DevOPs scripts in place of 55 | hard-coded values -- allowing your scripts to be seamlessly used for 56 | staging and production environments simply by switching the active 57 | account with `aem` and wrapping execution of the command with `aenv`. 58 | 59 | The environment variables set when selecting an account are: 60 | 61 | * `AMAZON_ACCESS_KEY_ID` and `AWS_ACCESS_KEY_ID` and `AWS_ACCESS_KEY` - API access key 62 | 63 | * `AMAZON_SECRET_ACCESS_KEY` and `AWS_SECRET_ACCESS_KEY` and `AWS_SECRET_KEY` - Secret API access key 64 | 65 | * `AMAZON_AWS_ID` - The integer ID of this AWS account 66 | 67 | When selecting an SSH key, the following environment variables are 68 | set: 69 | 70 | * `AMAZON_SSH_KEY_NAME` - Name of the keypair. 71 | * `AMAZON_SSH_KEY_FILE` - Full path to the public key PEM file 72 | 73 | **NOTE:** As of version 0.2.0, these are no longer set in the shell 74 | environment by default. You must run any command that requires AWS 75 | access with the `aenv` wrapper. 76 | 77 | ### Updating 78 | 79 | 1. Update repo (fetch && merge) or `gem update awsam` 80 | 81 | 2. Run `raem --init`. Ignore instructions to setup .bashrc if 82 | you've already done so. 83 | 84 | 3. Close and reopen your shell or `source ~/.bashrc`. 85 | 86 | # General Usage 87 | 88 | ### Add an account 89 | 90 | If the environment already contains AWS variables, these will be 91 | presented as defaults. 92 | 93 | $ aem add 94 | Creating a new AWS account... 95 | Short name: staging 96 | Description: Staging account 97 | AWS Region [us-east-1]: us-east-1 98 | Access key [12346]: 123 *from AWS credentials* 99 | Secret key [secret123456]: 455 *from AWS credentials* 100 | AWS ID: aws_account 101 | 102 | Note: if your shell can't find the `aem` command it is most likely because you haven't successfully sourced `.awsam/bash.rc` in the install steps. 103 | 104 | ### Select the active account 105 | 106 | This will update the current environment with the appropriate AWS 107 | environment variables. 108 | 109 | $ aem use staging 110 | 111 | When selecting an account you can mark it as the default account with 112 | the `--default` option: 113 | 114 | $ aem use --default staging 115 | 116 | ### List accounts 117 | 118 | The active account will be marked with an arrow. The default, if set, 119 | will be marked with an asterisk. 120 | 121 | $ aem list 122 | 123 | AWS Accounts: 124 | 125 | prod [Librato Production] [1 key: my-prod-key] 126 | => staging [Staging account] 127 | *dev [Librato Development] [1 key: devel-key] 128 | 129 | 130 | ### Import a key pair 131 | 132 | Add a key to the default account, or the specified account. Defaults 133 | chosen from current environment if set. IMPORTANT: `my-key-name` must 134 | match the logical name of the AWS EC2 keypair. 135 | 136 | $ aem key add my-key-name /path/to/my-keypair.pem 137 | Imported key pair my-key-name for account staging [Staging account] 138 | 139 | _The keypair *must* match the name of the keypair in AWS_ 140 | 141 | ### Select a key 142 | 143 | This will select an SSH keypair to use from your current account and 144 | set the environment variables `AMAZON_SSH_KEY_NAME` and 145 | `AMAZON_SSH_KEY_FILE` appropriately. It will also highlight the key in 146 | the list output with the '>' character. 147 | 148 | $ aem key use my-key-name 149 | 150 | $ aem list 151 | 152 | AWS Accounts: 153 | 154 | staging [Staging account] 155 | => dev [Librato Development] [1 key: >my-key-name] 156 | 157 | You can also define a default key for each account that will 158 | automatically be selected when the account is chosen. Just use the 159 | `--default` option when selecting a key to set a default key. Picking 160 | a default will place an asterisk next to the key name in the `aem 161 | list` output. 162 | 163 | $ aem key use --default my-key-name 164 | 165 | ### aenv utility: wrap command execution with AWS environment 166 | 167 | The `aenv` utility will wrap execution of any command with the AWS 168 | environment variables matching the currently selected account. This 169 | allows you to securely propagate environment variables only to 170 | commands that should have access to the current environment. Just 171 | prefix your command execution with `aenv` like: 172 | 173 | $ aenv aws s3 ls 174 | 175 | ### assh utility: SSH by instance ID 176 | 177 | Instance IDs will be looked up using the current account details. If 178 | the instance's keypair name exists, that keyfile will be used as the 179 | identity file to ssh. 180 | 181 | Usage: 182 | 183 | $ assh [user@] 184 | 185 | Example: 186 | 187 | $ assh ubuntu@i-123456 188 | warning: peer certificate won't be verified in this SSL session 189 | Loging in as ubuntu to ec2-1.2.3.4.compute-1.amazonaws.com 190 | 191 | ... 192 | 193 | ubuntu@host:~$ 194 | 195 | ### assh utility: SSH by tag name 196 | 197 | Instances will be looked up by their tag name. This tag name can be found assigned to the "value" key when you run ec2-describe-tags, using the AWS CLI Tools. 198 | 199 | Usage: 200 | 201 | $ assh [user@] 202 | 203 | Example: 204 | 205 | $ assh ubuntu@web-node-01 206 | warning: peer certificate won't be verified in this SSL session 207 | Loging in as ubuntu to ec2-1.2.3.4.compute-1.amazonaws.com 208 | 209 | ... 210 | 211 | ubuntu@web-node-01:~$ 212 | 213 | If you use assh with a substringed tag name which matches against several nodes, you will have the option to choose a specific node. For example, let's say you have 3 nginx nodes all running the same code and your nodes are named: 214 | 215 | web-node-01, web-node-02, web-node-03 216 | 217 | Then you run the following from within your terminal: 218 | 219 | Usage: 220 | 221 | $ assh ubuntu@web-node- 222 | Please select which node you wish to use: 223 | 0) web-node-01 (i-43dfed45) 224 | 1) web-node-02 (i-789eft24) 225 | 2) web-node-03 (i-546fer56) 226 | > 1 227 | 228 | You'll notice that you're given a list of the nodes in your account that match the "web-node-*" pattern. The instance ID associated with each node is appended to each option as well. You will then be given a prompt (>) where you enter the index of the node you want to connect to. 229 | 230 | Finally, if you use assh with a substringed tag name using the -f option, you can pass the base substring of a cluster of common nodes to connect to an **arbitrary** node within that cluster. The -f option assumes you have 'n' number of machines using a shared base name, all running mirrored environments. Once again, we will use the web-node-[01,02,03] scenario from our previous example: 231 | 232 | Usage: 233 | 234 | $ assh -f [user@]web-node- 235 | 236 | In this example, you would automatically connect to one of the machines in your account which matches the "web-node-*" pattern without having to explicitly choose a node. 237 | 238 | #### assh utility: questions/help? 239 | Run the following from your terminal: 240 | 241 | $ assh --help 242 | 243 | or: 244 | 245 | $ assh -h 246 | 247 | ### ascp utility: SCP by instance ID 248 | 249 | Instance IDs will be looked up using the current account details. If 250 | the instance's keypair name exists, that keyfile will be used as the 251 | identity file to scp. 252 | 253 | Usage: 254 | 255 | $ ascp [user@]:remote-file local-file 256 | $ ascp local-file [user@]:remote-file 257 | 258 | ### Default user 259 | 260 | You can specify a default user to *assh* by setting 261 | `AWS_DEFAULT_USER`: 262 | 263 | ``` 264 | $ AWS_DEFAULT_USER=ubuntu assh datanode 265 | Please select which node you wish to use: 266 | 267 | 0) metrics_facing-stg-v2-datanode-11 (i-30XXXXX, m1.large, 2014-02-12T20:46:29.000Z) 268 | 1) metrics_facing-stg-v2-datanode-12 (i-91XXXXX, m1.large, 2014-02-13T04:20:32.000Z) 269 | 2) metrics_facing-stg-v2-datanode-13 (i-64XXXXX, m1.large, 2014-03-04T18:59:26.000Z) 270 | q) Quit 271 | 272 | > 2 273 | 274 | Logging in as ubuntu to ec2-XXXX.compute-1.amazonaws.com 275 | ``` 276 | 277 | ### Remove a key 278 | 279 | You can remove ah SSH key from an account (defaults to the current 280 | account). 281 | 282 | $ aem key remove --acct prod my-prod-key 283 | 284 | ### Remove an account 285 | 286 | You can remove an account as long as it is not the active one. 287 | 288 | $ aem remove staging 289 | 290 | ## Contributing to awsam 291 | 292 | * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet 293 | * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it 294 | * Fork the project 295 | * Start a feature/bugfix branch 296 | * Commit and push until you are happy with your contribution 297 | * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally. 298 | * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it. 299 | 300 | ### TODO List 301 | 302 | assh utility: 303 | 304 | * ssh to a tag name (multiple?) 305 | * caches instance id => hostname for fast lookup 306 | * determines user? 307 | * supports complete SSH CLI options 308 | * inline commands, eg: `ssh user@instance sudo tail /var/log/messages` 309 | 310 | ## Copyright 311 | 312 | Copyright (c) 2011 Mike Heffner. See LICENSE.txt for 313 | further details. 314 | 315 | --------------------------------------------------------------------------------