dslimple 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.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rubocop.yml +28 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +157 -0
- data/Rakefile +2 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/dslimple.gemspec +28 -0
- data/exe/dslimple +4 -0
- data/lib/dslimple/applier.rb +68 -0
- data/lib/dslimple/cli.rb +81 -0
- data/lib/dslimple/domain.rb +48 -0
- data/lib/dslimple/dsl/domain.rb +29 -0
- data/lib/dslimple/dsl/error.rb +4 -0
- data/lib/dslimple/dsl/record.rb +25 -0
- data/lib/dslimple/dsl.rb +65 -0
- data/lib/dslimple/exporter.rb +55 -0
- data/lib/dslimple/query.rb +60 -0
- data/lib/dslimple/query_builder.rb +86 -0
- data/lib/dslimple/record.rb +114 -0
- data/lib/dslimple/version.rb +3 -0
- data/lib/dslimple.rb +18 -0
- metadata +139 -0
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
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
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
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
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,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
|
data/lib/dslimple/cli.rb
ADDED
@@ -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,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
|
data/lib/dslimple/dsl.rb
ADDED
@@ -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
|
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: []
|