clouddns 0.0.1 → 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,114 @@
1
+ clouddns
2
+ ========
3
+
4
+ A DSL for managing your DNS on cloud services through [fog](http://fog.io/).
5
+
6
+ Clouddns allows a convenient way to manage DNS on Amazon Route 53, bluebox, DNSimple, DNS Made Easy, Linode DNS, Slicehost DNS or Zerigo DNS.
7
+ DNS records are described by configuration files in ruby, which are in a readable commentable format.
8
+ This also allows keeping your DNS configuration in version control.
9
+ Since DNS records are described in ruby, they can be defined programmatically, allowing DNS records to be made from database entries or other sources like a [chef server](http://www.opscode.com/chef/).
10
+
11
+ Installation
12
+ ============
13
+
14
+ gem install clouddns
15
+
16
+ Usage
17
+ =====
18
+
19
+ To change your DNS to that in the config file run
20
+
21
+ clouddns migrate dns.rb
22
+
23
+ Where dns.rb is a file in the following format
24
+
25
+ Configuration file
26
+ ==================
27
+
28
+ The configuration files used by clouddns are written in ruby and specify zones at the top level, and records within.
29
+
30
+ *Zones* group all records within a domain (usually, second-level or third-level).
31
+ Within these zones, records are declared in the format `TYPE 'FULL.DOMAIN.NAME' 'VALUE'`
32
+
33
+ Example configuration
34
+ ---------------------
35
+
36
+ provider 'AWS'
37
+
38
+ defaults :ttl => 600
39
+
40
+ zone 'example.com' do
41
+ A 'example.com', '1.2.3.4'
42
+ CNAME 'www.example.com', 'example.com.'
43
+
44
+ A 'mail.example.com', '4.3.2.1'
45
+ MX 'www.example.com', '10 mail.example.com.', :ttl => 300
46
+ end
47
+
48
+ zone 'example.net' do
49
+ A 'www.example.net', '1.2.3.4'
50
+ end
51
+
52
+ Zone
53
+ ----
54
+
55
+ Defines a zone. Takes one argument, the fully qualified domain name, and a block, which declares the records associated with the zone.
56
+ Domain names can end with a dot, if missing, is is implied.
57
+
58
+ Records
59
+ -------
60
+
61
+ Records must be defined within a zone.
62
+ They are defined by calling the helper method for their type (A, CNAME, etc) with their FQDN, value, and any options.
63
+ If the domain name is missing the trailing dot, it is implied.
64
+
65
+ Value can be an array to specify multiple entries for that record (like in the case of Round-robin DNS, or multiple MX records).
66
+
67
+ Options is currently only :ttl, which is specified in seconds. :ttl defaults to 3600, which can be changed using the defaults directive.
68
+
69
+ Provider (optional)
70
+ -------------------
71
+
72
+ Credentials are required to access whichever service is desired through dns.
73
+ This can be specified in full in the config file
74
+
75
+ provider 'AWS',
76
+ :aws_secret_access_key => YOUR_SECRET_ACCESS_KEY,
77
+ :aws_access_key_id => YOUR_SECRET_ACCESS_KEY_ID
78
+
79
+ Alternatively, to avoid specifying access keys or access key in the file (A good idea if it's to enter version control),
80
+ fog will read credentials from `~/.fog` or the file specified in `ENV["FOG_RC"]`.
81
+ One can then specify the provider by itself in the config file (like `provider 'AWS'`) or using the `-p PROVIDER` option of the clouddns executable.
82
+
83
+ Defaults
84
+ --------
85
+
86
+ Defaults specified options which will be used by any zones or records defined in the file.
87
+ Currently, this can only be used to specify :ttl.
88
+
89
+
90
+ Copyright
91
+ =========
92
+
93
+ (The MIT License)
94
+
95
+ Copyright (c) 2011 [John Hawthorn](http://www.johnhawthorn.com/)
96
+
97
+ Permission is hereby granted, free of charge, to any person obtaining
98
+ a copy of this software and associated documentation files (the
99
+ "Software"), to deal in the Software without restriction, including
100
+ without limitation the rights to use, copy, modify, merge, publish,
101
+ distribute, sublicense, and/or sell copies of the Software, and to
102
+ permit persons to whom the Software is furnished to do so, subject to
103
+ the following conditions:
104
+
105
+ The above copyright notice and this permission notice shall be
106
+ included in all copies or substantial portions of the Software.
107
+
108
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
109
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
110
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
111
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
112
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
113
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
114
+
@@ -37,11 +37,17 @@ files = ARGV
37
37
  raise "No action specified" unless action
38
38
  raise "No files specified" if files.empty?
39
39
 
40
+ action = Clouddns::Actions.by_name(action)
41
+
40
42
  zones = files.map do |file|
41
- Clouddns::DSL.parse_file(file).zones
42
- end.flatten
43
+ dsl = Clouddns::DSL.parse_file(file)
44
+
45
+ dsloptions = {:fog => dsl.fog_options}.merge(options) do |key, oldval, newval|
46
+ oldval.merge(newval)
47
+ end
43
48
 
44
- zones.each do |zone|
45
- Clouddns::Actions.by_name(action).run(zone, options)
49
+ dsl.zones.each do |zone|
50
+ action.run(zone, dsloptions)
51
+ end
46
52
  end
47
53
 
@@ -1,10 +1,11 @@
1
1
  require "fog"
2
2
 
3
- require "clouddns/version"
3
+ require "clouddns/actions"
4
4
  require "clouddns/dsl"
5
- require "clouddns/zone"
6
5
  require "clouddns/record"
7
- require "clouddns/actions"
6
+ require "clouddns/version"
7
+ require "clouddns/zone"
8
+ require "clouddns/zone_migration"
8
9
 
9
10
  module Clouddns
10
11
  end
@@ -7,83 +7,61 @@ module Clouddns
7
7
 
8
8
  def run
9
9
  @fog = Fog::DNS.new(@options[:fog])
10
-
11
10
  @fog_zone = @fog.zones.find { |z| z.domain == @zone.name }
12
11
 
13
12
  puts
14
13
  puts "Migrating '#{@zone.name}'"
15
14
 
16
- unless @fog_zone
17
- puts
18
- puts "Zone '#{@zone.name}' does not exist. Creating..."
19
- require_confirmation!
20
- @fog_zone = @fog.zones.create(:domain => @zone.name)
21
- puts "Zone '#{@zone.name}' created."
22
- end
15
+ create_zone! unless @fog_zone
23
16
 
24
- # required to pick up nameservers
17
+ # required to pick up nameservers and records
25
18
  @fog_zone = @fog_zone.reload
26
19
 
27
- puts
28
- puts "Nameservers:"
29
- @fog_zone.nameservers.each do |nameserver|
30
- puts " #{nameserver}"
31
- end
32
-
33
- puts
34
- puts "Changes:"
35
- @records_to_remove = []
36
- @records_to_add = []
37
-
38
- @namelength = (@fog_zone.records.map(&:name) + @zone.records.map(&:name)).map(&:length).max
39
-
40
- @actions = []
41
-
42
- fog_records = Hash[@fog_zone.records.map {|r| [[r.name.gsub('\052', '*'), r.type], r] } ]
43
- @zone.records.each do |record|
44
- if (fog_record = fog_records.delete([record.name, record.type]))
45
- if equal?(record, fog_record)
46
- @actions << [' ', record]
47
- else
48
- @actions << ['-', fog_record]
49
- @actions << ['+', record]
50
- end
51
- else
52
- @actions << ['+', record]
53
- end
54
- end
20
+ print_nameservers
55
21
 
56
- fog_records.each do |name, record|
57
- @actions << ['-', record]
58
- end
22
+ @migration = ZoneMigration.new(@zone, @fog_zone)
59
23
 
60
- @actions.each do |(action, record)|
61
- print_record record, @namelength, action
62
- end
24
+ print_changes
63
25
 
64
26
  # no changes required
65
- return if @actions.all? {|(action, record)| action == ' ' }
27
+ return if @migration.completed?
66
28
 
67
29
  require_confirmation!
68
30
 
69
- @actions.each do |(action, record)|
70
- if action == '+'
71
- @fog_zone.records.create(:type => record.type, :name => record.name, :ip => record.value, :ttl => record.ttl)
72
- elsif action == '-'
73
- record.destroy
74
- end
75
- end
31
+ @migration.perform!
32
+
76
33
  puts "Done"
77
34
  end
78
35
 
79
36
  private
80
37
 
81
- def equal? record, fog_record
82
- # AWS replaces * with \052
83
- record.name == fog_record.name.gsub('\052', '*') &&
84
- record.type == fog_record.type &&
85
- record.value == fog_record.ip &&
86
- record.ttl.to_i == fog_record.ttl.to_i
38
+ def create_zone!
39
+ puts
40
+ puts "Zone '#{@zone.name}' does not exist. Creating..."
41
+ require_confirmation!
42
+ @fog_zone = @fog.zones.create(:domain => @zone.name)
43
+ puts "Zone '#{@zone.name}' created."
44
+ end
45
+
46
+ def print_nameservers
47
+ puts
48
+ puts "Nameservers:"
49
+ @fog_zone.nameservers.each do |nameserver|
50
+ puts " #{nameserver}"
51
+ end
52
+ end
53
+
54
+ def print_changes
55
+ namelength = @migration.changes.map do |(action, record)|
56
+ record.name.length
57
+ end.max
58
+
59
+ puts
60
+ puts "Changes:"
61
+
62
+ @migration.changes.each do |(action, record)|
63
+ print_record record, namelength, action
64
+ end
87
65
  end
88
66
 
89
67
  def require_confirmation!
@@ -1,20 +1,16 @@
1
1
  module Clouddns
2
2
  class DSL
3
3
  attr_reader :zones
4
+ attr_reader :fog_options
4
5
 
5
6
  # This need not be too strict. Only exists to help catch typos.
6
7
  DNS_REGEX = /\A.*\..*\.\z/
7
8
 
8
- def scope options={}
9
- oldscope = @scope
10
- @scope = @scope.merge(options)
11
- yield
12
- @scope = oldscope
13
- end
14
-
15
9
  def initialize
16
10
  @zones = []
17
- @scope = {}
11
+ @zone = nil
12
+ @defaults = {}
13
+ @fog_options = {}
18
14
  end
19
15
 
20
16
  def self.parse_string string
@@ -26,37 +22,59 @@ module Clouddns
26
22
  parse_string open(filename).read
27
23
  end
28
24
 
29
- def A *args
30
- add_record 'A', *args
31
- end
32
- def CNAME *args
33
- add_record 'CNAME', *args
34
- end
35
- def MX *args
36
- add_record 'MX', *args
37
- end
38
- def NS *args
39
- add_record 'NS', *args
40
- end
41
- def TXT name, value, options={}
42
- add_record 'TXT', name, "\"#{value}\"", options
25
+ protected
26
+
27
+ record_types = %w{A AAAA CNAME MX NS PTR SOA SPF SRV TXT}
28
+ record_types.each do |type|
29
+ define_method type do |*args|
30
+ add_record type, *args
31
+ end
43
32
  end
44
33
 
45
34
  def add_record type, name, value, options={}
46
- zone = @scope[:zone]
35
+ name = domainname(name)
36
+
37
+ value = "\"#{value}\"" if type == 'TXT'
47
38
 
48
- raise "records must be added inside a zone" unless zone
49
- raise "record's dns name must end with the current zone" unless name.end_with? zone.name
39
+ raise "records must be added inside a zone" unless @zone
40
+ raise "record's dns name must end with the current zone" unless name.end_with? @zone.name
50
41
 
51
- @scope[:zone].records << Record.new(type, name, value, options)
42
+ @zone.records << Record.new(type, name, value, options)
52
43
  end
53
44
 
54
- def zone name, &block
55
- raise "zone must be at the top level" if @scope[:zone]
45
+ def zone name
46
+ raise "zones cannot be nested" if @zone
56
47
 
57
- zone = Zone.new(name)
58
- @zones << zone
59
- scope :zone => zone, &block
48
+ @zone = Zone.new(domainname(name))
49
+ @zones << @zone
50
+ yield
51
+ @zone = nil
52
+ end
53
+
54
+ def defaults options={}
55
+ if block_given?
56
+ old = @defaults
57
+ @defaults = @defaults.merge(options)
58
+ yield
59
+ @defaults = old
60
+ else
61
+ raise "defaults must either take a block or be before any zone declarations" unless @zones.empty?
62
+ @defaults = @defaults.merge(options)
63
+ end
64
+ end
65
+
66
+ def provider name, options={}
67
+ @fog_options = options.merge({:provider => name})
68
+ end
69
+
70
+ private
71
+ def domainname name
72
+ if name.end_with? '.'
73
+ name
74
+ else
75
+ "#{name}."
76
+ end
60
77
  end
61
78
  end
62
79
  end
80
+
@@ -1,3 +1,3 @@
1
1
  module Clouddns
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -0,0 +1,61 @@
1
+ module Clouddns
2
+ class ZoneMigration
3
+ def initialize zone, fog_zone
4
+ @zone = zone
5
+ @fog_zone = fog_zone
6
+ @changes = nil
7
+ end
8
+ def changes
9
+ return @changes if @changes
10
+ @changes = []
11
+ fog_records = Hash[@fog_zone.records.map {|r| [[fog_record_name(r), r.type], r] } ]
12
+ @zone.records.each do |record|
13
+ if (fog_record = fog_records.delete([record.name, record.type]))
14
+ if records_equal?(record, fog_record)
15
+ @changes << [' ', record]
16
+ else
17
+ @changes << ['-', fog_record]
18
+ @changes << ['+', record]
19
+ end
20
+ else
21
+ @changes << ['+', record]
22
+ end
23
+ end
24
+
25
+ fog_records.each do |name, record|
26
+ @changes << ['-', record]
27
+ end
28
+ @changes
29
+ end
30
+
31
+ def perform!
32
+ changes.each do |(action, record)|
33
+ if action == '+'
34
+ @fog_zone.records.create(:type => record.type, :name => record.name, :ip => record.value, :ttl => record.ttl)
35
+ elsif action == '-'
36
+ record.destroy
37
+ end
38
+ end
39
+ end
40
+
41
+ def completed?
42
+ changes.all? do |(action, record)|
43
+ action == ' '
44
+ end
45
+ end
46
+
47
+ protected
48
+ def fog_record_name record
49
+ record.name.gsub('\052', '*')
50
+ end
51
+
52
+ def records_equal? record, fog_record
53
+ # AWS replaces * with \052
54
+ record.name == fog_record_name(fog_record) &&
55
+ record.type == fog_record.type &&
56
+ record.value == fog_record.ip &&
57
+ record.ttl.to_i == fog_record.ttl.to_i
58
+ end
59
+
60
+ end
61
+ end
@@ -19,5 +19,15 @@ class DslTest < Test::Unit::TestCase
19
19
  assert_equal 'www.example.com.', record.name
20
20
  assert_equal ['1.2.3.4'], record.value
21
21
  end
22
+ def test_trailing_dot_is_implied
23
+ dsl = Clouddns::DSL.parse_string <<-EOL
24
+ zone "example.com" do
25
+ A 'www.example.com', '1.2.3.4'
26
+ end
27
+ EOL
28
+
29
+ assert_equal 'example.com.', dsl.zones.first.name
30
+ assert_equal 'www.example.com.', dsl.zones.first.records.first.name
31
+ end
22
32
  end
23
33
 
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: clouddns
3
3
  version: !ruby/object:Gem::Version
4
- hash: 29
4
+ hash: 27
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 0
9
- - 1
10
- version: 0.0.1
9
+ - 2
10
+ version: 0.0.2
11
11
  platform: ruby
12
12
  authors:
13
13
  - John Hawthorn
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-07-11 00:00:00 Z
18
+ date: 2011-07-14 00:00:00 Z
19
19
  dependencies: []
20
20
 
21
21
  description: A DSL for managing DNS through cloud services
@@ -30,6 +30,7 @@ extra_rdoc_files: []
30
30
  files:
31
31
  - .gitignore
32
32
  - Gemfile
33
+ - README.md
33
34
  - Rakefile
34
35
  - bin/clouddns
35
36
  - clouddns.gemspec
@@ -43,6 +44,7 @@ files:
43
44
  - lib/clouddns/record.rb
44
45
  - lib/clouddns/version.rb
45
46
  - lib/clouddns/zone.rb
47
+ - lib/clouddns/zone_migration.rb
46
48
  - test/dsl_test.rb
47
49
  - test/test_helper.rb
48
50
  homepage: https://github.com/jhawthorn/clouddns