hostnamer 1.0.0

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.
Files changed (4) hide show
  1. data/README.md +56 -0
  2. data/bin/hostnamer +14 -0
  3. data/lib/hostnamer/hostnamer.rb +268 -0
  4. metadata +90 -0
data/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # Introduction
2
+
3
+ Hostnamer is a cluster member discovery and registration tool for Route 53. It discovers other cluster members using an incremental DNS search and self registers with a unique identifier.
4
+
5
+ ## Usage
6
+
7
+ ```
8
+ $ hostnamer -n adops -t prod,virginia -Z XYZ
9
+ # adops-prod-virginia-00
10
+
11
+ $ hostnamer --help
12
+
13
+ Usage: hostnamer [options]
14
+ -Z, --zone-id ZONEID Route 53 zone id
15
+ -n, --cluster-name [NAME] Name of the cluster. Defaults to first chef role found under /etc/chef/node.json
16
+ -j, --json-attributes [PATH] Chef json attributes file. Defaults to /etc/chef/node.json
17
+ -t, --tags [TAG,TAG] Tags to postpend, eg: --tags production,california
18
+ -p, --profile [PROFILE] AWS user profile. Uses the current IAM or the default profile located under ~/.aws
19
+ -r, --retries [RETRIES] Number of times to retry before failing. Defaults to 3
20
+ -w, --retry-wait SECONDS Retry wait time. Defaults to 10s
21
+ -v, --[no-]verbose Run verbosely
22
+ --version Show version
23
+ ```
24
+
25
+ ## Installation
26
+
27
+ ### Ubuntu/Debian
28
+
29
+ ```
30
+ wget https://s3.amazonaws.com/demandbase-pkgs-public/hostnamer_1.0.0_all.deb
31
+ sudo dpkg --install hostnamer_1.0.0_all.deb
32
+ sudo apt-get update -y && apt-get -f install # install any missing dependencies
33
+ ```
34
+
35
+ ### RubyGem
36
+
37
+ ```
38
+ gem install hostnamer
39
+ ```
40
+
41
+ ## Development
42
+
43
+ ### Publishing
44
+
45
+ ```
46
+ $ rake package # packages .deb and .gem
47
+ $ rake publish # publishes to s3
48
+ ```
49
+
50
+ ## Contributing
51
+
52
+ 1. Fork it
53
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
54
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
55
+ 4. Push to the branch (`git push origin my-new-feature`)
56
+ 5. Create new Pull Request
data/bin/hostnamer ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ require File.expand_path('../../lib/hostnamer/hostnamer', __FILE__)
3
+ $stdout.sync
4
+
5
+ begin
6
+ options = Hostnamer.parse_options(ARGV)
7
+ ENV['HOSTNAMER_VERBOSE'] = '1' if options[:verbose]
8
+ $stdout.puts Hostnamer.run(options)
9
+ rescue => e
10
+ raise e if ENV['DEBUG']
11
+ abort "hostnamer: failed: #{e.message}"
12
+ ensure
13
+ ENV.delete('HOSTNAMER_VERBOSE')
14
+ end
@@ -0,0 +1,268 @@
1
+ require 'socket'
2
+ require 'syslog'
3
+ require 'timeout'
4
+ require 'tempfile'
5
+ require 'optparse'
6
+
7
+ module Hostnamer
8
+ module_function
9
+ VERSION = "1.0.0"
10
+ class DNSInsertFailed < RuntimeError; end
11
+ class DNSQueryFailed < RuntimeError; end
12
+
13
+ def run(opts)
14
+ log "run" do
15
+ zoneid = opts[:zone_id] or raise "zone id must be present, specify using --zone-id ZONEID"
16
+ tags = opts[:tags] || []
17
+ name = opts[:cluster_name] || detect_first_chef_role(opts[:json_attrs]) || 'instance'
18
+ retries = opts[:retries] || 3
19
+ retrywait = opts[:retry_wait] || 10
20
+ profile = opts[:profile]
21
+ ip = detect_ip
22
+ tags.unshift(name)
23
+
24
+ retrying(retries) do
25
+ begin
26
+ host, domain = determine_available_host(tags, zoneid, profile)
27
+ add_host_record("#{host}.#{domain}", ip, zoneid, profile)
28
+ return host
29
+ rescue Exception => e
30
+ log "regisration failed.. retrying in #{retrywait}s"
31
+ sleep retrywait
32
+ raise e
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ def detect_first_chef_role(nodefile)
39
+ log "detect_first_chef_role" do
40
+ begin
41
+ if node_data = File.open(nodefile).read.strip
42
+ role = node_data.scan(/role\[([a-z\_\-\d]+)\]/).flatten[0]
43
+ if role
44
+ log("detected chef role #{role}")
45
+ role = role.gsub(/\_/,"-")
46
+ else
47
+ log("role is not present in #{nodefile}")
48
+ end
49
+ role
50
+ end
51
+ rescue Exception => e
52
+ raise "#{nodefile} is not found or invalid. Specify a cluster name using --cluster-name"
53
+ end
54
+ end
55
+ end
56
+
57
+ def detect_ip
58
+ log "detect_ip" do
59
+ if first_private_ip = Socket.ip_address_list.detect{|intf| intf.ipv4_private?}
60
+ ip = first_private_ip.ip_address
61
+ elsif first_public_ip = Socket.ip_address_list.detect{|intf| intf.ipv4? and !intf.ipv4_loopback? and !intf.ipv4_multicast? and !intf.ipv4_private?}
62
+ ip = first_public_ip.ip_address
63
+ else
64
+ nil
65
+ end
66
+ log "detected ip: #{ip}"
67
+ ip
68
+ end
69
+ end
70
+
71
+ def determine_available_host(tags, zoneid, profile=nil)
72
+ log "determining_available_host" do
73
+ n = 0
74
+ domain = detect_domain(zoneid, profile)
75
+ records = list_record_sets(zoneid, profile)
76
+ host = (tags + ["%02d" % n]).join('-')
77
+ while records.include?("#{host}.#{domain}") do
78
+ log "checking availability for #{host}.#{domain}"
79
+ host = (tags + ["%02d" % n+=1]).join('-')
80
+ end
81
+ log "#{host}.#{domain} is available"
82
+ [host, domain]
83
+ end
84
+ end
85
+
86
+ def detect_domain(zoneid, profile=nil)
87
+ log "detect_domain" do
88
+ cmd = "aws route53 get-hosted-zone --id #{zoneid} --output text"
89
+ cmd = "#{cmd} --profile #{profile}" if profile
90
+ debug "exec: #{cmd}"
91
+ result = `#{cmd} | grep HOSTEDZONE | awk '{print $4}'`.strip
92
+ log "detected domain: #{result}"
93
+ raise DNSQueryFailed unless $?.exitstatus.zero?
94
+ result
95
+ end
96
+ end
97
+
98
+ def list_record_sets(zoneid, profile=nil)
99
+ log "list_record_sets" do
100
+ cmd = "aws route53 list-resource-record-sets --hosted-zone-id #{zoneid} --output text"
101
+ cmd = "#{cmd} --profile #{profile}" if profile
102
+ debug "exec: #{cmd}"
103
+ records = `#{cmd} | grep RESOURCERECORDSETS | awk '{print $2}'`.strip
104
+ raise DNSQueryFailed unless $?.exitstatus.zero?
105
+ records.split
106
+ end
107
+ end
108
+
109
+ def record_unavailable?(record)
110
+ `dig #{record} +short`.strip != ''
111
+ end
112
+
113
+ def add_host_record(host, ip, zoneid, profile = nil)
114
+ log "add_host_record: #{host}" do
115
+ payload = Tempfile.new('payload')
116
+ payload.write %Q(
117
+ {
118
+ "Comment": "Added by hostnamer during instance bootstrap",
119
+ "Changes": [
120
+ {
121
+ "Action": "CREATE",
122
+ "ResourceRecordSet": {
123
+ "Name": "#{host}",
124
+ "Type": "A",
125
+ "TTL": 300,
126
+ "ResourceRecords": [
127
+ {
128
+ "Value": "#{ip}"
129
+ }
130
+ ]
131
+ }
132
+ }
133
+ ]
134
+ })
135
+ payload.close
136
+ cmd = 'aws', 'route53', 'change-resource-record-sets', '--hosted-zone-id', zoneid, '--change-batch', "file://#{payload.path}"
137
+ cmd += ['--profile', profile] if profile
138
+ log "exec: #{cmd.join(' ')}"
139
+ Process.wait(spawn(cmd.join(' '), :out => '/dev/null', :err => '/dev/null'))
140
+ raise DNSInsertFailed unless $?.exitstatus.zero?
141
+ end
142
+ end
143
+
144
+ def retrying(n)
145
+ begin
146
+ yield
147
+ rescue => e
148
+ n = n - 1
149
+ (debug "retrying #{n} times" and retry) if n > 0
150
+ raise e
151
+ end
152
+ end
153
+
154
+ def debug(msg, doprint = false)
155
+ msg = "hostnamer: #{msg}"
156
+ if ENV['HOSTNAMER_VERBOSE']
157
+ if doprint
158
+ $stdout.print(msg)
159
+ else
160
+ $stdout.puts(msg)
161
+ end
162
+ end
163
+ end
164
+
165
+ def log(msg, &block)
166
+ if block
167
+ start = Time.now
168
+ res = nil
169
+ log "#{msg} at=start"
170
+ begin
171
+ res = yield
172
+ rescue => e
173
+ log_error "#{msg} elapsed=#{Time.now - start}", e
174
+ raise e
175
+ end
176
+ log "hostnamer: #{msg} at=finish elapsed=#{Time.now - start}"
177
+ res
178
+ else
179
+ debug(msg)
180
+ Syslog.open("hostnamer", Syslog::LOG_PID, Syslog::LOG_DAEMON | Syslog::LOG_LOCAL3)
181
+ Syslog.log(Syslog::LOG_INFO, msg)
182
+ Syslog.close
183
+ end
184
+ end
185
+
186
+ def log_error(msg, e, dofail = false)
187
+ debug "error #{msg}"
188
+ Syslog.open("hostnamer", Syslog::LOG_PID, Syslog::LOG_DAEMON | Syslog::LOG_LOCAL3)
189
+ Syslog.log Syslog::LOG_ERR, "#{msg} at=error class='#{e.class}' message='#{e.message}'"
190
+ Syslog.close
191
+ end
192
+
193
+ def parse_options(argv)
194
+ options = {}
195
+ options[:json_attrs] = "/etc/chef/node.json"
196
+ parser = OptionParser.new do |opts|
197
+ opts.banner = "Usage: hostnamer [options]"
198
+ opts.on "-Z", "--zone-id ZONEID", "Route 53 zone id" do |z|
199
+ options[:zone_id] = z
200
+ end
201
+ opts.on "-n", "--cluster-name [NAME]", "Name of the cluster. Defaults to first chef role found under #{options[:json_attrs]}" do |c|
202
+ options[:cluster_name] = c
203
+ end
204
+ opts.on "-j", "--json-attributes [PATH]", "Chef json attributes file. Defaults to #{options[:json_attrs]}" do |j|
205
+ options[:json_attrs] = j
206
+ end
207
+ opts.on "-t", "--tags [TAG,TAG]", Array, "Tags to postpend, eg: --tags production,california" do |tags|
208
+ options[:tags] = tags
209
+ end
210
+ opts.on "-p", "--profile [PROFILE]", "AWS user profile. Uses the current IAM or the default profile located under ~/.aws" do |p|
211
+ options[:profile] = p
212
+ end
213
+ opts.on "-r", "--retries [RETRIES]", "Number of times to retry before failing. Defaults to 3" do |r|
214
+ options[:retries] = r
215
+ end
216
+ opts.on "-w", "--retry-wait SECONDS", "Retry wait time. Defaults to 10s" do |w|
217
+ opptions[:retry_wait] = w
218
+ end
219
+ opts.on("-v", "--[no-]verbose", "Run verbosely") do |v|
220
+ options[:verbose] = v
221
+ end
222
+ opts.on_tail("--version", "Show version") do
223
+ puts Hostnamer::VERSION
224
+ exit
225
+ end
226
+ end
227
+ begin
228
+ parser.parse!(argv)
229
+ rescue OptionParser::InvalidOption => e
230
+ raise "#{e.message}\n\n#{parser.help}"
231
+ end
232
+ options
233
+ end
234
+
235
+ # depricated methods
236
+
237
+ def detect_ns_zoneid(domain)
238
+ log "detect_ns_zoneid" do
239
+ if zid = `dig #{domain} txt +short | grep 'zone_id' | awk '{print $2}' | sed 's/\"//'`
240
+ if zid != ''
241
+ log "detected zone id: #{zid}"
242
+ zid.strip
243
+ else
244
+ raise "could not detect zone_id from #{domain} because \"zone_id\" TXT record is missing. Either insert a TXT record in the DNS or specify --zone-id ZONEID"
245
+ nil
246
+ end
247
+ end
248
+ end
249
+ end
250
+
251
+ def detect_soa_ttl(domain)
252
+ log "detect_soa_ttl" do
253
+ if answer = `dig +nocmd +noall +answer soa #{domain}`
254
+ # get the TTL of SOA record. The answer will look something like
255
+ # demandbase.co. 60 IN SOA ns-1659.awsdns-15.co.uk. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400
256
+ ttl = answer.split(' ')[1]
257
+ log "ttl is #{ttl}"
258
+ ttl
259
+ end
260
+ end
261
+ end
262
+
263
+ def detect_region
264
+ log "detect region" do
265
+ aws_region=`curl -s http://169.254.169.254/latest/meta-data/placement/availability-zone | grep -Po "(us|sa|eu|ap)-(north|south)?(east|west)?-[0-9]+"`
266
+ end
267
+ end
268
+ end
metadata ADDED
@@ -0,0 +1,90 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hostnamer
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Greg Osuri
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-11-06 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rake
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ description: Hostnamer is a cluster member discovery and registration tool for Route
47
+ 53. It discovers other cluster members using an incremental DNS search and self
48
+ registers with a unique identifier.
49
+ email:
50
+ - gosuri@gmail.com
51
+ executables:
52
+ - hostnamer
53
+ extensions: []
54
+ extra_rdoc_files: []
55
+ files:
56
+ - README.md
57
+ - bin/hostnamer
58
+ - lib/hostnamer/hostnamer.rb
59
+ homepage: https://github.com/gosuri/hostnamer
60
+ licenses:
61
+ - MIT
62
+ post_install_message:
63
+ rdoc_options: []
64
+ require_paths:
65
+ - lib
66
+ required_ruby_version: !ruby/object:Gem::Requirement
67
+ none: false
68
+ requirements:
69
+ - - ! '>='
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ segments:
73
+ - 0
74
+ hash: -333262311511193171
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ none: false
77
+ requirements:
78
+ - - ! '>='
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ segments:
82
+ - 0
83
+ hash: -333262311511193171
84
+ requirements: []
85
+ rubyforge_project:
86
+ rubygems_version: 1.8.23
87
+ signing_key:
88
+ specification_version: 3
89
+ summary: cluster member discovery and registration tool for Route 53
90
+ test_files: []