aws-ssh-resolver 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f935d670a01180e51698c25ea3ca209884f43556
4
+ data.tar.gz: ee1f14a33bcbe6b9ead7babf3fd625d96f3de9c0
5
+ SHA512:
6
+ metadata.gz: 02580ee44766981ab87b40e4f5f8bdac0cd1ab5e0e1ce3082f393272fac7b144bfa4f9b3c568834d6bb66b40b54f20c698cce7a2b162e737c516bd213fddd5fc
7
+ data.tar.gz: 013f95baba3d2fef334144d5995bdc373992b9e6ece6e88287ca2ee2937047c2fc70128ead909c8e50c3e2dedf34f0f2e70a819fa3e0aadad6b6c5d9c37933c9
@@ -0,0 +1,251 @@
1
+ # aws-ssh-resolver - Resolve AWS EC2 HostNames for OpenSSH configuration - $Release:0.0.2$
2
+
3
+ `aws-ssh-resolver` keeps AWS EC2 HostNames in OpenSSH configuration
4
+ file in sync with Amazon cloud making it easier for a user to use
5
+ OpenSSH, and related tools, on Amazon Platform.
6
+
7
+ ## The Problem
8
+
9
+ Every EC2 instance on Amazon platform has a Private IP address, and a
10
+ DNS hostname resolving to this address. Private IP cannot be reached
11
+ directly from the Internet, and the Private DNS name can be resolved
12
+ only on the network that the instance is in. An instance may be
13
+ assigned a Public IP Address, and corresponding Public DNS name. The
14
+ Public IP is accessible from the Internet, and the Public DNS name is
15
+ resolvable outside the network of the instance. Public IPs come from
16
+ Amazon's pool of public IP address, and an instance may not reuse the
17
+ IP address, once it is released. For example, stopping, or
18
+ terminating, an instance releases the Public IP Address. See Amazon
19
+ [documentation](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-instance-addressing.html)
20
+ for more details.
21
+
22
+
23
+ Amazon EC2 Instance IP Addressing presents several challenges for SSH
24
+ usage, or any SSH related tool e.g.
25
+ [ansible](http://www.ansible.com/home),
26
+ [fabric](http://www.fabfile.org/),
27
+ [serverspec](http://serverspec.org/) etc.
28
+
29
+ * Public DNS Name encodes the Public IP Address. Each time an instance
30
+ is assigned a new IP address, it also gets a new Public DNS name, In
31
+ essence this means that the task of managing DNS names becomes
32
+ comparable to the task of managing IP addresses.
33
+
34
+ * Using an IP address to contact an instance is complicated, because
35
+ Public IP Address, once released, cannot be reused. Using fixed IP
36
+ addresses requires keeping track of reserved address, and comes with
37
+ extra costs.
38
+
39
+ * EC2 instances, with only a Private IP Address, cannot be reached
40
+ directly from the Internet.
41
+
42
+ * Private DNS names also encode the IP address they map to. On top of
43
+ that, Private DNS names cannot resolved outside the cloud network.
44
+
45
+ ## The Solution
46
+
47
+ [aws-ssh-resolver](https://github.com/jarjuk/aws-ssh-resolver)
48
+ addresses the challenges above
49
+
50
+ * It accepts output of
51
+ [Amazon Command Line Interface](https://aws.amazon.com/cli/) to
52
+ create
53
+ [OpenSSH Configuration](http://www.openbsd.org/cgi-bin/man.cgi/OpenBSD-current/man5/ssh_config.5?query=ssh_config&sec=5)
54
+ entries mapping persistent, and human-understandable, EC2 Tag names
55
+ to mutable EC2 DNS names.
56
+
57
+ * Tag-name/DNS name mapping can be updated to reflect current cloud
58
+ configuration.
59
+
60
+ * Tag-name/DNS mapping together with
61
+ [ProxyCommand with Netcat](https://en.wikibooks.org/wiki/OpenSSH/Cookbook/Proxies_and_Jump_Hosts#ProxyCommand_with_Netcat)
62
+ configuration in OpenSSH allows users to create a transparent
63
+ multihop SSH connection to EC2 instances with Private IP Address
64
+ only
65
+
66
+ ## Usage
67
+
68
+ ### Installation
69
+
70
+ Add following lines to `Gemfile`
71
+
72
+ source 'https://rubygems.org'
73
+ gem 'aws-ssh-resolver'
74
+
75
+ and run
76
+
77
+ bundle install
78
+
79
+ ### Configuration
80
+
81
+ Create an initial
82
+ [OpenSSH Configuration](http://www.openbsd.org/cgi-bin/man.cgi/OpenBSD-current/man5/ssh_config.5?query=ssh_config&sec=5)
83
+ file `ssh/config.aws` with any fixed configuration. Running
84
+ **aws-ssh-resolver** updates this file, but does not interfere with
85
+ the content user has entered.
86
+
87
+ **Notice**: If `ssh/config.aws` -file does not exist, the first
88
+ **aws-ssh-resolver** run creates the initial version of
89
+ `ssh/config.aws` automatically using `ssh/config.init`. This avoids
90
+ the need to check in the mutable `ssh/config.aws` into a version
91
+ nncontrol system.
92
+
93
+ ### Update OpenSSH Configuration file
94
+
95
+ To update OpenSSH configuration with EC2 Tag/DNS mappings pipe the
96
+ result of `aws ec2 describe-instances` to **aws-ssh-resolver**
97
+ command:
98
+
99
+ aws ec2 describe-instances | bundle exec aws-ssh-resolver.rb resolve
100
+
101
+ The command extract EC2 Tag/DNS information, and writes
102
+ `host`/`HostName` configuration entries in `ssh/config.aws` -file. In
103
+ this file `host` value is taken from `Name` tag on an EC2 instance,
104
+ and `HostName` value is taken from `PublicDnsName` on an EC2
105
+ instance. If `PublicDnsName` is not defined, the command uses
106
+ `PrivateDnsName` instead.
107
+
108
+ When the network topology changes, i.e. an instance gets a new IP
109
+ address, an instance is terminated, or a new instance is launched,
110
+ rerun the command again to update content in `ssh/config.aws` to
111
+ reflect the new situation.
112
+
113
+ ## An Example
114
+
115
+ ### Example Setup
116
+
117
+ The example uses two Ubuntu EC2 instances with `Name` -tags `myFront`
118
+ and `myBack1`. Instance `myFront` has an internal IP
119
+ `10.0.0.246`. Instances on subnet `10.0.0.0/24` can be reached over
120
+ Internet, and `myFront` has been assigned a public IP `52.19.117.227`,
121
+ and an externally resolvable DNS name
122
+ `c2-52-19-117-227.eu-west-1.compute.amazonaws.com`. Instance `myBack1`
123
+ belongs to private subnet `10.0.1.0/24`, and cannot reached directly
124
+ from Internet. It has a private IP address `10.0.1.242` with a DNS
125
+ name `ip-10-0-1-242.eu-west-1.compute.internal`. Both of these
126
+ instances have been created using
127
+ [Amazon EC2 Key Pair](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html)
128
+ `demo_key`.
129
+
130
+
131
+ +----------------------------------------------------------------+
132
+ | Tags: [ "Name": "myFront" ], Ubuntu 14.04 LTS Trusty |
133
+ | 52.19.117.227/c2-52-19-117-227.eu-west-1.compute.amazonaws.com |
134
+ | 10.0.0.246/ip-10-0-0-246.eu-west-1.compute.internal |
135
+ ! 10.0.0.0/24 |
136
+ | .ssh/demo_key.pub |
137
+ +----------------------------------------------------------------+
138
+
139
+ +----------------------------------------------------------------+
140
+ | Tags: [ "Name": "myBack1" ], Ubuntu 14.04 LTS Trusty |
141
+ | |
142
+ |10.0.1.242//ip-10-0-1-242.eu-west-1.compute.internal |
143
+ |10.0.1.0/24 |
144
+ |.ssh/demo_key.pub |
145
+ +----------------------------------------------------------------+
146
+
147
+
148
+ ### Initial Configuration
149
+
150
+ We start by creating `ssh/config.aws` configuration file with the
151
+ following initial content
152
+
153
+ Host *.compute.internal
154
+ ProxyCommand ssh myFront1 -F ssh/config.aws nc -q0 %h 22
155
+
156
+ Host *
157
+ user ubuntu
158
+ StrictHostKeyChecking no
159
+ UserKnownHostsFile=/dev/null
160
+ IdentityFile ~/.ssh/demo-key/demo-key
161
+
162
+ This configuration instructs OpenSSH to use user name `ubuntu` and SSH
163
+ private key in `~/.ssh/demo-key/demo-key` for all SSH connections.
164
+
165
+ Amazon assigns DNS names ending with `compute.internal` to map to
166
+ Private IP address. The configuration tells OpenSSH to use `myFront1`
167
+ as a proxy to connect to instances with Private DNS name.
168
+
169
+
170
+ ### Read Network Topology, and Update OpenSSH Configuration
171
+
172
+ Running command
173
+
174
+ aws ec2 describe-instances | bundle exec aws-ssh-resolver.rb resolve
175
+
176
+ reads EC2 information from Amazon platform, extracts `Name` tags and
177
+ DNS names, and updates `ssh/config.aws` with `host` and `HostName`
178
+ information as shown below:
179
+
180
+ # +++ aws-ssh-resolver-cli update start here +++
181
+
182
+ # Content generated 2015-09-06-21:57:37
183
+
184
+ host myFront1
185
+ HostName ec2-52-19-117-227.eu-west-1.compute.amazonaws.com
186
+
187
+
188
+ host myBack1
189
+ HostName ip-10-0-1-242.eu-west-1.compute.internal
190
+
191
+
192
+ # +++ aws-ssh-resolver-cli update end here +++
193
+ Host *.compute.internal
194
+ ProxyCommand ssh myFront1 -F ssh/config.aws nc -q0 %h 22
195
+
196
+ Host *
197
+ user ubuntu
198
+ StrictHostKeyChecking no
199
+ UserKnownHostsFile=/dev/null
200
+ IdentityFile ~/.ssh/demo-key/demo-key
201
+
202
+ This configuration adds the host definition for `myFront1`, and
203
+ instructs OpenSSH to use a Public DNS name to connect the instance.
204
+
205
+ The HostName for `myBack1` ends with `compute.internal`, and the
206
+ OpenSSH uses the proxy definition to access it.
207
+
208
+ ### Using OpenSSH Configuration to Access ASW Instances
209
+
210
+ The configuration in `ssh/configaws` allows us to use tag name
211
+ `myFront1` to make a SSH connection to machine with the DNS name
212
+ `c2-52-19-117-227.eu-west-1.compute.amazonaws.com` simply with command
213
+
214
+ ssh myFront1 -F ssh/config.aws
215
+ Warning: Permanently added 'ec2-52-19-117-227.eu-west-1.compute.amazonaws.com,52.19.117.227' (ECDSA) to the list of known hosts.
216
+
217
+
218
+ The instance on subnet `10.0.1.0/24` cannot reached directly, and the
219
+ configuration instructs OpenSSH to use `myFront` as a intermediary to
220
+ create a
221
+ [transparent ssh connection](https://en.wikibooks.org/wiki/OpenSSH/Cookbook/Proxies_and_Jump_Hosts#ProxyCommand_with_Netcat)
222
+ to `myBack1`. This all takes place transparently, and the simple
223
+ command
224
+
225
+ ssh myBack1 -F ssh/config.aws
226
+ Warning: Permanently added 'ec2-52-19-117-227.eu-west-1.compute.amazonaws.com,52.19.117.227' (ECDSA) to the list of known hosts.
227
+ Warning: Permanently added 'ip-10-0-1-242.eu-west-1.compute.internal' (ECDSA) to the list of known hosts.
228
+
229
+ creates a SSH connection to `myBack1`.
230
+
231
+ Warnings shown above, are due to parameters `UserKnownHostsFile` and
232
+ `StrictHostKeyChecking`, which prevent ssh from updating the default
233
+ `.ssh/known_hosts` file with the fingerprints of the (temporary)
234
+ instances used in testing.
235
+
236
+
237
+ ### Updating OpenSSH Configuration
238
+
239
+ If the network configuration changes, rerunning
240
+
241
+ aws ec2 describe-instances | bundle exec aws-ssh-resolver.rb resolve
242
+
243
+ refreshes configuration in `ssh/config.aws`.
244
+
245
+ ## Changes
246
+
247
+ See [RELEASES](RELEASES.md)
248
+
249
+ ## License
250
+
251
+ MIT
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative "../lib/cli/aws-ssh-resolver-cli"
4
+
5
+ Cli.start
@@ -0,0 +1 @@
1
+ require_relative "utils/logger"
@@ -0,0 +1,339 @@
1
+ require 'thor'
2
+ require 'json'
3
+ require_relative "../aws-ssh-resolver"
4
+
5
+ class Cli < Thor
6
+
7
+ include Utils::MyLogger # mix logger
8
+ PROGNAME = "main" # logger progname
9
+
10
+ # ------------------------------------------------------------------
11
+ # constanst
12
+ DEFAULT_SSH_CONFIG_FILE = "ssh/config.aws"
13
+ DEFAULT_SSH_CONFIG_INIT = "ssh/config.init"
14
+ MAGIC_START = "# +++ aws-ssh-resolver-cli update start here +++"
15
+ MAGIC_END = "# +++ aws-ssh-resolver-cli update end here +++"
16
+ DEFAULT_DESCRIBE_INSTANCES = "aws ec2 describe-instances --filters 'Name=tag-key,Values=Name'"
17
+ DEFAULT_HOST_TAG = "Name"
18
+
19
+ # ------------------------------------------------------------------
20
+ # constructore
21
+
22
+ def initialize(*args)
23
+ super
24
+ @logger = getLogger( PROGNAME, options )
25
+ end
26
+
27
+ # ------------------------------------------------------------------
28
+ # make two thor tasks share options?
29
+ # http://stackoverflow.com/questions/14346285/how-to-make-two-thor-tasks-share-options
30
+
31
+ class << self
32
+ def add_shared_option(name, options = {})
33
+ @shared_options = {} if @shared_options.nil?
34
+ @shared_options[name] = options
35
+ end
36
+
37
+ def shared_options(*option_names)
38
+ option_names.each do |option_name|
39
+ opt = @shared_options[option_name]
40
+ raise "Tried to access shared option '#{option_name}' but it was not previously defined" if opt.nil?
41
+ option option_name, opt
42
+ end
43
+ end
44
+ end
45
+
46
+ # # ------------------------------------------------------------------
47
+
48
+ class_option :log, :aliases => "-l", :type =>:string, :default => nil,
49
+ :enum => [ "DEBUG", "INFO", "WARN", "ERROR" ],
50
+ :desc => "Set debug level "
51
+
52
+ # ------------------------------------------------------------------
53
+
54
+ add_shared_option :ssh_config_file, :type => :string, :default => DEFAULT_SSH_CONFIG_FILE, :aliases => "-c",
55
+ :desc => "OpenSSH config file to update/create"
56
+
57
+ add_shared_option :ssh_config_init, :type => :string, :default => DEFAULT_SSH_CONFIG_INIT, :aliases => "-i",
58
+ :desc => "Initialize :ssh-config-file files with this file"
59
+
60
+
61
+ add_shared_option :describe_instances, :type => :string, :default => DEFAULT_DESCRIBE_INSTANCES, :aliases => "-d",
62
+ :desc => "aws command to query ec2 instances"
63
+
64
+ add_shared_option :host_tag, :type => :string, :default => DEFAULT_HOST_TAG, :aliases => "-h",
65
+ :desc => "Tag defining name of host"
66
+
67
+
68
+ # ------------------------------------------------------------------
69
+ # common instruction
70
+ long_desc_notice_on_ssh_config_init = <<-EOS
71
+
72
+ NOTICE: By default ':ssh-config-file' seeded from ':ssh-config-init',
73
+ if it does not exist. Create an empty ':ssh-config-init' file, or
74
+ pass an empty string to ':ssh-config-init' to avoid an error.
75
+
76
+ EOS
77
+
78
+
79
+ # ------------------------------------------------------------------
80
+ # resolver
81
+
82
+ desc "resolve <json-file>", "Create/update OpenSSH config file with AWS HostNames from a JSON document"
83
+
84
+
85
+ long_desc <<-LONGDESC
86
+
87
+ Updates ':ssh_config_file' (creates the file if it does not exist) with host/hostname
88
+ configuration parsed from 'json_file' (defaults to $stdin).
89
+
90
+ Entries in ':ssh_config_file' start and end with special tag-lines, which allow the tool
91
+ to replace host/hostanme entries with new values for each run.
92
+
93
+ #{long_desc_notice_on_ssh_config_init}
94
+
95
+ LONGDESC
96
+
97
+ shared_options :ssh_config_file
98
+ shared_options :ssh_config_init
99
+ shared_options :host_tag
100
+
101
+ def resolve( json_file="-" )
102
+
103
+ @logger.info( "#{__method__} starting, options '#{options}'" )
104
+
105
+ host_tag = options[:host_tag]
106
+ ssh_config_init = options[:ssh_config_init]
107
+ ssh_config_file = options[:ssh_config_file]
108
+ # puts( "options=#{options}" )
109
+
110
+ # raw data from aws
111
+ ec2_instances = get_ec2_instances( json_file )
112
+
113
+ # hash with host => hostname
114
+ host_hostname_mappings = create_host_hostname_mappings( ec2_instances, host_tag )
115
+
116
+ # seed 'ssh_config_file' with 'ssh_config_init'
117
+ init_ssh_config_file( ssh_config_file, ssh_config_init )
118
+
119
+ # output to file
120
+ output_to_file( ssh_config_file, host_hostname_mappings )
121
+
122
+
123
+ end
124
+
125
+ # ------------------------------------------------------------------
126
+ # aws-cli
127
+ desc "aws", "Create/update OpenSSH config file with AWS HostNames using aws Commad Line query"
128
+
129
+ long_desc <<-LONGDESC
130
+
131
+ Uses `aws` Command Line Interface to query ec2 information and parse host/hostname
132
+ information update/create ':ssh_config_file'.
133
+
134
+ #{long_desc_notice_on_ssh_config_init}
135
+
136
+ LONGDESC
137
+
138
+ shared_options :ssh_config_file
139
+ shared_options :ssh_config_init
140
+ shared_options :describe_instances
141
+ shared_options :host_tag
142
+
143
+ def aws()
144
+
145
+ host_tag = options[:host_tag]
146
+ ssh_config_file = options[:ssh_config_file]
147
+ ssh_config_init = options[:ssh_config_init]
148
+ describe_instances = options[:describe_instances]
149
+
150
+ # run aws-cli query
151
+ ec2_instances = aws_cli_ec2_instances( describe_instances )
152
+
153
+ # hash with host => hostname
154
+ host_hostname_mappings = create_host_hostname_mappings( ec2_instances, host_tag )
155
+
156
+ # seed 'ssh_config_file' with 'ssh_config_init'
157
+ init_ssh_config_file( ssh_config_file, ssh_config_init )
158
+
159
+ # output to file
160
+ output_to_file( ssh_config_file, host_hostname_mappings )
161
+
162
+ end
163
+
164
+ # ------------------------------------------------------------------
165
+ # reset
166
+
167
+ desc "reset", "Removes automatic entries in OpenSSH config file"
168
+
169
+
170
+ long_desc <<-LONGDESC
171
+
172
+ Removes automatic entries starting with with special tag-lines from ':ssh_config_file'.
173
+
174
+ Delete the file if it becomes empty
175
+
176
+ LONGDESC
177
+
178
+
179
+ shared_options :ssh_config_file
180
+
181
+ def reset( )
182
+
183
+ ssh_config_file = options[:ssh_config_file]
184
+ # Read content of (without magic content) ssh_config_file into memory
185
+ ssh_config_file_content = read_ssh_config_file_content_minus_magic( ssh_config_file )
186
+ if ssh_config_file_content.empty?
187
+ File.delete( ssh_config_file )
188
+ else
189
+ File.open( ssh_config_file, 'w') do |f2|
190
+ ssh_config_file_content.each do |line|
191
+ f2.puts line
192
+ end
193
+ end
194
+
195
+ end
196
+
197
+
198
+ end
199
+
200
+ # ------------------------------------------------------------------
201
+ # subrus
202
+
203
+ no_commands do
204
+
205
+ # return raw ec2 describe-status JSON
206
+ def aws_cli_ec2_instances( describe_instances )
207
+
208
+ @logger.info( "#{__method__} describe_instances '#{describe_instances}'" )
209
+
210
+ json_string = %x{ #{describe_instances} }
211
+ ec2_instances = parse_json( json_string )
212
+
213
+ @logger.debug( "#{__method__} describe_instances '#{describe_instances}' --> #{ec2_instances}" )
214
+
215
+ return ec2_instances
216
+
217
+ end
218
+
219
+
220
+ # return raw ec2 describe-status JSON
221
+ def get_ec2_instances( file )
222
+
223
+ @logger.info( "#{__method__} read file '#{file}'" )
224
+
225
+ json_string = ( file == "-" ? $stdin.readlines.join : File.read(file) )
226
+ ec2_instances = parse_json( json_string )
227
+
228
+ @logger.debug( "#{__method__} file '#{file}' --> #{ec2_instances}" )
229
+ return ec2_instances
230
+
231
+ end
232
+
233
+ def parse_json( json_string )
234
+
235
+ @logger.debug( "#{__method__} json_string '#{json_string}'" )
236
+
237
+ ec2_instances = JSON.parse( json_string )
238
+
239
+ return ec2_instances
240
+
241
+ end
242
+
243
+
244
+ # map raw aws ec2-describe-status json to hash with Host/PublicDnsName props
245
+ def create_host_hostname_mappings( ec2_instances, host_tag )
246
+
247
+ @logger.info( "#{__method__} host_tag '#{host_tag}'" )
248
+
249
+ host_hostname_mappings = ec2_instances['Reservations']
250
+ .map{ |i| i['Instances'].first }
251
+ .select{ |i| i['Tags'].select{ |t| t['Key'] == host_tag }.any? }
252
+ .map{ |i| {
253
+ :Host => i['Tags'].select{ |t| t['Key'] == host_tag }.first['Value'],
254
+ :HostName => i['PublicDnsName'] && !i['PublicDnsName'].empty? ? i['PublicDnsName'] : i['PrivateDnsName']
255
+ } }
256
+
257
+ @logger.info( "#{__method__} host_hostname_mappings '#{host_hostname_mappings}'" )
258
+ return host_hostname_mappings
259
+ end
260
+
261
+ # add 'host_hostname_mappings' to 'ssh_config_file'
262
+ def output_to_file( ssh_config_file, host_hostname_mappings )
263
+
264
+ # Read content of (without magic content) ssh_config_file into memory
265
+ ssh_config_file_content = read_ssh_config_file_content_minus_magic( ssh_config_file )
266
+
267
+ # write new magic with host entries
268
+ File.open( ssh_config_file, 'w') do |f2|
269
+
270
+ f2.puts MAGIC_START
271
+ f2.puts <<-EOS
272
+
273
+ # Content generated #{Time.now.strftime("%Y-%m-%d-%H:%M:%S")}
274
+
275
+ EOS
276
+
277
+ host_hostname_mappings.each do |h|
278
+ host_entry = <<EOS
279
+ host #{h[:Host]}
280
+ HostName #{h[:HostName]}
281
+
282
+
283
+ EOS
284
+ f2.puts host_entry
285
+ end
286
+
287
+ f2.puts MAGIC_END
288
+
289
+ ssh_config_file_content.each do |line|
290
+ f2.puts line
291
+ end
292
+
293
+ end
294
+
295
+ end
296
+
297
+ # copy 'ssh_config_init' to 'ssh_config_file' - if it does not
298
+ # exist && 'ssh_config_init' define
299
+ def init_ssh_config_file( ssh_config_file, ssh_config_init )
300
+ File.open( ssh_config_file, 'w') { |f| f.write(File.read(ssh_config_init )) } if !File.exist?( ssh_config_file ) &&
301
+ ssh_config_init && !ssh_config_init.empty?
302
+ end
303
+
304
+ # read ssh_config from file/$stdin, remove old magic
305
+ def read_ssh_config_file_content_minus_magic( ssh_config_file )
306
+
307
+ ssh_config_file_content = File.exist?( ssh_config_file ) ? File.readlines( ssh_config_file ) : []
308
+
309
+ # remove old magic
310
+ within_magic = false
311
+ ssh_config_file_content = ssh_config_file_content.select do |line|
312
+ ret = case within_magic
313
+ when true
314
+ if line.chomp == MAGIC_END then
315
+ within_magic = false
316
+ end
317
+ false
318
+ when false
319
+ if line.chomp == MAGIC_START then
320
+ within_magic = true
321
+ end
322
+ (line.chomp == MAGIC_START ? false : true)
323
+ end
324
+ ret
325
+ end
326
+
327
+ return ssh_config_file_content
328
+
329
+ end
330
+
331
+
332
+
333
+
334
+ end # no_task
335
+
336
+
337
+
338
+
339
+ end # class
@@ -0,0 +1,70 @@
1
+ require 'logger'
2
+
3
+ # see http://hawkins.io/2013/08/using-the-ruby-logger/
4
+
5
+ module Utils
6
+
7
+ module MyLogger
8
+
9
+ # no logging done
10
+
11
+ class NullLoger < Logger
12
+ def initialize(*args)
13
+ end
14
+
15
+ def add(*args, &block)
16
+ end
17
+ end
18
+
19
+ LOGFILE="aws-ssh-resolver.log"
20
+
21
+ def getLogger( progname, options={} )
22
+
23
+ level = get_level( options )
24
+
25
+ if level.nil?
26
+
27
+ return NullLoger.new
28
+
29
+ else
30
+
31
+ logger = Logger.new( LOGFILE )
32
+ logger.level=level
33
+ logger.progname = progname
34
+ return logger
35
+
36
+ end
37
+
38
+ end # getLogger
39
+
40
+ # ------------------------------------------------------------------
41
+ private
42
+
43
+ def get_level( options )
44
+
45
+ # puts "#{__method__}: options=#{options}"
46
+
47
+ level_name = options && options[:log] ? options[:log] : ENV['LOG_LEVEL']
48
+
49
+ level = case level_name
50
+ when 'warn', 'WARN'
51
+ Logger::WARN
52
+ when 'info', 'INFO'
53
+ Logger::INFO
54
+ when 'debug', 'DEBUG'
55
+ Logger::DEBUG
56
+ when 'error', 'ERROR'
57
+ Logger::ERROR
58
+ else
59
+ nil
60
+ end
61
+
62
+ return level
63
+ end
64
+
65
+
66
+
67
+ end
68
+
69
+
70
+ end
@@ -0,0 +1,183 @@
1
+ require_relative "spec_helper"
2
+
3
+
4
+
5
+ describe Cli do
6
+
7
+ # ------------------------------------------------------------------
8
+ # constants
9
+ cmd = ""
10
+
11
+ # ------------------------------------------------------------------
12
+ # framework
13
+ describe "rspec framework" do
14
+ it "#works" do
15
+ expect( 1 ).to eql( 1 )
16
+ end
17
+ end
18
+
19
+ # ------------------------------------------------------------------
20
+ # interface
21
+ describe "interface" do
22
+
23
+ before :each do
24
+ @sut = Cli.new
25
+ end
26
+
27
+ it "#defines resolve" do
28
+ expect( @sut ).to respond_to( :resolve )
29
+ end
30
+
31
+ end
32
+
33
+ # ------------------------------------------------------------------
34
+ # help && options
35
+ interfaces = [
36
+ {
37
+ :command => "resolve",
38
+ :options => [
39
+ { :long=>"--log", :short => "-l"},
40
+ { :long=>"--ssh-config-file", :short => "-c"},
41
+ { :long=>"--ssh-config-init", :short => "-i"},
42
+ { :long=>"--host-tag", :short => "-h"},
43
+ ]
44
+ },
45
+ {
46
+ :command => "aws",
47
+ :options => [
48
+ { :long=>"--log", :short => "-l"},
49
+ { :long=>"--ssh-config-file", :short => "-c"},
50
+ { :long=>"--describe-instances", :short => "-d"},
51
+ { :long=>"--ssh-config-init", :short => "-i"},
52
+ { :long=>"--host-tag", :short => "-h"},
53
+ ]
54
+ },
55
+ {
56
+ :command => "reset",
57
+ :options => [
58
+ { :long=>"--log", :short => "-l"},
59
+ { :long=>"--ssh-config-file", :short => "-c"},
60
+ ]
61
+ },
62
+ ]
63
+
64
+ interfaces.each do |i|
65
+
66
+ describe "help #{i[:command]}" do
67
+
68
+ before :all do
69
+ @output = with_captured_stdout { Cli.start(["help", i[:command]]) }
70
+ end
71
+
72
+ i[:options].each do |o|
73
+
74
+ it "#option short #{o[:short]}" do
75
+ expect( @output ).to match( /#{o[:short]}/ )
76
+ end
77
+
78
+ it "#option short #{o[:long]}" do
79
+ expect( @output ).to match( /#{o[:long]}/ )
80
+ end
81
+
82
+
83
+ end # options
84
+
85
+ end
86
+
87
+ end # interfaces each
88
+
89
+
90
+ # ------------------------------------------------------------------
91
+ # resolve
92
+ command = "resolve"
93
+
94
+ describe "command '#{command}'" do
95
+
96
+ context "file" do
97
+
98
+ json_file = "spec/cli/fixtures/fixture1.json"
99
+
100
+ ssh_config_filename = Cli::DEFAULT_SSH_CONFIG_FILE
101
+
102
+ before :each do
103
+ @dbl_ssh_config_file = double( "ssh-config-file" )
104
+ expect( File ).to receive( :open ).with( ssh_config_filename, "w").and_yield( @dbl_ssh_config_file )
105
+ allow( @dbl_ssh_config_file ).to receive( :puts ).with( kind_of( String ))
106
+ end
107
+
108
+
109
+ context "when ':ssh-config-file' does not exist" do
110
+
111
+ before :each do
112
+ expect( File ).to receive( :exist? ).with( ssh_config_filename).and_return( false )
113
+ expect( File ).no_to receive( :readlines? ).with( ssh_config_filename )
114
+ end
115
+
116
+ end # context "when ':ssh-config-file' does not exist" do
117
+
118
+ context "when ':ssh-config-file' does exists" do
119
+
120
+ before :each do
121
+ @ssh_config_file_lines= [ "line 1", "line2" ]
122
+ expect( File ).to receive( :exist? ).twice.with( ssh_config_filename).and_return( true )
123
+ end
124
+
125
+ context "when NO previous resolves" do
126
+
127
+ before :each do
128
+ expect( File ).to receive( :readlines ).with( ssh_config_filename).and_return( @ssh_config_file_lines )
129
+ end
130
+
131
+ it "writes existing lines to ssh-config file" do
132
+ expect( @dbl_ssh_config_file ).to receive( :puts ).once.with( Cli::MAGIC_START ).ordered
133
+ expect( @dbl_ssh_config_file ).to receive( :puts ).twice.with( /^host\s+\w+\s*\n\s*HostName\s+\w?/ ).ordered
134
+ expect( @dbl_ssh_config_file ).to receive( :puts ).once.with( Cli::MAGIC_END ).ordered
135
+
136
+ @ssh_config_file_lines.each do |line|
137
+ expect( @dbl_ssh_config_file ).to receive( :puts ).with( line ).ordered
138
+ end
139
+
140
+ Cli.start( [command, json_file] )
141
+ end
142
+
143
+ end # context "when NO previous resolves"
144
+
145
+ context "when previous resolves" do
146
+
147
+ before :each do
148
+ content = [ Cli::MAGIC_START, "old magic", Cli::MAGIC_END ] + @ssh_config_file_lines
149
+ expect( File ).to receive( :readlines ).with( ssh_config_filename).and_return( content )
150
+ end
151
+
152
+ it "removes previous lines between MAGIC_START - MAGIC_END " do
153
+
154
+ expect( @dbl_ssh_config_file ).to receive( :puts ).once.with( Cli::MAGIC_START ).ordered
155
+ expect( @dbl_ssh_config_file ).to receive( :puts ).with( /^host\s+\w+\s*\n\s*HostName\s+\w?/ ).twice.ordered
156
+ expect( @dbl_ssh_config_file ).to receive( :puts ).once.with( Cli::MAGIC_END ).ordered
157
+
158
+ @ssh_config_file_lines.each do |line|
159
+ expect( @dbl_ssh_config_file ).to receive( :puts ).with( line ).ordered
160
+ end
161
+
162
+ Cli.start( [command, json_file] )
163
+ end
164
+
165
+
166
+ end
167
+
168
+ end # context "when ':ssh-config-file' does exists" do
169
+
170
+
171
+ # it "#writes to file" do
172
+ #
173
+ # expect( File ).to receive( :read ).with( file ).and_call_original
174
+ # expect( 1 ).to eql( 1 )
175
+ # end
176
+
177
+ end # context "file" do
178
+
179
+ end # describe "resolve" do
180
+
181
+
182
+
183
+ end
@@ -0,0 +1,237 @@
1
+ {
2
+ "Reservations": [
3
+ {
4
+ "OwnerId": "9999999999999",
5
+ "ReservationId": "r-0144b6f8",
6
+ "Groups": [],
7
+ "Instances": [
8
+ {
9
+ "Monitoring": {
10
+ "State": "disabled"
11
+ },
12
+ "PublicDnsName": "ec2-52-19-100-250.eu-west-1.compute.amazonaws.com",
13
+ "State": {
14
+ "Code": 16,
15
+ "Name": "running"
16
+ },
17
+ "EbsOptimized": false,
18
+ "LaunchTime": "2015-09-05T23:43:42.000Z",
19
+ "PublicIpAddress": "52.19.100.250",
20
+ "PrivateIpAddress": "10.0.0.16",
21
+ "ProductCodes": [],
22
+ "VpcId": "vpc-1025be75",
23
+ "StateTransitionReason": "",
24
+ "InstanceId": "i-9d460030",
25
+ "ImageId": "ami-d74437a0",
26
+ "PrivateDnsName": "ip-10-0-0-16.eu-west-1.compute.internal",
27
+ "KeyName": "demo-key",
28
+ "SecurityGroups": [
29
+ {
30
+ "GroupName": "suite2-MyDefaultSecurityGroup-JQ3LNYV0UMSP",
31
+ "GroupId": "sg-2bd02f4f"
32
+ }
33
+ ],
34
+ "ClientToken": "suite-myFro-1PI0COCRIL0BN",
35
+ "SubnetId": "subnet-678e193e",
36
+ "InstanceType": "t2.micro",
37
+ "NetworkInterfaces": [
38
+ {
39
+ "Status": "in-use",
40
+ "MacAddress": "0a:cf:49:ff:1e:49",
41
+ "SourceDestCheck": true,
42
+ "VpcId": "vpc-1025be75",
43
+ "Description": "",
44
+ "Association": {
45
+ "PublicIp": "52.19.100.250",
46
+ "PublicDnsName": "ec2-52-19-100-250.eu-west-1.compute.amazonaws.com",
47
+ "IpOwnerId": "amazon"
48
+ },
49
+ "NetworkInterfaceId": "eni-c0b4629b",
50
+ "PrivateIpAddresses": [
51
+ {
52
+ "PrivateDnsName": "ip-10-0-0-16.eu-west-1.compute.internal",
53
+ "Association": {
54
+ "PublicIp": "52.19.100.250",
55
+ "PublicDnsName": "ec2-52-19-100-250.eu-west-1.compute.amazonaws.com",
56
+ "IpOwnerId": "amazon"
57
+ },
58
+ "Primary": true,
59
+ "PrivateIpAddress": "10.0.0.16"
60
+ }
61
+ ],
62
+ "PrivateDnsName": "ip-10-0-0-16.eu-west-1.compute.internal",
63
+ "Attachment": {
64
+ "Status": "attached",
65
+ "DeviceIndex": 0,
66
+ "DeleteOnTermination": true,
67
+ "AttachmentId": "eni-attach-9364c5d6",
68
+ "AttachTime": "2015-09-05T23:43:42.000Z"
69
+ },
70
+ "Groups": [
71
+ {
72
+ "GroupName": "suite2-MyDefaultSecurityGroup-JQ3LNYV0UMSP",
73
+ "GroupId": "sg-2bd02f4f"
74
+ }
75
+ ],
76
+ "SubnetId": "subnet-678e193e",
77
+ "OwnerId": "9999999999999",
78
+ "PrivateIpAddress": "10.0.0.16"
79
+ }
80
+ ],
81
+ "SourceDestCheck": true,
82
+ "Placement": {
83
+ "Tenancy": "default",
84
+ "GroupName": "",
85
+ "AvailabilityZone": "eu-west-1c"
86
+ },
87
+ "Hypervisor": "xen",
88
+ "BlockDeviceMappings": [
89
+ {
90
+ "DeviceName": "/dev/sda1",
91
+ "Ebs": {
92
+ "Status": "attached",
93
+ "DeleteOnTermination": true,
94
+ "VolumeId": "vol-29df06e6",
95
+ "AttachTime": "2015-09-05T23:43:44.000Z"
96
+ }
97
+ }
98
+ ],
99
+ "Architecture": "x86_64",
100
+ "RootDeviceType": "ebs",
101
+ "RootDeviceName": "/dev/sda1",
102
+ "VirtualizationType": "hvm",
103
+ "Tags": [
104
+ {
105
+ "Value": "myFront1",
106
+ "Key": "Name"
107
+ },
108
+ {
109
+ "Value": "myFront1",
110
+ "Key": "aws:cloudformation:logical-id"
111
+ },
112
+ {
113
+ "Value": "arn:aws:cloudformation:eu-west-1:9999999999999:stack/suite2/cbabcd80-5427-11e5-a493-50d500fa4218",
114
+ "Key": "aws:cloudformation:stack-id"
115
+ },
116
+ {
117
+ "Value": "suite2",
118
+ "Key": "aws:cloudformation:stack-name"
119
+ }
120
+ ],
121
+ "AmiLaunchIndex": 0
122
+ }
123
+ ]
124
+ },
125
+ {
126
+ "OwnerId": "9999999999999",
127
+ "ReservationId": "r-9145b768",
128
+ "Groups": [],
129
+ "Instances": [
130
+ {
131
+ "Monitoring": {
132
+ "State": "disabled"
133
+ },
134
+ "PublicDnsName": "",
135
+ "State": {
136
+ "Code": 16,
137
+ "Name": "running"
138
+ },
139
+ "EbsOptimized": false,
140
+ "LaunchTime": "2015-09-05T23:43:42.000Z",
141
+ "PrivateIpAddress": "10.0.1.76",
142
+ "ProductCodes": [],
143
+ "VpcId": "vpc-1025be75",
144
+ "StateTransitionReason": "",
145
+ "InstanceId": "i-52387eff",
146
+ "ImageId": "ami-d74437a0",
147
+ "PrivateDnsName": "ip-10-0-1-76.eu-west-1.compute.internal",
148
+ "KeyName": "demo-key",
149
+ "SecurityGroups": [
150
+ {
151
+ "GroupName": "suite2-MyDefaultSecurityGroup-JQ3LNYV0UMSP",
152
+ "GroupId": "sg-2bd02f4f"
153
+ }
154
+ ],
155
+ "ClientToken": "suite-myBac-Q9KBSFHUQADW",
156
+ "SubnetId": "subnet-668e193f",
157
+ "InstanceType": "t2.micro",
158
+ "NetworkInterfaces": [
159
+ {
160
+ "Status": "in-use",
161
+ "MacAddress": "0a:a1:53:0b:f5:f5",
162
+ "SourceDestCheck": true,
163
+ "VpcId": "vpc-1025be75",
164
+ "Description": "",
165
+ "NetworkInterfaceId": "eni-c5b4629e",
166
+ "PrivateIpAddresses": [
167
+ {
168
+ "PrivateDnsName": "ip-10-0-1-76.eu-west-1.compute.internal",
169
+ "Primary": true,
170
+ "PrivateIpAddress": "10.0.1.76"
171
+ }
172
+ ],
173
+ "PrivateDnsName": "ip-10-0-1-76.eu-west-1.compute.internal",
174
+ "Attachment": {
175
+ "Status": "attached",
176
+ "DeviceIndex": 0,
177
+ "DeleteOnTermination": true,
178
+ "AttachmentId": "eni-attach-3e64c57b",
179
+ "AttachTime": "2015-09-05T23:43:42.000Z"
180
+ },
181
+ "Groups": [
182
+ {
183
+ "GroupName": "suite2-MyDefaultSecurityGroup-JQ3LNYV0UMSP",
184
+ "GroupId": "sg-2bd02f4f"
185
+ }
186
+ ],
187
+ "SubnetId": "subnet-668e193f",
188
+ "OwnerId": "9999999999999",
189
+ "PrivateIpAddress": "10.0.1.76"
190
+ }
191
+ ],
192
+ "SourceDestCheck": true,
193
+ "Placement": {
194
+ "Tenancy": "default",
195
+ "GroupName": "",
196
+ "AvailabilityZone": "eu-west-1c"
197
+ },
198
+ "Hypervisor": "xen",
199
+ "BlockDeviceMappings": [
200
+ {
201
+ "DeviceName": "/dev/sda1",
202
+ "Ebs": {
203
+ "Status": "attached",
204
+ "DeleteOnTermination": true,
205
+ "VolumeId": "vol-40de078f",
206
+ "AttachTime": "2015-09-05T23:43:45.000Z"
207
+ }
208
+ }
209
+ ],
210
+ "Architecture": "x86_64",
211
+ "RootDeviceType": "ebs",
212
+ "RootDeviceName": "/dev/sda1",
213
+ "VirtualizationType": "hvm",
214
+ "Tags": [
215
+ {
216
+ "Value": "arn:aws:cloudformation:eu-west-1:9999999999999:stack/suite2/cbabcd80-5427-11e5-a493-50d500fa4218",
217
+ "Key": "aws:cloudformation:stack-id"
218
+ },
219
+ {
220
+ "Value": "suite2",
221
+ "Key": "aws:cloudformation:stack-name"
222
+ },
223
+ {
224
+ "Value": "myBack1",
225
+ "Key": "aws:cloudformation:logical-id"
226
+ },
227
+ {
228
+ "Value": "myBack1",
229
+ "Key": "Name"
230
+ }
231
+ ],
232
+ "AmiLaunchIndex": 0
233
+ }
234
+ ]
235
+ }
236
+ ]
237
+ }
@@ -0,0 +1,21 @@
1
+ require_relative "../../lib/cli/aws-ssh-resolver-cli"
2
+
3
+ RSpec.configure do |config|
4
+
5
+
6
+ # http://stackoverflow.com/questions/14987362/how-can-i-capture-stdout-to-a-string
7
+ def with_captured_stdout
8
+ begin
9
+ old_stdout = $stdout
10
+ $stdout = StringIO.new('','w')
11
+ $stdout.sync = true
12
+ yield
13
+ $stdout.string
14
+ ensure
15
+ $stdout = old_stdout
16
+ $stdout.sync = true
17
+ end
18
+ end
19
+
20
+
21
+ end # RSpec.configure do |config|
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: aws-ssh-resolver
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - jarjuk
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-09-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: thor
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.18'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.18'
27
+ - !ruby/object:Gem::Dependency
28
+ name: json
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: |2
42
+ Update OpenSSH config file to map EC2 instance name in CloudFormation
43
+ to DNS-name on Amazon platform.
44
+ email:
45
+ executables:
46
+ - aws-ssh-resolver.rb
47
+ extensions: []
48
+ extra_rdoc_files: []
49
+ files:
50
+ - README.md
51
+ - bin/aws-ssh-resolver.rb
52
+ - lib/aws-ssh-resolver.rb
53
+ - lib/cli/aws-ssh-resolver-cli.rb
54
+ - lib/utils/logger.rb
55
+ - spec/cli/aws-ssh-resolver-cli_spec.rb
56
+ - spec/cli/fixtures/fixture1.json
57
+ - spec/cli/spec_helper.rb
58
+ homepage: https://github.com/jarjuk/aws-ssh-resolver
59
+ licenses:
60
+ - MIT
61
+ metadata: {}
62
+ post_install_message:
63
+ rdoc_options: []
64
+ require_paths:
65
+ - lib
66
+ required_ruby_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ requirements: []
77
+ rubyforge_project:
78
+ rubygems_version: 2.2.2
79
+ signing_key:
80
+ specification_version: 4
81
+ summary: Update OpenSSH config with CloudFormation EC2 instance DNS names'
82
+ test_files: []
83
+ has_rdoc: