clouddns 0.0.1 → 0.0.2

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