record_store 2.1.0 → 3.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 +4 -4
- data/bin/console +3 -2
- data/lib/record_store.rb +2 -0
- data/lib/record_store/changeset.rb +22 -3
- data/lib/record_store/cli.rb +59 -66
- data/lib/record_store/provider.rb +92 -82
- data/lib/record_store/provider/dnsimple.rb +123 -133
- data/lib/record_store/provider/dynect.rb +73 -71
- data/lib/record_store/record.rb +1 -0
- data/lib/record_store/version.rb +1 -1
- data/lib/record_store/zone.rb +62 -135
- data/lib/record_store/zone/config.rb +16 -9
- data/lib/record_store/zone/yaml_definitions.rb +105 -0
- metadata +3 -2
data/lib/record_store/version.rb
CHANGED
data/lib/record_store/zone.rb
CHANGED
@@ -1,108 +1,10 @@
|
|
1
1
|
module RecordStore
|
2
2
|
class Zone
|
3
|
-
module YamlDefinitions
|
4
|
-
include Enumerable
|
5
|
-
|
6
|
-
def all
|
7
|
-
defined.values
|
8
|
-
end
|
9
|
-
|
10
|
-
def defined
|
11
|
-
@defined ||= yaml_files.inject({}) { |zones, file| zones.merge(load_yml_zone_definition(file)) }
|
12
|
-
end
|
13
|
-
|
14
|
-
def [](name)
|
15
|
-
defined.fetch(name)
|
16
|
-
end
|
17
|
-
|
18
|
-
def each(&block)
|
19
|
-
defined.each(&block)
|
20
|
-
end
|
21
|
-
|
22
|
-
def find(name)
|
23
|
-
return unless File.exists?(zone_path = "#{RecordStore.zones_path}/#{name}.yml")
|
24
|
-
load_yml_zone_definition(zone_path).first.last
|
25
|
-
end
|
26
|
-
|
27
|
-
def write(name, config:, records:, format: :file)
|
28
|
-
raise ArgumentError, "format must be :directory or :file" unless %i(file directory).include?(format)
|
29
|
-
name = name.chomp('.')
|
30
|
-
zone_file = "#{RecordStore.zones_path}/#{name}.yml"
|
31
|
-
zone = { name => { config: config.to_hash } }
|
32
|
-
records = records.map(&:to_hash).sort_by! {|r| [r.fetch(:fqdn), r.fetch(:type), r[:nsdname] || r[:address]]}
|
33
|
-
|
34
|
-
if format == :file
|
35
|
-
zone[name][:records] = records
|
36
|
-
write_yml_file(zone_file, zone.deep_stringify_keys)
|
37
|
-
remove_record_files(name)
|
38
|
-
else
|
39
|
-
write_yml_file(zone_file, zone.deep_stringify_keys)
|
40
|
-
remove_record_files(name)
|
41
|
-
write_record_files(name, records)
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
private
|
46
|
-
|
47
|
-
def write_yml_file(filename, data)
|
48
|
-
lines = data.to_yaml.lines
|
49
|
-
lines.shift if lines.first == "---\n"
|
50
|
-
File.write(filename, lines.join)
|
51
|
-
end
|
52
|
-
|
53
|
-
def load_yml_zone_definition(filename)
|
54
|
-
result = {}
|
55
|
-
dir = File.dirname(filename)
|
56
|
-
YAML.load_file(filename).each do |name, definition|
|
57
|
-
definition['records'] ||= []
|
58
|
-
Dir["#{dir}/#{name}/*__*.yml"].each do |record_file|
|
59
|
-
definition['records'] += load_yml_record_definitions(name, record_file)
|
60
|
-
end
|
61
|
-
result[name] = Zone.from_yaml_definition(name, definition)
|
62
|
-
end
|
63
|
-
result
|
64
|
-
end
|
65
|
-
|
66
|
-
def load_yml_record_definitions(name, record_file)
|
67
|
-
type, domain = File.basename(record_file, '.yml').split('__')
|
68
|
-
Array.wrap(YAML.load_file(record_file)).map do |record_definition|
|
69
|
-
record_definition.merge(fqdn: "#{domain}.#{name}", type: type)
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
def remove_record_files(name)
|
74
|
-
dir = "#{RecordStore.zones_path}/#{name}"
|
75
|
-
File.unlink(*Dir["#{dir}/*"])
|
76
|
-
Dir.unlink(dir)
|
77
|
-
rescue Errno::ENOENT
|
78
|
-
end
|
79
|
-
|
80
|
-
def write_record_files(name, records)
|
81
|
-
dir = "#{RecordStore.zones_path}/#{name}"
|
82
|
-
Dir.mkdir(dir)
|
83
|
-
records.group_by { |record| [record.fetch(:fqdn), record.fetch(:type)] }.each do |(fqdn, type), grouped_records|
|
84
|
-
grouped_records.each do |record|
|
85
|
-
record.delete(:fqdn)
|
86
|
-
record.delete(:type)
|
87
|
-
record.deep_stringify_keys!
|
88
|
-
end
|
89
|
-
grouped_records = grouped_records.first if grouped_records.size == 1
|
90
|
-
domain = fqdn.chomp('.').chomp(name).chomp('.')
|
91
|
-
write_yml_file("#{dir}/#{type}__#{domain}.yml", grouped_records)
|
92
|
-
end
|
93
|
-
end
|
94
|
-
|
95
|
-
def yaml_files
|
96
|
-
Dir["#{RecordStore.zones_path}/*.yml"]
|
97
|
-
end
|
98
|
-
end
|
99
|
-
|
100
3
|
extend YamlDefinitions
|
101
4
|
include ActiveModel::Validations
|
102
5
|
|
103
6
|
attr_accessor :name
|
104
7
|
attr_reader :config
|
105
|
-
attr_writer :records
|
106
8
|
|
107
9
|
validates :name, presence: true, format: { with: Record::FQDN_REGEX, message: 'is not a fully qualified domain name' }
|
108
10
|
validate :validate_records
|
@@ -115,50 +17,80 @@ module RecordStore
|
|
115
17
|
validate :validate_can_handle_alias_records
|
116
18
|
validate :validate_alias_points_to_root
|
117
19
|
|
118
|
-
|
119
|
-
|
120
|
-
|
20
|
+
class << self
|
21
|
+
def download(name, provider_name, **write_options)
|
22
|
+
zone = Zone.new(name: name, config: {providers: [provider_name]})
|
23
|
+
raise ArgumentError, zone.errors.full_messages.join("\n") unless zone.valid?
|
121
24
|
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
25
|
+
zone.records = zone.providers.first.retrieve_current_records(zone: name)
|
26
|
+
|
27
|
+
zone.config = Zone::Config.new(
|
28
|
+
providers: [provider_name],
|
29
|
+
ignore_patterns: [{type: "NS", fqdn: "#{name}."}],
|
30
|
+
supports_alias: (zone.records.map(&:type).include?('ALIAS') || nil)
|
31
|
+
)
|
32
|
+
|
33
|
+
zone.write(**write_options)
|
34
|
+
end
|
131
35
|
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
36
|
+
def filter_records(current_records, ignore_patterns)
|
37
|
+
ignore_patterns.inject(current_records) do |remaining_records, pattern|
|
38
|
+
remaining_records.reject do |record|
|
39
|
+
pattern.all? { |(key, value)| record.respond_to?(key) && value === record.send(key) }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def modified
|
45
|
+
modified_zones, mutex = [], Mutex.new
|
46
|
+
self.all.map do |zone|
|
47
|
+
thread = Thread.new do
|
48
|
+
mutex.synchronize {modified_zones << zone} unless zone.unchanged?
|
49
|
+
end
|
50
|
+
end.each(&:join)
|
51
|
+
|
52
|
+
modified_zones
|
53
|
+
end
|
136
54
|
end
|
137
55
|
|
138
|
-
def
|
139
|
-
|
56
|
+
def initialize(name:, records: [], config: {})
|
57
|
+
@name = Record.ensure_ends_with_dot(name)
|
58
|
+
@config = RecordStore::Zone::Config.new(config.deep_symbolize_keys)
|
59
|
+
@records = build_records(records)
|
60
|
+
end
|
140
61
|
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
62
|
+
def build_changesets
|
63
|
+
@changesets ||= begin
|
64
|
+
providers.map do |provider|
|
65
|
+
Changeset.build_from(provider: provider, zone: self)
|
66
|
+
end
|
67
|
+
end
|
145
68
|
end
|
146
69
|
|
147
70
|
def unchanged?
|
148
|
-
|
71
|
+
build_changesets.all?(&:empty?)
|
72
|
+
end
|
73
|
+
|
74
|
+
def unrooted_name
|
75
|
+
@name.chomp('.')
|
149
76
|
end
|
150
77
|
|
151
78
|
def records
|
152
79
|
@records_cache ||= Zone.filter_records(@records, config.ignore_patterns)
|
153
80
|
end
|
154
81
|
|
82
|
+
def records=(records)
|
83
|
+
@records_cache = nil
|
84
|
+
@records = records
|
85
|
+
end
|
86
|
+
|
155
87
|
def config=(config)
|
156
88
|
@records_cache = nil
|
157
89
|
@config = config
|
158
90
|
end
|
159
91
|
|
160
|
-
def
|
161
|
-
Provider.const_get(
|
92
|
+
def providers
|
93
|
+
@providers ||= config.providers.map { |provider| Provider.const_get(provider) }
|
162
94
|
end
|
163
95
|
|
164
96
|
def write(**write_options)
|
@@ -232,28 +164,23 @@ module RecordStore
|
|
232
164
|
end
|
233
165
|
end
|
234
166
|
|
235
|
-
def self.filter_records(current_records, ignore_patterns)
|
236
|
-
ignore_patterns.inject(current_records) do |remaining_records, pattern|
|
237
|
-
remaining_records.reject do |record|
|
238
|
-
pattern.all? { |(key, value)| record.respond_to?(key) && value === record.send(key) }
|
239
|
-
end
|
240
|
-
end
|
241
|
-
end
|
242
|
-
|
243
167
|
def validate_provider_can_handle_zone_records
|
244
168
|
record_types = records.map(&:type).to_set
|
245
169
|
return unless config.valid?
|
246
|
-
provider_supported_record_types = Provider.const_get(config.provider).record_types
|
247
170
|
|
248
|
-
|
249
|
-
|
171
|
+
providers.each do |provider|
|
172
|
+
(record_types - provider.record_types).each do |record_type|
|
173
|
+
errors.add(:records, "#{record_type} is a not a supported record type in #{provider.to_s}")
|
174
|
+
end
|
250
175
|
end
|
251
176
|
end
|
252
177
|
|
253
178
|
def validate_can_handle_alias_records
|
254
179
|
return unless records.any? { |record| record.is_a?(Record::ALIAS) }
|
255
180
|
return if config.supports_alias?
|
256
|
-
|
181
|
+
|
182
|
+
# TODO(es): refactor to specify which provider
|
183
|
+
errors.add(:records, "one of the providers for #{unrooted_name} does not support ALIAS records")
|
257
184
|
end
|
258
185
|
|
259
186
|
def validate_alias_points_to_root
|
@@ -3,35 +3,42 @@ module RecordStore
|
|
3
3
|
class Config
|
4
4
|
include ActiveModel::Validations
|
5
5
|
|
6
|
-
attr_reader :ignore_patterns, :
|
6
|
+
attr_reader :ignore_patterns, :providers, :supports_alias
|
7
7
|
|
8
8
|
validate :validate_zone_config
|
9
9
|
|
10
|
-
def initialize(ignore_patterns: [],
|
10
|
+
def initialize(ignore_patterns: [], providers: nil, supports_alias: nil)
|
11
11
|
@ignore_patterns = ignore_patterns
|
12
|
-
@
|
12
|
+
@providers = providers
|
13
13
|
@supports_alias = supports_alias
|
14
14
|
end
|
15
15
|
|
16
16
|
def supports_alias?
|
17
|
-
@supports_alias.nil?
|
17
|
+
if @supports_alias.nil?
|
18
|
+
valid_providers? && providers.all? { |provider| Provider.const_get(provider).supports_alias? }
|
19
|
+
else
|
20
|
+
@supports_alias
|
21
|
+
end
|
18
22
|
end
|
19
23
|
|
20
24
|
def to_hash
|
21
|
-
{
|
22
|
-
|
25
|
+
config_hash = {
|
26
|
+
providers: providers,
|
23
27
|
ignore_patterns: ignore_patterns,
|
24
28
|
}
|
29
|
+
config_hash.merge!(supports_alias: supports_alias) if supports_alias
|
30
|
+
|
31
|
+
config_hash
|
25
32
|
end
|
26
33
|
|
27
34
|
private
|
28
35
|
|
29
36
|
def validate_zone_config
|
30
|
-
errors.add(:
|
37
|
+
errors.add(:providers, 'provider specified does not exist') unless valid_providers?
|
31
38
|
end
|
32
39
|
|
33
|
-
def
|
34
|
-
Provider.constants.include?(provider.to_s.to_sym)
|
40
|
+
def valid_providers?
|
41
|
+
providers.present? && providers.all? { |provider| Provider.constants.include?(provider.to_s.to_sym) }
|
35
42
|
end
|
36
43
|
end
|
37
44
|
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
module RecordStore
|
2
|
+
class Zone
|
3
|
+
module YamlDefinitions
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
def all
|
7
|
+
defined.values
|
8
|
+
end
|
9
|
+
|
10
|
+
def defined
|
11
|
+
@defined ||= yaml_files
|
12
|
+
.map { |file| load_yml_zone_definition(file) }
|
13
|
+
.index_by { |zone| zone.unrooted_name }
|
14
|
+
end
|
15
|
+
|
16
|
+
def [](name)
|
17
|
+
defined.fetch(name)
|
18
|
+
end
|
19
|
+
|
20
|
+
def each(&block)
|
21
|
+
defined.each(&block)
|
22
|
+
end
|
23
|
+
|
24
|
+
def find(name)
|
25
|
+
defined[name]
|
26
|
+
end
|
27
|
+
|
28
|
+
def reset
|
29
|
+
@defined = nil
|
30
|
+
end
|
31
|
+
|
32
|
+
def write(name, config:, records:, format: :file)
|
33
|
+
raise ArgumentError, "format must be :directory or :file" unless %i(file directory).include?(format)
|
34
|
+
name = name.chomp('.')
|
35
|
+
zone_file = "#{RecordStore.zones_path}/#{name}.yml"
|
36
|
+
zone = { name => { config: config.to_hash } }
|
37
|
+
records = records.map(&:to_hash).sort_by! {|r| [r.fetch(:fqdn), r.fetch(:type), r[:nsdname] || r[:address]]}
|
38
|
+
|
39
|
+
if format == :file
|
40
|
+
zone[name][:records] = records
|
41
|
+
write_yml_file(zone_file, zone.deep_stringify_keys)
|
42
|
+
remove_record_files(name)
|
43
|
+
else
|
44
|
+
write_yml_file(zone_file, zone.deep_stringify_keys)
|
45
|
+
remove_record_files(name)
|
46
|
+
write_record_files(name, records)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def write_yml_file(filename, data)
|
53
|
+
lines = data.to_yaml.lines
|
54
|
+
lines.shift if lines.first == "---\n"
|
55
|
+
File.write(filename, lines.join)
|
56
|
+
end
|
57
|
+
|
58
|
+
def load_yml_zone_definition(filename)
|
59
|
+
dir = File.dirname(filename)
|
60
|
+
data = YAML.load_file(filename)
|
61
|
+
raise 'more than one zone in file' if data.size > 1
|
62
|
+
name, definition = data.first
|
63
|
+
definition['records'] ||= []
|
64
|
+
definition['records'] = definition['records'].map(&:deep_symbolize_keys)
|
65
|
+
Dir["#{dir}/#{name}/*__*.yml"].each do |record_file|
|
66
|
+
definition['records'] += load_yml_record_definitions(name, record_file)
|
67
|
+
end
|
68
|
+
Zone.new(name: name, records: definition['records'], config: definition['config'])
|
69
|
+
end
|
70
|
+
|
71
|
+
def load_yml_record_definitions(name, record_file)
|
72
|
+
type, domain = File.basename(record_file, '.yml').split('__')
|
73
|
+
Array.wrap(YAML.load_file(record_file)).map do |record_definition|
|
74
|
+
record_definition.merge('fqdn' => "#{domain}.#{name}", 'type' => type)
|
75
|
+
end.map(&:deep_symbolize_keys)
|
76
|
+
end
|
77
|
+
|
78
|
+
def remove_record_files(name)
|
79
|
+
dir = "#{RecordStore.zones_path}/#{name}"
|
80
|
+
File.unlink(*Dir["#{dir}/*"])
|
81
|
+
Dir.unlink(dir)
|
82
|
+
rescue Errno::ENOENT
|
83
|
+
end
|
84
|
+
|
85
|
+
def write_record_files(name, records)
|
86
|
+
dir = "#{RecordStore.zones_path}/#{name}"
|
87
|
+
Dir.mkdir(dir)
|
88
|
+
records.group_by { |record| [record.fetch(:fqdn), record.fetch(:type)] }.each do |(fqdn, type), grouped_records|
|
89
|
+
grouped_records.each do |record|
|
90
|
+
record.delete(:fqdn)
|
91
|
+
record.delete(:type)
|
92
|
+
record.deep_stringify_keys!
|
93
|
+
end
|
94
|
+
grouped_records = grouped_records.first if grouped_records.size == 1
|
95
|
+
domain = fqdn.chomp('.').chomp(name).chomp('.')
|
96
|
+
write_yml_file("#{dir}/#{type}__#{domain}.yml", grouped_records)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def yaml_files
|
101
|
+
Dir["#{RecordStore.zones_path}/*.yml"]
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: record_store
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 3.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Willem van Bergen
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2016-
|
12
|
+
date: 2016-10-27 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: thor
|
@@ -234,6 +234,7 @@ files:
|
|
234
234
|
- lib/record_store/version.rb
|
235
235
|
- lib/record_store/zone.rb
|
236
236
|
- lib/record_store/zone/config.rb
|
237
|
+
- lib/record_store/zone/yaml_definitions.rb
|
237
238
|
- record_store.gemspec
|
238
239
|
- template/Gemfile
|
239
240
|
- template/bin/record-store
|