ssh-short 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +10 -0
- data/LICENSE +22 -0
- data/LICENSE.txt +21 -0
- data/README.md +190 -0
- data/bin/sshort +6 -0
- data/lib/ssh_short/cli.rb +57 -0
- data/lib/ssh_short/connection.rb +24 -0
- data/lib/ssh_short/keyset.rb +35 -0
- data/lib/ssh_short/nodemapper.rb +84 -0
- data/lib/ssh_short/parser.rb +92 -0
- data/lib/ssh_short/version.rb +3 -0
- data/spec/spec_helper.rb +6 -0
- data/spec/ssh_short/nodemapper_spec.rb +187 -0
- data/spec/ssh_short/parser_spec.rb +182 -0
- data/ssh-short.gemspec +22 -0
- metadata +68 -0
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
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,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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|