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.
- data/README.md +56 -0
- data/bin/hostnamer +14 -0
- data/lib/hostnamer/hostnamer.rb +268 -0
- 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: []
|