mattly-slicehost-dns 0.3

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/LICENSE ADDED
@@ -0,0 +1,18 @@
1
+ Copyright (c) 2009 Matthew Lyon
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the "Software"), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7
+ the Software, and to permit persons to whom the Software is furnished to do so,
8
+ subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Manifest ADDED
@@ -0,0 +1,9 @@
1
+ LICENSE
2
+ Manifest
3
+ README.mkdn
4
+ bin/slicehost-dns
5
+ lib/slicehost-dns.rb
6
+ lib/example.yml
7
+ lib/reconciler.rb
8
+ lib/zone.rb
9
+ lib/record.rb
data/README.mkdn ADDED
@@ -0,0 +1,39 @@
1
+ # slicehost-dns
2
+
3
+ by Matthew Lyon <matt@flowerpowered.com>
4
+
5
+ ## DESCRIPTION:
6
+
7
+ Helps you manage your DNS entries on the slicehost DNS server via a local YAML file.
8
+
9
+ ## FEATURES
10
+
11
+ - Create new Zones just by adding them to the YAML file under an IP address
12
+ - Automatically creates the root A record and a wildcard A record
13
+ - creation of MX and SPF (TXT) records for gmail if desired
14
+ - creation of SRV records for google chat if desired
15
+ - can dump your current settings to a YAML file
16
+
17
+ ## SYNOPSIS
18
+
19
+ Get an example file:
20
+
21
+ slicehost-dns --example > example.yml
22
+
23
+ Rename example.yml to config.yml, edit the "api" value to your api key, and dump your current settings
24
+ from slicehost to config.yml:
25
+
26
+ slicehost-dns --dump config.yml
27
+
28
+ Make some changes and push them back, but do a dry run first because we're not sure:
29
+
30
+ slicehost-dns config.yml --dry
31
+
32
+ OK we're cool, push the changes for real this time:
33
+
34
+ slicehost-dns config.yml
35
+
36
+ ## REQUIREMENTS
37
+
38
+ * activeresource
39
+ * rspec, if you wish to run the spec suite
data/bin/slicehost-dns ADDED
@@ -0,0 +1,2 @@
1
+ #! /usr/bin/env ruby
2
+ load File.dirname(__FILE__) + '/../lib/slicehost-dns.rb'
data/lib/example.yml ADDED
@@ -0,0 +1,26 @@
1
+ api: aldkgjhasklghaldghalghashglakdhgladshjg
2
+
3
+ 1.1.1.1:
4
+ example.com:
5
+ # these two A records are created by default, they are here as an example:
6
+ a:
7
+ example.com.: this
8
+ '*': this
9
+ # note that 'this' in this context is the current IP address
10
+ cname:
11
+ mail: web.mailhost.example.com.
12
+ mx: mailhost.example.com.
13
+ txt:
14
+ - "v=spf1 a mx ~all"
15
+
16
+ example.tld:
17
+ cname:
18
+ blog: mattly.tumblr.com.
19
+ goog: all
20
+ # goog: "mail" creates mx records for gmail and an spf record (unless one is given)
21
+ # goog: "all" does the same as mail, as well as creates srv records for jabber/xmpp
22
+
23
+ anotherexample.tld:
24
+ mx:
25
+ mailhost.example.com.: 5
26
+ backup.mailhost.example.com.: 10
data/lib/reconciler.rb ADDED
@@ -0,0 +1,178 @@
1
+ class Reconciler
2
+ def initialize(zone, ip, existing, desired, setup={})
3
+ @zone, @ip, @existing, @desired, @setup = zone, ip, existing, desired, setup
4
+ end
5
+
6
+ def log(msg, level=:info)
7
+ $LOG.send(level, msg) if $LOG
8
+ end
9
+
10
+ def process!
11
+ process_a_records
12
+ process_cname_records
13
+ process_mx_records
14
+ process_srv_records
15
+ process_txt_records
16
+ process_ns_records
17
+ end
18
+
19
+ def process_a_records
20
+ a_records = @existing.select {|r| r.a? }
21
+
22
+ desired = @desired['a'] ||= {}
23
+ desired[@zone.origin] = 'this'
24
+ desired['*'] ||= 'this'
25
+ desired.each_pair {|k,v| desired[k] = @ip if v == "this" }
26
+
27
+ crud_by_name(:a, a_records, desired)
28
+ end
29
+
30
+ def process_cname_records
31
+ cnames = @existing.select {|r| r.cname? }
32
+ desired = @desired['cname'] || {}
33
+ crud_by_name(:cname, cnames, desired)
34
+ end
35
+
36
+ def process_ns_records
37
+ ns = @existing.select {|r| r.ns? }
38
+ (1..3).to_a.each do |i|
39
+ unless ns.any? {|r| r.data == "ns#{i}.slicehost.net." }
40
+ log "> creating NS record ns#{i}.slicehost.net."
41
+ record = @zone.new_record(:record_type => "NS", :name => @zone.origin, :data => "ns#{i}.slicehost.net.")
42
+ record.save
43
+ else
44
+ r = ns.detect {|r| r.data == "ns#{i}.slicehost.net." }
45
+ ns.delete(r)
46
+ end
47
+ end
48
+ destroy_records ns
49
+ end
50
+
51
+ def process_mx_records
52
+ mx = @existing.select {|r| r.mx? }
53
+ desired = @desired['mx'] || {}
54
+
55
+ if @desired['goog']
56
+ desired = {
57
+ 'aspmx.l.google.com.' => 1, 'alt1.aspmx.l.google.com.' => 5, 'alt2.aspmx.l.google.com.' => 5,
58
+ 'aspmx2.googlemail.com.' => 10, 'aspmx3.googlemail.com.' => 10, 'aspmx4.googlemail.com.' => 10,
59
+ 'aspmx5.googlemail.com.' => 10
60
+ }
61
+ end
62
+
63
+ if desired.kind_of?(String)
64
+ desired = {desired => 5}
65
+ end
66
+
67
+ keepers = mx.select {|r| desired.has_key?(r.data) }
68
+
69
+ cru_by_data :mx, @zone.origin, keepers, desired
70
+
71
+ mx -= keepers
72
+ destroy_records mx
73
+ end
74
+
75
+ def process_srv_records
76
+ srv = @existing.select {|r| r.srv? }
77
+ desired = @desired['srv'] || {}
78
+
79
+ if @desired['goog'] && @desired['goog'] != 'mail'
80
+ desired.update({
81
+ "_xmpp-server._tcp.#{@zone.origin}" => {
82
+ '0 5269 xmpp-server.l.google.com.' => 5, '0 5269 xmpp-server1.l.google.com.' => 20,
83
+ '0 5269 xmpp-server2.l.google.com.' => 20, '0 5269 xmpp-server3.l.google.com.' => 20,
84
+ '0 5269 xmpp-server4.l.google.com.' => 20
85
+ },
86
+ "_jabber._tcp.#{@zone.origin}" => {
87
+ '0 5269 xmpp-server.l.google.com.' => 5, '0 5269 xmpp-server1.l.google.com.' => 20,
88
+ '0 5269 xmpp-server2.l.google.com.' => 20, '0 5269 xmpp-server3.l.google.com.' => 20,
89
+ '0 5269 xmpp-server4.l.google.com.' => 20
90
+ },
91
+ "_xmpp-client._tcp.#{@zone.origin}" => {
92
+ '0 5222 talk.l.google.com.' => 5, '0 5222 talk1.l.google.com.' => 20,
93
+ '0 5222 talk2.l.google.com.' => 20
94
+ }
95
+ })
96
+ end
97
+
98
+ desired.each_pair do |name, data_aux_pairs|
99
+ keepers = srv.select {|r| r.name == name }.select {|r| data_aux_pairs.has_key?(r.data) }
100
+ srv -= keepers
101
+ cru_by_data(:srv, name, keepers, data_aux_pairs)
102
+ end
103
+
104
+ destroy_records srv
105
+ end
106
+
107
+ def process_txt_records
108
+ txt = @existing.select {|r| r.txt? }
109
+ desired = @desired['txt'] || []
110
+
111
+ if @desired['goog'] && ! desired.any? {|t| t.match(/^v=spf1/)}
112
+ desired << "v=spf1 include:aspmx.googlemail.com ~all"
113
+ end
114
+
115
+ desired = desired.inject({}) {|memo, value| memo.update({value => 0}) }
116
+
117
+ keepers = txt.select {|r| desired.include?(r.data) }
118
+ txt -= keepers
119
+ cru_by_data(:txt, @zone.origin, keepers, desired)
120
+
121
+ destroy_records txt
122
+ end
123
+
124
+ def destroy_records(records=[])
125
+ records.each do |r|
126
+ log "> removing record: #{r.kind} #{r.name} #{r.data}"
127
+ r.destroy
128
+ end
129
+ end
130
+
131
+ # A, CNAME
132
+ def crud_by_name(type, records, desired)
133
+ type = type.to_s.upcase
134
+ records.each do |record|
135
+ if desired[record.name] == record.data
136
+ desired.delete(record.name)
137
+
138
+ elsif desired.has_key?(record.name)
139
+ log "> changing record: #{type} #{record.name} from #{record.data} to #{desired[record.name]}"
140
+ record.data = desired[record.name]
141
+ desired.delete(record.name)
142
+ record.save
143
+
144
+ else
145
+ log "> removing record: #{type} #{record.name} (#{record.data})"
146
+ record.destroy
147
+ end
148
+ end
149
+ (desired || {}).each_pair do |name, data|
150
+ log "> creating record: #{type} #{name} (#{data})"
151
+ record = @zone.new_record(:record_type => type, :name => name, :data => data)
152
+ record.save
153
+ end
154
+
155
+ end
156
+
157
+ # MX, SRV, TXT
158
+ def cru_by_data(type, name, keepers, desired)
159
+ type = type.to_s.upcase
160
+
161
+ keepers.each do |r|
162
+ unless r.aux == desired[r.data]
163
+ log "> changing record: #{type} #{r.name} #{r.data} from #{r.aux} to #{desired[r.data]}"
164
+ r.aux = desired[r.data]
165
+ r.save
166
+ end
167
+ desired.delete(r.data)
168
+ end
169
+
170
+ desired.each_pair do |data, aux|
171
+ log "> creating record: #{type} #{name} (#{data} / #{aux})"
172
+ r = @zone.new_record(:record_type => type, :name => name, :data => data, :aux => aux)
173
+ r.save
174
+ end
175
+
176
+ end
177
+
178
+ end
data/lib/record.rb ADDED
@@ -0,0 +1,13 @@
1
+ class Record < ActiveResource::Base
2
+
3
+ def kind
4
+ record_type.downcase
5
+ end
6
+
7
+ [:a,:mx,:cname,:srv,:ns,:txt].each do |kind|
8
+ define_method "#{kind}?" do
9
+ self.kind == "#{kind}"
10
+ end
11
+ end
12
+
13
+ end
@@ -0,0 +1,144 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'activeresource'
5
+ require 'yaml'
6
+ require 'logger'
7
+
8
+ class Logger
9
+ class Formatter
10
+ def call(severity, time, progname, msg)
11
+ "#{msg}\n"
12
+ end
13
+ end
14
+ end
15
+
16
+ # yes globals are evil but hey, i welcome patches.
17
+ # and, a riddle: what are singletons except globals reduced to a pattern?
18
+ $LOG = Logger.new($stdout)
19
+
20
+ def error(message)
21
+ $LOG.fatal(message)
22
+ exit
23
+ end
24
+
25
+ if ARGV.include? '--example'
26
+ example = File.dirname(__FILE__) + '/example.yml'
27
+ error open(example).read
28
+ end
29
+
30
+ error "Usage: slicehost-dns (--dump --dry) [config.yml]" if ARGV.empty?
31
+
32
+ dump = ARGV.delete('--dump')
33
+ dry_run = ARGV.delete('--dry')
34
+ filename = ARGV.shift
35
+ config = YAML.load_file(filename)
36
+ API = config.delete('api')
37
+ $TTL = 43200 # 12 hours.
38
+
39
+ $:.unshift File.dirname(__FILE__)
40
+ require 'zone'
41
+ require 'record'
42
+ require 'reconciler'
43
+
44
+ Zone.site = "https://#{API}@api.slicehost.com/"
45
+ Record.site = "https://#{API}@api.slicehost.com/"
46
+
47
+ zones = Zone.find :all
48
+ records = Record.find :all
49
+
50
+ if dry_run
51
+ class Zone
52
+ def save
53
+ $LOG.info("~> dry saving zone #{origin}")
54
+ end
55
+ def destroy
56
+ $LOG.info("~> dry destroying zone #{origin}")
57
+ end
58
+ end
59
+ class Record
60
+ def save
61
+ $LOG.info("~> dry saving record #{kind} #{name}: #{data}")
62
+ end
63
+ def destroy
64
+ $LOG.info("~> dry destroying record #{kind} #{name}: #{data}")
65
+ end
66
+ end
67
+ end
68
+
69
+ if dump
70
+ output = {}
71
+ output['api'] = API
72
+
73
+ zones.each do |zone|
74
+ local, records = records.partition {|r| r.zone_id == zone.id }
75
+
76
+ primary_a = local.detect {|r| r.a? && r.name == zone.origin }
77
+ local.delete(primary_a)
78
+ ip = primary_a.data
79
+
80
+ wildcard = local.detect {|r| r.a? && (r.name == "*" || r.name == "*.#{zone.origin}")}
81
+ local.delete(wildcard)
82
+
83
+ domain = {}
84
+
85
+ google = false
86
+
87
+ local.each do |record|
88
+ case record.kind
89
+ when 'ns'
90
+ when 'srv'
91
+ domain['srv'] ||= {}
92
+ domain['srv'][record.name] ||= {}
93
+ domain['srv'][record.name][record.data] = record.aux
94
+ google = true if record.data.match(/xmpp-server\d?\.l\.google/)
95
+ when 'mx'
96
+ domain['mx'] ||= {}
97
+ domain['mx'][record.data] = record.aux
98
+ google = 'mail' if record.data.match(/google/i) && ! google
99
+ when 'txt'
100
+ domain['txt'] ||= []
101
+ domain['txt'] << record.data
102
+ else
103
+ domain[record.kind] ||= {}
104
+ domain[record.kind][record.name] = record.data == ip ? 'this' : record.data
105
+ end
106
+ end
107
+
108
+ if google
109
+ domain['goog'] = google
110
+ domain.delete('mx')
111
+ unless google == 'mail'
112
+ domain['srv'].delete("_xmpp-server._tcp.#{zone.origin}")
113
+ domain['srv'].delete("_jabber._tcp.#{zone.origin}")
114
+ domain['srv'].delete("_xmpp-client._tcp.#{zone.origin}")
115
+ domain.delete('srv') if domain['srv'].empty?
116
+ end
117
+ # fuck it, we'll leave the spf record in there.
118
+ end
119
+
120
+ if domain['mx'] && domain['mx'].size == 1
121
+ domain['mx'] = domain['mx'].keys.first
122
+ end
123
+
124
+ output[ip] ||= {}
125
+ output[ip][zone.domain] = domain
126
+ end
127
+
128
+ File.open(filename, 'w') {|f| YAML.dump(output, f) }
129
+ else
130
+ config.each_pair do |ip, domains|
131
+ unless ip.match(/(\d+\.){3}\d+/)
132
+ $LOG.warn "\n~ skipping key #{ip}, not a valid ip address"
133
+ next
134
+ end
135
+ domains.each_pair do |domain, desired_records|
136
+ $LOG.info "\ndomain #{domain}"
137
+ zone = zones.detect {|z| z.domain == domain } || Zone.create_for_domain(domain)
138
+ records_for_domain, records = records.partition {|r| r.zone_id == zone.id }
139
+ Reconciler.new(zone, ip, records_for_domain, desired_records).process!
140
+ zones.delete(zone)
141
+ end
142
+ puts "\n\ni'd delete #{zones.map{|z| z.domain}.join(', ')} now but you'd hate me."
143
+ end
144
+ end
data/lib/zone.rb ADDED
@@ -0,0 +1,18 @@
1
+ class Zone < ActiveResource::Base
2
+
3
+ def domain
4
+ origin.sub(/\.$/,'')
5
+ end
6
+
7
+ def self.create_for_domain(domain)
8
+ $LOG.info "> domain record doesn't exist yet, creating" if $LOG
9
+ zone = self.new(:origin => "#{domain}.", :ttl => $TTL)
10
+ zone.save
11
+ zone
12
+ end
13
+
14
+ def new_record(attrs={})
15
+ Record.new(attrs.merge({:zone_id => id}))
16
+ end
17
+
18
+ end
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mattly-slicehost-dns
3
+ version: !ruby/object:Gem::Version
4
+ version: "0.3"
5
+ platform: ruby
6
+ authors:
7
+ - Matthew Lyon
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-01-11 00:00:00 -08:00
13
+ default_executable: slicehost-dns
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: activeresource
17
+ version_requirement:
18
+ version_requirements: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: "0"
23
+ version:
24
+ description: because YAML is the one true config format
25
+ email: matt@flowerpowered.com
26
+ executables:
27
+ - slicehost-dns
28
+ extensions: []
29
+
30
+ extra_rdoc_files: []
31
+
32
+ files:
33
+ - LICENSE
34
+ - Manifest
35
+ - README.mkdn
36
+ - bin/slicehost-dns
37
+ - lib/example.yml
38
+ - lib/reconciler.rb
39
+ - lib/record.rb
40
+ - lib/zone.rb
41
+ - lib/slicehost-dns.rb
42
+ has_rdoc: false
43
+ homepage: http://github.com/mattly/slicehost-dns
44
+ post_install_message:
45
+ rdoc_options: []
46
+
47
+ require_paths:
48
+ - lib
49
+ - bin
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: "0"
55
+ version:
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: "0"
61
+ version:
62
+ requirements: []
63
+
64
+ rubyforge_project:
65
+ rubygems_version: 1.2.0
66
+ signing_key:
67
+ specification_version: 2
68
+ summary: Manages DNS settings on your slicehost account via a YAML file
69
+ test_files: []
70
+