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.
@@ -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
@@ -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 < 2 && ! header
103
+ if lineno == 0 && ! header
93
104
  next
94
105
  end
95
- output = ""
96
- line.each_with_index { |value, index|
97
- output = output + value.to_s + spaces(maxwidth[index] - value.to_s.length + 2)
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
@@ -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"]
@@ -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
@@ -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 = true
20
+ $debug += 1
23
21
  end
24
22
 
25
23
  def create_args
@@ -1,3 +1,3 @@
1
1
  module Vzcdn
2
- VERSION = "0.1.9"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -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 ZoneDelete < Command
68
+ class ZoneAudit< Command
31
69
  def execute(args, rest)
32
70
  zone = @input["zone"]
33
- zone.delete
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, "Records" => {"A" => [] } }
125
+ zone = { "Comment" => comment, "DomainName" => name, "Status" => status, "ZoneType" => type }
82
126
  puts "creating zone on server"
83
- Route.do.add_zone(zone)
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
- Route.do.update_zone(zone)
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 local_filename(zone)
155
- file_name = zone["DomainName"].sub(/\.\Z/, '')
156
- file_name = file_name + '.' + zone["ZoneId"].to_s
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 write_zone_file(zone, hide_dup = false)
160
- basename = local_filename(zone)
161
- if hide_dup
162
- mk_config_dir("zones")
163
- file_name = config_file("zones", basename)
164
- File.delete(file_name) if File.exists?(file_name)
165
- File.open(file_name, "w").write(JSON.pretty_generate zone)
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
- file_name = write_zone_file(zone, true)
176
- puts "'#{file_name}' saved locally"
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(File.join("#{config_dir}", "zones", "*.#{id}"))
376
+ files = Dir.glob(config_file(ZONEDIR, "*.#{id}"))
181
377
  files.each { |file| File.delete(file) }
182
- files = Dir.glob(File.join("zones", "*.#{id}"))
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
- id, name = id_and_name
188
- if (id.nil?)
189
- zone = get_zone(true)
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
- id, name = id_and_name
213
- if @zname
214
- if ! forceRest
215
- if id.nil?
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
- if (zone.nil?)
229
- dputs "getting zone with id=#{id} and name=#{name}"
230
- @sep_char = "~"
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
- # let user lowercase and abbreviate key; abbreviation must match one key
257
- def match_key(abbrev, struct)
258
- abbrev = abbrev.downcase
259
- key = nil
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