vzcdn 0.1.9 → 0.2.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/README.md +5 -1
- data/bin/ec +14 -4
- data/bin/vzcdn +14 -4
- data/lib/args.rb +50 -31
- data/lib/clui_config.rb +1 -2
- data/lib/command.rb +81 -28
- data/lib/common.rb +2 -2
- data/lib/config_reader.rb +7 -15
- data/lib/diff.rb +80 -0
- data/lib/print.rb +23 -15
- data/lib/route.rb +74 -0
- data/lib/util.rb +25 -3
- data/lib/vzcdn.rb +1 -3
- data/lib/vzcdn/version.rb +1 -1
- data/lib/zone.rb +247 -97
- data/lib/zone_add.rb +68 -15
- data/lib/zone_delete.rb +51 -0
- data/lib/zone_hc.rb +78 -0
- data/lib/zone_update.rb +60 -0
- metadata +6 -2
data/lib/diff.rb
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
class Diff
|
|
4
|
+
def initialize(old, new)
|
|
5
|
+
@old = old
|
|
6
|
+
@new = new
|
|
7
|
+
@diffs = Hash.new([ ]) # entries will be "level" => [{ old: old_value, new: new_value, desc: description }, ...]
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def sprint_struct(struct)
|
|
11
|
+
if (struct.class == Array)
|
|
12
|
+
output = "[#{struct.length}]"
|
|
13
|
+
elsif (struct.class == Hash)
|
|
14
|
+
values = []
|
|
15
|
+
# print top level elements of hash
|
|
16
|
+
struct.keys.each { |key|
|
|
17
|
+
value = struct[key]
|
|
18
|
+
value = sprint_struct(value)
|
|
19
|
+
values << "#{key}=#{value}"
|
|
20
|
+
}
|
|
21
|
+
output = values.join(", ")
|
|
22
|
+
elsif (struct.nil?)
|
|
23
|
+
return "null"
|
|
24
|
+
else
|
|
25
|
+
output = "#{struct}"
|
|
26
|
+
end
|
|
27
|
+
output
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def add_diff(old, new, level)
|
|
31
|
+
diffs = @diffs[level].clone
|
|
32
|
+
diffs << {old: sprint_struct(old), new: sprint_struct(new)}
|
|
33
|
+
@diffs[level] = diffs
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def idiff(old, new, level)
|
|
37
|
+
if old.class != new.class
|
|
38
|
+
add_diff(old, new, level)
|
|
39
|
+
return
|
|
40
|
+
end
|
|
41
|
+
case [old.class]
|
|
42
|
+
when [Array]
|
|
43
|
+
num_to_check = [old.length, new.length].max
|
|
44
|
+
for i in 0...num_to_check
|
|
45
|
+
idiff(old[i], new[i], level + "[#{i+1}]")
|
|
46
|
+
end
|
|
47
|
+
when [Hash]
|
|
48
|
+
old.keys.each { |key|
|
|
49
|
+
idiff(old[key], new[key], level + ".#{key}")
|
|
50
|
+
}
|
|
51
|
+
else
|
|
52
|
+
if old != new
|
|
53
|
+
add_diff(old, new, level)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def print_diffs
|
|
59
|
+
@diffs.each { |level, diffs|
|
|
60
|
+
puts "at level #{level}"
|
|
61
|
+
diffs.each { |diff|
|
|
62
|
+
puts " old: #{diff[:old]} | new: #{diff[:new]}"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def run
|
|
68
|
+
idiff(@old, @new, "base")
|
|
69
|
+
print_diffs
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
file1 = ARGV[0]
|
|
74
|
+
file2 = ARGV[1]
|
|
75
|
+
|
|
76
|
+
old = JSON.parse(File.open(file1).read)
|
|
77
|
+
new = JSON.parse(File.open(file2).read)
|
|
78
|
+
|
|
79
|
+
diff = Diff.new(old, new)
|
|
80
|
+
diff.run
|
data/lib/print.rb
CHANGED
|
@@ -22,11 +22,6 @@ class TablePrint
|
|
|
22
22
|
@width = nil
|
|
23
23
|
@sep_char = sep_char
|
|
24
24
|
@lines << titles
|
|
25
|
-
separator = [ ]
|
|
26
|
-
titles.each { |title|
|
|
27
|
-
separator << char_repeat(title.length, @sep_char)
|
|
28
|
-
}
|
|
29
|
-
@lines << separator
|
|
30
25
|
end
|
|
31
26
|
|
|
32
27
|
def append(*columns)
|
|
@@ -87,16 +82,31 @@ class TablePrint
|
|
|
87
82
|
maxwidth
|
|
88
83
|
end
|
|
89
84
|
|
|
85
|
+
def print_line_normally(maxwidth, line)
|
|
86
|
+
output = ""
|
|
87
|
+
line.each_with_index { |value, index|
|
|
88
|
+
output = output + value.to_s + spaces(maxwidth[index] - value.to_s.length + 2)
|
|
89
|
+
}
|
|
90
|
+
puts output[0...-2]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def generate_separator_line(maxwidth)
|
|
94
|
+
line = []
|
|
95
|
+
maxwidth.each { |width|
|
|
96
|
+
line << separator(width)
|
|
97
|
+
}
|
|
98
|
+
line
|
|
99
|
+
end
|
|
100
|
+
|
|
90
101
|
def print_table_normally(maxwidth, header)
|
|
91
102
|
@lines.each_with_index { |line, lineno|
|
|
92
|
-
if lineno
|
|
103
|
+
if lineno == 0 && ! header
|
|
93
104
|
next
|
|
94
105
|
end
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
puts output[0...-2]
|
|
106
|
+
print_line_normally(maxwidth, line)
|
|
107
|
+
if lineno == 0 && header
|
|
108
|
+
print_line_normally(maxwidth, generate_separator_line(maxwidth))
|
|
109
|
+
end
|
|
100
110
|
}
|
|
101
111
|
end
|
|
102
112
|
|
|
@@ -114,7 +124,6 @@ class TablePrint
|
|
|
114
124
|
@width = minw
|
|
115
125
|
puts separator(term_width)
|
|
116
126
|
@lines.each_with_index { |line, lineno|
|
|
117
|
-
next if lineno == 1 # skip line of separators
|
|
118
127
|
if lineno == 0 && ! header
|
|
119
128
|
next
|
|
120
129
|
end
|
|
@@ -163,7 +172,6 @@ class TablePrint
|
|
|
163
172
|
}
|
|
164
173
|
end
|
|
165
174
|
|
|
166
|
-
# TODO: suppress headers
|
|
167
175
|
def print_table(header = true)
|
|
168
176
|
return if @lines.nil?
|
|
169
177
|
return if @ncols == 0
|
|
@@ -176,7 +184,7 @@ class TablePrint
|
|
|
176
184
|
# will lines fit in terminal
|
|
177
185
|
width_needed = maxwidth.reduce(:+) + (@ncols-1) * 2
|
|
178
186
|
term_width, h = Util.detect_terminal_size
|
|
179
|
-
if width_needed <= term_width
|
|
187
|
+
if (width_needed <= term_width) || (not header)
|
|
180
188
|
print_table_normally(maxwidth, header)
|
|
181
189
|
else
|
|
182
190
|
print_staggered(maxwidth, term_width, header)
|
|
@@ -221,7 +229,7 @@ class StructurePrint
|
|
|
221
229
|
|
|
222
230
|
def structure_print(struct, level, header = true)
|
|
223
231
|
istructure_print(struct, level, header)
|
|
224
|
-
if $debug
|
|
232
|
+
if $debug > 0
|
|
225
233
|
puts "level = #{level}"
|
|
226
234
|
end
|
|
227
235
|
@table.print_table(header) if @table
|
data/lib/route.rb
CHANGED
|
@@ -98,6 +98,7 @@ class Route
|
|
|
98
98
|
end
|
|
99
99
|
|
|
100
100
|
def untranslate_groups(group_array)
|
|
101
|
+
return if group_array.nil?
|
|
101
102
|
group_array.each { |group|
|
|
102
103
|
group_element = group["Group"]
|
|
103
104
|
["A", "AAAA", "CNAME"].each { |record_type|
|
|
@@ -114,16 +115,89 @@ class Route
|
|
|
114
115
|
}
|
|
115
116
|
end
|
|
116
117
|
|
|
118
|
+
def each_record(group, &block)
|
|
119
|
+
group['Group'].each { |rtype, array|
|
|
120
|
+
# elements of array have <HealthCheck>, <Record> and either <IsPrimary> or <Weight>
|
|
121
|
+
array.each { |entry|
|
|
122
|
+
yield entry
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def normalize_group(group_arr)
|
|
128
|
+
group_arr.each { |group|
|
|
129
|
+
ttl = -1
|
|
130
|
+
each_record(group) { |entry|
|
|
131
|
+
record = entry['Record']
|
|
132
|
+
if ttl == -1 then ttl = record['TTL'] end
|
|
133
|
+
entry['Rdata'] = record['Rdata']
|
|
134
|
+
entry.delete('Record')
|
|
135
|
+
}
|
|
136
|
+
group['TTL'] = ttl
|
|
137
|
+
}
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def denormalize_group(group_arr)
|
|
141
|
+
return if group_arr.nil?
|
|
142
|
+
group_arr.each { |group|
|
|
143
|
+
ttl = group['TTL']
|
|
144
|
+
name = group['Name']
|
|
145
|
+
each_record(group) { |entry|
|
|
146
|
+
rdata = entry['Rdata']
|
|
147
|
+
entry.delete('Rdata')
|
|
148
|
+
entry['Record'] = { 'Name' => name, 'Rdata' => rdata, 'TTL' => ttl }
|
|
149
|
+
}
|
|
150
|
+
group.delete('TTL')
|
|
151
|
+
}
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def denormalize_zone(zone)
|
|
155
|
+
records = zone['Records']
|
|
156
|
+
empty = true
|
|
157
|
+
if records
|
|
158
|
+
records.keys.each { |key|
|
|
159
|
+
if records[key].length > 0
|
|
160
|
+
empty = false
|
|
161
|
+
break
|
|
162
|
+
end
|
|
163
|
+
}
|
|
164
|
+
if empty
|
|
165
|
+
zone.delete('Records')
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
denormalize_group(zone['LoadBalancingGroups'])
|
|
169
|
+
denormalize_group(zone['FailoverGroups'])
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def normalize_zone(zone)
|
|
173
|
+
if zone['Records'].nil?
|
|
174
|
+
zone['Records'] = {
|
|
175
|
+
"A"=> [ ],
|
|
176
|
+
"AAAA"=> [ ],
|
|
177
|
+
"CNAME"=> [ ],
|
|
178
|
+
"MX"=> [ ],
|
|
179
|
+
"NS"=> [ ],
|
|
180
|
+
"SPF"=> [ ],
|
|
181
|
+
"SRV"=> [ ],
|
|
182
|
+
"TXT"=> [ ]
|
|
183
|
+
}
|
|
184
|
+
end
|
|
185
|
+
normalize_group(zone['LoadBalancingGroups'])
|
|
186
|
+
normalize_group(zone['FailoverGroups'])
|
|
187
|
+
end
|
|
188
|
+
|
|
117
189
|
def translate_zone(zone)
|
|
118
190
|
return nil if zone.nil?
|
|
119
191
|
translate_value(:zone_status, zone, "Status")
|
|
120
192
|
translate_value(:zone_type, zone, "ZoneType")
|
|
121
193
|
translate_groups zone["LoadBalancingGroups"]
|
|
122
194
|
translate_groups zone["FailoverGroups"]
|
|
195
|
+
normalize_zone(zone)
|
|
123
196
|
end
|
|
124
197
|
|
|
125
198
|
def untranslate_zone(zone)
|
|
126
199
|
return nil if zone.nil?
|
|
200
|
+
denormalize_zone(zone)
|
|
127
201
|
untranslate_value(:zone_status, zone, "Status")
|
|
128
202
|
untranslate_value(:zone_type, zone, "ZoneType")
|
|
129
203
|
untranslate_groups zone["LoadBalancingGroups"]
|
data/lib/util.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
class Util
|
|
2
2
|
|
|
3
|
-
def self.parse_line(line)
|
|
4
|
-
idx = line.index(
|
|
3
|
+
def self.parse_line(line, char='=')
|
|
4
|
+
idx = line.index(char)
|
|
5
5
|
if (idx)
|
|
6
6
|
return line[0...idx], line[idx+1..-1]
|
|
7
7
|
else
|
|
@@ -9,6 +9,24 @@ class Util
|
|
|
9
9
|
end
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
+
# let user lowercase and abbreviate key; abbreviation must match one key
|
|
13
|
+
def self.match_key(abbrev, struct)
|
|
14
|
+
abbrev = abbrev.downcase
|
|
15
|
+
key = nil
|
|
16
|
+
struct.keys.each { |fullkey|
|
|
17
|
+
testkey = fullkey.downcase
|
|
18
|
+
return fullkey if abbrev == testkey
|
|
19
|
+
if fullkey.downcase.start_with?(abbrev)
|
|
20
|
+
if (! key)
|
|
21
|
+
key = fullkey
|
|
22
|
+
else
|
|
23
|
+
return nil
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
}
|
|
27
|
+
key
|
|
28
|
+
end
|
|
29
|
+
|
|
12
30
|
# Determines if a shell command exists by searching for it in ENV['PATH'].
|
|
13
31
|
def self.command_exists?(command)
|
|
14
32
|
ENV['PATH'].split(File::PATH_SEPARATOR).any? {|d| File.exists? File.join(d, command) }
|
|
@@ -34,6 +52,10 @@ end
|
|
|
34
52
|
class String
|
|
35
53
|
def full_match(re)
|
|
36
54
|
m = re.match(self)
|
|
37
|
-
m && (m.begin(0) == 0) && (m.end(0) == self.length)
|
|
55
|
+
if m && (m.begin(0) == 0) && (m.end(0) == self.length)
|
|
56
|
+
m
|
|
57
|
+
else
|
|
58
|
+
nil
|
|
59
|
+
end
|
|
38
60
|
end
|
|
39
61
|
end
|
data/lib/vzcdn.rb
CHANGED
|
@@ -5,8 +5,6 @@ require_relative 'clui_config'
|
|
|
5
5
|
require_relative 'zone'
|
|
6
6
|
require_relative 'vzcdn/version'
|
|
7
7
|
|
|
8
|
-
$debug = false
|
|
9
|
-
|
|
10
8
|
class VzcdnApp < Command
|
|
11
9
|
def add_options
|
|
12
10
|
add_option({ short_name: 'v', long_name: 'version', method: :print_version, takes_value: false})
|
|
@@ -19,7 +17,7 @@ class VzcdnApp < Command
|
|
|
19
17
|
end
|
|
20
18
|
|
|
21
19
|
def set_debug
|
|
22
|
-
$debug
|
|
20
|
+
$debug += 1
|
|
23
21
|
end
|
|
24
22
|
|
|
25
23
|
def create_args
|
data/lib/vzcdn/version.rb
CHANGED
data/lib/zone.rb
CHANGED
|
@@ -4,19 +4,57 @@ require_relative 'print'
|
|
|
4
4
|
require_relative 'command'
|
|
5
5
|
require_relative 'args'
|
|
6
6
|
require_relative 'zone_add'
|
|
7
|
+
require_relative 'zone_update'
|
|
8
|
+
require_relative 'zone_delete'
|
|
9
|
+
require_relative 'zone_hc'
|
|
10
|
+
|
|
11
|
+
ZONEDIR="zones"
|
|
7
12
|
|
|
8
13
|
class ZoneList < Command
|
|
14
|
+
include ConfigReader
|
|
15
|
+
|
|
16
|
+
LOCAL_STATUS = { 1 => "-", 3 => "*", 5 => "!", 7 => "!*"}
|
|
17
|
+
|
|
9
18
|
def execute(args, rest)
|
|
19
|
+
zone_instance = @input['zone']
|
|
10
20
|
zones = Route.do.get_all_zones
|
|
11
21
|
Route.do.translate_zonelist(zones)
|
|
22
|
+
zones.each { | zone |
|
|
23
|
+
file_name = zone_instance.local_filename(zone['DomainName'], zone['ZoneId'])
|
|
24
|
+
local_status = 0
|
|
25
|
+
if File.exists? (file_name)
|
|
26
|
+
local_status += 1
|
|
27
|
+
if not FileUtils.compare_file(file_name, config_file(file_name))
|
|
28
|
+
local_status += 2
|
|
29
|
+
end
|
|
30
|
+
contents = File.open(file_name).read
|
|
31
|
+
local_zone = JSON.parse(File.open(file_name).read)
|
|
32
|
+
if local_zone['Version'] != zone['Version']
|
|
33
|
+
local_status += 4
|
|
34
|
+
end
|
|
35
|
+
local_status = LOCAL_STATUS[local_status]
|
|
36
|
+
else
|
|
37
|
+
local_status = ''
|
|
38
|
+
end
|
|
39
|
+
zone["DomainName"] = sprintf("%-2s %s", local_status, zone["DomainName"])
|
|
40
|
+
}
|
|
12
41
|
StructurePrint.new('~').structure_print(zones, "zonelist")
|
|
13
42
|
end
|
|
14
43
|
end
|
|
15
44
|
|
|
16
45
|
class ZonePull < Command
|
|
46
|
+
|
|
47
|
+
def save_zone_to_file(file)
|
|
48
|
+
@zone_file = file
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def add_options
|
|
52
|
+
add_option({ short_name: 'f', long_name: 'file', method: :save_zone_to_file, takes_value: true})
|
|
53
|
+
end
|
|
54
|
+
|
|
17
55
|
def execute(args, rest)
|
|
18
56
|
zone = @input["zone"]
|
|
19
|
-
zone.pull
|
|
57
|
+
zone.pull @zone_file
|
|
20
58
|
end
|
|
21
59
|
end
|
|
22
60
|
|
|
@@ -27,19 +65,24 @@ class ZonePush < Command
|
|
|
27
65
|
end
|
|
28
66
|
end
|
|
29
67
|
|
|
30
|
-
class
|
|
68
|
+
class ZoneAudit< Command
|
|
31
69
|
def execute(args, rest)
|
|
32
70
|
zone = @input["zone"]
|
|
33
|
-
zone.
|
|
71
|
+
zone.show_audit
|
|
34
72
|
end
|
|
35
73
|
end
|
|
36
74
|
|
|
37
75
|
class ZonePrint < Command
|
|
38
76
|
def add_options
|
|
39
77
|
add_option({ short_name: 'j', long_name: 'json', method: :set_format, takes_value: false})
|
|
78
|
+
add_option({ short_name: 'n', long_name: 'no-header', method: :unset_header, takes_value: false})
|
|
40
79
|
# add_option({ short_name: 'f', long_name: 'force-online', method: :force_online, takes_value: false})
|
|
41
80
|
end
|
|
42
81
|
|
|
82
|
+
def unset_header
|
|
83
|
+
@no_header = true
|
|
84
|
+
end
|
|
85
|
+
|
|
43
86
|
def set_format
|
|
44
87
|
@format = :json
|
|
45
88
|
end
|
|
@@ -51,7 +94,7 @@ class ZonePrint < Command
|
|
|
51
94
|
if @format == :json
|
|
52
95
|
puts JSON.pretty_generate(struct)
|
|
53
96
|
else
|
|
54
|
-
StructurePrint.new(sep_char).structure_print(struct, level)
|
|
97
|
+
StructurePrint.new(sep_char).structure_print(struct, level, (not @no_header))
|
|
55
98
|
end
|
|
56
99
|
end
|
|
57
100
|
end
|
|
@@ -70,6 +113,7 @@ class ZoneCreate < Command
|
|
|
70
113
|
|
|
71
114
|
def execute(arghash, rest)
|
|
72
115
|
name = arghash["name"]
|
|
116
|
+
zone_command = @input["zone"]
|
|
73
117
|
if name[-1] != '.'
|
|
74
118
|
name = name + '.'
|
|
75
119
|
end
|
|
@@ -78,23 +122,31 @@ class ZoneCreate < Command
|
|
|
78
122
|
comment = "" if comment.nil?
|
|
79
123
|
type = arghash["type"]
|
|
80
124
|
dputs "name = #{name}, comment=#{comment}, status=#{status}, type=#{type}"
|
|
81
|
-
zone = { "Comment" => comment, "DomainName" => name, "Status" => status, "ZoneType" => type
|
|
125
|
+
zone = { "Comment" => comment, "DomainName" => name, "Status" => status, "ZoneType" => type }
|
|
82
126
|
puts "creating zone on server"
|
|
83
|
-
Route.do.
|
|
127
|
+
Route.do.untranslate_zone(zone)
|
|
128
|
+
new_zone = Route.do.add_zone(zone)
|
|
129
|
+
Route.do.translate_zone(new_zone)
|
|
130
|
+
zone_command.write_new_zone(new_zone)
|
|
84
131
|
end
|
|
85
132
|
end
|
|
86
133
|
|
|
87
134
|
class Zone < Command
|
|
88
135
|
include ConfigReader
|
|
89
136
|
|
|
137
|
+
attr_reader :zone_file
|
|
138
|
+
|
|
90
139
|
def create_args
|
|
91
|
-
add_cmd(ZoneList, "list", "list all owned zones")
|
|
140
|
+
add_cmd(ZoneList, "list", "list all owned zones. ! outdated, * edited, - pulled")
|
|
92
141
|
add_cmd(ZonePull, "pull", "retrieve local copy of zone data")
|
|
93
142
|
add_cmd(ZonePush, "push", "push local changes to server")
|
|
143
|
+
add_cmd(ZoneAudit, "audit", "show the edits done on the zone locally")
|
|
94
144
|
add_cmd(ZonePrint, "print", "print zone data. ~~~~ online, ---- local copy")
|
|
95
145
|
add_cmd(ZoneAdd, "add", "add record/group/healthcheck")
|
|
146
|
+
add_cmd(ZoneUpdate, "update", "update field")
|
|
96
147
|
add_cmd(ZoneCreate, "create", "create new zone")
|
|
97
148
|
add_cmd(ZoneDelete, "delete", "delete zone")
|
|
149
|
+
add_cmd(ZoneSetHealthCheck, "sethc", "set health check for group member")
|
|
98
150
|
add_arg("zname", "name or id of zone", :alnum_not_command)
|
|
99
151
|
add_arg("drill-down-param", "names of fields, or indexes of items, for drilling down", :alnum_not_command)
|
|
100
152
|
end
|
|
@@ -105,8 +157,12 @@ class Zone < Command
|
|
|
105
157
|
add_flow_from_usage("<zname> pull")
|
|
106
158
|
add_flow_from_usage("<zname> push")
|
|
107
159
|
add_flow_from_usage("<zname> delete")
|
|
160
|
+
add_flow_from_usage("<zname> audit")
|
|
108
161
|
add_flow_from_usage("<zname> [<drill-down-param>...] add")
|
|
162
|
+
add_flow_from_usage("<zname> [<drill-down-param>...] update")
|
|
163
|
+
add_flow_from_usage("<zname> <drill-down-param>... delete")
|
|
109
164
|
add_flow_from_usage("<zname> [<drill-down-param>...] [print]")
|
|
165
|
+
add_flow_from_usage("<zname> <drill-down-param>... sethc")
|
|
110
166
|
end
|
|
111
167
|
|
|
112
168
|
def alnum_not_command(value)
|
|
@@ -121,8 +177,67 @@ class Zone < Command
|
|
|
121
177
|
return "invalid argument", false
|
|
122
178
|
end
|
|
123
179
|
|
|
180
|
+
def set_zone_file(zone = nil)
|
|
181
|
+
if zone
|
|
182
|
+
@zone_id = zone["ZoneId"]
|
|
183
|
+
@zone_file = local_filename(zone["DomainName"], @zone_id)
|
|
184
|
+
return
|
|
185
|
+
end
|
|
186
|
+
return if @zname.nil?
|
|
187
|
+
if File.exists?(@zname)
|
|
188
|
+
@zone_file = @zname
|
|
189
|
+
@standard_zone_file = false
|
|
190
|
+
return
|
|
191
|
+
end
|
|
192
|
+
@standard_zone_file = true
|
|
193
|
+
id, name = id_and_name
|
|
194
|
+
if id.nil?
|
|
195
|
+
r = name + '\.(\d+)'
|
|
196
|
+
else
|
|
197
|
+
r = '.*\.(' + id + ')'
|
|
198
|
+
end
|
|
199
|
+
regex = Regexp.new(r)
|
|
200
|
+
Dir.new(user_file(ZONEDIR)).each { |file|
|
|
201
|
+
if (m = file.full_match(regex))
|
|
202
|
+
@zone_file = user_file(ZONEDIR, file)
|
|
203
|
+
@zone_id = m[1]
|
|
204
|
+
break
|
|
205
|
+
end
|
|
206
|
+
}
|
|
207
|
+
unless @zone_file
|
|
208
|
+
zones = Route.do.get_all_zones
|
|
209
|
+
zones.each { |zonehash|
|
|
210
|
+
zonehash_name = zonehash['DomainName'].sub!(/\.\Z/, '')
|
|
211
|
+
if name && zonehash_name == name
|
|
212
|
+
id = zonehash['ZoneId']
|
|
213
|
+
break
|
|
214
|
+
elsif id && zonehash['ZoneId'].to_s == id
|
|
215
|
+
name = zonehash_name
|
|
216
|
+
puts "set_zone_file, name = #{name}, id = #{id}"
|
|
217
|
+
break
|
|
218
|
+
end
|
|
219
|
+
}
|
|
220
|
+
if name && id
|
|
221
|
+
@zone_file = local_filename(name, id)
|
|
222
|
+
@zone_id = id
|
|
223
|
+
else
|
|
224
|
+
raise "Bad zname or id (#{@zname})"
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def write_new_zone(zone)
|
|
230
|
+
set_zone_file(zone)
|
|
231
|
+
@standard_zone_file = true
|
|
232
|
+
write_zone_file(zone, true)
|
|
233
|
+
zonelist = ZoneList.new(nil, nil)
|
|
234
|
+
zonelist.add_input("zone", self)
|
|
235
|
+
zonelist.execute(nil, nil)
|
|
236
|
+
end
|
|
237
|
+
|
|
124
238
|
def execute(args, rest)
|
|
125
239
|
@zname = args["zname"]
|
|
240
|
+
set_zone_file
|
|
126
241
|
command = args["command"]
|
|
127
242
|
ddparams = args["drill-down-param"]
|
|
128
243
|
if ddparams
|
|
@@ -132,6 +247,7 @@ class Zone < Command
|
|
|
132
247
|
command.add_input("level", level)
|
|
133
248
|
command.add_input("sep_char", @sep_char)
|
|
134
249
|
command.add_input("toplevel", zone)
|
|
250
|
+
command.add_input("rest", rest)
|
|
135
251
|
end
|
|
136
252
|
if command
|
|
137
253
|
command.add_input("zone", self)
|
|
@@ -141,57 +257,132 @@ class Zone < Command
|
|
|
141
257
|
end
|
|
142
258
|
end
|
|
143
259
|
|
|
260
|
+
def group_sanity_check(zone, groupname)
|
|
261
|
+
zone[groupname].each_with_index { | group, index |
|
|
262
|
+
numRecords = 0
|
|
263
|
+
primaries = 0
|
|
264
|
+
grouppath = "#{groupname}[#{index+1}]"
|
|
265
|
+
group['Group'].each { |rtype, array|
|
|
266
|
+
if array.length > 0
|
|
267
|
+
if array.length > 0
|
|
268
|
+
numRecords += array.length
|
|
269
|
+
if array.length < 2
|
|
270
|
+
@errors << "need 2 '#{rtype}' records in #{grouppath}"
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
array.each { | element |
|
|
274
|
+
if element['IsPrimary']
|
|
275
|
+
primaries += 1
|
|
276
|
+
end
|
|
277
|
+
}
|
|
278
|
+
end
|
|
279
|
+
}
|
|
280
|
+
if numRecords == 0
|
|
281
|
+
@errors << "empty group: #{grouppath}"
|
|
282
|
+
end
|
|
283
|
+
if groupname == 'FailoverGroups'
|
|
284
|
+
if numRecords != 2
|
|
285
|
+
@errors << "failover group #{grouppath} must have exactly 2 records"
|
|
286
|
+
end
|
|
287
|
+
if primaries != 1
|
|
288
|
+
@errors << "failover group #{grouppath} must have exactly 1 primary"
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
}
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def sanity_check(zone)
|
|
295
|
+
@errors = [ ]
|
|
296
|
+
group_sanity_check(zone, 'LoadBalancingGroups')
|
|
297
|
+
group_sanity_check(zone, 'FailoverGroups')
|
|
298
|
+
if @errors.length > 0
|
|
299
|
+
raise @errors.join("\n")
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
144
303
|
def push
|
|
145
|
-
puts "pushing zone to server"
|
|
146
304
|
zone = get_zone
|
|
305
|
+
sanity_check(zone)
|
|
306
|
+
puts "pushing zone to server"
|
|
147
307
|
Route.do.untranslate_zone(zone)
|
|
148
|
-
if $debug
|
|
308
|
+
if $debug > 1
|
|
149
309
|
File.open("untranslated.zone","w").write(JSON.pretty_generate zone)
|
|
150
310
|
end
|
|
151
|
-
|
|
311
|
+
dputs "in push, version before update = #{zone["Version"]}"
|
|
312
|
+
zone = Route.do.update_zone(zone)
|
|
313
|
+
Route.do.translate_zone(zone)
|
|
314
|
+
dputs "in push, version after update = #{zone["Version"]}"
|
|
315
|
+
write_zone_file(zone, true)
|
|
316
|
+
remove_audit
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def local_filename(name, id)
|
|
320
|
+
name.sub!(/\.\Z/, '')
|
|
321
|
+
user_file(ZONEDIR, "#{name}.#{id}")
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def write_zone_to_single_file(zone, filename)
|
|
325
|
+
File.delete(filename) if File.exists?(filename)
|
|
326
|
+
File.open(filename, "w") { |file|
|
|
327
|
+
file.write(JSON.pretty_generate zone)
|
|
328
|
+
}
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def write_zone_file(zone, save_backup_ok)
|
|
332
|
+
write_zone_to_single_file(zone, @zone_file)
|
|
333
|
+
audit
|
|
334
|
+
if save_backup_ok && @standard_zone_file
|
|
335
|
+
write_zone_to_single_file(zone, config_file(@zone_file))
|
|
336
|
+
end
|
|
152
337
|
end
|
|
153
338
|
|
|
154
|
-
def
|
|
155
|
-
|
|
156
|
-
|
|
339
|
+
def audit
|
|
340
|
+
audit_file = config_file( "audit", @zone_file)
|
|
341
|
+
File.open(audit_file, 'a') { | file |
|
|
342
|
+
file.write($USER_ENTERED_COMMAND)
|
|
343
|
+
file.write("\n")
|
|
344
|
+
}
|
|
157
345
|
end
|
|
158
346
|
|
|
159
|
-
def
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
347
|
+
def remove_audit
|
|
348
|
+
audit_file = config_file( "audit", @zone_file)
|
|
349
|
+
File.delete(audit_file)
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def show_audit
|
|
353
|
+
audit_file = config_file( "audit", @zone_file)
|
|
354
|
+
if File.exists? audit_file
|
|
355
|
+
puts File.read(audit_file)
|
|
356
|
+
else
|
|
357
|
+
puts "no edits on the zone"
|
|
166
358
|
end
|
|
167
|
-
file_name = user_file("zones", basename)
|
|
168
|
-
File.delete(file_name) if File.exists?(file_name)
|
|
169
|
-
File.open(file_name, "w").write(JSON.pretty_generate zone)
|
|
170
|
-
file_name
|
|
171
359
|
end
|
|
172
360
|
|
|
173
|
-
def pull
|
|
361
|
+
def pull(target_file)
|
|
174
362
|
zone = get_zone(true)
|
|
175
|
-
|
|
176
|
-
|
|
363
|
+
if target_file
|
|
364
|
+
@zone_file = target_file # user supplied file with -f option
|
|
365
|
+
end
|
|
366
|
+
dputs "in zone.pull, @zname=#{@zname}, zone_file=#{@zone_file}"
|
|
367
|
+
if @zone_file == @zname
|
|
368
|
+
raise "zone filename can't be same as zone name"
|
|
369
|
+
end
|
|
370
|
+
write_zone_file(zone, true)
|
|
371
|
+
remove_audit
|
|
372
|
+
puts "'#{@zone_file}' saved locally"
|
|
177
373
|
end
|
|
178
374
|
|
|
179
375
|
def delete_local_zone_files(id)
|
|
180
|
-
files = Dir.glob(
|
|
376
|
+
files = Dir.glob(config_file(ZONEDIR, "*.#{id}"))
|
|
181
377
|
files.each { |file| File.delete(file) }
|
|
182
|
-
files = Dir.glob(
|
|
378
|
+
files = Dir.glob(user_file(ZONEDIR, "*.#{id}"))
|
|
183
379
|
files.each { |file| File.delete(file) }
|
|
184
380
|
end
|
|
185
381
|
|
|
186
382
|
def delete
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
id = zone["ZoneId"]
|
|
191
|
-
end
|
|
192
|
-
puts "deleting zone with id #{id}"
|
|
193
|
-
delete_local_zone_files(id)
|
|
194
|
-
Route.do.delete_zone(id)
|
|
383
|
+
puts "deleting zone with id #{@zone_id}"
|
|
384
|
+
delete_local_zone_files(@zone_id)
|
|
385
|
+
Route.do.delete_zone(@zone_id)
|
|
195
386
|
end
|
|
196
387
|
|
|
197
388
|
def id_and_name
|
|
@@ -206,69 +397,26 @@ class Zone < Command
|
|
|
206
397
|
end
|
|
207
398
|
|
|
208
399
|
def get_zone(forceRest = false)
|
|
209
|
-
@sep_char = "-"
|
|
210
|
-
zonedir = mk_user_dir("zones")
|
|
211
400
|
zone = nil
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if
|
|
215
|
-
|
|
216
|
-
r = name + '\.\d+'
|
|
217
|
-
else
|
|
218
|
-
r = '.*\.' + id
|
|
219
|
-
end
|
|
220
|
-
regex = Regexp.new(r)
|
|
221
|
-
Dir.new(zonedir).each { |file|
|
|
222
|
-
if (file.full_match regex) && (! @force_online)
|
|
223
|
-
path = File.join(zonedir, file)
|
|
224
|
-
zone = JSON.parse(File.open(path).read)
|
|
225
|
-
end
|
|
226
|
-
}
|
|
401
|
+
if forceRest || (not File.exists?(@zone_file))
|
|
402
|
+
@sep_char = "~"
|
|
403
|
+
if @zone_id.nil?
|
|
404
|
+
raise "unable to calculate zone id"
|
|
227
405
|
end
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
zone = Route.do.get_zone(id, name)
|
|
232
|
-
if zone['Records'].nil?
|
|
233
|
-
zone['Records'] = {
|
|
234
|
-
"A"=> [ ],
|
|
235
|
-
"AAAA"=> [ ],
|
|
236
|
-
"CNAME"=> [ ],
|
|
237
|
-
"MX"=> [ ],
|
|
238
|
-
"NS"=> [ ],
|
|
239
|
-
"SPF"=> [ ],
|
|
240
|
-
"SRV"=> [ ],
|
|
241
|
-
"TXT"=> [ ]
|
|
242
|
-
}
|
|
243
|
-
end
|
|
244
|
-
if zone.nil?
|
|
245
|
-
raise "zone doesn't exist or is empty"
|
|
246
|
-
end
|
|
247
|
-
if $debug
|
|
248
|
-
File.open("raw.zone","w").write(zone)
|
|
249
|
-
end
|
|
250
|
-
Route.do.translate_zone(zone)
|
|
406
|
+
zone = Route.do.get_zone(@zone_id, nil)
|
|
407
|
+
if $debug > 1
|
|
408
|
+
File.open("raw.zone","w").write(zone)
|
|
251
409
|
end
|
|
410
|
+
Route.do.translate_zone(zone)
|
|
411
|
+
else
|
|
412
|
+
@sep_char = "-"
|
|
413
|
+
zone = JSON.parse(File.open(@zone_file).read)
|
|
252
414
|
end
|
|
253
|
-
zone
|
|
254
|
-
end
|
|
255
415
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
struct.keys.each { |fullkey|
|
|
261
|
-
testkey = fullkey.downcase
|
|
262
|
-
return fullkey if abbrev == testkey
|
|
263
|
-
if fullkey.downcase.start_with?(abbrev)
|
|
264
|
-
if (! key)
|
|
265
|
-
key = fullkey
|
|
266
|
-
else
|
|
267
|
-
return nil
|
|
268
|
-
end
|
|
269
|
-
end
|
|
270
|
-
}
|
|
271
|
-
key
|
|
416
|
+
if zone.nil?
|
|
417
|
+
raise "zone doesn't exist or is empty"
|
|
418
|
+
end
|
|
419
|
+
zone
|
|
272
420
|
end
|
|
273
421
|
|
|
274
422
|
def drill_down(struct, args, level)
|
|
@@ -288,6 +436,9 @@ class Zone < Command
|
|
|
288
436
|
}
|
|
289
437
|
return result, result_level
|
|
290
438
|
end
|
|
439
|
+
if struct.length == 0
|
|
440
|
+
raise "cannot drill down empty array"
|
|
441
|
+
end
|
|
291
442
|
if args[0] =~ /\A\d+\Z/
|
|
292
443
|
index = args.shift.to_i
|
|
293
444
|
if index > struct.length || index <= 0
|
|
@@ -304,7 +455,7 @@ class Zone < Command
|
|
|
304
455
|
elsif (struct.class == Hash)
|
|
305
456
|
args = args.clone
|
|
306
457
|
key = args.shift
|
|
307
|
-
fullkey = match_key(key, struct)
|
|
458
|
+
fullkey = Util.match_key(key, struct)
|
|
308
459
|
raise "no (unique) key matches " + key if ! fullkey
|
|
309
460
|
drill_down(struct[fullkey], args, level + ".#{fullkey}")
|
|
310
461
|
else
|
|
@@ -313,7 +464,6 @@ class Zone < Command
|
|
|
313
464
|
end
|
|
314
465
|
|
|
315
466
|
def set_format
|
|
316
|
-
puts "in zone.set_format"
|
|
317
467
|
@format = :json
|
|
318
468
|
end
|
|
319
469
|
|