dslimple 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|