ssh-short 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA512:
3
+ data.tar.gz: 57d071b428d1f9cd7c1d4d35468d3ab0aed81826f7958a234b49f29b3a5b31ba9a996daf921846b4c137140a9c40c9d13a707036f8fa1abe7242091b6476985f
4
+ metadata.gz: d78446918da032b69cef111cc5fd1ad863b9998d3c7d000aea20f509a54448fbf47d9b1a95f182c2ea5220e51c43e002b887ca26ac0bb462dc4e9f4c907a16f7
5
+ SHA1:
6
+ data.tar.gz: a104fd336517f5d569c8910c44e9a9cbf9090c11
7
+ metadata.gz: 92180626001f2978d271f23f00d4b48abab2bd97
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source 'https://rubygems.org/'
2
+
3
+ group :development do
4
+ gem 'rake'
5
+ end
6
+
7
+ group :test do
8
+ gem 'rspec', '~> 3.2.0'
9
+ gem 'mocha'#, :require => false
10
+ end
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 TomPoulton
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 TomPoulton
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,190 @@
1
+ # ssh-short
2
+
3
+ The aim of ssh-short is reduce the amount you have to type when ssh-ing into common hosts,
4
+ from something like:
5
+ ```
6
+ ssh -i /path/to/my/key.pem username@10.55.2.16
7
+ ```
8
+ to:
9
+ ```
10
+ sshort 16
11
+ ```
12
+ or even an alias if you set one:
13
+ ```
14
+ sshort fav
15
+ ```
16
+
17
+ The command `sshort` was chosen because it's easy and quick to type,
18
+ even if it's not the most descriptive name (suggestions welcome!)
19
+
20
+ ### Config
21
+
22
+ If you often ssh into a set of hosts that have similar IPs with a common username (such as Amazon EC2 instances),
23
+ then ssh-short can use a combination of config and map files to reduce the repetitive typing when ssh-ing into these hosts.
24
+
25
+ You first create a config file (`~/.ssh-short/config.yml`) like the following:
26
+ ```
27
+ ---
28
+ :keys_dir: '~/my/ssh/keys'
29
+ :ip_mask: 10.55.2.0
30
+ :default_user: my-user
31
+ ```
32
+
33
+ ##### `keys_dir`
34
+
35
+ This is where ssh-short will look for your ssh keys, it assumes that they're all in here
36
+
37
+ ##### `ip_mask`
38
+
39
+ The mask provides a way of avoiding having to type the full IP every time.
40
+
41
+ If all your IPs start with `10.55.2` for example, then set the mask to `10.55.2.0`,
42
+ now you only have to provide the last segment and ssh-short will add what's missing
43
+ using the sections from the mask.
44
+ Any sections you pass in overwrite the mask, so you can still connect to IPs that
45
+ are completely different from the mask
46
+
47
+ Here's an example of masks, input and outputs
48
+
49
+ | Mask | Input | Result |
50
+ |-----------|--------------|--------------|
51
+ | 10.55.2.0 | 16 | 10.55.2.16 |
52
+ | 10.55.2.0 | 2.16 | 10.55.2.16 |
53
+ | 10.55.2.0 | 55.2.16 | 10.55.2.16 |
54
+ | 10.55.2.0 | 10.55.2.16 | 10.55.2.16 |
55
+ | 10.55.2.0 | 192.168.1.2 | 192.168.1.2 |
56
+ | 10.60.0.0 | 16 | 10.60.0.16 |
57
+ | 10.60.0.0 | 22.2 | 10.60.22.2 |
58
+ | 10.60.0.0 | 222.22.2 | 10.222.22.2 |
59
+
60
+ ##### `default_user`
61
+
62
+ Hopefully this is self explanatory, it's the username that will be used
63
+ to connect to the host if no user is provided.
64
+
65
+ You can override the user for a node, see args section below
66
+
67
+ ### Keys
68
+
69
+ When you connect to a host for the first time with ssh-short, it will scan your `keys_dir`
70
+ and present you with a list of numbered keys:
71
+
72
+ ```
73
+ user@localhost $ sshort 16
74
+ Select a key:
75
+ 0) Dev.pem
76
+ 1) Test.pem
77
+ 2) Admin.pem
78
+ ```
79
+
80
+ Simply type the number of the key and press enter. ssh-short will connect you via `ssh`,
81
+ and it will also remember your choice in `~/.ssh-short/nodemap.yml` so the next time you connect
82
+ it will look for the key name in the nodemap file and use that automatically:
83
+ ```
84
+ user@localhost $ sshort 16
85
+ Connecting as my-user to 10.55.2.16 using Test.pem
86
+ ...
87
+ ```
88
+
89
+ It stores the name of the file (e.g. `Test.pem`), not the number you select, or the full path to the key.
90
+ This way if you add keys or change your keys directory it doesn't matter,
91
+ as long as the file name stays the same
92
+
93
+ If you chose the wrong key or need to change the key, see args section below
94
+
95
+ ### Args
96
+
97
+ #### `-u`
98
+ Specify a user. This will be used instead of the defualt user. The user is saved in the nodemap so you only have to set it once. If the user does change, use this command to update the nodemap:
99
+
100
+ ```
101
+ user@localhost $ sshort 16
102
+ Connecting as my-user to 10.55.2.16 using Test.pem
103
+ ...
104
+ user@localhost $ sshort 16 -u new-user
105
+ Connecting as new-user to 10.55.2.16 using Test.pem
106
+ ...
107
+ user@localhost $ sshort 16
108
+ Connecting as new-user to 10.55.2.16 using Test.pem
109
+ ...
110
+ ```
111
+ #### `-k`
112
+ This forces an update to the key for a node and you will be presented with the list of keys again as if connecting for the first time:
113
+
114
+ ```
115
+ user@localhost $ sshort 16 -k
116
+ Select a key:
117
+ 0) Dev.pem
118
+ ...
119
+ ```
120
+
121
+ #### `-a`
122
+ Add an alias to a node, or move an exisiting alias to a new node, see Aliases section below
123
+
124
+ ### Aliases
125
+
126
+ If you want to connect to a host using an alias instead of an IP, you can set an alias
127
+ ```
128
+ sshort 16 -a fred
129
+ ```
130
+ Next time you can just use `fred`
131
+ ```
132
+ sshort fred
133
+ ```
134
+
135
+ If you set an alias that already exists it will be moved to the new host,
136
+ and a message will inform you:
137
+ ```
138
+ user@localhost $ sshort 0.77 -a fred
139
+ Moving alias fred from 10.55.2.16 to 10.55.0.77
140
+ ...
141
+ ```
142
+
143
+ To list all the saved aliases, use the `--list` action:
144
+ ```
145
+ user@localhost $ sshort --list
146
+ fav
147
+ fred
148
+ ```
149
+
150
+ ### Push and Pull
151
+
152
+ ssh-short also provides a way to push/pull files using `scp`:
153
+ ```
154
+ sshort 16 --pull /foo/bar.txt /tmp/
155
+ ```
156
+
157
+ This will pull the file `/foo/bar.txt` from the host to the `/tmp` directory on your local machine.
158
+ The opposite is `--push`:
159
+ ```
160
+ sshort 16 --push /tmp/bar.txt /foo/
161
+ ```
162
+
163
+ Behind the scenes ssh-short uses the same key lookup process,
164
+ and then just passes the path arguments to `scp`:
165
+ ```
166
+ scp -r -i /path/to/key.pem /tmp/bar.txt my-user@10.55.2.16:/foo/
167
+ ```
168
+
169
+ ## Installation
170
+
171
+ Install the gem:
172
+ ```
173
+ gem install ssh-short
174
+ ```
175
+
176
+ Create your config file
177
+
178
+ ## SSH Config
179
+
180
+ The SSH config file (`~/.ssh/config`) has support for a lot of this already, as well as extra stuff like `LocalForward` etc
181
+
182
+ Maybe this tool can evolve to use the SSH config for persistence instead of `~/.ssh-short/nodemap.yml`
183
+ and support all the features that SSH and it's config file offer,
184
+ however reading and writing wouldn't be as simple as serialising a simple array to/from YAML.
185
+
186
+ Plus the user might want to make changes to the SSH config outside of ssh-short, so we'd have to careful not to overwrite these "external" changes
187
+
188
+ ## ToDo
189
+ - Make `ip_mask` and `default_user` optional config settings
190
+ - IPv6??
data/bin/sshort ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'ssh_short/cli'
5
+
6
+ SshShort::CLI.run ARGV
@@ -0,0 +1,57 @@
1
+ require 'ssh_short/parser'
2
+ require 'ssh_short/nodemapper'
3
+ require 'ssh_short/keyset'
4
+ require 'ssh_short/connection'
5
+
6
+ module SshShort
7
+
8
+ CONFIG_DIR = File.expand_path('~/.ssh-short')
9
+ CONFIG_FILE = File.join(CONFIG_DIR, 'config.yml')
10
+ NODEMAP_FILE = File.join(CONFIG_DIR, 'nodemap.yml')
11
+ Dir.mkdir(CONFIG_DIR) unless File.exists?(CONFIG_DIR)
12
+
13
+ class CLI
14
+
15
+ def self.run(argv)
16
+ config = SshShort::Parser.parse_config
17
+ args = SshShort::Parser.parse_input(config, argv)
18
+ key_set = SshShort::KeySet.new(config[:keys_dir])
19
+ node_mapper = SshShort::NodeMapper.new
20
+
21
+ if args[:action] == :list_aliases
22
+ node_mapper.get_aliases.each { |node_alias| puts node_alias }
23
+ exit
24
+ end
25
+
26
+ node = node_mapper.get_node args[:node]
27
+ node ||= {:host => args[:node]}
28
+
29
+ if node[:key].nil? or args[:force_key_prompt]
30
+ node[:key] = key_set.prompt_for_key
31
+ end
32
+ node = CLI.add_options_if_present(node, args, [:alias, :user])
33
+
34
+ node_mapper.update_node(node)
35
+ key_path = key_set.get_key node[:key]
36
+
37
+ case args[:action]
38
+ when :connect
39
+ Connection.connect node[:host], node[:user], key_path
40
+ when :push
41
+ Connection.push node[:host], node[:user], key_path, args[:source], args[:target]
42
+ when :pull
43
+ Connection.pull node[:host], node[:user], key_path, args[:source], args[:target]
44
+ else
45
+ abort 'Unknown action'
46
+ end
47
+ end
48
+
49
+ def self.add_options_if_present(node, args, options)
50
+ options.each { |option|
51
+ node[option] = args[option] if args.include?(option)
52
+ }
53
+ node
54
+ end
55
+
56
+ end
57
+ end
@@ -0,0 +1,24 @@
1
+ module SshShort
2
+
3
+ class Connection
4
+
5
+ def self.connect(ip_address, username, key_path)
6
+ key_name = File.basename key_path
7
+ puts "Connecting as #{username} to #{ip_address} using #{key_name}"
8
+ system "ssh -i #{key_path} #{username}@#{ip_address}"
9
+ end
10
+
11
+ def self.push(ip_address, username, key_path, source, target)
12
+ key_name = File.basename key_path
13
+ puts "Pushing #{source} to #{ip_address} at #{target} as #{username} using #{key_name}"
14
+ system "scp -r -i #{key_path} #{source} #{username}@#{ip_address}:#{target}"
15
+ end
16
+
17
+ def self.pull(ip_address, username, key_path, source, target)
18
+ key_name = File.basename key_path
19
+ puts "Pulling #{source} from #{ip_address} as #{username} to #{target} using #{key_name}"
20
+ system "scp -r -i #{key_path} #{username}@#{ip_address}:#{source} #{target}"
21
+ end
22
+
23
+ end
24
+ end
@@ -0,0 +1,35 @@
1
+ require 'YAML'
2
+
3
+ module SshShort
4
+
5
+ class KeySet
6
+
7
+ def initialize(keys_dir)
8
+ @keys_dir = keys_dir
9
+ end
10
+
11
+ def prompt_for_key
12
+ abort "Error: Cannot find keys directory at #{@keys_dir}" unless File.exist? @keys_dir
13
+ keys = Dir.glob("#{@keys_dir}/*").select{ |e| File.file? e }
14
+ abort "Error: No keys found in #{@keys_dir}" unless keys.count > 0
15
+
16
+ key_names = keys.collect { |key| File.basename key }
17
+
18
+ puts 'Select a key:'
19
+ key_names.each_with_index { |key_name, i| puts "#{i}) #{key_name}" }
20
+
21
+ key_selection = STDIN.gets.to_i
22
+ abort "#{key_selection} is not a valid key" if (key_selection >= key_names.count)
23
+
24
+ key_names[key_selection]
25
+ end
26
+
27
+ def get_key(key_name)
28
+ key = "#{@keys_dir}/#{key_name}"
29
+ abort "Error: Cannot find #{key}" unless File.exist? key
30
+ key
31
+ end
32
+
33
+ end
34
+
35
+ end
@@ -0,0 +1,84 @@
1
+ module SshShort
2
+
3
+ class NodeMapper
4
+
5
+ def initialize
6
+ @node_map_file = SshShort::NODEMAP_FILE
7
+ @node_map = NodeMapper.read_node_map(@node_map_file)
8
+ end
9
+
10
+ def is_alias?(input)
11
+ aliases = get_aliases
12
+ aliases.include? input
13
+ end
14
+
15
+ # returns nil if no matching node is found
16
+ def get_node(host_or_alias)
17
+ if is_alias? host_or_alias
18
+ get_node_by_alias host_or_alias
19
+ else
20
+ get_node_by_host host_or_alias
21
+ end
22
+ end
23
+
24
+ def update_node(node)
25
+ alias_changed = alias_changed? node
26
+ if alias_changed
27
+ existing_node_with_alias = get_node_by_alias(node[:alias])
28
+ if existing_node_with_alias
29
+ puts "Moving alias #{node[:alias]} from #{existing_node_with_alias[:host]} to #{node[:host]}"
30
+ existing_node_with_alias.delete :alias
31
+ upsert_node existing_node_with_alias
32
+ end
33
+ end
34
+
35
+ upsert_node node
36
+
37
+ NodeMapper.save_node_map(@node_map_file, @node_map)
38
+ @node_map
39
+ end
40
+
41
+ def get_aliases
42
+ @node_map.collect { |node| node[:alias] }.compact
43
+ end
44
+
45
+ class << self
46
+ def read_node_map(node_map_file)
47
+ File.exist?(node_map_file) ? YAML.load_file(node_map_file) : []
48
+ end
49
+
50
+ def save_node_map(node_map_file, node_map)
51
+ File.open(node_map_file, 'w') { |fo| fo.puts node_map.to_yaml }
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def alias_changed?(node)
58
+ old_node = get_node_by_host node[:host]
59
+ old_node ? old_node[:alias] != node[:alias] : true
60
+ end
61
+
62
+ def upsert_node(node)
63
+ existing_node = get_node_by_host node[:host]
64
+ if existing_node
65
+ @node_map.delete existing_node
66
+ end
67
+ @node_map.push node
68
+ end
69
+
70
+ def get_node_by_alias(node_alias)
71
+ nodes = @node_map.find_all { |node| node[:alias] == node_alias }
72
+ # abort "Error: More than one Node has alias #{node_alias} in Node Map" if (nodes.count > 1)
73
+ nodes[0]
74
+ end
75
+
76
+ def get_node_by_host(host)
77
+ nodes = @node_map.find_all { |node| node[:host] == host }
78
+ # abort "Error: More than one Node has host #{host} in Node Map" if (nodes.count > 1)
79
+ nodes[0]
80
+ end
81
+
82
+ end
83
+
84
+ end
@@ -0,0 +1,92 @@
1
+ module SshShort
2
+
3
+ class Parser
4
+
5
+ def self.parse_config
6
+ abort "Error: Cannot find config file #{SshShort::CONFIG_FILE}" unless config_file_exists?
7
+ config = YAML.load_file(SshShort::CONFIG_FILE)
8
+ abort 'Error: Keys directory must be specified' unless config[:keys_dir]
9
+ config[:keys_dir] = File.expand_path(config[:keys_dir])
10
+ config
11
+ end
12
+
13
+ def self.config_file_exists?
14
+ File.exist? SshShort::CONFIG_FILE
15
+ end
16
+
17
+ def self.parse_input(config, args)
18
+ options = self.key_options args, {}
19
+ options = self.alias_option args, options
20
+ options = self.action_options args, options
21
+ options = self.user_options args, options, config[:default_user]
22
+ options = self.node_options args, options, config[:ip_mask]
23
+ options
24
+ end
25
+
26
+ private
27
+
28
+ def self.key_options(args, options)
29
+ options[:force_key_prompt] = args.include? '-k'
30
+ args.delete '-k'
31
+ options
32
+ end
33
+
34
+ def self.alias_option(args, options)
35
+ alias_index = args.index('-a')
36
+ if alias_index
37
+ options[:alias] = args[alias_index + 1]
38
+ 2.times { args.delete_at alias_index }
39
+ end
40
+ options
41
+ end
42
+
43
+ def self.user_options(args, options, default_user)
44
+ user_index = args.index('-u')
45
+ if user_index
46
+ options[:user] = args[user_index + 1]
47
+ 2.times { args.delete_at user_index }
48
+ else
49
+ abort 'No user was provided and no default is set' unless default_user
50
+ options[:user] = default_user
51
+ end
52
+ options
53
+ end
54
+
55
+ def self.action_options(args, options)
56
+ push_pull_index = args.index('--push') || args.index('--pull')
57
+ if args.include? '--list'
58
+ options[:action] = :list_aliases
59
+ args.delete '--list'
60
+ elsif push_pull_index
61
+ options[:action] = args[push_pull_index].gsub('-', '').to_sym
62
+ options[:source] = args[push_pull_index + 1]
63
+ options[:target] = args[push_pull_index + 2]
64
+ 3.times { args.delete_at push_pull_index }
65
+ else
66
+ options[:action] = :connect
67
+ end
68
+ options
69
+ end
70
+
71
+ def self.node_options(args, options, ip_mask)
72
+ input = args[0]
73
+ options[:node] = input_is_ip?(input) ? apply_ip_mask(input, ip_mask) : input
74
+ options
75
+ end
76
+
77
+ def self.input_is_ip?(input)
78
+ input ? input.match(/^[\d\.]+$/) : false
79
+ end
80
+
81
+ def self.apply_ip_mask(ip, ip_mask)
82
+ return ip unless ip.match(/^[\d\.]+$/)
83
+ sections = ip.split('.').count
84
+ if sections < 4
85
+ "#{ip_mask.split('.')[0..(3 - sections)].join('.')}.#{ip}"
86
+ else
87
+ ip
88
+ end
89
+ end
90
+
91
+ end
92
+ end
@@ -0,0 +1,3 @@
1
+ module SshShort
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,6 @@
1
+ # require 'rspec'
2
+ require 'mocha/test_unit'
3
+
4
+ RSpec.configure do |c|
5
+ c.color = true
6
+ end
@@ -0,0 +1,187 @@
1
+ require 'spec_helper'
2
+ require 'ssh_short/nodemapper'
3
+
4
+ module SshShort
5
+ NODEMAP_FILE = nil
6
+ end
7
+
8
+ describe SshShort::NodeMapper do
9
+
10
+ let(:nodemapper) { SshShort::NodeMapper.new }
11
+ let(:nodemap) {
12
+ [
13
+ {:host => '10.0.0.1', :alias => 'alice', :key => 'key_for_alice.pem' },
14
+ {:host => '10.0.0.2', :alias => 'bob', :key => 'key_for_bob.pem' },
15
+ {:host => '10.0.0.3', :key => 'key_for_03.pem' },
16
+ ]
17
+ }
18
+
19
+ before(:each) do
20
+ allow(File).to receive(:exist?).and_return(true)
21
+ allow(YAML).to receive(:load_file).and_return(nodemap)
22
+ end
23
+
24
+ describe 'is_alias?' do
25
+
26
+ it 'returns true if the alias is in the nodemap' do
27
+ result = nodemapper.is_alias? 'alice'
28
+ expect(result).to eq true
29
+ end
30
+
31
+ it 'returns false if the alias is not in the nodemap' do
32
+ result = nodemapper.is_alias? 'non-ex'
33
+ expect(result).to eq false
34
+ end
35
+
36
+ it 'returns false if the alias is nil' do
37
+ result = nodemapper.is_alias? nil
38
+ expect(result).to eq false
39
+ end
40
+
41
+ end
42
+
43
+ describe 'get_node' do
44
+
45
+ it 'returns a hash' do
46
+ node = nodemapper.get_node('10.0.0.1')
47
+ expect(node).to be_a Hash
48
+ end
49
+
50
+ it 'returns the node matching the host' do
51
+ host = '10.0.0.1'
52
+ node = nodemapper.get_node(host)
53
+ expect(node[:alias]).to eq 'alice'
54
+ end
55
+
56
+ it 'returns the node matching the alias' do
57
+ node_alias = 'bob'
58
+ node = nodemapper.get_node(node_alias)
59
+ expect(node[:host]).to eq '10.0.0.2'
60
+ end
61
+
62
+ it 'returns nil when node does not exist' do
63
+ node = nodemapper.get_node('non-ex')
64
+ expect(node).to eq nil
65
+ end
66
+
67
+ end
68
+
69
+ describe 'update_node' do
70
+
71
+ # Keep the node map simple for these tests
72
+ let(:nodemap) {
73
+ [ {:host => '10.0.0.1', :alias => 'alice', :key => 'key_for_alice.pem'} ]
74
+ }
75
+
76
+ before(:each) do
77
+ allow(SshShort::NodeMapper).to receive(:save_node_map).and_return(nil)
78
+ allow(nodemapper).to receive(:puts).and_return(nil)
79
+ end
80
+
81
+ it 'returns the updated node map' do
82
+ # This is just so we can get the array for testing,
83
+ # although it might be useful in the future?
84
+ node = nodemap[0]
85
+ updated_nodemap = nodemapper.update_node(node)
86
+ expect(updated_nodemap).to eq nodemap
87
+ end
88
+
89
+ it 'adds a new node to the map' do
90
+ node = {:host => '10.0.0.2', :key => 'key_for_bob.pem'}
91
+ updated_nodemap = nodemapper.update_node(node)
92
+ expect(updated_nodemap.count).to eq 2
93
+ expect(updated_nodemap[1]).to eq node
94
+ end
95
+
96
+ it 'updates an existing node' do
97
+ node = {:host => '10.0.0.1', :alias => 'alice', :key => 'new_key.pem'}
98
+ updated_nodemap = nodemapper.update_node(node)
99
+ expect(updated_nodemap.count).to eq 1
100
+ expect(updated_nodemap[0]).to eq node
101
+ end
102
+
103
+ context 'when updating the user' do
104
+
105
+ let(:node) {
106
+ node = nodemap[0].clone
107
+ node[:user] = 'new-user'
108
+ node
109
+ }
110
+ subject(:updated_nodemap) { nodemapper.update_node(node) }
111
+
112
+ it 'updates to user' do
113
+ expect(updated_nodemap[0][:user]).to eq 'new-user'
114
+ end
115
+
116
+ it 'preserves the alias' do
117
+ expect(updated_nodemap[0][:alias]).to eq 'alice'
118
+ end
119
+
120
+ end
121
+
122
+ context 'when the alias already exists on another node' do
123
+
124
+ let(:node) { {:host => '10.0.0.6', :alias => 'alice', :key => 'key_for_6.pem'} }
125
+ subject(:updated_nodemap) { nodemapper.update_node(node) }
126
+
127
+ it 'adds the alias to the new node' do
128
+ expect(updated_nodemap[1][:alias]).to eq 'alice'
129
+ end
130
+
131
+ it 'removes the alias from the old node' do
132
+ expect(updated_nodemap[0][:alias]).to eq nil
133
+ end
134
+
135
+ it 'prints info message' do
136
+ allow(nodemapper).to receive(:puts).and_call_original
137
+ expect {
138
+ nodemapper.update_node(node)
139
+ }.to output(/Moving alias alice from .*1 to .*6/).to_stdout
140
+ end
141
+
142
+ end
143
+
144
+ end
145
+
146
+ describe 'get_aliases' do
147
+
148
+ it 'returns all aliases as array' do
149
+ aliases = nodemapper.get_aliases
150
+ expect(aliases).to eq ['alice', 'bob']
151
+ end
152
+
153
+ context 'when there are no aliases' do
154
+
155
+ let(:nodemap) {
156
+ [
157
+ {:host => '10.0.0.1', :key => 'key_for_alice.pem' },
158
+ {:host => '10.0.0.2', :key => 'key_for_bob.pem' },
159
+ ]
160
+ }
161
+
162
+ it 'returns an empty array' do
163
+ aliases = nodemapper.get_aliases
164
+ expect(aliases).to eq []
165
+ end
166
+
167
+ end
168
+
169
+ end
170
+
171
+ describe 'read_node_map' do
172
+
173
+ it 'loads the node array from YAML' do
174
+ allow(File).to receive(:exist?).and_return(true)
175
+ expect(YAML).to receive(:load_file).once.and_return([])
176
+ SshShort::NodeMapper.read_node_map(nil)
177
+ end
178
+
179
+ it 'returns an empty array if file does not exist' do
180
+ allow(File).to receive(:exist?).and_return(false)
181
+ result = SshShort::NodeMapper.read_node_map(nil)
182
+ expect(result).to eq([])
183
+ end
184
+
185
+ end
186
+
187
+ end
@@ -0,0 +1,182 @@
1
+ require 'spec_helper'
2
+ require 'ssh_short/parser'
3
+
4
+ describe SshShort::Parser do
5
+
6
+ describe 'parse_config' do
7
+
8
+ let(:config) {
9
+ {
10
+ :default_user => 'ec2-user',
11
+ :ip_mask => '10.72.0.0',
12
+ :keys_dir => '~/.ssh/accuity'
13
+ } }
14
+
15
+ before(:each) do
16
+ stub_const('SshShort::CONFIG_FILE', 'config')
17
+ allow(SshShort::Parser).to receive(:config_file_exists?).and_return(true)
18
+ allow(YAML).to receive(:load_file).and_return(config)
19
+ end
20
+
21
+ it 'returns a hash' do
22
+ config = SshShort::Parser.parse_config
23
+ expect(config).to be_a Hash
24
+ end
25
+
26
+ it 'parses config keys as symbols' do
27
+ config = SshShort::Parser.parse_config
28
+ config.each { |k, v|
29
+ expect(k).to be_a Symbol
30
+ }
31
+ end
32
+
33
+ context 'when config file does not exist' do
34
+
35
+ before(:each) do
36
+ allow(SshShort::Parser).to receive(:config_file_exists?).and_return(false)
37
+ end
38
+
39
+ it 'aborts with error message' do
40
+ expect {
41
+ expect {
42
+ SshShort::Parser.parse_config
43
+ }.to raise_error SystemExit
44
+ }.to output(/Cannot find config file/).to_stderr
45
+ end
46
+ end
47
+
48
+ context 'when keys_dir is null' do
49
+
50
+ before(:each) do
51
+ config.delete :keys_dir
52
+ end
53
+
54
+ it 'aborts with error message' do
55
+ expect {
56
+ expect {
57
+ SshShort::Parser.parse_config
58
+ }.to raise_error SystemExit
59
+ }.to output(/Keys directory must be specified/).to_stderr
60
+ end
61
+ end
62
+
63
+ end
64
+
65
+ describe 'parse_input' do
66
+
67
+ let(:standard_args) {
68
+ ['6']
69
+ }
70
+ let(:config) {
71
+ { :ip_mask => '10.0.0.0', :default_user => 'user' }
72
+ }
73
+
74
+ before(:each) do
75
+ end
76
+
77
+ it 'applies mask to ip' do
78
+ options = SshShort::Parser.parse_input(config, standard_args)
79
+ expect(options[:node]).to eq '10.0.0.6'
80
+ end
81
+
82
+ it 'uses all sections of input ip' do
83
+ options = SshShort::Parser.parse_input(config, ['1.2.3.4'])
84
+ expect(options[:node]).to eq '1.2.3.4'
85
+ end
86
+
87
+ it 'uses input as host when input is not ip' do
88
+ options = SshShort::Parser.parse_input(config, ['frank'])
89
+ expect(options[:node]).to eq 'frank'
90
+ end
91
+
92
+ context 'when no user is provided' do
93
+
94
+ it 'uses the default user from config' do
95
+ options = SshShort::Parser.parse_input(config, standard_args)
96
+ expect(options[:user]).to eq 'user'
97
+ end
98
+
99
+ context 'and when config user is null' do
100
+
101
+ let(:no_user_config) { config.delete_if{|k, v| k == :default_user} }
102
+
103
+ it 'aborts with error message' do
104
+ expect {
105
+ expect {
106
+ SshShort::Parser.parse_input(no_user_config, standard_args)
107
+ }.to raise_error SystemExit
108
+ }.to output(/No user was provided/).to_stderr
109
+ end
110
+ end
111
+ end
112
+
113
+ context 'when a user is provided' do
114
+
115
+ let(:user_args) { ['6', '-u', 'new-user'] }
116
+
117
+ it 'extracts the user' do
118
+ options = SshShort::Parser.parse_input(config, user_args)
119
+ expect(options[:user]).to eq 'new-user'
120
+ end
121
+
122
+ it 'removes the user args' do
123
+ options = SshShort::Parser.parse_input(config, user_args)
124
+ expect(user_args).to_not include '-u'
125
+ expect(user_args).to_not include 'new-user'
126
+ end
127
+ end
128
+
129
+ context 'when a new alias is provided' do
130
+
131
+ let(:alias_args) { ['6', '-a', 'fred'] }
132
+
133
+ it 'extracts the alias' do
134
+ options = SshShort::Parser.parse_input(config, alias_args)
135
+ expect(options[:alias]).to eq 'fred'
136
+ end
137
+
138
+ it 'removes the alias args' do
139
+ options = SshShort::Parser.parse_input(config, alias_args)
140
+ expect(alias_args).to_not include '-a'
141
+ expect(alias_args).to_not include 'fred'
142
+ end
143
+ end
144
+
145
+ it 'skips key prompt when -k flag is not set' do
146
+ options = SshShort::Parser.parse_input(config, standard_args)
147
+ expect(options[:force_key_prompt]).to eq false
148
+ end
149
+
150
+ context 'when -k flag is set' do
151
+
152
+ let(:key_args) { ['6', '-k'] }
153
+
154
+ it 'forces key prompt' do
155
+ options = SshShort::Parser.parse_input(config, key_args)
156
+ expect(options[:force_key_prompt]).to eq true
157
+ end
158
+
159
+ it 'removes -k flag from args' do
160
+ options = SshShort::Parser.parse_input(config, key_args)
161
+ expect(key_args).to_not include '-k'
162
+ end
163
+ end
164
+
165
+ context 'when --list flag is set' do
166
+
167
+ let(:key_args) { ['--list'] }
168
+
169
+ it 'sets the action to :list_aliases' do
170
+ options = SshShort::Parser.parse_input(config, key_args)
171
+ expect(options[:action]).to eq :list_aliases
172
+ end
173
+
174
+ it 'removes --list flag from args' do
175
+ SshShort::Parser.parse_input(config, key_args)
176
+ expect(key_args).to_not include '--list'
177
+ end
178
+ end
179
+
180
+ end
181
+
182
+ end
data/ssh-short.gemspec ADDED
@@ -0,0 +1,22 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+
5
+ require 'ssh_short/version'
6
+
7
+ excude = ['.gitignore', 'Rakefile']
8
+
9
+ Gem::Specification.new do |gem|
10
+ gem.name = 'ssh-short'
11
+ gem.version = SshShort::VERSION
12
+ gem.description = 'Easy ssh'
13
+ gem.summary = 'Fewer keystrokes to get things done'
14
+ gem.author = 'Tom Poulton'
15
+ gem.license = 'MIT'
16
+ gem.homepage = 'http://github.com/TomPoulton/ssh-short'
17
+
18
+ gem.files = `git ls-files`.split($/).reject { |file| file =~ /^(#{excude.join('|')})$/ }
19
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
20
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
21
+ gem.require_paths = ['lib']
22
+ end
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ssh-short
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Tom Poulton
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2015-09-23 00:00:00 Z
13
+ dependencies: []
14
+
15
+ description: Easy ssh
16
+ email:
17
+ executables:
18
+ - sshort
19
+ extensions: []
20
+
21
+ extra_rdoc_files: []
22
+
23
+ files:
24
+ - Gemfile
25
+ - LICENSE
26
+ - LICENSE.txt
27
+ - README.md
28
+ - bin/sshort
29
+ - lib/ssh_short/cli.rb
30
+ - lib/ssh_short/connection.rb
31
+ - lib/ssh_short/keyset.rb
32
+ - lib/ssh_short/nodemapper.rb
33
+ - lib/ssh_short/parser.rb
34
+ - lib/ssh_short/version.rb
35
+ - spec/spec_helper.rb
36
+ - spec/ssh_short/nodemapper_spec.rb
37
+ - spec/ssh_short/parser_spec.rb
38
+ - ssh-short.gemspec
39
+ homepage: http://github.com/TomPoulton/ssh-short
40
+ licenses:
41
+ - MIT
42
+ metadata: {}
43
+
44
+ post_install_message:
45
+ rdoc_options: []
46
+
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - &id001
52
+ - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: "0"
55
+ required_rubygems_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - *id001
58
+ requirements: []
59
+
60
+ rubyforge_project:
61
+ rubygems_version: 2.0.14
62
+ signing_key:
63
+ specification_version: 4
64
+ summary: Fewer keystrokes to get things done
65
+ test_files:
66
+ - spec/spec_helper.rb
67
+ - spec/ssh_short/nodemapper_spec.rb
68
+ - spec/ssh_short/parser_spec.rb