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