aws-ssh-resolver 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: