├── .github └── workflows │ └── main.yml ├── .gitignore ├── .rspec ├── ChangeLog.md ├── Gemfile ├── Guardfile ├── MIT-LICENSE ├── README.md ├── Rakefile ├── bash └── ec2ssh.bash ├── bin └── ec2ssh ├── ec2ssh.gemspec ├── example └── example.ec2ssh ├── fixtures └── vcr_cassettes │ └── ec2-instances.yml ├── lib ├── ec2ssh.rb └── ec2ssh │ ├── builder.rb │ ├── cli.rb │ ├── command.rb │ ├── command │ ├── init.rb │ ├── remove.rb │ └── update.rb │ ├── dsl.rb │ ├── ec2_instances.rb │ ├── exceptions.rb │ ├── ssh_config.rb │ └── version.rb ├── spec ├── aws_sdk_compatibility_spec.rb ├── lib │ └── ec2ssh │ │ ├── builder_spec.rb │ │ ├── command │ │ ├── init_spec.rb │ │ ├── remove_spec.rb │ │ └── update_spec.rb │ │ ├── dsl_spec.rb │ │ ├── ec2_instances_spec.rb │ │ └── ssh_config_spec.rb └── spec_helper.rb └── zsh └── _ec2ssh /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Ruby 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | pull_request: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | name: Ruby ${{ matrix.ruby }} 14 | strategy: 15 | matrix: 16 | ruby: 17 | - '2.6' 18 | - '2.7' 19 | - '3.0' 20 | - '3.1' 21 | - 'head' 22 | 23 | steps: 24 | - uses: actions/checkout@v2 25 | - name: Set up Ruby 26 | uses: ruby/setup-ruby@v1 27 | with: 28 | ruby-version: ${{ matrix.ruby }} 29 | bundler-cache: true 30 | - name: Run the default task 31 | run: bundle exec rake 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --order rand 3 | -------------------------------------------------------------------------------- /ChangeLog.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 5.1.0 4 | * Drop support outdated Ruby 2.4 and 2.5 (#60, #61) 5 | * CI against for Ruby 3.0 and 3.1 (#61) 6 | * Fix arguments warnings of ERB.new on Ruby 3.1 (#61) 7 | * Migrate CI from TravisCI to GitHub Actions (#62) 8 | 9 | ## 5.0.0 10 | * Remove `--aws-key` option and add `--path` option in shellcomp (#56) 11 | * Use aws-sdk v3 and stop using v2 (#54) 12 | * Delete `rubyforge_project=` in gemspec (#51) 13 | * Relax thor and highline versions (#49) 14 | * CI against Ruby 2.5, 2.6 and 2.7 (#45, #55) 15 | * Drop support outdated Ruby 2.2 and 2.3 (#59) 16 | 17 | ## 4.0.0 18 | * Use aws-sdk v2 and stop using v1 (#44) 19 | * Support AssumeRole with `~/.aws/credentials` (#44) 20 | * `aws_keys` requires region (#44) 21 | Thanks to @yujideveloper 22 | * Support `filters` for listing ec2 instances (#43) 23 | Thanks to @satotakumi 24 | 25 | ## 3.1.1 26 | * Fix a bug in `--verbose` option (#41) 27 | Thanks to @adamlazz 28 | 29 | ## 3.1.0 30 | * Use credentials from `~/.aws/credentials` as default. Credential profiles are set as `profiles` in dotfile. 31 | * Revive path option for changing ssh config path (#34) 32 | Thanks to @cynipe 33 | 34 | ## 3.0.3 35 | * Use "%-" for ERB's trim\_mode at `host\_line` in dotfile (#29) 36 | * Add 'shellcomp' command: loading completion functions easily in bash/zsh (#27) 37 | Thanks to @hayamiz 38 | 39 | ## 3.0.2 40 | * Add zsh completion file (#26) 41 | Thanks to @hayamiz 42 | 43 | ## 3.0.1 44 | * Ignore unnamed instances as default (#22, #24, #25) 45 | Thanks to @r7kamura and @kainoku 46 | 47 | ## 3.0.0 48 | * Dotfile (.ec2ssh) format has been changed from YAML to Ruby DSL. 49 | * Refactored 50 | 51 | ## 2.0.7 52 | 53 | * Add ssh_options (#11) 54 | Thanks to @frsyuki 55 | 56 | ## 2.0.6 57 | 58 | * Change thor version specifier from 0.14.6 to 0.14 (#13) 59 | Thanks to @memerelics 60 | 61 | ## 2.0.5 62 | 63 | * Updated README.md along with fixing a bug at version 2.0.4 (#9) 64 | 65 | ## 2.0.4 66 | 67 | * Store multiple hosts info per `--aws_key` (#8) 68 | 69 | ## 2.0.3 70 | 71 | * Fix bug: Fix undefined method `empty?` when aws keys not set 72 | Thanks to @2get #6 73 | 74 | ## 2.0.2 75 | 76 | * Fix bug: Raises nil.empty? exception on ec2ssh update 77 | if there're ec2 instances which have empty Name tag. 78 | Thanks to @chiastolite #4 79 | 80 | ## 2.0.1 81 | 82 | * Fix bugs around initializing dotfile. 83 | 84 | ## 2.0.0 85 | 86 | * Add dotfile (.ec2ssh); supports multiple aws keys with `$ ec2ssh update --aws-key keyname` option. 87 | Thanks to @kentaro #3 88 | 89 | * Replace a gem `ktheory-right_aws` with `aws-sdk`. 90 | 91 | * Write tests. 92 | 93 | ## 1.0.3 94 | 95 | * Surpress thor warnings. Thanks to @mururu #1, @sanemat #2 96 | 97 | ## 1.0.2 98 | 99 | * Fix bug: Blank HostName is created if there're some "stopped" instances. 100 | 101 | ## 1.0.1 102 | 103 | * First Release. 104 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec 3 | 4 | gem 'rake', '>= 12.0.0' 5 | gem 'rspec', '~> 3.0' 6 | gem 'rspec-its', '~> 1.0' 7 | gem 'guard-rspec', '~> 4.3' 8 | gem 'webmock', '~> 3.14' 9 | gem 'rb-fsevent', '~> 0.10' 10 | gem 'timecop', '~> 0.9' 11 | gem 'fakefs', '~> 1.4', require: 'fakefs/safe' 12 | gem 'vcr', '~> 6.1' 13 | 14 | gem 'rexml' if RUBY_VERSION >= '3.0.0' 15 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | guard :rspec, cmd: 'bundle exec rspec' do 2 | watch(%r{^spec/.+_spec\.rb$}) 3 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } 4 | watch('spec/spec_helper.rb') { "spec" } 5 | end 6 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Issei Naruta 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Gem Version](https://badge.fury.io/rb/ec2ssh.svg)](https://badge.fury.io/rb/ec2ssh) 2 | [![Build Status](https://github.com/mirakui/ec2ssh/actions/workflows/main.yml/badge.svg)](https://github.com/mirakui/ec2ssh/actions/workflows/main.yml) 3 | 4 | # Introduction 5 | ec2ssh is a ssh_config manager for Amazon EC2. 6 | 7 | `ec2ssh` command adds `Host` descriptions to ssh_config (~/.ssh/config default). 'Name' tag of instances are used as `Host` descriptions. 8 | 9 | # How to use 10 | ### 1. Set 'Name' tag to your instances 11 | eg. Tag 'app-server-1' as 'Name' to an instance i-xxxxx in us-west-1 region. 12 | 13 | ### 2. Write ~/.aws/credentials 14 | ``` 15 | # ~/.aws/credentials 16 | 17 | [default] 18 | aws_access_key_id=... 19 | aws_secret_access_key=... 20 | 21 | [myprofile] 22 | aws_access_key_id=... 23 | aws_secret_access_key=... 24 | ``` 25 | 26 | If you need more details about `~/.aws/credentials`, check [A New and Standardized Way to Manage Credentials in the AWS SDKs](http://blogs.aws.amazon.com/security/post/Tx3D6U6WSFGOK2H/A-New-and-Standardized-Way-to-Manage-Credentials-in-the-AWS-SDKs) 27 | 28 | ### 3. Install ec2ssh 29 | 30 | ``` 31 | $ gem install ec2ssh 32 | ``` 33 | 34 | ### 4. Execute `ec2ssh init` 35 | 36 | ``` 37 | $ ec2ssh init 38 | ``` 39 | 40 | ### 5. Edit `.ec2ssh` 41 | 42 | ``` 43 | $ vi ~/.ec2ssh 44 | --- 45 | profiles 'default', 'myprofile', ... 46 | regions 'us-east-1', 'ap-northeast-1', ... 47 | 48 | # Ignore unnamed instances 49 | reject {|instance| !instance.tag('Name') } 50 | 51 | # You can specify filters on DescribeInstances (default: lists 'running' instances only) 52 | filters([ 53 | { name: 'instance-state-name', values: ['running', 'stopped'] } 54 | ]) 55 | 56 | # You can use methods of AWS::EC2::Instance and tag(key) method. 57 | # See https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/EC2/Instance.html 58 | host_line <.<%= placement.availability_zone %> 60 | HostName <%= public_dns_name || private_ip_address %> 61 | END 62 | ``` 63 | 64 | ### 6. Execute `ec2ssh update` 65 | 66 | ``` 67 | $ ec2ssh update 68 | ``` 69 | Then host-names of your instances are generated and wrote to .ssh/config 70 | 71 | ### 7. And you can ssh to your instances with your tagged name. 72 | 73 | ``` 74 | $ ssh app-server-1.us-east-1a 75 | ``` 76 | 77 | # Commands 78 | ``` 79 | $ ec2ssh help [TASK] # Describe available tasks or one specific task 80 | $ ec2ssh init # Add ec2ssh mark to ssh_config 81 | $ ec2ssh update # Update ec2 hosts list in ssh_config 82 | $ ec2ssh remove # Remove ec2ssh mark from ssh_config 83 | ``` 84 | 85 | ## Options 86 | ### --dotfile 87 | Each command can use `--dotfile` option to set dotfile (.ec2ssh) path. `~/.ec2ssh` is default. 88 | 89 | ``` 90 | $ ec2ssh init --dotfile /path/to/ssh_config 91 | ``` 92 | 93 | # ssh_config and mark lines 94 | `ec2ssh init` command inserts mark lines your `.ssh/config` such as: 95 | 96 | ``` 97 | ### EC2SSH BEGIN ### 98 | # Generated by ec2ssh http://github.com/mirakui/ec2ssh 99 | # DO NOT edit this block! 100 | # Updated Sun Dec 05 00:00:14 +0900 2010 101 | ### EC2SSH END ### 102 | ``` 103 | 104 | `ec2ssh update` command inserts 'Host' descriptions between 'BEGIN' line and 'END' line. 105 | 106 | ``` 107 | ### EC2SSH BEGIN ### 108 | # Generated by ec2ssh http://github.com/mirakui/ec2ssh 109 | # DO NOT edit this block! 110 | # Updated Sun Dec 05 00:00:14 +0900 2010 111 | 112 | # section: default 113 | Host app-server-1.us-west-1 114 | HostName ec2-xxx-xxx-xxx-xxx.us-west-1.compute.amazonaws.com 115 | Host db-server-1.ap-southeast-1 116 | HostName ec2-xxx-xxx-xxx-xxx.ap-southeast-1.compute.amazonaws.com 117 | : 118 | : 119 | ### EC2SSH END ### 120 | ``` 121 | 122 | `ec2ssh remove` command removes the mark lines. 123 | 124 | # How to upgrade from 3.x 125 | Dotfile (`.ec2ssh`) format has been changed from 3.x. 126 | 127 | * A instance tag access I/F has been changed from `tags['Name']` to `tag('Name')` 128 | * `Aws::EC2::Instance` methods have been changed to AWS SDK v3 129 | * The `aws_keys` structure has been changed 130 | * `aws_keys[profile_name][region] # => Aws::Credentials` 131 | * For example: 132 | 133 | ``` 134 | aws_keys( 135 | my_prof1: { 136 | 'ap-northeast-1' => Aws::Credentials.new(ENV['AWS_ACCESS_KEY_ID'], ENV['AWS_SECRET_ACCESS_KEY']) 137 | } 138 | ) 139 | ``` 140 | 141 | # Notice 142 | `ec2ssh` command updates your `.ssh/config` file default. You should make a backup of it. 143 | 144 | # Zsh completion support 145 | Use `zsh/_ec2ssh`. 146 | 147 | # License 148 | Copyright (c) 2022 Issei Naruta. ec2ssh is released under the MIT license. 149 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | RSpec::Core::RakeTask.new("spec") 4 | task :default => :spec 5 | -------------------------------------------------------------------------------- /bash/ec2ssh.bash: -------------------------------------------------------------------------------- 1 | # bash completion support for ec2ssh 2 | 3 | _ec2ssh() { 4 | local cmd cur prev subcmd 5 | cmd=$1 6 | cur=$2 7 | prev=$3 8 | 9 | subcmds="help init remove update version" 10 | common_opts="--path --dotfile --verbose" 11 | 12 | # contextual completion 13 | case $prev in 14 | ec2ssh) 15 | case "$cur" in 16 | -*) 17 | COMPREPLY=( $(compgen -W "$common_opts" $cur) ) 18 | ;; 19 | *) 20 | COMPREPLY=( $(compgen -W "$subcmds" $cur) ) 21 | esac 22 | return 0 23 | ;; 24 | --path | --dotfile) 25 | COMPREPLY=( $(compgen -o default -- "$cur")) 26 | return 0; 27 | ;; 28 | esac 29 | 30 | # complete options 31 | subcmd=${COMP_WORDS[1]} 32 | 33 | case $subcmd in 34 | help) 35 | COMPREPLY=( $(compgen -W "$subcmds" $cur) ) 36 | ;; 37 | *) 38 | COMPREPLY=( $(compgen -W "$common_opts" -- "$cur") ) 39 | ;; 40 | esac 41 | 42 | return 0 43 | 44 | } 45 | 46 | complete -F _ec2ssh ec2ssh 47 | -------------------------------------------------------------------------------- /bin/ec2ssh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $: << File.expand_path('../../lib', __FILE__) 3 | require 'ec2ssh/cli' 4 | Ec2ssh::CLI.start 5 | -------------------------------------------------------------------------------- /ec2ssh.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "ec2ssh/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "ec2ssh" 7 | s.version = Ec2ssh::VERSION 8 | s.platform = Gem::Platform::RUBY 9 | s.authors = ["Issei Naruta"] 10 | s.email = ["mimitako@gmail.com"] 11 | s.homepage = "http://github.com/mirakui/ec2ssh" 12 | s.license = "MIT" 13 | s.summary = %q{A ssh_config manager for AWS EC2} 14 | s.description = %q{ec2ssh is a ssh_config manager for AWS EC2} 15 | s.required_ruby_version = ">= 2.6.0" 16 | 17 | s.add_dependency "thor", ">= 1.2", "< 2.0" 18 | s.add_dependency "highline", ">= 1.6", "< 3.0" 19 | s.add_dependency "aws-sdk-core", "~> 3" 20 | s.add_dependency "aws-sdk-ec2", "~> 1" 21 | 22 | s.files = `git ls-files`.split("\n") 23 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 24 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 25 | s.require_paths = ["lib"] 26 | end 27 | -------------------------------------------------------------------------------- /example/example.ec2ssh: -------------------------------------------------------------------------------- 1 | path "#{ENV['HOME']}/.ssh/config" 2 | profiles 'default', 'myprofile' 3 | regions 'ap-northeast-1', 'us-east-1' 4 | reject {|instance| instance.tag('Name') =~ /.../ } 5 | 6 | host_line < 8 | HostName <%= private_ip_address %> 9 | END 10 | -------------------------------------------------------------------------------- /fixtures/vcr_cassettes/ec2-instances.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: https://ec2.us-west-1.amazonaws.com/ 6 | body: 7 | encoding: UTF-8 8 | string: Action=DescribeInstances&Filter.1.Name=instance-state-name&Filter.1.Value.1=running&Timestamp=2017-02-11T09%3A25%3A44Z&Version=2014-10-01 9 | headers: 10 | Content-Type: 11 | - application/x-www-form-urlencoded; charset=utf-8 12 | Accept-Encoding: 13 | - '' 14 | Content-Length: 15 | - '137' 16 | User-Agent: 17 | - aws-sdk-ruby/1.66.0 ruby/2.3.3 x86_64-darwin15 memoizing 18 | Host: 19 | - ec2.us-west-1.amazonaws.com 20 | X-Amz-Date: 21 | - 20170211T092544Z 22 | X-Amz-Content-Sha256: 23 | - b522eeb0f435881e058aaba87cf05aad80856e36205835b070f61e3a2cea2f66 24 | Accept: 25 | - "*/*" 26 | response: 27 | status: 28 | code: 200 29 | message: OK 30 | headers: 31 | Content-Type: 32 | - text/xml;charset=UTF-8 33 | Transfer-Encoding: 34 | - chunked 35 | Vary: 36 | - Accept-Encoding 37 | Date: 38 | - Sat, 11 Feb 2017 09:25:44 GMT 39 | Server: 40 | - AmazonEC2 41 | body: 42 | encoding: UTF-8 43 | string: |- 44 | 45 | 46 | 9b40910f-51b7-44e2-b75b-96078a730000 47 | 48 | 49 | r-058c6185fab780000 50 | 000000000000 51 | 52 | 53 | 54 | i-09e547eca106b0000 55 | ami-165a0000 56 | 57 | 16 58 | running 59 | 60 | ip-172-31-9-193.us-west-1.compute.internal 61 | ec2-54-215-142-250.us-west-1.compute.amazonaws.com 62 | 63 | aws.pem 64 | 0 65 | 66 | t2.micro 67 | 2017-02-11T09:22:47.000Z 68 | 69 | us-west-1c 70 | 71 | default 72 | 73 | 74 | disabled 75 | 76 | subnet-b02b0000 77 | vpc-f8790000 78 | 172.31.9.193 79 | 54.215.142.250 80 | true 81 | 82 | 83 | sg-a83f0000 84 | default 85 | 86 | 87 | sg-4db20000 88 | ssh 89 | 90 | 91 | x86_64 92 | ebs 93 | /dev/xvda 94 | 95 | 96 | /dev/xvda 97 | 98 | vol-02a0f01045aadbf7f 99 | attached 100 | 2017-02-11T09:22:48.000Z 101 | true 102 | 103 | 104 | 105 | hvm 106 | XDpiz1486804967277 107 | 108 | 109 | Role 110 | ec2ssh-test 111 | 112 | 113 | Name 114 | ec2ssh-test-02 115 | 116 | 117 | xen 118 | 119 | 120 | eni-f79a7ff6 121 | subnet-b02b2ef6 122 | vpc-f879939d 123 | 124 | 000000000000 125 | in-use 126 | 06:05:50:b3:b3:d8 127 | 172.31.9.193 128 | ip-172-31-9-193.us-west-1.compute.internal 129 | true 130 | 131 | 132 | sg-a83f0000 133 | default 134 | 135 | 136 | sg-4db20000 137 | ssh 138 | 139 | 140 | 141 | eni-attach-e6ce6187 142 | 0 143 | attached 144 | 2017-02-11T09:22:47.000Z 145 | true 146 | 147 | 148 | 54.215.142.250 149 | ec2-54-215-142-250.us-west-1.compute.amazonaws.com 150 | amazon 151 | 152 | 153 | 154 | 172.31.9.193 155 | ip-172-31-9-193.us-west-1.compute.internal 156 | true 157 | 158 | 54.215.142.250 159 | ec2-54-215-142-250.us-west-1.compute.amazonaws.com 160 | amazon 161 | 162 | 163 | 164 | 165 | 166 | 167 | arn:aws:iam::000000000000:instance-profile/ec2ssh-test-role 168 | AIPAJF7H4IAXZXXXXXXXX 169 | 170 | false 171 | 172 | 173 | 174 | 175 | 176 | http_version: 177 | recorded_at: Sat, 11 Feb 2017 09:25:45 GMT 178 | recorded_with: VCR 3.0.3 179 | -------------------------------------------------------------------------------- /lib/ec2ssh.rb: -------------------------------------------------------------------------------- 1 | module Ec2ssh 2 | end 3 | -------------------------------------------------------------------------------- /lib/ec2ssh/builder.rb: -------------------------------------------------------------------------------- 1 | require 'ec2ssh/ec2_instances' 2 | require 'erb' 3 | require 'stringio' 4 | 5 | module Ec2ssh 6 | class Builder 7 | def initialize(container) 8 | @container = container 9 | @host_lines_erb = ERB.new @container.host_line, trim_mode: '%-' 10 | end 11 | 12 | def build_host_lines 13 | out = StringIO.new 14 | aws_keys.each do |name, key| 15 | out.puts "# section: #{name}" 16 | ec2s.instances(name).each do |instance| 17 | bind = instance.instance_eval { binding } 18 | next if @container.reject && @container.reject.call(instance) 19 | line = @host_lines_erb.result(bind).rstrip 20 | out.puts line unless line.empty? 21 | end 22 | end 23 | out.string.rstrip 24 | end 25 | 26 | def ec2s 27 | @ec2s ||= Ec2Instances.new aws_keys, filters 28 | end 29 | 30 | def aws_keys 31 | @aws_keys ||= if @container.profiles 32 | keys = {} 33 | @container.profiles.each do |profile_name| 34 | keys[profile_name] = {} 35 | @container.regions.each do |region| 36 | keys[profile_name][region] = Ec2Instances.expand_profile_name_to_credential profile_name, region 37 | end 38 | end 39 | keys 40 | else 41 | @container.aws_keys 42 | end 43 | end 44 | 45 | def filters 46 | @filters = @container.filters || [{ 47 | name: 'instance-state-name', 48 | values: ['running'] 49 | }] 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/ec2ssh/cli.rb: -------------------------------------------------------------------------------- 1 | require 'thor' 2 | require 'highline' 3 | require 'ec2ssh/ssh_config' 4 | require 'ec2ssh/exceptions' 5 | 6 | module Ec2ssh 7 | class CLI < Thor 8 | class_option :path, banner: "/path/to/ssh_config" 9 | class_option :dotfile, banner: '$HOME/.ec2ssh', default: "#{ENV['HOME']}/.ec2ssh" 10 | class_option :verbose, banner: 'enable debug log', type: 'boolean' 11 | 12 | desc 'init', 'Add ec2ssh mark to ssh_config' 13 | def init 14 | command = make_command :init 15 | command.run 16 | rescue MarkAlreadyExists 17 | red "Marker already exists in #{command.ssh_config_path}" 18 | end 19 | 20 | desc 'update', 'Update ec2 hosts list in ssh_config' 21 | def update 22 | check_dotfile_existence 23 | set_aws_logging 24 | command = make_command :update 25 | command.run 26 | green "Updated #{command.ssh_config_path}" 27 | rescue AwsKeyNotFound 28 | red "Set aws keys at #{command.dotfile_path}" 29 | rescue MarkNotFound 30 | red "Marker not found in #{command.ssh_config_path}" 31 | red "Execute '#{$0} init' first!" 32 | end 33 | 34 | desc 'remove', 'Remove ec2ssh mark from ssh_config' 35 | def remove 36 | check_dotfile_existence 37 | command = make_command :remove 38 | command.run 39 | green "Removed mark from #{command.ssh_config_path}" 40 | rescue MarkNotFound 41 | red "Marker not found in #{command.ssh_config_path}" 42 | end 43 | 44 | desc 'shellcomp [-]', 'Initialize shell completion for bash/zsh' 45 | def shellcomp(_ = false) 46 | if args.include?("-") 47 | print_rc = true 48 | else 49 | print_rc = false 50 | end 51 | 52 | # print instructions for automatically enabling shell completion 53 | unless print_rc 54 | puts </dev/null 2>&1 && eval "$(ec2ssh shellcomp -)" 59 | 60 | EOS 61 | exit(false) 62 | end 63 | 64 | # print shell script for enabling shell completion 65 | zsh_comp_file = File.expand_path("../../../zsh/_ec2ssh", __FILE__) 66 | bash_comp_file = File.expand_path("../../../bash/ec2ssh.bash", __FILE__) 67 | puts <.<%= placement.availability_zone %> 45 | HostName <%= public_dns_name || private_ip_address %> 46 | END 47 | DOTFILE 48 | 49 | File.open(dotfile_path, 'w') {|f| f.write example } 50 | end 51 | 52 | def init_ssh_config 53 | if ssh_config.mark_exist? 54 | raise MarkAlreadyExists 55 | else 56 | ssh_config.append_mark! 57 | cli.green "Added mark to #{ssh_config_path}" 58 | end 59 | end 60 | 61 | def ssh_config 62 | @ssh_config ||= SshConfig.new(ssh_config_path) 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/ec2ssh/command/remove.rb: -------------------------------------------------------------------------------- 1 | require 'ec2ssh/exceptions' 2 | require 'ec2ssh/command' 3 | require 'ec2ssh/ssh_config' 4 | 5 | module Ec2ssh 6 | module Command 7 | class Remove < Base 8 | def initialize(cli) 9 | super 10 | end 11 | 12 | def run 13 | ssh_config = SshConfig.new(ssh_config_path) 14 | raise MarkNotFound unless ssh_config.mark_exist? 15 | 16 | ssh_config.replace! "" 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/ec2ssh/command/update.rb: -------------------------------------------------------------------------------- 1 | require 'ec2ssh/exceptions' 2 | require 'ec2ssh/command' 3 | require 'ec2ssh/ssh_config' 4 | require 'ec2ssh/builder' 5 | require 'ec2ssh/dsl' 6 | 7 | module Ec2ssh 8 | module Command 9 | class Update < Base 10 | def initialize(cli) 11 | super 12 | end 13 | 14 | def run 15 | ssh_config = SshConfig.new(ssh_config_path) 16 | raise MarkNotFound unless ssh_config.mark_exist? 17 | 18 | ssh_config.parse! 19 | lines = builder.build_host_lines 20 | ssh_config_str = ssh_config.wrap lines 21 | ssh_config.replace! ssh_config_str 22 | cli.yellow ssh_config_str 23 | end 24 | 25 | def builder 26 | @builder ||= Builder.new dsl 27 | end 28 | 29 | def dsl 30 | @dsl ||= Ec2ssh::Dsl::Parser.parse File.read(dotfile_path) 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/ec2ssh/dsl.rb: -------------------------------------------------------------------------------- 1 | require 'ec2ssh/exceptions' 2 | require 'aws-sdk-core' 3 | 4 | module Ec2ssh 5 | class Dsl 6 | attr_reader :_result 7 | 8 | CREDENTIAL_CLASSES = [Aws::Credentials, Aws::SharedCredentials, Aws::InstanceProfileCredentials, Aws::AssumeRoleCredentials].freeze 9 | private_constant :CREDENTIAL_CLASSES 10 | 11 | def initialize 12 | @_result = Container.new 13 | end 14 | 15 | def aws_keys(keys) 16 | unless keys.all? {|_, v| v.is_a?(Hash) && v.each_value.all? {|c| CREDENTIAL_CLASSES.any?(&c.method(:is_a?)) } } 17 | raise DotfileValidationError, <<-MSG 18 | Since v4.0, `aws_keys` in the dotfile must be specified regions as a hash key. 19 | See: https://github.com/mirakui/ec2ssh#how-to-upgrade-from-3x 20 | MSG 21 | end 22 | @_result.aws_keys = keys 23 | end 24 | 25 | def profiles(*profiles) 26 | @_result.profiles = profiles 27 | end 28 | 29 | def regions(*regions) 30 | @_result.regions = regions 31 | end 32 | 33 | def host_line(erb) 34 | @_result.host_line = erb 35 | end 36 | 37 | def reject(&block) 38 | @_result.reject = block 39 | end 40 | 41 | def filters(filters) 42 | @_result.filters = filters 43 | end 44 | 45 | def path(str) 46 | @_result.path = str 47 | end 48 | 49 | class Container < Struct.new(*%i[ 50 | aws_keys 51 | profiles 52 | regions 53 | host_line 54 | reject 55 | filters 56 | path 57 | ]) 58 | end 59 | 60 | module Parser 61 | def self.parse(dsl_str) 62 | dsl = Dsl.new 63 | dsl.instance_eval dsl_str 64 | dsl._result.tap {|result| validate result } 65 | rescue SyntaxError => e 66 | raise DotfileSyntaxError, e.to_s 67 | end 68 | 69 | def self.parse_file(path) 70 | raise DotfileNotFound, path.to_s unless File.exist?(path) 71 | parse File.read(path) 72 | end 73 | 74 | def self.validate(result) 75 | if result.aws_keys && result.profiles 76 | raise DotfileValidationError, "`aws_keys` and `profiles` doesn't work together in dotfile." 77 | end 78 | end 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/ec2ssh/ec2_instances.rb: -------------------------------------------------------------------------------- 1 | require 'aws-sdk-core' 2 | require 'aws-sdk-ec2' 3 | require 'ec2ssh/exceptions' 4 | 5 | module Ec2ssh 6 | class Ec2Instances 7 | attr_reader :ec2s, :aws_keys 8 | 9 | class InstanceWrapper 10 | class TagsWrapper 11 | def initialize(tags) 12 | @tags = tags 13 | end 14 | 15 | # simulate 16 | def [](key) 17 | if key.is_a? ::String 18 | raise DotfileValidationError, <<-MSG 19 | `tags[String]` syntax in the dotfile has been deleted since v4.0. Use `tag(String)` instead. 20 | See: https://github.com/mirakui/ec2ssh#how-to-upgrade-from-3x 21 | MSG 22 | end 23 | super 24 | end 25 | 26 | private 27 | 28 | def method_missing(name, *args, &block) 29 | @tags.public_send(name, *args, &block) 30 | end 31 | 32 | def respond_to_missing?(symbol, include_private) 33 | @tags.respond_to?(symbol, include_private) 34 | end 35 | end 36 | 37 | def initialize(ec2_instance) 38 | @ec2_instance = ec2_instance 39 | @_tags ||= @ec2_instance.tags.each_with_object({}) {|t, h| h[t.key] = t.value } 40 | end 41 | 42 | def tag(key) 43 | @_tags[key] 44 | end 45 | 46 | def tags 47 | TagsWrapper.new(super) 48 | end 49 | 50 | private 51 | 52 | def method_missing(name, *args, &block) 53 | @ec2_instance.public_send(name, *args, &block) 54 | end 55 | 56 | def respond_to_missing?(symbol, include_private) 57 | @ec2_instance.respond_to?(symbol, include_private) 58 | end 59 | end 60 | 61 | def initialize(aws_keys, filters) 62 | @aws_keys = aws_keys 63 | @filters = filters 64 | end 65 | 66 | def make_ec2s 67 | _ec2s = {} 68 | aws_keys.each_pair do |name, keys| 69 | _ec2s[name] = {} 70 | keys.each_pair do |region, key| 71 | client = Aws::EC2::Client.new region: region, credentials: key 72 | _ec2s[name][region] = Aws::EC2::Resource.new client: client 73 | end 74 | end 75 | _ec2s 76 | end 77 | 78 | def ec2s 79 | @ec2s ||= make_ec2s 80 | end 81 | 82 | def instances(key_name) 83 | aws_keys[key_name].each_key.map {|region| 84 | ec2s[key_name][region].instances( 85 | filters: @filters 86 | ). 87 | map {|ins| InstanceWrapper.new(ins) }. 88 | sort_by {|ins| ins.tag('Name').to_s } 89 | }.flatten 90 | end 91 | 92 | def self.expand_profile_name_to_credential(profile_name, region) 93 | client = Aws::STS::Client.new(profile: profile_name, region: region) 94 | client.config.credentials 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /lib/ec2ssh/exceptions.rb: -------------------------------------------------------------------------------- 1 | module Ec2ssh 2 | class DotfileNotFound < StandardError; end 3 | class DotfileSyntaxError < StandardError; end 4 | class DotfileValidationError < StandardError; end 5 | class ObsoleteDotfile < StandardError; end 6 | class InvalidDotfile < StandardError; end 7 | class MarkNotFound < StandardError; end 8 | class MarkAlreadyExists < StandardError; end 9 | class AwsKeyNotFound < StandardError; end 10 | end 11 | -------------------------------------------------------------------------------- /lib/ec2ssh/ssh_config.rb: -------------------------------------------------------------------------------- 1 | require 'time' 2 | require 'pathname' 3 | 4 | module Ec2ssh 5 | class SshConfig 6 | HEADER = "### EC2SSH BEGIN ###" 7 | FOOTER = "### EC2SSH END ###" 8 | 9 | attr_reader :path, :sections 10 | 11 | def initialize(path=nil) 12 | @path = path || "#{ENV['HOME']}/.ssh/config" 13 | @sections = {} 14 | end 15 | 16 | def parse! 17 | return unless mark_exist? 18 | ec2_config = config_src.match(/#{HEADER}\n(.*)#{FOOTER}/m).to_s 19 | 20 | current_section = 'default' 21 | @sections[current_section] = Section.new('default') 22 | 23 | ec2_config.split(/\n+/).each do |line| 24 | if line =~ /#{Section::HEADER} (.+)/ 25 | current_section = $1 26 | @sections[current_section] ||= Section.new(current_section) 27 | elsif line =~ /^#/ # ignore 28 | elsif line =~ /^$/ # ignore 29 | else 30 | @sections[current_section].append("#{line}\n") 31 | end 32 | end 33 | end 34 | 35 | def append_mark! 36 | replace! "" 37 | File.open(@path, "a") do |f| 38 | f.puts wrap("") 39 | end 40 | end 41 | 42 | def mark_exist? 43 | config_src =~ /#{HEADER}\n.*#{FOOTER}\n/m 44 | end 45 | 46 | def replace!(str) 47 | save! config_src.gsub(/#{HEADER}\n.*#{FOOTER}\n/m, str) 48 | end 49 | 50 | def config_src 51 | unless File.exist?(@path) 52 | File.open(@path, "w", 0600).close() 53 | end 54 | 55 | @config_src ||= File.open(@path, "r") do |f| 56 | f.read 57 | end 58 | end 59 | 60 | def save!(str) 61 | File.open(@path, "w") do |f| 62 | f.puts str 63 | end 64 | end 65 | 66 | def wrap(text) 67 | return <<-END 68 | #{HEADER} 69 | # Generated by ec2ssh http://github.com/mirakui/ec2ssh 70 | # DO NOT edit this block! 71 | # Updated #{Time.now.iso8601} 72 | #{text} 73 | #{FOOTER} 74 | END 75 | end 76 | 77 | class Section 78 | HEADER = "# section:" 79 | 80 | attr_accessor :name 81 | attr_reader :text 82 | 83 | def initialize(name, text = '') 84 | @name = name 85 | @text = text 86 | end 87 | 88 | def append(text) 89 | @text << text 90 | end 91 | 92 | def replace!(text) 93 | @text = text 94 | end 95 | 96 | def to_s 97 | if text.empty? 98 | "" 99 | else 100 | <<-EOS 101 | #{HEADER} #{@name} 102 | #{@text} 103 | EOS 104 | end 105 | end 106 | end 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /lib/ec2ssh/version.rb: -------------------------------------------------------------------------------- 1 | module Ec2ssh 2 | VERSION = '5.1.0' 3 | end 4 | -------------------------------------------------------------------------------- /spec/aws_sdk_compatibility_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'ec2ssh/ec2_instances' 3 | 4 | describe 'aws-sdk compatibility' do 5 | let(:region) { 'us-west-1' } 6 | let(:root_device) { '/dev/xvda' } 7 | 8 | let!(:ec2_instances) do 9 | VCR.use_cassette('ec2-instances') do 10 | Ec2ssh::Ec2Instances.new( 11 | {'foo' => {'us-west-1' => Aws::Credentials.new('access_key_id', 'secret_access_key')}}, 12 | [{ name: 'instance-state-name', values: ['running'] }] 13 | ).instances('foo') 14 | end 15 | end 16 | 17 | let(:ins) { ec2_instances.first } 18 | 19 | it { expect(ec2_instances.count).to be == 1 } 20 | 21 | it { expect(ins.tag('Name')).to match /.+/ } 22 | it { expect(ins.tag('Role')).to match /.+/ } 23 | it { expect(ins.tags).to match_array([have_attributes(key: 'Name', value: /.+/), have_attributes(key: 'Role', value: /.+/)]) } 24 | it { expect(ins.ami_launch_index).to be == 0 } 25 | it { expect(ins.architecture).to be == 'x86_64' } 26 | it do 27 | expect(ins.block_device_mappings).to match [ 28 | have_attributes( 29 | device_name: root_device, 30 | ebs: have_attributes( 31 | volume_id: /\Avol-\w+\z/, 32 | status: 'attached', 33 | attach_time: an_instance_of(Time), 34 | delete_on_termination: true 35 | ) 36 | )] 37 | end 38 | # it { expect(ins.capacity_reservation_id).to be_nil} 39 | # it { expect(ins.capacity_reservation_specification).to be_nil } 40 | it { expect(ins.classic_address).to be_a(Aws::EC2::ClassicAddress) } 41 | it { expect(ins.client).to be_a(Aws::EC2::Client) } 42 | it { expect(ins.client_token).to match /\A\w{18}\z/ } 43 | # it { expect(ins.cpu_options).to be_nil } 44 | it { expect(ins.ebs_optimized).to be_falsy } 45 | it { expect(ins.elastic_gpu_associations).to be_nil } 46 | # it { expect(ins.elastic_inference_accelerator_associations).to be_nil } 47 | it { expect(ins.ena_support).to be_falsy } 48 | # it { expect(ins.hibernation_options).to be_nil} 49 | it { expect(ins.hypervisor).to be == 'xen' } 50 | it { expect(ins.iam_instance_profile).to have_attributes(arn: /\Aarn:aws:iam::\d+:instance-profile\/[\w\-]+\z/, id: /\A\w{21}\z/) } 51 | it { expect(ins.id).to match /\Ai-\w+\z/ } 52 | it { expect(ins.image).to be_a(Aws::EC2::Image) } 53 | it { expect(ins.image_id).to match /\Aami-\w+\z/ } 54 | it { expect(ins.instance_id).to match /\Ai-\w+\z/ } 55 | it { expect(ins.instance_lifecycle).to be_nil } 56 | it { expect(ins.instance_type).to match /\A[trmci][1248]\.\w+\z/ } 57 | it { expect(ins.kernel_id).to be_nil } 58 | it { expect(ins.key_name).to match /\A.+\.pem\z/ } 59 | it { expect(ins.key_pair).to be_a(Aws::EC2::KeyPairInfo) } 60 | it { expect(ins.launch_time).to be_a(Time) } 61 | # it { expect(ins.licenses).to all have_attributes(license_configuration_arn: '') } 62 | it { expect(ins.monitoring).to have_attributes(state: 'disabled') } 63 | it { expect(ins.network_interfaces).to all match(an_instance_of(Aws::EC2::NetworkInterface)) } 64 | it { expect(ins.placement).to have_attributes(availability_zone: /\A#{region}[a-c]\z/, group_name: '', tenancy: 'default') } 65 | it { expect(ins.placement_group).to be_a(Aws::EC2::PlacementGroup) } 66 | it { expect(ins.platform).to be_nil } 67 | it { expect(ins.private_dns_name).to match /\Aip-[\w\-]+\.#{region}\.compute\.internal\z/ } 68 | it { expect(ins.private_ip_address).to match /\A[\d\.]+\z/ } 69 | it { expect(ins.product_codes).to be == [] } 70 | it { expect(ins.public_dns_name).to match /\Aec2-[\w\-]+\.#{region}\.compute\.amazonaws\.com\z/ } 71 | it { expect(ins.public_ip_address).to match /\A[\d\.]+\z/ } 72 | it { expect(ins.ramdisk_id).to be_nil } 73 | it { expect(ins.root_device_name).to eq root_device } 74 | it { expect(ins.root_device_type).to be == 'ebs' } 75 | it do 76 | expect(ins.security_groups).to all have_attributes( 77 | group_id: /\Asg-\w+\z/, 78 | group_name: /\A.+\z/ 79 | ) 80 | end 81 | it { expect(ins.source_dest_check).to be true } 82 | it { expect(ins.spot_instance_request_id).to be_nil } 83 | it { expect(ins.sriov_net_support).to be_nil } 84 | it { expect(ins.state).to have_attributes(code: 16, name: 'running') } 85 | it { expect(ins.state_reason).to be_nil } 86 | it { expect(ins.state_transition_reason).to be == '' } 87 | it { expect(ins.subnet).to be_a(Aws::EC2::Subnet) } 88 | it { expect(ins.subnet_id).to match /\Asubnet-\w+\z/ } 89 | it { expect(ins.virtualization_type).to be == 'hvm' } 90 | it { expect(ins.vpc).to be_a(Aws::EC2::Vpc) } 91 | it { expect(ins.vpc_id).to match /\Avpc-\w+\z/ } 92 | end 93 | -------------------------------------------------------------------------------- /spec/lib/ec2ssh/builder_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'ec2ssh/dsl' 3 | require 'ec2ssh/builder' 4 | 5 | describe Ec2ssh::Builder do 6 | describe '#build_host_lines' do 7 | let(:container) do 8 | Ec2ssh::Dsl::Container.new.tap do |c| 9 | c.aws_keys = { 10 | 'key1' => { 'us-west-1' => Aws::Credentials.new('KEY1', 'SEC1') }, 11 | 'key2' => { 'us-west-1' => Aws::Credentials.new('KEY2', 'SEC2') } 12 | } 13 | c.host_line = "Host <%= tag('Name') %>" 14 | end 15 | end 16 | 17 | let(:builder) do 18 | Ec2ssh::Builder.new(container).tap do |bldr| 19 | allow(bldr).to receive(:ec2s) { ec2s } 20 | end 21 | end 22 | 23 | let(:ec2s) do 24 | double('ec2s', aws_keys: container.aws_keys).tap do |dbl| 25 | allow(dbl).to receive(:instances) {|name| instances[name] } 26 | end 27 | end 28 | 29 | let(:instances) do 30 | { 31 | 'key1' => [ 32 | double('instance').tap {|m| allow(m).to receive(:tag).with('Name').and_return('srv1') }, 33 | double('instance').tap {|m| allow(m).to receive(:tag).with('Name').and_return('srv2') } 34 | ], 35 | 'key2' => [ 36 | double('instance').tap {|m| allow(m).to receive(:tag).with('Name').and_return('srv3') }, 37 | double('instance').tap {|m| allow(m).to receive(:tag).with('Name').and_return('srv4') } 38 | ] 39 | } 40 | end 41 | 42 | it do 43 | expect(builder.build_host_lines).to eq <<-END.rstrip 44 | # section: key1 45 | Host srv1 46 | Host srv2 47 | # section: key2 48 | Host srv3 49 | Host srv4 50 | END 51 | end 52 | 53 | context 'with #reject' do 54 | before do 55 | container.reject = lambda {|ins| ins.tag('Name') == 'srv1' } 56 | end 57 | 58 | it do 59 | expect(builder.build_host_lines).to eq <<-END.rstrip 60 | # section: key1 61 | Host srv2 62 | # section: key2 63 | Host srv3 64 | Host srv4 65 | END 66 | end 67 | end 68 | 69 | context 'checking erb trim_mode' do 70 | before do 71 | container.host_line = <<-END 72 | % if tag('Name') 73 | <%- if tag('Name') == 'srv3' -%> 74 | Host <%= tag('Name') %> 75 | HostName <%= tag('Name') %> 76 | <%- end -%> 77 | % end 78 | END 79 | end 80 | 81 | it do 82 | expect(builder.build_host_lines).to eq <<-END.rstrip 83 | # section: key1 84 | # section: key2 85 | Host srv3 86 | HostName srv3 87 | END 88 | end 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /spec/lib/ec2ssh/command/init_spec.rb: -------------------------------------------------------------------------------- 1 | require 'ec2ssh/command/init' 2 | 3 | describe Ec2ssh::Command::Init do 4 | describe '#run' do 5 | let(:command) do 6 | described_class.new(cli).tap do |cmd| 7 | allow(cmd).to receive(:ssh_config_path).and_return('/path/to/ssh/config') 8 | allow(cmd).to receive(:dotfile_path).and_return('/path/to/dotfile') 9 | end 10 | end 11 | let(:cli) do 12 | double(:cli, red: nil, yellow: nil, green: nil) 13 | end 14 | let(:ssh_config) do 15 | double(:ssh_config, mark_exist?: nil, append_mark!: nil) 16 | end 17 | 18 | before do 19 | expect(ssh_config).to receive(:mark_exist?).and_return(mark_exist) 20 | allow(command).to receive(:ssh_config).and_return(ssh_config) 21 | end 22 | 23 | context 'when the marker already exists' do 24 | let(:mark_exist) { true } 25 | 26 | it do 27 | expect { command.run }.to raise_error(Ec2ssh::MarkAlreadyExists) 28 | expect(ssh_config).to_not have_received(:append_mark!) 29 | end 30 | end 31 | 32 | context 'when the marker does not exists' do 33 | let(:mark_exist) { false } 34 | 35 | it do 36 | expect { command.run }.not_to raise_error 37 | expect(ssh_config).to have_received(:append_mark!).once 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/lib/ec2ssh/command/remove_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'ec2ssh/command/remove' 3 | 4 | describe Ec2ssh::Command::Remove do 5 | include FakeFS::SpecHelpers 6 | 7 | describe '#run' do 8 | let(:command) do 9 | described_class.new(cli).tap do |cmd| 10 | allow(cmd).to receive(:options).and_return(options) 11 | end 12 | end 13 | let(:options) do 14 | double(:options, path: '/ssh_config', dotfile: '/dotfile', aws_key: 'default') 15 | end 16 | let(:cli) do 17 | double(:cli, options: options, red: nil, yellow: nil, green: nil) 18 | end 19 | 20 | let(:dotfile_str) { <<-END } 21 | path '/dotfile' 22 | profiles 'default' 23 | regions 'us-west-1' 24 | host_line < 26 | HostName <%= private_ip_address %> 27 | EOS 28 | END 29 | 30 | before do 31 | File.open('/ssh_config', 'w') {|f| f.write ssh_config_str } 32 | File.open('/dotfile', 'w') {|f| f.write dotfile_str } 33 | end 34 | 35 | context 'with unmarked ssh_config' do 36 | let(:ssh_config_str) { '' } 37 | 38 | it do 39 | expect { command.run }.to raise_error(Ec2ssh::MarkNotFound) 40 | end 41 | end 42 | 43 | context 'with marked ssh_config' do 44 | let(:ssh_config_str) { <<-END } 45 | # before lines... 46 | 47 | ### EC2SSH BEGIN ### 48 | ### EC2SSH END ### 49 | 50 | # after lines... 51 | END 52 | 53 | before do 54 | Timecop.freeze(Time.utc(2014,1,1)) do 55 | command.run 56 | end 57 | end 58 | 59 | it do 60 | expect(File.read('/ssh_config')).to eq(<<-END) 61 | # before lines... 62 | 63 | 64 | # after lines... 65 | END 66 | end 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /spec/lib/ec2ssh/command/update_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'ec2ssh/command/update' 3 | require 'ec2ssh/exceptions' 4 | 5 | describe Ec2ssh::Command::Update do 6 | include FakeFS::SpecHelpers 7 | 8 | describe '#run' do 9 | let(:command) do 10 | described_class.new(cli).tap do |cmd| 11 | allow(cmd).to receive(:options).and_return(options) 12 | allow(cmd.builder).to receive(:aws_keys) { aws_keys } 13 | allow(cmd.builder.ec2s).to receive(:instances) { instances } 14 | end 15 | end 16 | let(:options) do 17 | double(:options, path: '/ssh_config', dotfile: '/dotfile', aws_key: 'default') 18 | end 19 | let(:cli) do 20 | double(:cli, options: options, red: nil, yellow: nil, green: nil) 21 | end 22 | let(:aws_keys) do 23 | {'default' => {'us-west-1' => Aws::Credentials.new('access_key_id', 'secret_access_key')}} 24 | end 25 | let(:instances) do 26 | [ 27 | double('instance', private_ip_address: '10.0.0.1').tap {|m| allow(m).to receive(:tag).with('Name').and_return('srv1') }, 28 | double('instance', private_ip_address: '10.0.0.2').tap {|m| allow(m).to receive(:tag).with('Name').and_return('srv2') } 29 | ] 30 | end 31 | 32 | before do 33 | File.open('/ssh_config', 'w') {|f| f.write ssh_config_str } 34 | File.open('/dotfile', 'w') {|f| f.write dotfile_str } 35 | end 36 | 37 | context 'with unmarked ssh_config' do 38 | let(:ssh_config_str) { '' } 39 | let(:dotfile_str) { <<-END } 40 | path '/dotfile' 41 | profiles 'default' 42 | regions 'us-west-1' 43 | host_line < 45 | HostName <%= private_ip_address %> 46 | EOS 47 | END 48 | 49 | it do 50 | expect { command.run }.to raise_error(Ec2ssh::MarkNotFound) 51 | end 52 | end 53 | 54 | context 'with marked ssh_config' do 55 | let(:ssh_config_str) { <<-END } 56 | # before lines... 57 | 58 | ### EC2SSH BEGIN ### 59 | ### EC2SSH END ### 60 | 61 | # after lines... 62 | END 63 | 64 | let(:dotfile_str) { <<-END } 65 | path '/dotfile' 66 | profiles 'default' 67 | regions 'us-west-1' 68 | host_line < 70 | HostName <%= private_ip_address %> 71 | EOS 72 | END 73 | 74 | before do 75 | Timecop.freeze(Time.utc(2014,1,1)) do 76 | command.run 77 | end 78 | end 79 | 80 | it do 81 | expect(File.read('/ssh_config')).to eq(<<-END) 82 | # before lines... 83 | 84 | ### EC2SSH BEGIN ### 85 | # Generated by ec2ssh http://github.com/mirakui/ec2ssh 86 | # DO NOT edit this block! 87 | # Updated 2014-01-01T00:00:00Z 88 | # section: default 89 | Host srv1 90 | HostName 10.0.0.1 91 | Host srv2 92 | HostName 10.0.0.2 93 | ### EC2SSH END ### 94 | 95 | # after lines... 96 | END 97 | end 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /spec/lib/ec2ssh/dsl_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'ec2ssh/dsl' 3 | 4 | describe Ec2ssh::Dsl do 5 | context 'with profiles' do 6 | let(:dsl_str) do 7 | <<-END 8 | profiles 'default', 'myprofile' 9 | regions 'ap-northeast-1', 'us-east-1' 10 | host_line 'host lines' 11 | reject {|instance| instance } 12 | path 'path' 13 | END 14 | end 15 | 16 | subject(:result) { Ec2ssh::Dsl::Parser.parse dsl_str } 17 | 18 | its(:profiles) { should == ['default', 'myprofile'] } 19 | its(:aws_keys) { should be_nil } 20 | its(:regions) { should == ['ap-northeast-1', 'us-east-1'] } 21 | its(:host_line) { should == 'host lines' } 22 | it { expect(result.reject.call(123)).to eq(123) } 23 | its(:path) { should == 'path' } 24 | end 25 | 26 | context 'with aws_keys' do 27 | let(:dsl_str) do 28 | <<-END 29 | aws_keys( 30 | 'key1' => { 'ap-northeast-1' => Aws::Credentials.new('ACCESS_KEY1', 'SECRET1') }, 31 | 'key2' => { 'us-east-1' => Aws::Credentials.new('ACCESS_KEY2', 'SECRET2') } 32 | ) 33 | host_line 'host lines' 34 | reject {|instance| instance } 35 | path 'path' 36 | END 37 | end 38 | 39 | subject(:result) { Ec2ssh::Dsl::Parser.parse dsl_str } 40 | 41 | its(:profiles) { should be_nil } 42 | it do 43 | expect(result.aws_keys).to match( 44 | 'key1' => { 'ap-northeast-1' => be_a(Aws::Credentials).and(have_attributes(access_key_id: 'ACCESS_KEY1', secret_access_key: 'SECRET1')) } , 45 | 'key2' => { 'us-east-1' => be_a(Aws::Credentials).and(have_attributes(access_key_id: 'ACCESS_KEY2', secret_access_key: 'SECRET2')) } 46 | ) 47 | end 48 | its(:host_line) { should == 'host lines' } 49 | it { expect(result.reject.call(123)).to eq(123) } 50 | its(:path) { should == 'path' } 51 | end 52 | 53 | context 'with profiles and aws_keys both' do 54 | let(:dsl_str) do 55 | <<-END 56 | aws_keys( 57 | 'key1' => { 'ap-northeast-1' => Aws::Credentials.new('ACCESS_KEY1', 'SECRET1') }, 58 | 'key2' => { 'us-east-1' => Aws::Credentials.new('ACCESS_KEY2', 'SECRET2') } 59 | ) 60 | profiles 'default', 'myprofile' 61 | regions 'ap-northeast-1', 'us-east-1' 62 | host_line 'host lines' 63 | reject {|instance| instance } 64 | path 'path' 65 | END 66 | end 67 | 68 | it do 69 | expect { Ec2ssh::Dsl::Parser.parse dsl_str }.to raise_error Ec2ssh::DotfileValidationError 70 | end 71 | end 72 | 73 | context 'with old structure aws_keys' do 74 | let(:dsl_str) do 75 | <<-END 76 | aws_keys( 77 | key1: { access_key_id: 'ACCESS_KEY1', secret_access_key: 'SECRET1' }, 78 | key2: { access_key_id: 'ACCESS_KEY2', secret_access_key: 'SECRET2' } 79 | ) 80 | regions 'ap-northeast-1', 'us-east-1' 81 | host_line 'host lines' 82 | reject {|instance| instance } 83 | path 'path' 84 | END 85 | end 86 | 87 | it { expect { Ec2ssh::Dsl::Parser.parse dsl_str }.to raise_error Ec2ssh::DotfileValidationError } 88 | end 89 | 90 | context 'with filters' do 91 | let(:dsl_str) do 92 | <<-END 93 | regions 'ap-northeast-1', 'us-east-1' 94 | filters [{ 95 | name: 'instance-state-name', 96 | values: ['running', 'stopped'] 97 | }] 98 | END 99 | end 100 | 101 | subject(:result) { Ec2ssh::Dsl::Parser.parse dsl_str } 102 | 103 | it do 104 | expect(result.filters).to eq([{name:'instance-state-name', values:['running', 'stopped']}]) 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /spec/lib/ec2ssh/ec2_instances_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'ec2ssh/ec2_instances' 3 | 4 | describe Ec2ssh::Ec2Instances do 5 | describe '#instances' do 6 | let(:key_name) { 7 | "dummy_key_name" 8 | } 9 | 10 | let(:region) { 11 | "ap-northeast-1" 12 | } 13 | 14 | let(:mock) do 15 | described_class.new( 16 | {key_name => {region => ''}}, 17 | [{ name: 'instance-state-name', values: ['running'] }] 18 | ).tap do |e| 19 | allow(e).to receive(:ec2s) { ec2s } 20 | end 21 | end 22 | 23 | let(:ec2s) { 24 | { 25 | "#{key_name}" => { 26 | "#{region}" => instances.tap do |m| 27 | allow(m).to receive(:instances) { m } 28 | end 29 | } 30 | } 31 | } 32 | 33 | let(:instances) { 34 | mock_instances.tap do |m| 35 | allow(m).to receive(:filter) { m } 36 | end 37 | } 38 | 39 | context 'with non-empty names' do 40 | let(:mock_instances) { 41 | [ 42 | double('instance', n: 1, tags: [double('tag', key: 'Name', value: 'srvB')]), 43 | double('instance', n: 2, tags: [double('tag', key: 'Name', value: 'srvA')]), 44 | double('instance', n: 3, tags: [double('tag', key: 'Name', value: 'srvC')]) 45 | ] 46 | } 47 | 48 | it do 49 | result = mock.instances(key_name) 50 | expect(result.map {|ins| ins.n}).to match_array([2, 1, 3]) 51 | end 52 | end 53 | 54 | context 'with names including empty one' do 55 | let(:mock_instances) { 56 | [ 57 | double('instance', n: 1, tags: [double('tag', key: 'Name', value: 'srvA')]), 58 | double('instance', n: 2, tags: []), 59 | double('instance', n: 3, tags: [double('tag', key: 'Name', value: 'srvC')]) 60 | ] 61 | } 62 | 63 | it do 64 | result = mock.instances(key_name) 65 | expect(result.map {|ins| ins.n}).to match_array([2, 1, 3]) 66 | end 67 | end 68 | end 69 | 70 | describe Ec2ssh::Ec2Instances::InstanceWrapper do 71 | let(:mock_instance) { 72 | double('instance', n: 1, tags: [double('tag', key: 'Name', value: 'srvA')]) 73 | } 74 | let(:instance) { described_class.new(mock_instance) } 75 | 76 | describe '#tag' do 77 | it { expect(instance.tag('Name')).to eq 'srvA' } 78 | end 79 | 80 | describe '#tags' do 81 | it { expect(instance.tags).to match_array(have_attributes(key: 'Name', value: 'srvA')) } 82 | it { expect(instance.tags[0]).to have_attributes(key: 'Name', value: 'srvA') } 83 | it { expect { instance.tags['Name'] }.to raise_error Ec2ssh::DotfileValidationError } 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /spec/lib/ec2ssh/ssh_config_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'ec2ssh/ssh_config' 3 | 4 | describe Ec2ssh::SshConfig do 5 | include FakeFS::SpecHelpers 6 | 7 | let(:path) { Pathname('/ssh_config') } 8 | 9 | subject(:ssh_config) do 10 | described_class.new(path).tap(&:parse!) 11 | end 12 | 13 | before do 14 | File.open(path, 'w') {|f| f.write config_str } 15 | end 16 | 17 | context 'expect be false' do 18 | let(:config_str) { <<-END } 19 | Host host1 20 | HostName 0.0.0.0 21 | END 22 | 23 | it { expect(ssh_config.mark_exist?).to be_falsey } 24 | end 25 | 26 | describe '#mark_exist?' do 27 | context 'expect be true' do 28 | let(:config_str) { <<-END } 29 | ### EC2SSH BEGIN ### 30 | # Generated by ec2ssh http://github.com/mirakui/ec2ssh 31 | # DO NOT edit this block! 32 | # Updated 2013-01-01T00:00:00+00:00 33 | Host db-01.ap-northeast-1 34 | HostName ec2-1-1-1-1.ap-northeast-1.ec2.amazonaws.com 35 | 36 | ### EC2SSH END ### 37 | END 38 | 39 | it { expect(ssh_config.mark_exist?).to be_truthy } 40 | end 41 | end 42 | 43 | describe '#parse!' do 44 | context 'when no section exists' do 45 | let(:config_str) { ; <<-END } 46 | ### EC2SSH BEGIN ### 47 | # Generated by ec2ssh http://github.com/mirakui/ec2ssh 48 | # DO NOT edit this block! 49 | # Updated 2013-01-01T00:00:00+00:00 50 | Host db-01.ap-northeast-1 51 | HostName ec2-1-1-1-1.ap-northeast-1.ec2.amazonaws.com 52 | 53 | ### EC2SSH END ### 54 | END 55 | 56 | it { expect(ssh_config.sections.size).to be == 1 } 57 | it { expect(ssh_config.sections['default']).to be_an_instance_of Ec2ssh::SshConfig::Section } 58 | end 59 | 60 | context 'when a section exists' do 61 | let(:config_str) { <<-END } 62 | Host foo.bar.com 63 | HostName 1.2.3.4 64 | ### EC2SSH BEGIN ### 65 | # Generated by ec2ssh http://github.com/mirakui/ec2ssh 66 | # DO NOT edit this block! 67 | # Updated 2013-01-01T00:00:00+00:00 68 | # section: foo 69 | Host db-01.ap-northeast-1 70 | HostName ec2-1-1-1-1.ap-northeast-1.ec2.amazonaws.com 71 | 72 | ### EC2SSH END ### 73 | END 74 | 75 | it { expect(subject.sections.size).to be == 2 } 76 | it { expect(subject.sections['foo']).to be_an_instance_of Ec2ssh::SshConfig::Section } 77 | end 78 | 79 | context 'when multiple sections exist' do 80 | let(:config_str) { <<-END } 81 | Host foo.bar.com 82 | HostName 1.2.3.4 83 | ### EC2SSH BEGIN ### 84 | # Generated by ec2ssh http://github.com/mirakui/ec2ssh 85 | # DO NOT edit this block! 86 | # Updated 2013-01-01T00:00:00+00:00 87 | # section: foo 88 | Host db-01.ap-northeast-1 89 | HostName ec2-1-1-1-1.ap-northeast-1.ec2.amazonaws.com 90 | # section: bar 91 | Host db-02.ap-northeast-1 92 | HostName ec2-1-1-1-2.ap-northeast-1.ec2.amazonaws.com 93 | 94 | ### EC2SSH END ### 95 | END 96 | 97 | it { expect(ssh_config.sections.size).to be == 3 } 98 | it { expect(ssh_config.sections['foo']).to be_an_instance_of Ec2ssh::SshConfig::Section } 99 | it { expect(ssh_config.sections['bar']).to be_an_instance_of Ec2ssh::SshConfig::Section } 100 | end 101 | end 102 | end 103 | 104 | describe Ec2ssh::SshConfig::Section do 105 | describe '#append' do 106 | let(:section) { Ec2ssh::SshConfig::Section.new('test') } 107 | before { section.append('foo') } 108 | 109 | it { expect(section.text).to eq 'foo' } 110 | end 111 | 112 | describe '#replace' do 113 | let(:section) { Ec2ssh::SshConfig::Section.new('test', 'foo') } 114 | before { section.replace!('bar') } 115 | 116 | it { expect(section.text).to eq 'bar' } 117 | end 118 | 119 | describe '#to_s' do 120 | context 'when no text given' do 121 | let(:section) { Ec2ssh::SshConfig::Section.new('test') } 122 | 123 | it { expect(section.to_s).to eq '' } 124 | end 125 | 126 | context 'when empty text given' do 127 | let(:section) { Ec2ssh::SshConfig::Section.new('test', '') } 128 | 129 | it { expect(section.to_s).to eq '' } 130 | end 131 | 132 | context 'when some text given' do 133 | let(:section) { Ec2ssh::SshConfig::Section.new('test', 'foo') } 134 | 135 | it { 136 | expect(section.to_s).to eq <commands' \ 51 | '(-)*:: :->option-or-argument' && return 52 | 53 | case $state in 54 | (commands) 55 | _ec2ssh_commands && ret=0 56 | ;; 57 | (option-or-argument) 58 | if (( $+functions[_ec2ssh-$words[1]] )); then 59 | _call_function ret _ec2ssh-$words[1] 60 | else 61 | _message 'no completion' 62 | fi 63 | ;; 64 | esac 65 | 66 | return ret 67 | } 68 | 69 | _ec2ssh_commands() { 70 | _values 'command' \ 71 | 'help[Describe available commands or one specific command]' \ 72 | 'init[Add ec2ssh mark to ssh_config]' \ 73 | 'remove[Remove ec2ssh mark from ssh_config]' \ 74 | 'update[Update ec2 hosts list in ssh_config]' \ 75 | 'version[Show version]' 76 | } 77 | 78 | compdef _ec2ssh ec2ssh 79 | --------------------------------------------------------------------------------