dslimple 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d3524b1943dd7ea604e2dc0405d806500a4c5c3f
4
+ data.tar.gz: bbd8df2e9fa8193f6fa6941784503115335a0e9b
5
+ SHA512:
6
+ metadata.gz: b50b823159fdbcb21b32c6db9723a0728f951affc184e354b3e786d48098042c552ce1dbf74c150f93db37aedb51d1803b7c03d6d51f8163d91b0871017b83da
7
+ data.tar.gz: 4f1ea991bdbe56a6a6e8c58ccc444c4e22bacff0b0cc4da2f0a8796e49e897383720a2a0565414d50dcabb5908637b77a53901a1f63d73755c4522273b8a40da
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /Domainfile
11
+ /domainfiles
data/.rubocop.yml ADDED
@@ -0,0 +1,28 @@
1
+ AllCops:
2
+ Include:
3
+ - "*.gemspec"
4
+ Exclude:
5
+ - "vendor/**/*"
6
+ - "db/schema.rb"
7
+ DisplayCopNames: true
8
+
9
+ Style/Alias:
10
+ EnforcedStyle: prefer_alias_method
11
+
12
+ Style/ClassAndModuleChildren:
13
+ EnforcedStyle: compact
14
+
15
+ Style/CaseEquality:
16
+ Enabled: false
17
+
18
+ Style/Documentation:
19
+ Enabled: false
20
+
21
+ Metrics/LineLength:
22
+ Max: 160
23
+
24
+ Metrics/MethodLength:
25
+ Max: 20
26
+
27
+ Metrics/AbcSize:
28
+ Max: 25
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in dslimple.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Sho Kusano
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,157 @@
1
+ # DSLimple
2
+
3
+ __DSLimple__ is a tool to manage [DNSimple](https://dnsimple.com/).
4
+
5
+ It defines the state of DNSimple using DSL, and updates DNSimple according to DSL.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'dslimple'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install dslimple
22
+
23
+ ## Usage
24
+
25
+ ```shell
26
+ export DLSIMPLE_EMAIL="..."
27
+ export DLSIMPLE_API_TOKEN="..."
28
+ dslimple export -f Domainfile
29
+ vi Domainfile
30
+ dslimple apply --dry-run -f Domainfile
31
+ dslimple apply --yes -f Domainfile
32
+ ```
33
+
34
+ ### Help
35
+
36
+ ```
37
+ $ dslimple help
38
+ Commands:
39
+ dslimple apply # Apply domain specifications
40
+ dslimple export # Export domain specifications
41
+ dslimple help [COMMAND] # Describe available commands or one specific command
42
+
43
+ Options:
44
+ -e, [--email=EMAIL] # Your E-Mail address
45
+ -t, [--api-token=API_TOKEN] # Your API token
46
+ -dt, [--domain-token=DOMAIN_TOKEN] # Your Domain API token
47
+ [--sandbox], [--no-sandbox] # Use sandbox API(at sandbox.dnsimple.com)
48
+ # Default: true
49
+ [--debug], [--no-debug]
50
+ ```
51
+
52
+ #### help apply
53
+
54
+ ```
55
+ $ dslimple help apply
56
+ Usage:
57
+ dslimple apply
58
+
59
+ Options:
60
+ -o, [--only=one two three] # Specify domains for apply
61
+ -d, [--dry-run], [--no-dry-run]
62
+ -f, [--file=FILE] # Source Domainfile path
63
+ # Default: Domainfile
64
+ [--addition], [--no-addition] # Add specified records
65
+ # Default: true
66
+ [--modification], [--no-modification] # Modify specified records
67
+ # Default: true
68
+ [--deletion], [--no-deletion] # Delete unspecified records
69
+ # Default: true
70
+ -y, [--yes], [--no-yes] # Do not confirm on before apply
71
+ -e, [--email=EMAIL] # Your E-Mail address
72
+ -t, [--api-token=API_TOKEN] # Your API token
73
+ -dt, [--domain-token=DOMAIN_TOKEN] # Your Domain API token
74
+ [--sandbox], [--no-sandbox] # Use sandbox API(at sandbox.dnsimple.com)
75
+ # Default: true
76
+ [--debug], [--no-debug]
77
+
78
+ Apply domain specifications
79
+ ```
80
+
81
+ #### help export
82
+
83
+ ```
84
+ $ dslimple help export
85
+ Usage:
86
+ dslimple export
87
+
88
+ Options:
89
+ -o, [--only=one two three] # Specify domains for export
90
+ -f, [--file=FILE] # Export Domainfile path
91
+ # Default: Domainfile
92
+ -d, [--dir=DIR] # Export directory path for split
93
+ # Default: ./domainfiles
94
+ -s, [--split], [--no-split] # Export with split by domains
95
+ -m, [--modeline], [--no-modeline] # Export with modeline for Vim
96
+ [--soa-and-ns], [--no-soa-and-ns] # Export without SOA and NS records
97
+ -e, [--email=EMAIL] # Your E-Mail address
98
+ -t, [--api-token=API_TOKEN] # Your API token
99
+ -dt, [--domain-token=DOMAIN_TOKEN] # Your Domain API token
100
+ [--sandbox], [--no-sandbox] # Use sandbox API(at sandbox.dnsimple.com)
101
+ # Default: true
102
+ [--debug], [--no-debug]
103
+
104
+ Export domain specifications
105
+ ```
106
+
107
+ ## Domainfile Examples
108
+
109
+ ### Basic
110
+
111
+ The following defines are all the same meaning
112
+
113
+ ```ruby
114
+ domain "example.com" do
115
+ a_record ttl: 3600 do
116
+ "0.0.0.0"
117
+ end
118
+
119
+ record type: :a, ttl: 3600 do
120
+ "0.0.0.0"
121
+ end
122
+
123
+ a_record do
124
+ ttl 3600
125
+ content "0.0.0.0"
126
+ end
127
+ end
128
+ ```
129
+
130
+ ### Dynamic
131
+
132
+ DSLimple's DSL works on ruby.
133
+
134
+ ```ruby
135
+ require 'open-uri'
136
+ require 'json'
137
+
138
+ domain "example.internal" do
139
+ JSON.parse(open('http://my.internal.service/records.json', &:read)).each do |record_data|
140
+ recored record_data['name'], record_data['options'] { record_data['content'] }
141
+ end
142
+ end
143
+ ```
144
+
145
+ ## Contributing
146
+
147
+ Bug reports and pull requests are welcome on GitHub at https://github.com/zeny-io/dslimple.
148
+
149
+
150
+ ## License
151
+
152
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
153
+
154
+ ## Inspired by
155
+
156
+ - [roadworker](https://github.com/winebarrel/roadworker)
157
+
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require 'bundler/gem_tasks'
2
+ task default: :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "dslimple"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/dslimple.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'dslimple/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'dslimple'
8
+ spec.version = Dslimple::VERSION
9
+ spec.authors = ['Sho Kusano']
10
+ spec.email = ['sho-kusano@zeny.io']
11
+
12
+ spec.summary = 'DSLimple is a tool to manage DNSimple.'
13
+ spec.description = 'DSLimple is a tool to manage DNSimple. It defines the state of DNSimple using DSL, and updates DNSimple according to DSL.'
14
+ spec.homepage = 'https://github.com/zeny-io/dslimple'
15
+ spec.license = 'MIT'
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = 'exe'
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ['lib']
21
+
22
+ spec.add_development_dependency 'bundler', '~> 1.11'
23
+ spec.add_development_dependency 'rake', '~> 10.0'
24
+ spec.add_development_dependency 'rubocop'
25
+
26
+ spec.add_dependency 'dnsimple', '~> 2.1'
27
+ spec.add_dependency 'thor', '~> 0.19'
28
+ end
data/exe/dslimple ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'dslimple/cli'
4
+ Dslimple::CLI.start(ARGV)
@@ -0,0 +1,68 @@
1
+ require 'dslimple'
2
+ require 'pp'
3
+
4
+ class Dslimple::Applier
5
+ OPERATION_COLORS = {
6
+ addition: :green,
7
+ modification: :yellow,
8
+ deletion: :red
9
+ }
10
+
11
+ attr_reader :api_client, :shell, :options
12
+
13
+ def initialize(api_client, shell, options = {})
14
+ @api_client = api_client
15
+ @shell = shell
16
+ @options = options
17
+ end
18
+
19
+ def execute
20
+ dsl = Dslimple::DSL.new(options[:file], options)
21
+
22
+ dsl.execute
23
+
24
+ expected_domains = dsl.transform
25
+ expected_domains.select! { |domain| options[:only].include?(domain.name) } if options[:only].any?
26
+
27
+ @buildler = Dslimple::QueryBuilder.new(fetch_domains, expected_domains)
28
+ @buildler.execute
29
+ queries = @buildler.filtered_queries(options)
30
+
31
+ if queries.empty?
32
+ shell.say('No Changes', :bold)
33
+ return
34
+ end
35
+
36
+ show_plan(queries)
37
+
38
+ return if options[:dry_run] || !(options[:yes] || shell.yes?("Apply #{queries.size} changes. OK?(y/n) >"))
39
+
40
+ apply(queries)
41
+ end
42
+
43
+ def fetch_domains
44
+ domains = api_client.domains.list.map { |domain| Dslimple::Domain.new(domain.name, api_client, id: domain.id) }
45
+ domains.each(&:fetch_records!)
46
+ domains.select! { |domain| options[:only].include?(domain.name) } if options[:only].any?
47
+ domains
48
+ end
49
+
50
+ def show_plan(queries)
51
+ shell.say("Changes", :bold)
52
+ queries.each do |query|
53
+ show_query(query)
54
+ end
55
+ end
56
+
57
+ def apply(queries)
58
+ shell.say('Apply', :bold)
59
+ queries.each do |query|
60
+ show_query(query)
61
+ query.execute(api_client)
62
+ end
63
+ end
64
+
65
+ def show_query(query)
66
+ shell.say("#{shell.set_color(query.operation.to_s[0..2], OPERATION_COLORS[query.operation])} #{query.to_s}")
67
+ end
68
+ end
@@ -0,0 +1,81 @@
1
+ require 'thor'
2
+ require 'dnsimple'
3
+ require 'json'
4
+ require 'dslimple'
5
+
6
+ class Dslimple::CLI < Thor
7
+ include Thor::Actions
8
+
9
+ SANDBOX_API_ENDPOINT = 'https://api.sandbox.dnsimple.com'.freeze
10
+ USER_AGENT = "DSLimple: Simple CLI DNSimple client(v#{Dslimple::VERSION})".freeze
11
+
12
+ class_option :email, type: :string, aliases: %w(-e), desc: 'Your E-Mail address'
13
+ class_option :api_token, type: :string, aliases: %w(-t), desc: 'Your API token'
14
+ class_option :domain_token, type: :string, aliases: %w(-dt), desc: 'Your Domain API token'
15
+ class_option :sandbox, type: :boolean, default: ENV['DLSIMPLE_ENV'] == 'test', desc: 'Use sandbox API(at sandbox.dnsimple.com)'
16
+ class_option :debug, type: :boolean, default: false
17
+
18
+ desc 'export', 'Export domain specifications'
19
+ method_option :only, type: :array, default: [], aliases: %w(-o), desc: 'Specify domains for export'
20
+ method_option :file, type: :string, default: 'Domainfile', aliases: %w(-f), desc: 'Export Domainfile path'
21
+ method_option :dir, type: :string, default: './domainfiles', aliases: %w(-d), desc: 'Export directory path for split'
22
+ method_option :split, type: :boolean, default: false, aliases: %w(-s), desc: 'Export with split by domains'
23
+ method_option :modeline, type: :boolean, default: false, aliases: %w(-m), desc: 'Export with modeline for Vim'
24
+ method_option :soa_and_ns, type: :boolean, default: false, desc: 'Export without SOA and NS records'
25
+ def export
26
+ exporter = Dslimple::Exporter.new(api_client, options)
27
+
28
+ exporter.execute
29
+ rescue => e
30
+ rescue_from(e)
31
+ end
32
+
33
+ desc 'apply', 'Apply domain specifications'
34
+ method_option :only, type: :array, default: [], aliases: %w(-o), desc: 'Specify domains for apply'
35
+ method_option :dry_run, type: :boolean, default: false, aliases: %w(-d)
36
+ method_option :file, type: :string, default: 'Domainfile', aliases: %w(-f), desc: 'Source Domainfile path'
37
+ method_option :addition, type: :boolean, default: true, desc: 'Add specified records'
38
+ method_option :modification, type: :boolean, default: true, desc: 'Modify specified records'
39
+ method_option :deletion, type: :boolean, default: true, desc: 'Delete unspecified records'
40
+ method_option :yes, type: :boolean, default: false, aliases: %w(-y), desc: 'Do not confirm on before apply'
41
+ def apply
42
+ applier = Dslimple::Applier.new(api_client, self, options)
43
+
44
+ applier.execute
45
+ rescue => e
46
+ rescue_from(e)
47
+ rescue Dslimple::DSL::Error => e
48
+ rescue_from(e)
49
+ end
50
+
51
+ private
52
+
53
+ def api_client
54
+ @api_client ||= Dnsimple::Client.new(
55
+ username: options[:email] || ENV['DLSIMPLE_EMAIL'],
56
+ api_token: options[:api_token] || ENV['DLSIMPLE_API_TOKEN'],
57
+ domain_api_token: options[:domain_token] || ENV['DLSIMPLE_DOMAIN_TOKEN'],
58
+ api_endpoint: options[:sandbox] ? SANDBOX_API_ENDPOINT : nil,
59
+ user_agent: USER_AGENT
60
+ )
61
+ end
62
+
63
+ def rescue_from(e)
64
+ raise e if options[:debug]
65
+
66
+ case e
67
+ when Dnsimple::AuthenticationError
68
+ error(set_color(e.message, :red, :bold))
69
+ when Dnsimple::RequestError
70
+ error(set_color("#{e.message}: #{JSON.parse(e.response.body)['message']}", :yellow, :bold))
71
+ when Dslimple::DSL::Error
72
+ error(set_color(e.message, :yellow, :bold))
73
+ else
74
+ error(set_color("#{e.class}: #{e.message}", :red, :bold))
75
+ end
76
+ e.backtrace.each do |bt|
77
+ say(" #{set_color('from', :green)} #{bt}")
78
+ end
79
+ exit 1
80
+ end
81
+ end
@@ -0,0 +1,48 @@
1
+ require 'dslimple'
2
+
3
+ class Dslimple::Domain
4
+ attr_reader :name, :id
5
+ attr_accessor :api_client, :records
6
+
7
+ def initialize(name, api_client, options = {})
8
+ @name = name
9
+ @id = options[:id]
10
+ @api_client = api_client
11
+ @records = []
12
+ end
13
+
14
+ def escaped_name
15
+ Dslimple.escape_single_quote(name)
16
+ end
17
+
18
+ def records_without_soa_ns
19
+ records.select { |record| record.type != :soa && record.type != :ns }
20
+ end
21
+
22
+ def fetch_records
23
+ api_client.domains.records(name).map do |record|
24
+ Dslimple::Record.new(self, record.record_type, record.name, record.content, ttl: record.ttl, priority: record.priority, id: record.id)
25
+ end
26
+ end
27
+
28
+ def fetch_records!
29
+ @records = fetch_records
30
+ cleanup_records!
31
+ end
32
+
33
+ def cleanup_records!
34
+ @records = Dslimple::Record.cleanup_records(records)
35
+ end
36
+
37
+ def ==(other)
38
+ other.is_a?(Dslimple::Domain) && other.name == name
39
+ end
40
+ alias_method :eql, :==
41
+
42
+ def to_dsl(options = {})
43
+ <<"EOD"
44
+ domain '#{escaped_name}' do
45
+ #{(options[:soa_and_ns] ? records : records_without_soa_ns).map(&:to_dsl).join("\n")}end
46
+ EOD
47
+ end
48
+ end
@@ -0,0 +1,29 @@
1
+ require 'dslimple/dsl'
2
+
3
+ class Dslimple::DSL::Domain
4
+ attr_reader :name, :records
5
+
6
+ def initialize(name, &block)
7
+ @name = name
8
+ @records = []
9
+
10
+ instance_eval(&block)
11
+ end
12
+
13
+ def record(name = {}, options = {}, &block)
14
+ if name.is_a?(Hash)
15
+ options = options.merge(name)
16
+ name = ''
17
+ end
18
+
19
+ @records << Dslimple::DSL::Record.new(name, options, &block)
20
+ end
21
+
22
+ Dslimple::Record::RECORD_TYPES.each do |type|
23
+ class_eval(<<-EOC)
24
+ def #{type}_record(name = {}, options = {}, &block)
25
+ record(name, options.merge(type: :#{type}), &block)
26
+ end
27
+ EOC
28
+ end
29
+ end
@@ -0,0 +1,4 @@
1
+ require 'dslimple/dsl'
2
+
3
+ class Dslimple::DSL::Error < Exception
4
+ end
@@ -0,0 +1,25 @@
1
+ require 'dslimple/dsl'
2
+
3
+ class Dslimple::DSL::Record
4
+ attr_reader :name, :content, :options
5
+
6
+ def initialize(name, options = {}, &block)
7
+ @name = name
8
+ @options = options
9
+
10
+ returned_content = instance_eval(&block)
11
+ @content ||= returned_content
12
+ end
13
+
14
+ def priority(n)
15
+ @options[:priority] = n.to_s.to_i
16
+ end
17
+
18
+ def ttl(n)
19
+ @options[:ttl] = n.to_s.to_i
20
+ end
21
+
22
+ def content(c = nil)
23
+ c ? @content = c : @content
24
+ end
25
+ end
@@ -0,0 +1,65 @@
1
+ require 'pathname'
2
+ require 'dslimple'
3
+
4
+ class Dslimple::DSL
5
+ def initialize(file, context = {})
6
+ @file = Pathname.new(file)
7
+ @dir = @file.dirname
8
+ @domains = []
9
+ @files = []
10
+
11
+ @context = context
12
+ end
13
+
14
+ def execute
15
+ evaluate(@file)
16
+ end
17
+
18
+ def require(path)
19
+ if @dir.join(path).exist?
20
+ evaluate(@dir.join(path))
21
+ elsif @dir.join("#{path}.rb").exist?
22
+ evaluate(@dir.join("#{path}.rb"))
23
+ else
24
+ Kernel.require(path)
25
+ end
26
+ end
27
+
28
+ def evaluate(file)
29
+ @files << file.to_s
30
+ instance_eval(File.read(file), file.to_s)
31
+ rescue ScriptError => e
32
+ raise Dslimple::DSL::Error, "#{e.class}: #{e.message}", cleanup_backtrace(e.backtrace)
33
+ rescue StandardError => e
34
+ raise Dslimple::DSL::Error, "#{e.class}: #{e.message}", cleanup_backtrace(e.backtrace)
35
+ end
36
+
37
+ def domain(name, &block)
38
+ @domains << Dslimple::DSL::Domain.new(name, &block)
39
+ end
40
+
41
+ def transform
42
+ @domains.map do |domain|
43
+ Dslimple::Domain.new(domain.name, nil).tap do |model|
44
+ model.records = domain.records.map do |record|
45
+ Dslimple::Record.new(model, record.options[:type], record.name, record.content, record.options)
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def cleanup_backtrace(backtrace)
54
+ return backtrace if @context[:debug]
55
+
56
+ backtrace.select do |bt|
57
+ path = bt.split(':')[0..-3].join(':')
58
+ @files.include?(path)
59
+ end
60
+ end
61
+ end
62
+
63
+ require 'dslimple/dsl/domain'
64
+ require 'dslimple/dsl/record'
65
+ require 'dslimple/dsl/error'
@@ -0,0 +1,55 @@
1
+ require 'pathname'
2
+ require 'dslimple'
3
+
4
+ class Dslimple::Exporter
5
+ attr_reader :api_client, :options, :domains
6
+
7
+ def initialize(api_client, options)
8
+ @api_client = api_client
9
+ @options = options
10
+ @domains = []
11
+ end
12
+
13
+ def execute
14
+ @domains = fetch_domains
15
+
16
+ if options[:split] && options[:dir]
17
+ split_export(options[:dir], options[:file])
18
+ else
19
+ export(options[:file], domains)
20
+ end
21
+ end
22
+
23
+ def fetch_domains
24
+ domains = api_client.domains.list.map { |domain| Dslimple::Domain.new(domain.name, api_client) }
25
+ domains.each(&:fetch_records!)
26
+ domains.select! { |domain| options[:only].include?(domain.name) } unless options[:only].empty?
27
+ domains
28
+ end
29
+
30
+ def export(file, export_domains)
31
+ File.open(file.to_s, 'w') do |fd|
32
+ write_modeline(fd)
33
+ fd.puts export_domains.map { |domain| domain.to_dsl(options) }.join("\n")
34
+ end
35
+ end
36
+
37
+ def split_export(dir, file)
38
+ dir = Pathname.new(dir)
39
+ file = Pathname.new(file)
40
+ Dir.mkdir(dir.to_s) unless dir.directory?
41
+
42
+ File.open(file.to_s, 'w') do |fd|
43
+ write_modeline(fd)
44
+ domains.each do |domain|
45
+ domainfile = dir.join(domain.name)
46
+ export(domainfile, [domain])
47
+ fd.puts "require '#{domainfile.relative_path_from(file.dirname)}'"
48
+ end
49
+ end
50
+ end
51
+
52
+ def write_modeline(fd)
53
+ fd << "# -*- mode: ruby -*-\n# vi: set ft=ruby :\n\n" if options[:modeline]
54
+ end
55
+ end
@@ -0,0 +1,60 @@
1
+ require 'dslimple'
2
+
3
+ class Dslimple::Query
4
+ attr_reader :operation, :target, :domain, :params
5
+
6
+ def initialize(operation, target, domain, params = {})
7
+ @operation = operation
8
+ @target = target
9
+ @domain = domain
10
+ @params = params
11
+ end
12
+
13
+ %i(addition modification deletion).each do |operation|
14
+ class_eval(<<-EOC)
15
+ def #{operation}?
16
+ operation == :#{operation}
17
+ end
18
+ EOC
19
+ end
20
+
21
+ def to_s
22
+ if target == :domain
23
+ "#{domain.name}"
24
+ else
25
+ %(#{params[:record_type].to_s.rjust(5)} #{params[:name].to_s.rjust(10)}.#{domain.name} (#{record_options.join(', ')}) "#{params[:content]}")
26
+ end
27
+ end
28
+
29
+ def record_options
30
+ options = []
31
+ params.each_pair do |k, v|
32
+ options << "#{k}: #{v}" if %i(ttl prio).include?(k) && v
33
+ end
34
+ options
35
+ end
36
+
37
+ def execute(api_client)
38
+ __send__("execute_#{target}", api_client)
39
+ end
40
+
41
+ def execute_domain(api_client)
42
+ case operation
43
+ when :addition
44
+ api_client.domains.create(name: domain.name)
45
+ when :deletion
46
+ api_client.domains.delete(domain.name)
47
+ end
48
+ end
49
+
50
+ def execute_record(api_client)
51
+ case operation
52
+ when :addition
53
+ api_client.domains.create_record(domain.name, params)
54
+ when :modification
55
+ api_client.domains.update_record(domain.name, params[:id], params)
56
+ when :deletion
57
+ api_client.domains.delete_record(domain.name, params[:id])
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,86 @@
1
+ require 'dslimple'
2
+
3
+ class Dslimple::QueryBuilder
4
+ attr_reader :queries
5
+ attr_reader :expected_domains, :current_domains
6
+ attr_reader :append_records, :change_records, :delete_records
7
+
8
+ def initialize(current_domains, expected_domains)
9
+ @current_domains = Hash[*current_domains.map { |domain| [domain.name, domain] }.flatten]
10
+ @expected_domains = Hash[*expected_domains.map { |domain| [domain.name, domain] }.flatten]
11
+ end
12
+
13
+ def append_domains
14
+ @append_domains ||= expected_domains.values.reject { |domain| current_domains.key?(domain.name) }
15
+ end
16
+
17
+ def delete_domains
18
+ @delete_domains ||= current_domains.values.reject { |domain| expected_domains.key?(domain.name) }
19
+ end
20
+
21
+ def execute
22
+ @append_records = append_domains.map(&:records_without_soa_ns).flatten
23
+ @change_records = []
24
+ @delete_records = delete_domains.map(&:records_without_soa_ns).flatten
25
+
26
+ expected_domains.each_pair do |name, domain|
27
+ execute_records(name, domain)
28
+ end
29
+
30
+ build_queries
31
+ end
32
+
33
+ def execute_records(domain_name, domain)
34
+ current_domain = current_domains[domain_name]
35
+ return unless current_domain
36
+
37
+ current_records = current_domain.records_without_soa_ns.dup
38
+ domain.records_without_soa_ns.each do |record|
39
+ at = current_records.index { |current| current == record }
40
+ current_record = at ? current_records.slice!(at) : nil
41
+ like_record = current_records.find { |current| current === record }
42
+
43
+ if !like_record && !current_record
44
+ @append_records << record
45
+ elsif like_record
46
+ @change_records << [like_record, record]
47
+ current_records.delete(like_record)
48
+ end
49
+ end
50
+ @delete_records.concat(current_records)
51
+ end
52
+
53
+ def build_queries
54
+ @queries = []
55
+
56
+ append_domains.each do |domain|
57
+ @queries << Dslimple::Query.new(:addition, :domain, domain)
58
+ end
59
+
60
+ append_records.each do |record|
61
+ @queries << Dslimple::Query.new(:addition, :record, record.domain, record.to_params)
62
+ end
63
+
64
+ change_records.each do |old, new|
65
+ @queries << Dslimple::Query.new(:modification, :record, new.domain, new.to_params.merge(id: old.id))
66
+ end
67
+
68
+ delete_records.each do |record|
69
+ @queries << Dslimple::Query.new(:deletion, :record, record.domain, record.to_params)
70
+ end
71
+
72
+ delete_domains.each do |domain|
73
+ @queries << Dslimple::Query.new(:deletion, :domain, domain)
74
+ end
75
+
76
+ @queries
77
+ end
78
+
79
+ def filtered_queries(options)
80
+ queries.select do |query|
81
+ (query.addition? && options[:addition]) ||
82
+ (query.modification? && options[:modification]) ||
83
+ (query.deletion? && options[:deletion])
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,114 @@
1
+ require 'dslimple'
2
+
3
+ class Dslimple::Record
4
+ RECORD_TYPES = %i(a alias cname mx spf url txt ns srv naptr ptr aaaa sshfp hinfo pool).freeze
5
+ ALIAS_PREFIX = /\AALIAS for /
6
+ SPF_PREFIX = /\Av=spf1 /
7
+
8
+ attr_reader :domain, :type, :name, :id, :ttl, :priority, :content
9
+
10
+ def self.cleanup_records(records)
11
+ records = records.dup
12
+ alias_records = records.select(&:alias?)
13
+ spf_records = records.select(&:spf?)
14
+ txt_records = records.select { |record| record.like_spf? || record.like_alias? }
15
+
16
+ txt_records.each do |record|
17
+ reject = record.like_spf? ? spf_records.any? { |r| record.eql_spf?(r) } : alias_records.any? { |r| record.eql_alias?(r) }
18
+
19
+ records.delete(record) if reject
20
+ end
21
+
22
+ records
23
+ end
24
+
25
+ RECORD_TYPES.each do |type|
26
+ class_eval(<<-EOC)
27
+ def #{type}?
28
+ type == :#{type}
29
+ end
30
+ EOC
31
+ end
32
+
33
+ def initialize(domain, type, name, content, options = {})
34
+ @domain = domain
35
+ @type = type.to_s.downcase.to_sym
36
+ @name = name
37
+ @content = content
38
+ @ttl = options[:ttl]
39
+ @priority = options[:priority]
40
+ @id = options[:id]
41
+ end
42
+
43
+ def escaped_name
44
+ Dslimple.escape_single_quote(name)
45
+ end
46
+
47
+ def escaped_content
48
+ Dslimple.escape_single_quote(content)
49
+ end
50
+
51
+ def like_spf?
52
+ txt? && content.match(SPF_PREFIX)
53
+ end
54
+
55
+ def like_alias?
56
+ txt? && content.match(ALIAS_PREFIX)
57
+ end
58
+
59
+ def eql_spf?(spf_record)
60
+ spf_record.ttl == ttl && spf_record.content == content
61
+ end
62
+
63
+ def eql_alias?(alias_record)
64
+ alias_record.ttl == ttl && content == "ALIAS for #{alias_record.content}"
65
+ end
66
+
67
+ def ==(other)
68
+ other.is_a?(Dslimple::Record) && other.domain == domain && other.hash == hash
69
+ end
70
+ alias_method :eql, :==
71
+
72
+ def ===(other)
73
+ other.is_a?(Dslimple::Record) && other.domain == domain && other.rough_hash == rough_hash
74
+ end
75
+
76
+ def hash
77
+ "#{type}:#{name}:#{content}:#{ttl}:#{priority}"
78
+ end
79
+
80
+ def rough_hash
81
+ "#{type}:#{name}:#{content}"
82
+ end
83
+
84
+ def to_dsl_options
85
+ options = []
86
+ options << "'#{escaped_name}'" unless escaped_name.empty?
87
+ options << "ttl: #{ttl}" if ttl
88
+ options << "priority: #{priority}" if priority
89
+ options.join(', ')
90
+ end
91
+
92
+ def to_dsl
93
+ <<"EOD"
94
+ #{type}_record #{to_dsl_options} do
95
+ '#{escaped_content}'
96
+ end
97
+ EOD
98
+ end
99
+
100
+ def to_params
101
+ {
102
+ id: id,
103
+ record_type: type.to_s.upcase,
104
+ name: name,
105
+ content: content,
106
+ ttl: ttl,
107
+ prio: priority
108
+ }
109
+ end
110
+
111
+ def inspect
112
+ "<Dslimple::Record #{type.to_s.upcase} #{name}: #{content}>"
113
+ end
114
+ end
@@ -0,0 +1,3 @@
1
+ module Dslimple
2
+ VERSION = '1.0.0'.freeze
3
+ end
data/lib/dslimple.rb ADDED
@@ -0,0 +1,18 @@
1
+ require 'dslimple/version'
2
+ require 'dslimple/domain'
3
+ require 'dslimple/record'
4
+ require 'dslimple/dsl'
5
+ require 'dslimple/query'
6
+ require 'dslimple/query_builder'
7
+ require 'dslimple/exporter'
8
+ require 'dslimple/applier'
9
+
10
+ module Dslimple
11
+ ESCAPE_SINGLE_QUOTE_REGEXP = /[^\\]'/
12
+
13
+ def self.escape_single_quote(string)
14
+ string.gsub(ESCAPE_SINGLE_QUOTE_REGEXP) do |match|
15
+ match[0] + "\\'"
16
+ end
17
+ end
18
+ end
metadata ADDED
@@ -0,0 +1,139 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dslimple
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Sho Kusano
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-02-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.11'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.11'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rubocop
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: dnsimple
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.1'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.1'
69
+ - !ruby/object:Gem::Dependency
70
+ name: thor
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.19'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.19'
83
+ description: DSLimple is a tool to manage DNSimple. It defines the state of DNSimple
84
+ using DSL, and updates DNSimple according to DSL.
85
+ email:
86
+ - sho-kusano@zeny.io
87
+ executables:
88
+ - dslimple
89
+ extensions: []
90
+ extra_rdoc_files: []
91
+ files:
92
+ - ".gitignore"
93
+ - ".rubocop.yml"
94
+ - Gemfile
95
+ - LICENSE.txt
96
+ - README.md
97
+ - Rakefile
98
+ - bin/console
99
+ - bin/setup
100
+ - dslimple.gemspec
101
+ - exe/dslimple
102
+ - lib/dslimple.rb
103
+ - lib/dslimple/applier.rb
104
+ - lib/dslimple/cli.rb
105
+ - lib/dslimple/domain.rb
106
+ - lib/dslimple/dsl.rb
107
+ - lib/dslimple/dsl/domain.rb
108
+ - lib/dslimple/dsl/error.rb
109
+ - lib/dslimple/dsl/record.rb
110
+ - lib/dslimple/exporter.rb
111
+ - lib/dslimple/query.rb
112
+ - lib/dslimple/query_builder.rb
113
+ - lib/dslimple/record.rb
114
+ - lib/dslimple/version.rb
115
+ homepage: https://github.com/zeny-io/dslimple
116
+ licenses:
117
+ - MIT
118
+ metadata: {}
119
+ post_install_message:
120
+ rdoc_options: []
121
+ require_paths:
122
+ - lib
123
+ required_ruby_version: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - ">="
126
+ - !ruby/object:Gem::Version
127
+ version: '0'
128
+ required_rubygems_version: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ requirements: []
134
+ rubyforge_project:
135
+ rubygems_version: 2.5.1
136
+ signing_key:
137
+ specification_version: 4
138
+ summary: DSLimple is a tool to manage DNSimple.
139
+ test_files: []