vzcdn 0.1.7 → 0.1.8
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 +4 -0
- data/bin/ec +2 -1
- data/bin/vzcdn +2 -1
- data/lib/args.rb +142 -183
- data/lib/clui_config.rb +33 -18
- data/lib/command.rb +66 -45
- data/lib/common.rb +16 -2
- data/lib/config_handler.rb +0 -1
- data/lib/proxy_credentials.rb +30 -0
- data/lib/route.rb +6 -3
- data/lib/vzcdn.rb +6 -5
- data/lib/vzcdn/version.rb +1 -1
- data/lib/zone.rb +138 -81
- data/lib/zone_add.rb +36 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5defb8c0d907a56ce79b0d129ca104687c4e1db1
|
4
|
+
data.tar.gz: 122ceba67885edbdacba2d6178b1cc3a2889afef
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 432815ea229141614d538475134234d79412b2e1d402b0a0de6b393aa744fafd8ce5c008996591b3815dcae8b3e119232b86c042928e0865a33bba5d1817552f
|
7
|
+
data.tar.gz: 24e95955bbeff970bead4b7061f8d38952a36abe86f273cb3922a2fb6c6a809a458c545958c7e9948bb42b65143de2191522267cf38f2a7077a1e67ce6364ed3
|
data/README.md
CHANGED
@@ -4,6 +4,10 @@ Commandline UI for Edgecast API
|
|
4
4
|
|
5
5
|
##RELEASE NOTES
|
6
6
|
|
7
|
+
2014/04/22 0.1.8
|
8
|
+
* Create zone support
|
9
|
+
* Add record support
|
10
|
+
|
7
11
|
2014/04/17 0.1.7
|
8
12
|
* Default action - print
|
9
13
|
* Presentation changes for zone display ( <object> to "...", array elements to [num] etc)
|
data/bin/ec
CHANGED
@@ -2,6 +2,8 @@
|
|
2
2
|
|
3
3
|
require 'vzcdn'
|
4
4
|
|
5
|
+
# $debug = true
|
6
|
+
|
5
7
|
def add_prefix(prefix, text)
|
6
8
|
result = ""
|
7
9
|
text.each_line { |line|
|
@@ -18,7 +20,6 @@ end
|
|
18
20
|
|
19
21
|
begin
|
20
22
|
VzcdnApp.new("vzcdn", "vzcdn").run(ARGV)
|
21
|
-
|
22
23
|
exit 0
|
23
24
|
rescue Exception => e
|
24
25
|
if e.class != SystemExit
|
data/bin/vzcdn
CHANGED
@@ -2,6 +2,8 @@
|
|
2
2
|
|
3
3
|
require 'vzcdn'
|
4
4
|
|
5
|
+
# $debug = true
|
6
|
+
|
5
7
|
def add_prefix(prefix, text)
|
6
8
|
result = ""
|
7
9
|
text.each_line { |line|
|
@@ -18,7 +20,6 @@ end
|
|
18
20
|
|
19
21
|
begin
|
20
22
|
VzcdnApp.new("vzcdn", "vzcdn").run(ARGV)
|
21
|
-
|
22
23
|
exit 0
|
23
24
|
rescue Exception => e
|
24
25
|
if e.class != SystemExit
|
data/lib/args.rb
CHANGED
@@ -1,37 +1,85 @@
|
|
1
1
|
require_relative 'route'
|
2
2
|
|
3
|
-
class
|
4
|
-
attr_accessor :
|
5
|
-
attr_reader :value
|
3
|
+
class Cell
|
4
|
+
attr_accessor :required, :repeatable, :value, :default
|
6
5
|
|
7
|
-
def initialize(
|
8
|
-
@
|
6
|
+
def initialize(object, hash = { })
|
7
|
+
@obj = object
|
9
8
|
hash.each {|k,v| instance_variable_set("@#{k}",v)}
|
10
9
|
if @repeatable
|
11
|
-
@
|
10
|
+
@value = [ ]
|
12
11
|
end
|
13
12
|
end
|
14
13
|
|
15
14
|
def full?
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
15
|
+
@full
|
16
|
+
end
|
17
|
+
|
18
|
+
def command
|
19
|
+
@obj.class <= Command ? @obj : nil
|
20
|
+
end
|
21
|
+
|
22
|
+
# return nil on success, error message on failure
|
23
|
+
def set(value, target)
|
24
|
+
case
|
25
|
+
when @obj.class == Arg
|
26
|
+
error = @obj.validate?(value, target)
|
27
|
+
if error
|
28
|
+
if @repeatable
|
29
|
+
@full = true
|
30
|
+
end
|
31
|
+
return error
|
32
|
+
end
|
33
|
+
if @obj.validator.class == Hash
|
34
|
+
value = @obj.validator[value]
|
35
|
+
end
|
36
|
+
if @repeatable
|
37
|
+
@value << value
|
38
|
+
else
|
39
|
+
@value = value
|
40
|
+
@full = true
|
41
|
+
end
|
42
|
+
when @obj.class <= Command
|
43
|
+
if value == @obj.name
|
44
|
+
@value = @obj
|
45
|
+
@full = true
|
46
|
+
else
|
47
|
+
return "incorrect command name"
|
48
|
+
end
|
20
49
|
end
|
50
|
+
nil
|
21
51
|
end
|
22
52
|
|
23
|
-
def
|
24
|
-
if @
|
25
|
-
@
|
26
|
-
@value << value
|
27
|
-
else
|
28
|
-
@value = value
|
53
|
+
def missing_value
|
54
|
+
if @value.nil? && @default
|
55
|
+
set(@default, nil)
|
29
56
|
end
|
57
|
+
return false unless @required
|
58
|
+
@value.nil?
|
30
59
|
end
|
31
60
|
|
32
|
-
|
33
|
-
|
61
|
+
def name
|
62
|
+
@obj.name
|
63
|
+
end
|
64
|
+
|
65
|
+
def desc
|
66
|
+
@obj.desc
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
34
70
|
|
71
|
+
class Arg
|
72
|
+
attr_reader :name, :desc, :validator
|
73
|
+
|
74
|
+
def initialize(name, desc, validator)
|
75
|
+
@name = name
|
76
|
+
@desc = desc
|
77
|
+
@validator = validator
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
# return nil on success
|
82
|
+
# error message on failure
|
35
83
|
def validate?(value, obj)
|
36
84
|
vclass = @validator.class
|
37
85
|
if vclass == Regexp
|
@@ -39,10 +87,10 @@ class Arg
|
|
39
87
|
on_error = "Invalid Format for " + @name
|
40
88
|
elsif vclass == Array
|
41
89
|
test = @validator.include? value
|
42
|
-
on_error = "Invalid choice for
|
90
|
+
on_error = "Invalid choice for #{@name}, must be one of #{@validator}"
|
43
91
|
elsif vclass == Hash
|
44
92
|
test = @validator.has_key? value
|
45
|
-
on_error = "Invalid choice for
|
93
|
+
on_error = "Invalid choice for #{@name}, must be one of #{@validator.keys}"
|
46
94
|
elsif vclass == String
|
47
95
|
result = {}
|
48
96
|
Route.new.send("get_available_" + @validator).each { |hash|
|
@@ -55,13 +103,12 @@ class Arg
|
|
55
103
|
test = false
|
56
104
|
end
|
57
105
|
elsif vclass == Symbol
|
58
|
-
|
106
|
+
if obj
|
107
|
+
on_error, test = obj.send(@validator, value)
|
108
|
+
end
|
59
109
|
else
|
60
110
|
test = false
|
61
|
-
on_error = "unknown type of validator
|
62
|
-
end
|
63
|
-
if not test && @repeatable
|
64
|
-
@full = true
|
111
|
+
on_error = "unknown type of validator:#{@validator}, class=#{@validator.class}"
|
65
112
|
end
|
66
113
|
if test
|
67
114
|
nil
|
@@ -72,162 +119,103 @@ class Arg
|
|
72
119
|
end
|
73
120
|
|
74
121
|
class Flow
|
75
|
-
|
76
|
-
|
77
|
-
def initialize(*args)
|
78
|
-
@root = nil
|
79
|
-
add(*args)
|
80
|
-
end
|
81
|
-
|
82
|
-
def iargs(node, args)
|
83
|
-
if node.nil?
|
84
|
-
return args
|
85
|
-
end
|
86
|
-
if (node.class == Arg)
|
87
|
-
if node.value
|
88
|
-
new_args = args.clone
|
89
|
-
new_args[node.name] = node.value
|
90
|
-
args = new_args
|
91
|
-
end
|
92
|
-
iargs(node.next_node, args)
|
93
|
-
elsif node.is_a? Command
|
94
|
-
args["command"] = node
|
95
|
-
return args
|
96
|
-
end
|
97
|
-
end
|
122
|
+
attr_accessor :usage_string
|
98
123
|
|
99
|
-
def
|
100
|
-
|
124
|
+
def initialize
|
125
|
+
@cells = [ ]
|
101
126
|
end
|
102
127
|
|
103
|
-
def
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
end
|
108
|
-
response
|
128
|
+
def add(*cells)
|
129
|
+
cells.each { |cell|
|
130
|
+
@cells << cell
|
131
|
+
}
|
109
132
|
end
|
110
133
|
|
111
|
-
def
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
error = validate_and_set(value, node)
|
120
|
-
if error && node.repeatable
|
121
|
-
return isetvalue(value, node.next_node)
|
134
|
+
def parse(args, obj = nil)
|
135
|
+
@target = obj
|
136
|
+
rest = [ ]
|
137
|
+
args.each_with_index { |arg, index|
|
138
|
+
name, value = Util.parse_line(arg)
|
139
|
+
if value.nil?
|
140
|
+
value = name
|
141
|
+
error, finished = setvalue(value)
|
122
142
|
else
|
123
|
-
|
143
|
+
error, finished = set(name, value)
|
124
144
|
end
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
return nil, nil
|
129
|
-
else
|
130
|
-
return "incorrect command name", nil
|
145
|
+
if error
|
146
|
+
result = add_errline(result, error)
|
147
|
+
finished = :finished
|
131
148
|
end
|
149
|
+
if finished
|
150
|
+
rest = args[index..-1]
|
151
|
+
break
|
152
|
+
end
|
153
|
+
}
|
154
|
+
missing = missing_args
|
155
|
+
unless missing.empty?
|
156
|
+
result = add_errline(result, "missing args: #{missing}")
|
132
157
|
end
|
158
|
+
# dputs "parse for class #{obj.class} result=#{result}, rest=#{rest}"
|
159
|
+
return result, rest
|
133
160
|
end
|
134
161
|
|
135
162
|
def setvalue(value)
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
def iset(name, value, node)
|
140
|
-
if node.nil?
|
141
|
-
return "could not find argument with name #{name}", :finished
|
142
|
-
end
|
143
|
-
if (node.class == Arg )
|
144
|
-
if (node.name == name)
|
145
|
-
error = validate_and_set(value, node)
|
146
|
-
return error, nil
|
163
|
+
@cells.each { |cell|
|
164
|
+
if cell.full?
|
165
|
+
next
|
147
166
|
else
|
148
|
-
|
167
|
+
error = cell.set(value, @target)
|
168
|
+
if error && cell.repeatable
|
169
|
+
next
|
170
|
+
end
|
171
|
+
return error, nil
|
149
172
|
end
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
# return true for success
|
154
|
-
# false for unable to find arg name
|
155
|
-
# String for invalid arg
|
156
|
-
def set(name, value)
|
157
|
-
iset(name, value, @root)
|
158
|
-
end
|
159
|
-
|
160
|
-
def iadd(arg, node)
|
161
|
-
if node.next_node.nil?
|
162
|
-
node.next_node = arg
|
163
|
-
else
|
164
|
-
iadd(arg, node.next_node)
|
165
|
-
end
|
173
|
+
}
|
174
|
+
return nil, :finished
|
166
175
|
end
|
167
176
|
|
168
|
-
def
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
iadd(arg, @root)
|
177
|
+
def missing_args
|
178
|
+
missing = [ ]
|
179
|
+
@cells.each { |cell|
|
180
|
+
if cell.missing_value
|
181
|
+
missing << cell.name
|
182
|
+
end
|
175
183
|
}
|
176
|
-
|
184
|
+
missing
|
177
185
|
end
|
178
186
|
|
179
|
-
def
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
187
|
+
def set(name, value)
|
188
|
+
@cells.each { |cell|
|
189
|
+
if cell.name == name
|
190
|
+
return cell.set(value, @target), nil
|
191
|
+
end
|
184
192
|
}
|
185
|
-
|
186
|
-
end
|
187
|
-
|
188
|
-
def print_flow(node)
|
189
|
-
if node.nil?
|
190
|
-
return
|
191
|
-
end
|
192
|
-
result = node.name + " - " + node.desc
|
193
|
-
if (node.value)
|
194
|
-
result += ":" + node.value
|
195
|
-
end
|
196
|
-
puts result
|
197
|
-
print_flow(node.next_node)
|
193
|
+
return "argument named #{name} not found", nil
|
198
194
|
end
|
199
195
|
|
200
|
-
def
|
201
|
-
|
202
|
-
|
203
|
-
names << (node.class == Arg ? "<#{node.name}>" : node.name)
|
204
|
-
desc << node.desc
|
205
|
-
iusage node.next_node, names, desc
|
196
|
+
def command
|
197
|
+
if @cells[-1]
|
198
|
+
@cells[-1].command
|
206
199
|
end
|
207
|
-
return names, desc
|
208
200
|
end
|
209
201
|
|
210
|
-
def
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
if (node.required) && (node.value.nil?)
|
218
|
-
missing << node.name
|
219
|
-
end
|
220
|
-
ivalidate node.next_node, missing
|
221
|
-
elsif node.is_a? Command
|
222
|
-
if node.required? && (not node.full?)
|
223
|
-
missing << node.name
|
224
|
-
end
|
225
|
-
end
|
226
|
-
missing
|
202
|
+
def arghash
|
203
|
+
result = { }
|
204
|
+
@cells.each { |cell|
|
205
|
+
result[cell.name] = cell.value
|
206
|
+
}
|
207
|
+
result["command"] = command
|
208
|
+
result
|
227
209
|
end
|
228
210
|
|
229
|
-
def
|
230
|
-
|
211
|
+
def usage
|
212
|
+
names = [ ]
|
213
|
+
desc = [ ]
|
214
|
+
@cells.each { |cell|
|
215
|
+
names << cell.name
|
216
|
+
desc << cell.desc
|
217
|
+
}
|
218
|
+
return names, desc
|
231
219
|
end
|
232
220
|
|
233
221
|
def add_errline(str, error)
|
@@ -237,33 +225,4 @@ class Flow
|
|
237
225
|
error
|
238
226
|
end
|
239
227
|
end
|
240
|
-
|
241
|
-
def parse(args, obj = nil)
|
242
|
-
result = nil
|
243
|
-
rest = [ ]
|
244
|
-
@target = obj
|
245
|
-
args.each_with_index { |arg, index|
|
246
|
-
name, value = Util.parse_line(arg)
|
247
|
-
if value.nil?
|
248
|
-
value = name
|
249
|
-
error, finished = setvalue(value)
|
250
|
-
else
|
251
|
-
error, finished = set(name, value)
|
252
|
-
end
|
253
|
-
|
254
|
-
if error
|
255
|
-
result = add_errline(result, error)
|
256
|
-
finished = :finished
|
257
|
-
end
|
258
|
-
if finished
|
259
|
-
rest = args[index...args.length]
|
260
|
-
break
|
261
|
-
end
|
262
|
-
}
|
263
|
-
missing = validate
|
264
|
-
if ! missing.empty?
|
265
|
-
result = add_errline(result, "missing args:" + missing.to_s)
|
266
|
-
end
|
267
|
-
return result, rest
|
268
|
-
end
|
269
228
|
end
|
data/lib/clui_config.rb
CHANGED
@@ -6,11 +6,13 @@ class ConfigInit < Command
|
|
6
6
|
|
7
7
|
include ConfigReader
|
8
8
|
|
9
|
+
def create_args
|
10
|
+
add_arg("acct-num", "EdgeCast account number", /[[:alnum:]]+/)
|
11
|
+
add_arg("token", "Web Services REST API Token (see my.edgecase.com)", /.*/)
|
12
|
+
end
|
13
|
+
|
9
14
|
def create_flows
|
10
|
-
|
11
|
-
flow.add(Arg.new("acct-num", desc: "EdgeCast account number", required: true, validator: /[[:alnum:]]+/))
|
12
|
-
flow.add(Arg.new("token", desc: "Web Services REST API Token (see my.edgecase.com)", required: true, validator: /.*/))
|
13
|
-
add_flow(flow)
|
15
|
+
add_flow_from_usage("<acct-num> <token>")
|
14
16
|
end
|
15
17
|
|
16
18
|
def execute(args, ignore)
|
@@ -29,10 +31,12 @@ class ConfigInit < Command
|
|
29
31
|
end
|
30
32
|
|
31
33
|
class ConfigGet < Command
|
34
|
+
def create_args
|
35
|
+
add_arg("param", "name of configuration parameter", /[-a-zA-Z]+/)
|
36
|
+
end
|
37
|
+
|
32
38
|
def create_flows
|
33
|
-
|
34
|
-
flow.add(Arg.new("param", desc: "name of configuration parameter", required: true, validator: /[-a-zA-Z]+/))
|
35
|
-
add_flow(flow)
|
39
|
+
add_flow_from_usage("<param>")
|
36
40
|
end
|
37
41
|
|
38
42
|
def execute(args, ignore)
|
@@ -42,10 +46,12 @@ class ConfigGet < Command
|
|
42
46
|
end
|
43
47
|
|
44
48
|
class ConfigDelete < Command
|
49
|
+
def create_args
|
50
|
+
add_arg("param", "name of configuration parameter", /[-a-zA-Z]+/)
|
51
|
+
end
|
52
|
+
|
45
53
|
def create_flows
|
46
|
-
|
47
|
-
flow.add(Arg.new("param", desc: "name of configuration parameter", required: true, validator: /[-a-zA-Z]+/))
|
48
|
-
add_flow(flow)
|
54
|
+
add_flow_from_usage("<param>")
|
49
55
|
end
|
50
56
|
|
51
57
|
def execute(args, ignore)
|
@@ -55,11 +61,13 @@ class ConfigDelete < Command
|
|
55
61
|
end
|
56
62
|
|
57
63
|
class ConfigSet < Command
|
64
|
+
def create_args
|
65
|
+
add_arg("param", "name of configuration parameter", /[-a-zA-Z]+/)
|
66
|
+
add_arg("value", "value of configuration parameter", /.*/)
|
67
|
+
end
|
68
|
+
|
58
69
|
def create_flows
|
59
|
-
|
60
|
-
flow.add(Arg.new("param", desc: "name of configuration parameter", required: true, validator: /[-a-zA-Z]+/))
|
61
|
-
flow.add(Arg.new("value", desc: "value of configuration parameter", required: true, validator: /.*/))
|
62
|
-
add_flow(flow)
|
70
|
+
add_flow_from_usage("<param> <value>")
|
63
71
|
end
|
64
72
|
|
65
73
|
def execute(args, ignore)
|
@@ -69,11 +77,18 @@ class ConfigSet < Command
|
|
69
77
|
end
|
70
78
|
|
71
79
|
class ConfigCommand < Command
|
80
|
+
def create_args
|
81
|
+
add_cmd(ConfigInit, "init", "initialize in current directory")
|
82
|
+
add_cmd(ConfigGet, "get", "print configuration parameter's value")
|
83
|
+
add_cmd(ConfigDelete, "delete", "delete configuration parameter")
|
84
|
+
add_cmd(ConfigSet, "set", "set (or reset) a configuration parameter to specified value")
|
85
|
+
end
|
86
|
+
|
72
87
|
def create_flows
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
88
|
+
add_flow_from_usage("init")
|
89
|
+
add_flow_from_usage("get")
|
90
|
+
add_flow_from_usage("delete")
|
91
|
+
add_flow_from_usage("set")
|
77
92
|
end
|
78
93
|
|
79
94
|
def execute(args, subargs)
|
data/lib/command.rb
CHANGED
@@ -12,36 +12,15 @@ class Command
|
|
12
12
|
@name = name
|
13
13
|
@desc = description
|
14
14
|
@options = [{ short_name: 'h', long_name: 'help', method: :show_help_option, takes_value: false }]
|
15
|
-
init
|
16
15
|
@flows = [ ]
|
17
|
-
@
|
16
|
+
@cellhash = { }
|
17
|
+
@subcommands = [ ]
|
18
|
+
@input = { }
|
19
|
+
|
20
|
+
init
|
18
21
|
add_options
|
19
22
|
create_args
|
20
23
|
create_flows
|
21
|
-
@args = nil
|
22
|
-
@full = false
|
23
|
-
@required = true
|
24
|
-
end
|
25
|
-
|
26
|
-
# TODO: make Flow keep track of next_node and fullness
|
27
|
-
def next_node
|
28
|
-
nil
|
29
|
-
end
|
30
|
-
|
31
|
-
def full?
|
32
|
-
return @full
|
33
|
-
end
|
34
|
-
|
35
|
-
def setfull
|
36
|
-
@full = true
|
37
|
-
end
|
38
|
-
|
39
|
-
def required?
|
40
|
-
@required
|
41
|
-
end
|
42
|
-
|
43
|
-
def required=(value)
|
44
|
-
@required = value
|
45
24
|
end
|
46
25
|
|
47
26
|
def init
|
@@ -53,7 +32,13 @@ class Command
|
|
53
32
|
def create_args
|
54
33
|
end
|
55
34
|
|
35
|
+
# default is to have one flow with no arguments
|
56
36
|
def create_flows
|
37
|
+
add_flow(Flow.new)
|
38
|
+
end
|
39
|
+
|
40
|
+
def add_input(name, value)
|
41
|
+
@input[name] = value
|
57
42
|
end
|
58
43
|
|
59
44
|
def add_option(option)
|
@@ -67,33 +52,69 @@ class Command
|
|
67
52
|
|
68
53
|
def show_help
|
69
54
|
puts "#{@name} - #{@desc}"
|
55
|
+
puts "Usage:"
|
70
56
|
@flows.each { |flow|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
if
|
81
|
-
|
82
|
-
|
57
|
+
puts " #{@name} #{flow.usage_string}"
|
58
|
+
}
|
59
|
+
puts "Where:"
|
60
|
+
@cellhash.each { |name,arg|
|
61
|
+
if arg.class == Arg
|
62
|
+
choice = ""
|
63
|
+
if arg.validator.class == String
|
64
|
+
error, ignore = arg.validate?("asdf", nil)
|
65
|
+
end
|
66
|
+
if arg.validator.class == Array
|
67
|
+
choice = " - value from #{arg.validator}"
|
68
|
+
elsif arg.validator.class == Hash
|
69
|
+
choice = " - value from #{arg.validator.keys}"
|
83
70
|
end
|
71
|
+
|
72
|
+
puts " #{arg.name} : #{arg.desc}#{choice}"
|
84
73
|
end
|
85
74
|
}
|
86
75
|
end
|
87
76
|
|
88
|
-
def add_arg(name,
|
89
|
-
|
77
|
+
def add_arg(name, desc, validator)
|
78
|
+
name = name.to_s
|
79
|
+
arg = Arg.new(name, desc, validator)
|
80
|
+
@cellhash[name] = arg
|
90
81
|
end
|
91
82
|
|
92
|
-
def
|
83
|
+
def add_cmd(cmd_class, name, desc)
|
84
|
+
name = name.to_s
|
85
|
+
cmd = cmd_class.new(name, desc)
|
86
|
+
@cellhash[name] = cmd
|
87
|
+
@subcommands << name
|
88
|
+
end
|
89
|
+
|
90
|
+
def command_name?(str)
|
91
|
+
@subcommands.include?(str.to_s)
|
92
|
+
end
|
93
|
+
|
94
|
+
def add_flow_from_usage(usage)
|
93
95
|
flow = Flow.new
|
94
|
-
|
95
|
-
|
96
|
-
|
96
|
+
flow.usage_string = usage
|
97
|
+
line = usage
|
98
|
+
re = /\A
|
99
|
+
( # matches one arg spec
|
100
|
+
(?<optional> \[)? # turn on 'optional' if we see an open square bracket
|
101
|
+
<?(?<argname> [[:alnum:]-]+)>? # argname is inside optional angle brackets and consists of alphanumerics and dashes
|
102
|
+
(=\'(?<default>[^\']+)\')? # default value has form ='value'
|
103
|
+
(?<repeatable> \s*\.\.\.)? # three dots turn on 'repeatable'
|
104
|
+
\]? # matching close square bracket
|
105
|
+
) # end of arg spec
|
106
|
+
\s* /x # skips white space
|
107
|
+
index = 0
|
108
|
+
while(m = re.match(line))
|
109
|
+
argname = m[:argname]
|
110
|
+
required = m[:optional].nil?
|
111
|
+
repeatable = (not m[:repeatable].nil?)
|
112
|
+
default = m[:default]
|
113
|
+
index = m.end(0)
|
114
|
+
line = line[index..-1]
|
115
|
+
cell = Cell.new(@cellhash[argname], required: required, repeatable: repeatable, default: default)
|
116
|
+
flow.add(cell)
|
117
|
+
end
|
97
118
|
add_flow(flow)
|
98
119
|
end
|
99
120
|
|
@@ -115,7 +136,7 @@ class Command
|
|
115
136
|
@flows.each { |flow|
|
116
137
|
error, rest = flow.parse args, self
|
117
138
|
if error.nil?
|
118
|
-
execute(flow.
|
139
|
+
execute(flow.arghash, rest)
|
119
140
|
return
|
120
141
|
end
|
121
142
|
}
|
data/lib/common.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'rest_client'
|
2
2
|
require 'json'
|
3
3
|
require 'config_reader'
|
4
|
+
require_relative 'proxy_credentials'
|
4
5
|
|
5
6
|
class CommonRestUtils
|
6
7
|
include ConfigReader
|
@@ -19,12 +20,25 @@ class CommonRestUtils
|
|
19
20
|
when :post
|
20
21
|
RestClient.post(url, json_body, @headers)
|
21
22
|
when :delete
|
22
|
-
RestClient.(url, @headers)
|
23
|
+
RestClient.delete(url, @headers)
|
23
24
|
else
|
24
25
|
"unsupported http method:" + http_method
|
25
26
|
end
|
26
27
|
rescue Exception => e
|
27
|
-
puts "
|
28
|
+
puts "Problem executing REST command to server " + param("rest_base_url")
|
29
|
+
puts e.message
|
30
|
+
puts "#{e.response}"
|
31
|
+
proxy_auth_req = (e.message =~ /\A407 /)
|
32
|
+
if e.message =~ /\A407 /
|
33
|
+
ProxyCredentials.proxy_credentials param("proxy")
|
34
|
+
if @proxy_retries && @proxy_retries > 3
|
35
|
+
raise e.message + " too many retries"
|
36
|
+
else
|
37
|
+
@proxy_retries = @proxy_retries ? @proxy_retries + 1 : 1
|
38
|
+
return callMethod(url_suffix, http_method, body)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
exit
|
28
42
|
if $debug
|
29
43
|
puts "full URL is:" + url
|
30
44
|
if (body)
|
data/lib/config_handler.rb
CHANGED
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'rubygems/user_interaction'
|
3
|
+
|
4
|
+
class ProxyCredentials
|
5
|
+
include Gem::UserInteraction
|
6
|
+
def self.proxy_credentials(proxy)
|
7
|
+
new.get_credentials(proxy)
|
8
|
+
end
|
9
|
+
|
10
|
+
def get_credentials(proxy)
|
11
|
+
puts "need credentials for proxy #{proxy}"
|
12
|
+
uri = URI(proxy)
|
13
|
+
proxy_host = uri.hostname
|
14
|
+
proxy_port = uri.port
|
15
|
+
proxy_user = ask("username:")
|
16
|
+
proxy_password = ask_for_password("password:")
|
17
|
+
|
18
|
+
uri = URI("http://blogsearch.google.com/ping/RPC2")
|
19
|
+
req = Net::HTTP::Get.new(uri)
|
20
|
+
net = Net::HTTP.new(uri.hostname, uri.port, proxy_host, proxy_port, proxy_user, proxy_password)
|
21
|
+
puts "created net"
|
22
|
+
net.read_timeout = 20
|
23
|
+
net.open_timeout = 20
|
24
|
+
|
25
|
+
net.start { |http|
|
26
|
+
response = http.request(req)
|
27
|
+
puts "response code from internet web service is #{response.code}, message is #{response.message}"
|
28
|
+
}
|
29
|
+
end
|
30
|
+
end
|
data/lib/route.rb
CHANGED
@@ -110,6 +110,7 @@ class Route
|
|
110
110
|
return nil if zonelist.nil?
|
111
111
|
zonelist.each { |zone|
|
112
112
|
translate_value(:zone_status, zone, "Status")
|
113
|
+
translate_value(:zone_type, zone, "ZoneType")
|
113
114
|
}
|
114
115
|
end
|
115
116
|
|
@@ -136,16 +137,18 @@ class Route
|
|
136
137
|
end
|
137
138
|
|
138
139
|
def delete_zone(id)
|
139
|
-
suffix = customer_suffix("dns/routezone
|
140
|
+
suffix = customer_suffix("dns/routezone/#{id}")
|
140
141
|
@common.callMethod(suffix, :delete)
|
141
142
|
end
|
142
143
|
|
143
|
-
def add_zone()
|
144
|
+
def add_zone(zone)
|
145
|
+
suffix = customer_suffix("dns/routezone")
|
146
|
+
@common.callMethod(suffix, :post, zone)
|
144
147
|
end
|
145
148
|
|
146
149
|
def update_zone(zone)
|
147
150
|
suffix = customer_suffix("dns/routezone")
|
148
|
-
@common.callMethod(suffix, :put, zone)
|
151
|
+
@common.callMethod(suffix, :put, zone)
|
149
152
|
end
|
150
153
|
|
151
154
|
def xlate_name(symbol, name)
|
data/lib/vzcdn.rb
CHANGED
@@ -14,7 +14,8 @@ class VzcdnApp < Command
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def print_version
|
17
|
-
puts "vzcdn
|
17
|
+
puts "vzcdn Version:" + Vzcdn::VERSION
|
18
|
+
exit
|
18
19
|
end
|
19
20
|
|
20
21
|
def set_debug
|
@@ -22,13 +23,13 @@ class VzcdnApp < Command
|
|
22
23
|
end
|
23
24
|
|
24
25
|
def create_args
|
25
|
-
|
26
|
-
|
26
|
+
add_cmd(Zone, "zone", "dns zone command")
|
27
|
+
add_cmd(ConfigCommand, "config", "configuration control")
|
27
28
|
end
|
28
29
|
|
29
30
|
def create_flows
|
30
|
-
|
31
|
-
|
31
|
+
add_flow_from_usage("zone")
|
32
|
+
add_flow_from_usage("config")
|
32
33
|
end
|
33
34
|
|
34
35
|
def execute(args, subargs)
|
data/lib/vzcdn/version.rb
CHANGED
data/lib/zone.rb
CHANGED
@@ -3,26 +3,38 @@ require_relative 'util'
|
|
3
3
|
require_relative 'print'
|
4
4
|
require_relative 'command'
|
5
5
|
require_relative 'args'
|
6
|
+
require_relative 'zone_add'
|
6
7
|
|
7
8
|
class ZoneList < Command
|
8
|
-
def execute
|
9
|
-
|
9
|
+
def execute(args, rest)
|
10
|
+
zones = Route.do.get_all_zones
|
11
|
+
Route.do.translate_zonelist(zones)
|
12
|
+
StructurePrint.new('~').structure_print(zones, "zonelist")
|
10
13
|
end
|
11
14
|
end
|
12
15
|
|
13
16
|
class ZonePull < Command
|
14
|
-
def execute
|
17
|
+
def execute(args, rest)
|
18
|
+
zone = @input["zone"]
|
15
19
|
zone.pull
|
16
20
|
end
|
17
21
|
end
|
18
22
|
|
19
|
-
class
|
20
|
-
|
23
|
+
class ZonePush < Command
|
24
|
+
def execute(args, rest)
|
25
|
+
zone = @input["zone"]
|
26
|
+
zone.push
|
27
|
+
end
|
28
|
+
end
|
21
29
|
|
22
|
-
|
23
|
-
|
30
|
+
class ZoneDelete < Command
|
31
|
+
def execute(args, rest)
|
32
|
+
zone = @input["zone"]
|
33
|
+
zone.delete
|
24
34
|
end
|
35
|
+
end
|
25
36
|
|
37
|
+
class ZonePrint < Command
|
26
38
|
def add_options
|
27
39
|
add_option({ short_name: 'j', long_name: 'json', method: :set_format, takes_value: false})
|
28
40
|
# add_option({ short_name: 'f', long_name: 'force-online', method: :force_online, takes_value: false})
|
@@ -32,18 +44,43 @@ class ZonePrint < Command
|
|
32
44
|
@format = :json
|
33
45
|
end
|
34
46
|
|
35
|
-
def execute(args,
|
47
|
+
def execute(args, rest)
|
48
|
+
struct = @input["struct"]
|
49
|
+
level = @input["level"]
|
50
|
+
sep_char = @input["sep_char"]
|
36
51
|
if @format == :json
|
37
|
-
puts JSON.pretty_generate(
|
52
|
+
puts JSON.pretty_generate(struct)
|
38
53
|
else
|
39
|
-
StructurePrint.new(
|
54
|
+
StructurePrint.new(sep_char).structure_print(struct, level)
|
40
55
|
end
|
41
56
|
end
|
42
57
|
end
|
43
58
|
|
44
|
-
class
|
45
|
-
def
|
46
|
-
zone
|
59
|
+
class ZoneCreate < Command
|
60
|
+
def create_args
|
61
|
+
add_arg("name", "name of zone", /[[:alnum:]\.-]+/)
|
62
|
+
add_arg("comment", "zone comment", /.*/)
|
63
|
+
add_arg("status", "zone status", 'zone_statuses')
|
64
|
+
add_arg("type", "zone type", 'zone_types')
|
65
|
+
end
|
66
|
+
|
67
|
+
def create_flows
|
68
|
+
add_flow_from_usage("<name> <status> [comment] [type='Primary']")
|
69
|
+
end
|
70
|
+
|
71
|
+
def execute(arghash, rest)
|
72
|
+
name = arghash["name"]
|
73
|
+
if name[-1] != '.'
|
74
|
+
name = name + '.'
|
75
|
+
end
|
76
|
+
status = arghash["status"]
|
77
|
+
comment = arghash["comment"]
|
78
|
+
comment = "" if comment.nil?
|
79
|
+
type = arghash["type"]
|
80
|
+
puts "name = #{name}, comment=#{comment}, status=#{status}, type=#{type}"
|
81
|
+
zone = { "Comment" => comment, "DomainName" => name, "Status" => status, "ZoneType" => type, "Records" => {"A" => [] } }
|
82
|
+
puts "creating zone on server"
|
83
|
+
Route.do.add_zone(zone)
|
47
84
|
end
|
48
85
|
end
|
49
86
|
|
@@ -51,28 +88,31 @@ class Zone < Command
|
|
51
88
|
include ConfigReader
|
52
89
|
|
53
90
|
def create_args
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
add_arg(
|
62
|
-
add_arg(
|
91
|
+
add_cmd(ZoneList, "list", "list all owned zones")
|
92
|
+
add_cmd(ZonePull, "pull", "retrieve local copy of zone data")
|
93
|
+
add_cmd(ZonePush, "push", "push local changes to server")
|
94
|
+
add_cmd(ZonePrint, "print", "print zone data. ~~~~ online, ---- local copy")
|
95
|
+
add_cmd(ZoneAdd, "add", "add record/group/healthcheck")
|
96
|
+
add_cmd(ZoneCreate, "create", "create new zone")
|
97
|
+
add_cmd(ZoneDelete, "delete", "delete zone")
|
98
|
+
add_arg("zname", "name or id of zone", :alnum_not_command)
|
99
|
+
add_arg("drill-down-param", "names of fields, or indexes of items, for drilling down", :alnum_not_command)
|
63
100
|
end
|
64
101
|
|
65
102
|
def create_flows
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
103
|
+
add_flow_from_usage("list")
|
104
|
+
add_flow_from_usage("create")
|
105
|
+
add_flow_from_usage("<zname> pull")
|
106
|
+
add_flow_from_usage("<zname> push")
|
107
|
+
add_flow_from_usage("<zname> delete")
|
108
|
+
add_flow_from_usage("<zname> [<drill-down-param>...] add")
|
109
|
+
add_flow_from_usage("<zname> [<drill-down-param>...] [print]")
|
70
110
|
end
|
71
111
|
|
72
|
-
def
|
112
|
+
def alnum_not_command(value)
|
73
113
|
test = value =~ /[[:alnum:]]+/
|
74
114
|
if test
|
75
|
-
if
|
115
|
+
if command_name? value
|
76
116
|
return "name of command", false
|
77
117
|
else
|
78
118
|
return nil, true
|
@@ -81,36 +121,77 @@ class Zone < Command
|
|
81
121
|
return "invalid argument", false
|
82
122
|
end
|
83
123
|
|
84
|
-
def execute(args,
|
124
|
+
def execute(args, rest)
|
85
125
|
@zname = args["zname"]
|
86
126
|
command = args["command"]
|
87
127
|
ddparams = args["drill-down-param"]
|
88
128
|
if ddparams
|
89
|
-
@zname = ddparams.shift
|
90
129
|
zone = get_zone
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
command.
|
96
|
-
command.level = @level
|
97
|
-
command.sep_char = @sep_char
|
130
|
+
struct, level = drill_down(zone, ddparams, "zone")
|
131
|
+
command.add_input("struct", struct)
|
132
|
+
command.add_input("level", level)
|
133
|
+
command.add_input("sep_char", @sep_char)
|
134
|
+
command.add_input("toplevel", zone)
|
98
135
|
end
|
99
|
-
if command
|
100
|
-
command.
|
101
|
-
|
102
|
-
command.execute self
|
136
|
+
if command
|
137
|
+
command.add_input("zone", self)
|
138
|
+
command.run(rest)
|
103
139
|
else
|
104
140
|
raise "error"
|
105
141
|
end
|
106
142
|
end
|
107
143
|
|
108
|
-
def
|
109
|
-
|
110
|
-
|
111
|
-
|
144
|
+
def push
|
145
|
+
puts "pushing zone to server"
|
146
|
+
zone = get_zone
|
147
|
+
Route.do.untranslate_zone(zone)
|
148
|
+
if $debug
|
149
|
+
File.open("untranslated.zone","w").write(JSON.pretty_generate zone)
|
150
|
+
end
|
151
|
+
Route.do.update_zone(zone)
|
152
|
+
end
|
153
|
+
|
154
|
+
def local_filename(zone)
|
155
|
+
file_name = zone["DomainName"].sub(/\.\Z/, '')
|
156
|
+
file_name = file_name + '.' + zone["ZoneId"].to_s
|
157
|
+
end
|
158
|
+
|
159
|
+
def write_zone_file(zone, hide_dup = false)
|
160
|
+
basename = local_filename(zone)
|
161
|
+
if hide_dup
|
162
|
+
file_name = config_file("zones", basename)
|
163
|
+
File.delete(file_name) if File.exists?(file_name)
|
164
|
+
File.open(file_name, "w").write(JSON.pretty_generate zone)
|
165
|
+
end
|
166
|
+
file_name = user_file("zones", basename)
|
167
|
+
File.delete(file_name) if File.exists?(file_name)
|
168
|
+
File.open(file_name, "w").write(JSON.pretty_generate zone)
|
169
|
+
file_name
|
170
|
+
end
|
171
|
+
|
172
|
+
def pull
|
173
|
+
zone = get_zone(true)
|
174
|
+
file_name = write_zone_file(zone, true)
|
175
|
+
puts "'#{file_name}' saved locally"
|
176
|
+
end
|
177
|
+
|
178
|
+
def delete_local_zone_files(id)
|
179
|
+
files = Dir.glob(File.join("#{config_dir}", "zones", "*.#{id}"))
|
180
|
+
files.each { |file| File.delete(file) }
|
181
|
+
files = Dir.glob(File.join("zones", "*.#{id}"))
|
182
|
+
files.each { |file| File.delete(file) }
|
112
183
|
end
|
113
184
|
|
185
|
+
def delete
|
186
|
+
id, name = id_and_name
|
187
|
+
if (id.nil?)
|
188
|
+
zone = get_zone(true)
|
189
|
+
id = zone["ZoneId"]
|
190
|
+
end
|
191
|
+
puts "deleting zone with id #{id}"
|
192
|
+
delete_local_zone_files(id)
|
193
|
+
Route.do.delete_zone(id)
|
194
|
+
end
|
114
195
|
|
115
196
|
def id_and_name
|
116
197
|
id = name = nil
|
@@ -147,6 +228,18 @@ class Zone < Command
|
|
147
228
|
dputs "getting zone with id=#{id} and name=#{name}"
|
148
229
|
@sep_char = "~"
|
149
230
|
zone = Route.do.get_zone(id, name)
|
231
|
+
if zone['Records'].nil?
|
232
|
+
zone['Records'] = {
|
233
|
+
"A"=> [ ],
|
234
|
+
"AAAA"=> [ ],
|
235
|
+
"CNAME"=> [ ],
|
236
|
+
"MX"=> [ ],
|
237
|
+
"NS"=> [ ],
|
238
|
+
"SPF"=> [ ],
|
239
|
+
"SRV"=> [ ],
|
240
|
+
"TXT"=> [ ]
|
241
|
+
}
|
242
|
+
end
|
150
243
|
if zone.nil?
|
151
244
|
raise "zone doesn't exist or is empty"
|
152
245
|
end
|
@@ -159,24 +252,6 @@ class Zone < Command
|
|
159
252
|
zone
|
160
253
|
end
|
161
254
|
|
162
|
-
def push
|
163
|
-
puts "in zone.push"
|
164
|
-
zone = get_zone
|
165
|
-
Route.do.untranslate_zone(zone)
|
166
|
-
File.open("untranslated.zone","w").write(JSON.pretty_generate zone)
|
167
|
-
Route.do.update_zone(zone)
|
168
|
-
end
|
169
|
-
|
170
|
-
def pull
|
171
|
-
zone = get_zone(true)
|
172
|
-
file_name = zone["DomainName"].sub(/\.\Z/, '')
|
173
|
-
file_name = file_name + '.' + zone["ZoneId"].to_s
|
174
|
-
file_name = user_file("zones", file_name)
|
175
|
-
File.delete(file_name) if File.exists?(file_name)
|
176
|
-
File.open(file_name, "w").write(JSON.pretty_generate zone)
|
177
|
-
puts "'#{file_name}' saved locally"
|
178
|
-
end
|
179
|
-
|
180
255
|
# let user lowercase and abbreviate key; abbreviation must match one key
|
181
256
|
def match_key(abbrev, struct)
|
182
257
|
abbrev = abbrev.downcase
|
@@ -244,22 +319,4 @@ class Zone < Command
|
|
244
319
|
def force_online
|
245
320
|
@force_online = true
|
246
321
|
end
|
247
|
-
|
248
|
-
|
249
|
-
def add(args)
|
250
|
-
if @zname
|
251
|
-
puts "unimplemented"
|
252
|
-
else
|
253
|
-
@zone = {
|
254
|
-
:Comment => "",
|
255
|
-
:DomainName => @zname,
|
256
|
-
:FailoverGroups => [ ],
|
257
|
-
:LoadBalancingGroups => [ ],
|
258
|
-
:Records => null,
|
259
|
-
:Status => 1,
|
260
|
-
:ZoneType => 1
|
261
|
-
}
|
262
|
-
|
263
|
-
end
|
264
|
-
end
|
265
322
|
end
|
data/lib/zone_add.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
class ZoneAdd < Command
|
2
|
+
def create_args
|
3
|
+
# for level zone.Records.?
|
4
|
+
add_arg('name', 'Name of the node to which this record pertains', /[[:alnum:]]+/)
|
5
|
+
add_arg('rdata', 'Additional resource record data, such as IP Address', /.*/)
|
6
|
+
add_arg('ttl', 'Time to live, in seconds', /\d+/)
|
7
|
+
end
|
8
|
+
|
9
|
+
def create_flows
|
10
|
+
add_flow_from_usage("<name> <rdata> <ttl>")
|
11
|
+
end
|
12
|
+
|
13
|
+
def handle_level(level)
|
14
|
+
levels = level.split('.')
|
15
|
+
if levels.length == 3 && levels[1] == 'Records'
|
16
|
+
rtype = levels[2]
|
17
|
+
struct = @input["struct"]
|
18
|
+
struct << @record
|
19
|
+
StructurePrint.new("-").structure_print(struct, level)
|
20
|
+
else
|
21
|
+
raise "Illegal or unimplemented add location"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def execute(arghash, rest)
|
26
|
+
struct = @input["struct"]
|
27
|
+
level = @input["level"]
|
28
|
+
@zone_struct = @input["toplevel"]
|
29
|
+
zone = @input["zone"]
|
30
|
+
|
31
|
+
@record = { "Name" => arghash["name"], "Rdata" => arghash["rdata"], "TTL" => arghash["ttl"].to_i }
|
32
|
+
handle_level(level)
|
33
|
+
zone.write_zone_file(@zone_struct)
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: vzcdn
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Steve Preston
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2014-04-
|
13
|
+
date: 2014-04-22 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: rest-client
|
@@ -83,6 +83,7 @@ files:
|
|
83
83
|
- lib/method.rb
|
84
84
|
- lib/mkbind.sh
|
85
85
|
- lib/print.rb
|
86
|
+
- lib/proxy_credentials.rb
|
86
87
|
- lib/realTimeStats.rb
|
87
88
|
- lib/reporting.rb
|
88
89
|
- lib/route.rb
|
@@ -94,6 +95,7 @@ files:
|
|
94
95
|
- lib/vzcdn.rb
|
95
96
|
- lib/vzcdn/version.rb
|
96
97
|
- lib/zone.rb
|
98
|
+
- lib/zone_add.rb
|
97
99
|
- pkg/vzcdn-0.0.1.gem
|
98
100
|
- test/test_all.rb
|
99
101
|
- test/test_clui.rb
|