growthforecast 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,11 +1,16 @@
1
1
  # GrowthForecast
2
2
 
3
- Client library to operate GrowthForecast.
3
+ Client library and command to operate GrowthForecast.
4
4
 
5
5
  * http://kazeburo.github.com/GrowthForecast/ (Japanese)
6
6
  * https://github.com/kazeburo/growthforecast
7
7
 
8
- Update graph value, or create/edit/delete basic graphs and complex graphs.
8
+ Features:
9
+
10
+ * Update graph value, or create/edit/delete basic graphs and complex graphs from your own code
11
+ * Check, or edit/create basic/complex graphs with YAML specs, keywords and `gfclient` command
12
+
13
+ **USE GrowthForecast v0.33 or later**
9
14
 
10
15
  ## Installation
11
16
 
@@ -23,6 +28,8 @@ Or install it yourself as:
23
28
 
24
29
  ## Usage
25
30
 
31
+ ### Client Library
32
+
26
33
  Update graph's value:
27
34
 
28
35
  ```ruby
@@ -179,11 +186,154 @@ complex_spec.description = '....'
179
186
  gf.add(complex_spec) # add() ignores 'id' attribute
180
187
  ```
181
188
 
189
+ ### Basic Authentication
190
+
191
+ Set `username` and `password`:
192
+
193
+ ```ruby
194
+ gf = GrowthForecast.new(hostname, portnum)
195
+ gf.username = 'whoami'
196
+ gf.password = 'secret'
197
+
198
+ # ...
199
+ ```
200
+
201
+ ### gfclient
202
+
203
+ ```
204
+ usage: gfclient SPEC_PATH TARGET_NAME [KEYWORD1 KEYWORD2 ...]
205
+ -f, --force create/edit graphs with spec (default: check only)
206
+
207
+ -H, --host=HOST set growthforecast hostname (overwrite configuration in spec)
208
+ -P, --port=PORT set growthforecast port number
209
+
210
+ -u, --user=USER set username for growthforecast basic authentication
211
+ -p, --pass=PASS set password
212
+
213
+ --prefix=PREFIX set growthforecast uri prefix
214
+
215
+ -l, --list=PATH set keywords list file path (keywords set per line)
216
+ do check/edit many times with this option
217
+ (ignore default keywords)
218
+ -a, --get-all get and cache all graph data at first (default: get incrementally)
219
+ -g, --debug enable debug mode
220
+
221
+ -h, --help show this message
222
+ ```
223
+
224
+ `gfclient` checks all patterns of `TARGET_NAME` in spec yaml file's `specs -> check` section and `specs -> edit` section. Spec file examples are `example/testspec.yaml` and `example/spec.yaml`.
225
+
226
+ Minimal example is:
227
+
228
+ ```yaml
229
+ config:
230
+ host: your.gf.host.local
231
+ port: 5125
232
+ specs:
233
+ - name: 'example1'
234
+ keywords:
235
+ - 'target'
236
+ - 'groupname'
237
+ check:
238
+ - name: 'metrics1'
239
+ path: 'pageviews/${target}/total'
240
+ color: '#1111ff'
241
+ - name: 'metrics2'
242
+ path: 'pageviews/${target}/bot'
243
+ color: '#ff1111'
244
+ edit:
245
+ - name: 'all metrics'
246
+ path: '${groupname}/pageviews/all'
247
+ complex: true
248
+ description: 'Pageviews graph (team: ${groupname}, service: ${target})'
249
+ stack: false
250
+ type: 'LINE2'
251
+ data:
252
+ - path: 'pageviews/${target}/total'
253
+ - path: 'pageviews/${target}/bot'
254
+ type: 'LINE1'
255
+ ```
256
+
257
+ With configurations above and command line below:
258
+
259
+ ```sh
260
+ $ gfclient spec.yaml example1 myservice super_team
261
+ ```
262
+
263
+ `gfclient` works with GrowthForecast at http://your.gf.host.local:5125/ like this:
264
+
265
+ 1. check `pageviews/myservice/total` and `pageviews/myservice/bot` exists or not
266
+ 2. check these are configured with specified configuration parameters or not
267
+ 3. **check** `super_team/pageviews/all` complex graph exists or not, and is configured correctly or not
268
+ 4. report result
269
+
270
+ `gfclient` do check only without options. With **-f or --force** option, graphs specified in `edit` section are created/editted.
271
+
272
+ 1. check `pageviews/myservice/total` and `pageviews/myservice/bot` exists or not, and are configured correctly or not
273
+ 2. **abort** when any mismatches found for `check` section specs
274
+ 3. check graph specs in `edit` section
275
+ 4. **create** graph if specified path is not found
276
+ 5. **edit** graph if specified graph's configuration doesn't matches with spec
277
+
278
+ `name` and `path` attributes must be specified in each check/edit items, and `complex: true` must be specified for complex graph. Others are optional (missing configuration attributes are ignored for check, and specified as default value for create).
279
+
280
+ (bold item is required)
281
+
282
+ * basic graph
283
+ * **name** (label of this spec item)
284
+ * **path** (string like `service/section/graph`)
285
+ * description (text)
286
+ * mode (string: 'gauge', 'subtract' or 'both')
287
+ * sort (number: 19-0)
288
+ * color (string like '#0088ff')
289
+ * type (string: 'AREA', 'LINE1' or 'LINE2') (LINE2 meas bold)
290
+ * ulimit, llimit (number: effective range upper/lower limit)
291
+ * stype (string: 'AREA', 'LINE1' or 'LINE2') (mode of subtract graph)
292
+ * sulimit, llimit (number: effective range of subtract graph)
293
+ * complex graph
294
+ * **name** (label of this spec item)
295
+ * **path** (string like `service/section/graph`)
296
+ * **complex** (true or false: true must be specified for complex graph)
297
+ * description (text)
298
+ * sort (number: 19-0)
299
+ * sumup (true or false: display sum up value or not)
300
+ * mode/type/stack (global options for items of `data`, and may be overwritten by mode/type/stack of each items of `data`)
301
+ * **data** (list of basic graph in this complex graph)
302
+ * **path** (string: path of graph like `service/section/graph`)
303
+ * mode (string: 'gauge' or 'subtract')
304
+ * type (string: 'AREA', 'LINE1' or 'LINE2')
305
+ * stack (true or false)
306
+
307
+ Spec file configurations (all of these are optional, and may be overwritten by command line options):
308
+
309
+ ```yaml
310
+ config:
311
+ host: 'hostname.of.growthforecast' # default: localhost
312
+ port: 80 # default: 5125
313
+ prefix: '/gf' # default: '/' (for cases if you mount GrowthForecast on subpath of web server)
314
+ username: 'name' # username of GrowthForecast's HTTP Basic authentication (default: none)
315
+ password: 'pass' # password (default: none)
316
+ debug: false # show errors in growthforecast http response or not (default: false)
317
+ getall: false # get and cache all graph informations before all checks
318
+ # (default false, but you should specify true if your gf has many graphs)
319
+ ```
320
+
321
+ You can check/edit many graphs with keywords list file like `gfclient -l listfile spec.yaml targetname`:
322
+
323
+ ```
324
+ # keyword1 keyword2
325
+ xx1 yy1
326
+ xx2 yy2
327
+
328
+ # blank and comments are ok (but invalid for line-end comment)
329
+ aa1 bb1
330
+ aa2 bb2
331
+ ```
332
+
182
333
  ## TODO
183
334
 
184
335
  * validations of specs
185
336
  * diff of 2 graph objects
186
- * bin/gfclient
187
337
  * tests
188
338
 
189
339
  ## Copyright
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # gfclient spec.yaml TARGET ARG1 ARG2
4
+
5
+ $0 = 'gfclient'
6
+
7
+ $LOAD_PATH.push('lib')
8
+
9
+ options = {}
10
+
11
+ require 'optparse'
12
+ opt = OptionParser.new
13
+ opt.on('-f', '--force'){|v| options[:force] = v }
14
+ opt.on('-H HOST', '--host=HOST'){|v| options[:host] = v }
15
+ opt.on('-P PORT', '--port=PORT'){|v| options[:port] = v }
16
+ opt.on('-u USER', '--user=USER'){|v| options[:username] = v }
17
+ opt.on('-p PASS', '--pass=PASS'){|v| options[:password] = v }
18
+ opt.on('--prefix=PREFIX'){|v| options[:prefix] = v }
19
+
20
+ opt.on('-l PATH', '--list=PATH'){|v| options[:list] = v }
21
+ opt.on('-a', '--get-all'){|v| options[:getall] = v }
22
+ opt.on('-g', '--debug'){|v| options[:debug] = v }
23
+ opt.on('-h', '--help'){|v| options[:help] = v }
24
+ begin
25
+ opt.parse!(ARGV)
26
+ rescue OptionParser::ParseError
27
+ options[:error] = true
28
+ end
29
+
30
+ specfile_path = ARGV[0]
31
+ target_name = ARGV[1]
32
+ keywords = ARGV[2,ARGV.size]
33
+
34
+ unless specfile_path && target_name && keywords && !options[:error] && !options[:help]
35
+ puts <<"EOH"
36
+ usage: gfclient SPEC_PATH TARGET_NAME [KEYWORD1 KEYWORD2 ...]
37
+ -f, --force create/edit graphs with spec (default: check only)
38
+
39
+ -H, --host=HOST set growthforecast hostname (overwrite configuration in spec)
40
+ -P, --port=PORT set growthforecast port number
41
+
42
+ -u, --user=USER set username for growthforecast basic authentication
43
+ -p, --pass=PASS set password
44
+
45
+ --prefix=PREFIX set growthforecast uri prefix
46
+
47
+ -l, --list=PATH set keywords list file path (keywords set per line)
48
+ do check/edit many times with this option
49
+ (ignore default keywords)
50
+ -a, --get-all get and cache all graph data at first (default: get incrementally)
51
+ -g, --debug enable debug mode
52
+
53
+ -h, --help show this message
54
+ EOH
55
+ exit(0) if options[:help]
56
+ exit(1)
57
+ end
58
+
59
+ begin
60
+ require 'growthforecast/client'
61
+ rescue LoadError
62
+ require 'rubygems'
63
+ require 'growthforecast/client'
64
+ end
65
+
66
+ retval = if options[:list]
67
+ unless File.file?(options[:list])
68
+ warn "keywords list file not found: #{options[:list]}"
69
+ exit(1)
70
+ end
71
+ keywords_list = File.open(options[:list]){|file| file.readlines }.select{|s| s !~ /^(#.*|\s+)$/}.map{|line| line.split(/\s+/)}
72
+ GrowthForecast::Client.execute_bulk(options[:force], specfile_path, target_name, keywords_list, options)
73
+ else
74
+ GrowthForecast::Client.execute(options[:force], specfile_path, target_name, keywords, options)
75
+ end
76
+ exit retval
@@ -0,0 +1,6 @@
1
+ a1 a2
2
+ b1 b2
3
+ c1 c2
4
+ d1 d2
5
+ e1 e2
6
+ f1 f2
@@ -0,0 +1,149 @@
1
+ # gfclient [options] set_name keyword1 [keyword2 ...]
2
+ config:
3
+ host: gf.local
4
+ port: 5127
5
+ debug: true
6
+ getall: true
7
+ specs:
8
+ - name: 'httpstatus'
9
+ keywords:
10
+ - 'servicename'
11
+ check:
12
+ - name: 'httpstatus_2xx_percentage'
13
+ path: 'accesslog/httpstatus/${servicename}_2xx_percentage'
14
+ color: '#1111cc'
15
+ - name: 'httpstatus_2xx_rate'
16
+ path: 'accesslog/httpstatus/${servicename}_2xx_rate'
17
+ color: '#1111cc'
18
+ - name: 'httpstatus_3xx_percentage'
19
+ path: 'accesslog/httpstatus/${servicename}_3xx_percentage'
20
+ color: '#11cc11'
21
+ - name: 'httpstatus_3xx_rate'
22
+ path: 'accesslog/httpstatus/${servicename}_3xx_rate'
23
+ color: '#11cc11'
24
+ - name: 'httpstatus_429_percentage'
25
+ path: 'accesslog/httpstatus/${servicename}_429_percentage'
26
+ color: '#cccc77'
27
+ - name: 'httpstatus_429_rate'
28
+ path: 'accesslog/httpstatus/${servicename}_429_rate'
29
+ color: '#cccc77'
30
+ - name: 'httpstatus_4xx_percentage'
31
+ path: 'accesslog/httpstatus/${servicename}_4xx_percentage'
32
+ color: '#cc7711'
33
+ - name: 'httpstatus_4xx_rate'
34
+ path: 'accesslog/httpstatus/${servicename}_4xx_rate'
35
+ color: '#cc7711'
36
+ - name: 'httpstatus_5xx_percentage'
37
+ path: 'accesslog/httpstatus/${servicename}_5xx_percentage'
38
+ color: '#cc1111'
39
+ - name: 'httpstatus_5xx_rate'
40
+ path: 'accesslog/httpstatus/${servicename}_5xx_rate'
41
+ color: '#cc1111'
42
+ - name: 'httpstatus_unmatched_percentage'
43
+ path: 'accesslog/httpstatus/${servicename}_unmatched_percentage'
44
+ color: '#7711cc'
45
+ - name: 'httpstatus_unmatched_rate'
46
+ path: 'accesslog/httpstatus/${servicename}_unmatched_rate'
47
+ color: '#7711cc'
48
+ - name: 'httpstatus'
49
+ path: '${servicename}/accesslog/status'
50
+ complex: true
51
+ description: 'レスポンスのステータスコード別の割合'
52
+ sort: 19
53
+ gmode: 'gauge'
54
+ stack: true
55
+ type: 'AREA'
56
+ data:
57
+ - path: 'accesslog/httpstatus/${servicename}_2xx_percentage'
58
+ - path: 'accesslog/httpstatus/${servicename}_3xx_percentage'
59
+ - path: 'accesslog/httpstatus/${servicename}_429_percentage'
60
+ - path: 'accesslog/httpstatus/${servicename}_4xx_percentage'
61
+ - path: 'accesslog/httpstatus/${servicename}_5xx_percentage'
62
+ - path: 'accesslog/httpstatus/${servicename}_unmatched_percentage'
63
+ - name: 'responsetime'
64
+ keywords:
65
+ - 'servicename'
66
+ check:
67
+ - name: 'min'
68
+ path: 'accesslog/responsetime/${servicename}_min'
69
+ color: '#cccc77'
70
+ - name: 'avg'
71
+ path: 'accesslog/responsetime/${servicename}_avg'
72
+ color: '#1111cc'
73
+ - name: 'max'
74
+ path: 'accesslog/responsetime/${servicename}_max'
75
+ color: '#77cccc'
76
+ - name: 'percentile_50'
77
+ path: 'accesslog/responsetime/${servicename}_percentile_50'
78
+ color: '#11cc11'
79
+ - name: 'percentile_90'
80
+ path: 'accesslog/responsetime/${servicename}_percentile_90'
81
+ color: '#cc1111'
82
+ - name: 'percentile_95'
83
+ path: 'accesslog/responsetime/${servicename}_percentile_95'
84
+ color: '#336699'
85
+ - name: 'percentile_98'
86
+ path: 'accesslog/responsetime/${servicename}_percentile_98'
87
+ color: '#cc1177'
88
+ - name: 'percentile_99'
89
+ path: 'accesslog/responsetime/${servicename}_percentile_99'
90
+ color: '#cccc11'
91
+ - name: 'u100ms'
92
+ path: 'accesslog/responsetime/${servicename}_u100ms_percentage'
93
+ color: '#11cccc'
94
+ - name: 'u500ms'
95
+ path: 'accesslog/responsetime/${servicename}_u500ms_percentage'
96
+ color: '#1111cc'
97
+ - name: 'u1s'
98
+ path: 'accesslog/responsetime/${servicename}_u1s_percentage'
99
+ color: '#77cc11'
100
+ - name: 'u3s'
101
+ path: 'accesslog/responsetime/${servicename}_u3s_percentage'
102
+ color: '#cc7711'
103
+ - name: 'long'
104
+ path: 'accesslog/responsetime/${servicename}_long_percentage'
105
+ color: '#cc1111'
106
+ - name: 'unmatched'
107
+ path: 'accesslog/responsetime/${servicename}_unmatched_percentage'
108
+ color: '#7711cc'
109
+ - name: 'responsetime_percentage'
110
+ path: '${servicename}/accesslog/response_time_percentage'
111
+ complex: true
112
+ description: 'レスポンス所要時間(duration)毎の割合'
113
+ sort: 18
114
+ type: 'AREA'
115
+ stack: true
116
+ data:
117
+ - path: 'accesslog/responsetime/${servicename}_unmatched_percentage'
118
+ - path: 'accesslog/responsetime/${servicename}_u100ms_percentage'
119
+ - path: 'accesslog/responsetime/${servicename}_u500ms_percentage'
120
+ - path: 'accesslog/responsetime/${servicename}_u1s_percentage'
121
+ - path: 'accesslog/responsetime/${servicename}_u3s_percentage'
122
+ - path: 'accesslog/responsetime/${servicename}_long_percentage'
123
+ - name: 'responsetime'
124
+ path: '${servicename}/accesslog/response_time'
125
+ complex: true
126
+ description: 'レスポンス所要時間(duration:マイクロ秒)統計'
127
+ sort: 17
128
+ type: 'LINE2'
129
+ stack: false
130
+ data:
131
+ - path: 'accesslog/responsetime/${servicename}_min'
132
+ - path: 'accesslog/responsetime/${servicename}_avg'
133
+ - path: 'accesslog/responsetime/${servicename}_percentile_50'
134
+ - path: 'accesslog/responsetime/${servicename}_percentile_90'
135
+ - path: 'accesslog/responsetime/${servicename}_percentile_95'
136
+ - path: 'accesslog/responsetime/${servicename}_percentile_98'
137
+ - path: 'accesslog/responsetime/${servicename}_percentile_99'
138
+ - name: 'responsetime2'
139
+ path: '${servicename}/accesslog/response_time2'
140
+ complex: true
141
+ description: 'レスポンス所要時間(duration:マイクロ秒) avg/99%tile/max'
142
+ sort: 16
143
+ type: 'LINE2'
144
+ stack: false
145
+ data:
146
+ - path: 'accesslog/responsetime/${servicename}_avg'
147
+ - path: 'accesslog/responsetime/${servicename}_percentile_99'
148
+ - path: 'accesslog/responsetime/${servicename}_max'
149
+
@@ -0,0 +1,72 @@
1
+ config:
2
+ host: localhost
3
+ port: 5125
4
+ specs:
5
+ - name: 'spectest'
6
+ keywords:
7
+ - 'arg1'
8
+ - 'arg2'
9
+ check:
10
+ - name: 'test_first_graph'
11
+ path: 'example/spectest/${arg1}'
12
+ description: 'test1 for ${arg1} and ${arg2}'
13
+ color: '#ff1111'
14
+ - name: 'test_second_graph'
15
+ path: 'example/spectest/${arg1}_second'
16
+ description: 'test2 for ${arg1} and ${arg2}'
17
+ color: '#1111ff'
18
+ - name: 'test1_complex'
19
+ path: 'example/spectest/comp1_${arg1}'
20
+ complex: true
21
+ description: 'complex test for ${arg1} and ${arg2}'
22
+ stack: true
23
+ mode: 'gauge'
24
+ type: 'LINE1'
25
+ data:
26
+ - path: 'example/spectest/${arg1}'
27
+ - path: 'example/spectest/${arg1}_second'
28
+ type: 'LINE2'
29
+ - name: 'test2_complex'
30
+ path: 'example/spectest/comp2_${arg1}'
31
+ complex: true
32
+ description: 'complex test 2 for ${arg1} and ${arg2}'
33
+ stack: false
34
+ mode: 'gauge'
35
+ type: 'AREA'
36
+ data:
37
+ - path: 'example/spectest/${arg1}'
38
+ - path: 'example/spectest/${arg1}_second'
39
+ - name: 'spectest2'
40
+ keywords:
41
+ - 'arg1'
42
+ - 'arg2'
43
+ edit:
44
+ - name: 'test_first_graph'
45
+ path: 'example/spectest/${arg1}'
46
+ description: 'test1 for ${arg1} and ${arg2}'
47
+ color: '#ff1111'
48
+ - name: 'test_second_graph'
49
+ path: 'example/spectest/${arg1}_second'
50
+ description: 'test2 for ${arg1} and ${arg2}'
51
+ color: '#1111ff'
52
+ - name: 'test1_complex'
53
+ path: 'example/spectest/comp1_${arg1}'
54
+ complex: true
55
+ description: 'complex test for ${arg1} and ${arg2}'
56
+ stack: true
57
+ mode: 'gauge'
58
+ type: 'LINE1'
59
+ data:
60
+ - path: 'example/spectest/${arg1}'
61
+ - path: 'example/spectest/${arg1}_second'
62
+ type: 'LINE2'
63
+ - name: 'test2_complex'
64
+ path: 'example/spectest/comp2_${arg1}'
65
+ complex: true
66
+ description: 'complex test 2 for ${arg1} and ${arg2}'
67
+ stack: false
68
+ mode: 'gauge'
69
+ type: 'AREA'
70
+ data:
71
+ - path: 'example/spectest/${arg1}'
72
+ - path: 'example/spectest/${arg1}_second'
@@ -14,8 +14,9 @@ require 'json'
14
14
 
15
15
  class GrowthForecast
16
16
  attr_accessor :host, :port, :prefix, :timeout, :debug
17
+ attr_accessor :username, :password
17
18
 
18
- def initialize(host='localhost', port=5125, prefix=nil, timeout=30, debug=false)
19
+ def initialize(host='localhost', port=5125, prefix=nil, timeout=30, debug=false, username=nil, password=nil)
19
20
  @host = host
20
21
  @port = port.to_i
21
22
  @prefix = if prefix && (prefix =~ /^\//) then prefix
@@ -25,11 +26,14 @@ class GrowthForecast
25
26
  @prefix.chop! if @prefix =~ /\/$/
26
27
  @timeout = timeout.to_i
27
28
  @debug = debug ? true : false
29
+
30
+ @username = username
31
+ @password = password
28
32
  end
29
33
 
30
34
  def debug(mode=nil)
31
35
  if mode.nil?
32
- return GrowthForecast.new(@host,@port,@prefix,@timeout,true)
36
+ return GrowthForecast.new(@host,@port,@prefix,@timeout,true,@username,@password)
33
37
  end
34
38
  @mode = mode ? true : false
35
39
  self
@@ -68,6 +72,7 @@ class GrowthForecast
68
72
 
69
73
  def complexes
70
74
  list = request('GET', "/json/list/complex", {}, '', true)
75
+ return nil if list.nil?
71
76
  list.each do |path|
72
77
  path.complex = true
73
78
  end
@@ -172,8 +177,8 @@ class GrowthForecast
172
177
  obj
173
178
  when obj.is_a?(Array)
174
179
  obj.map{|e| concrete(e)}
175
- when GrowthForecast::Path.path?(obj)
176
- GrowthForecast::Path.new(obj)
180
+ when Path.path?(obj)
181
+ Path.new(obj)
177
182
  when obj['complex']
178
183
  Complex.new(obj)
179
184
  else
@@ -185,11 +190,25 @@ class GrowthForecast
185
190
  concrete(http_request(method, path, header, content, getlist))
186
191
  end
187
192
 
188
- def http_request(method, path, header={}, content='', getlist=false)
193
+ def http_request(method, path, header={}, content=nil, getlist=false)
189
194
  conn = Net::HTTP.new(@host, @port)
190
195
  conn.open_timeout = conn.read_timeout = @timeout
191
196
  request_path = @prefix + path
192
- res = conn.send_request(method, request_path, content, header)
197
+ req = case method
198
+ when 'GET'
199
+ Net::HTTP::Get.new(request_path, header)
200
+ when 'POST'
201
+ Net::HTTP::Post.new(request_path, header)
202
+ else
203
+ raise ArgumentError, "Invalid HTTP method for GrowthForecast: '#{method}'"
204
+ end
205
+ if content
206
+ req.body = content
207
+ end
208
+ if @username || @password
209
+ req.basic_auth(@username, @password)
210
+ end
211
+ res = conn.request(req)
193
212
 
194
213
  unless res.is_a?(Net::HTTPSuccess)
195
214
  return [] if getlist and res.code == '404'
@@ -0,0 +1,46 @@
1
+ class GrowthForecast::Cache
2
+ def initialize(client, tree=nil, reload=true)
3
+ @client = client
4
+ @tree = tree
5
+ @reload = reload
6
+
7
+ @graphs = []
8
+ @complexes = []
9
+ end
10
+
11
+ def get(service, section, graph, force_reload=false)
12
+ if @tree && (!force_reload)
13
+ return @tree.fetch(service, {}).fetch(section, {})[graph]
14
+ end
15
+
16
+ item = check_cached_path(service, section, graph)
17
+ if item.is_a?(GrowthForecast::Path)
18
+ if item.complex?
19
+ item = @client.complex(item.id)
20
+ else
21
+ item = @client.graph(item.id)
22
+ end
23
+ end
24
+ item
25
+ end
26
+
27
+ def check_cached_path(service, section, graph, atfirst=true)
28
+ list = @graphs.select{|item| item.service_name == service && item.section_name == section && item.graph_name == graph}
29
+ unless list.empty?
30
+ return list.first
31
+ end
32
+
33
+ list = @complexes.select{|item| item.service_name == service && item.section_name == section && item.graph_name == graph}
34
+ unless list.empty?
35
+ return list.first
36
+ end
37
+
38
+ if @reload && atfirst
39
+ @graphs = @client.graphs
40
+ @complexes = @client.complexes
41
+ return check_cached_path(service, section, graph, false)
42
+ end
43
+
44
+ nil
45
+ end
46
+ end
@@ -0,0 +1,142 @@
1
+ require 'growthforecast'
2
+ require 'growthforecast/cache'
3
+ require 'growthforecast/spec'
4
+
5
+ require 'yaml'
6
+
7
+ module GrowthForecast::Client
8
+ def self.execute_bulk(force, spec_path, target, keywords_list, options)
9
+ gf, cache, specs = setup(spec_path, options)
10
+ spec = specs.select{|s| s['name'] == target}
11
+ if spec.size != 1
12
+ warn "#{spec.size} specs with name '#{target}'" if spec.size > 1
13
+ warn "Spec #{target} not found" if spec.size < 1
14
+ return 2
15
+ end
16
+ spec = spec.first
17
+
18
+ keywords_list.each do |keywords|
19
+ retval = execute_once(gf, cache, force, spec, keywords)
20
+ if force && retval != 0
21
+ warn "aborting batch mode."
22
+ return retval
23
+ end
24
+ end
25
+ return 0
26
+ end
27
+
28
+ def self.execute(force, spec_path, target, keywords, options)
29
+ gf, cache, specs = setup(spec_path, options)
30
+ spec = specs.select{|s| s['name'] == target}
31
+ if spec.size != 1
32
+ warn "#{spec.size} specs with name '#{target}'" if spec.size > 1
33
+ warn "Spec #{target} not found" if spec.size < 1
34
+ return 2
35
+ end
36
+ spec = spec.first
37
+
38
+ execute_once(gf, cache, force, spec, keywords)
39
+ end
40
+
41
+ def self.execute_once(gf, cache, force, spec, keywords)
42
+ # generate dictionary
43
+ if spec['keywords'].size != keywords.size
44
+ warn "Keyword mismatch, in spec: #{spec.keywords.join('/')}"
45
+ exit(2)
46
+ end
47
+ dic = Hash[[spec['keywords'], keywords].transpose]
48
+
49
+ check_success = true
50
+ # check [and fix] all specs
51
+ check_target = if force
52
+ spec['check'] || []
53
+ else
54
+ (spec['check'] || []) + (spec['edit'] || [])
55
+ end
56
+ check_target.each do |check|
57
+ c = GrowthForecast::Spec.new(dic, check)
58
+ result,errors = c.check(cache)
59
+ unless result
60
+ errors.each{|e|
61
+ warn "#{c.name}(#{c.service_name}/#{c.section_name}/#{c.graph_name} [#{c.complex? ? 'complex' : 'graph'}]) #{e}"
62
+ }
63
+ check_success = false
64
+ end
65
+ end
66
+
67
+ unless check_success
68
+ warn "Some check failure exists, aborting."
69
+ return 1
70
+ end
71
+ return 0 unless force
72
+
73
+ (spec['edit'] || []).each do |edit|
74
+ e = GrowthForecast::Spec.new(dic, edit)
75
+ result,errors = e.check(cache)
76
+ next if result
77
+
78
+ target = cache.get(e.service_name, e.section_name, e.graph_name)
79
+ if target
80
+ if target.complex? ^ e.complex?
81
+ warn "#{e.name}(#{e.service_name}/#{e.section_name}/#{e.graph_name}) complex type is not match: skip."
82
+ next
83
+ end
84
+ # edit
85
+ warn "update #{e.name}(#{e.service_name}/#{e.section_name}/#{e.graph_name})"
86
+ target = e.merge(cache, target)
87
+ unless target
88
+ warn "aborting with error."
89
+ return 1
90
+ end
91
+ gf.debug.edit(target)
92
+ else
93
+ # generate
94
+ warn "create #{e.name}(#{e.service_name}/#{e.section_name}/#{e.graph_name})"
95
+ target = e.merge(cache, nil)
96
+ unless target
97
+ warn "aborting with error."
98
+ return 1
99
+ end
100
+ gf.debug.add(target)
101
+ unless target.complex? # basic graph creation cannot handle options (except for 'color')
102
+ target = e.merge(cache, cache.get(e.service_name, e.section_name, e.graph_name, true))
103
+ gf.debug.edit(target)
104
+ end
105
+ end
106
+ end
107
+ return 0
108
+ end
109
+
110
+ def self.setup(spec_path, options)
111
+ spec_data = nil
112
+ begin
113
+ spec_data = YAML.load_file(spec_path)
114
+ rescue => e
115
+ warn "Spec file load error: #{e.message}"
116
+ exit(2)
117
+ end
118
+ specs = spec_data['specs']
119
+ spec_config = spec_data['config']
120
+
121
+ config = lambda{|name| options[name] || spec_config[name.to_s]}
122
+
123
+ host = config.call(:host) || 'localhost'
124
+ port = config.call(:port) || 5125
125
+ prefix = config.call(:prefix)
126
+ gf = GrowthForecast.new(host, port, prefix)
127
+
128
+ gf.debug = true if config.call(:debug)
129
+
130
+ gf.username = config.call(:username)
131
+ gf.password = config.call(:password)
132
+
133
+ tree = if config.call(:getall)
134
+ gf.tree()
135
+ else
136
+ nil
137
+ end
138
+ cache = GrowthForecast::Cache.new(gf, tree)
139
+
140
+ return gf, cache, specs
141
+ end
142
+ end
@@ -0,0 +1,239 @@
1
+ class GrowthForecast::Spec
2
+ attr_accessor :name, :path, :complex
3
+ attr_accessor :service_name, :section_name, :graph_name
4
+
5
+ def complex?
6
+ @complex
7
+ end
8
+
9
+ def initialize(dic, spec_yaml, complex=false)
10
+ @dic = dic
11
+
12
+ @spec = spec_yaml.dup
13
+
14
+ @name = @spec.delete('name')
15
+ @path = @spec.delete('path')
16
+ @complex = @spec.delete('complex') || complex
17
+
18
+ @service_name, @section_name, @graph_name = replace_keywords(@path).split('/')
19
+
20
+ unless @service_name and @section_name and @graph_name and
21
+ not @service_name.empty? and not @section_name.empty? and not @graph_name.empty?
22
+ raise ArgumentError, "'path' must be as service/section/graph (#{@path})"
23
+ end
24
+ end
25
+
26
+ def replace_keywords(str)
27
+ @dic.reduce(str){|r, pair| r.gsub('${' + pair[0] + '}', pair[1])}
28
+ end
29
+
30
+ # return [true/false, [error notifications]]
31
+ def check(cache)
32
+ target = cache.get(@service_name, @section_name, @graph_name)
33
+
34
+ if target.nil?
35
+ return false, ["target path #{@service_name}/#{@section_name}/#{@graph_name} not exists"]
36
+ elsif self.complex? ^ target.complex?
37
+ return false, ["complex type is not match"]
38
+ end
39
+
40
+ if self.complex?
41
+ self.check_complex(cache, target)
42
+ else
43
+ self.check_graph(cache, target)
44
+ end
45
+ end
46
+
47
+ def merge(cache, target)
48
+ if self.complex?
49
+ self.merge_complex(cache, target)
50
+ else
51
+ self.merge_graph(cache, target)
52
+ end
53
+ end
54
+
55
+ GRAPH_ATTRIBUTES = [
56
+ :description, :mode, :sort, :color, :gmode,
57
+ :type, :ulimit, :llimit, :stype, :sulimit, :sllimit,
58
+ :adjust, :adjustval, :unit,
59
+ ]
60
+ def check_graph(cache, target)
61
+ errors = []
62
+ GRAPH_ATTRIBUTES.each do |attr|
63
+ next unless @spec.has_key?(attr.to_s)
64
+ target_val = target.send(attr)
65
+ spec_val = if (attr == :description) && @spec[attr.to_s]
66
+ replace_keywords(@spec[attr.to_s])
67
+ else
68
+ @spec[attr.to_s]
69
+ end
70
+ unless target_val == spec_val
71
+ errors.push("attribute #{attr} value mismatch, spec '#{spec_val}' but '#{target_val}'")
72
+ end
73
+ end
74
+ return errors.empty?, errors
75
+ end
76
+
77
+ def merge_graph(cache, target)
78
+ if target.nil?
79
+ target = GrowthForecast::Graph.new({
80
+ service_name: @service_name, section_name: @section_name, graph_name: @graph_name,
81
+ description: '',
82
+ })
83
+ end
84
+ GRAPH_ATTRIBUTES.each do |attr|
85
+ next unless @spec.has_key?(attr.to_s)
86
+ val = if attr == :description
87
+ replace_keywords(@spec[attr.to_s])
88
+ else
89
+ @spec[attr.to_s]
90
+ end
91
+ target.send((attr.to_s + '=').to_sym, val)
92
+ end
93
+ target
94
+ end
95
+
96
+ COMPLEX_ATTRIBUTES = [ :description, :sort, :sumup ]
97
+ def check_complex(cache, target)
98
+ errors = []
99
+ COMPLEX_ATTRIBUTES.each do |attr|
100
+ next unless @spec.has_key?(attr.to_s)
101
+ target_val = target.send(attr)
102
+ spec_val = if (attr == :description) && @spec[attr.to_s]
103
+ replace_keywords(@spec[attr.to_s])
104
+ else
105
+ @spec[attr.to_s]
106
+ end
107
+ unless target_val == spec_val
108
+ errors.push("attribute #{attr} value mismatch, spec '#{spec_val}' but '#{target_val}'")
109
+ end
110
+ end
111
+
112
+ unless @spec.has_key?('data')
113
+ return errors.empty?, errors
114
+ end
115
+
116
+ # @spec has 'data'
117
+ spec_data_tmpl = {}
118
+ spec_data_tmpl['gmode'] = @spec['gmode'] if @spec.has_key?('gmode')
119
+ spec_data_tmpl['stack'] = @spec['stack'] if @spec.has_key?('stack')
120
+ spec_data_tmpl['type'] = @spec['type'] if @spec.has_key?('type')
121
+
122
+ target_data = target.send(:data)
123
+
124
+ @spec['data'].each_with_index do |item, index|
125
+ specitem = spec_data_tmpl.merge(item)
126
+
127
+ #path
128
+ unless specitem['path']
129
+ errors.push("data[#{index}]: path missing")
130
+ next
131
+ end
132
+ replaced_path = replace_keywords(specitem['path'])
133
+ path_element = replaced_path.split('/')
134
+ unless path_element.size == 3
135
+ errors.push("data[#{index}]: path is not like SERVICE/SECTION/GRAPH")
136
+ end
137
+ specitem_graph = cache.get(*path_element)
138
+ unless specitem_graph
139
+ errors.push("data[#{index}]: specified graph '#{replaced_path}' not found")
140
+ end
141
+
142
+ #data(sub graph) size
143
+ unless target_data[index]
144
+ errors.push("data[#{index}]: graph member missing")
145
+ next
146
+ end
147
+
148
+ #data graph_id
149
+ if specitem_graph.id.to_i != target_data[index].graph_id.to_i
150
+ errors.push("data[#{index}]: mismatch, spec '#{replaced_path}'(graph id #{specitem_graph.id}) but id '#{target_data[index].graph_id}'")
151
+ end
152
+ #gmode, type
153
+ if specitem.has_key?('gmode') && specitem['gmode'] != target_data[index].gmode
154
+ errors.push("data[#{index}]: gmode mismatch, spec '#{specitem['gmode']}' but '#{target_data[index].gmode}'")
155
+ end
156
+ if specitem.has_key?('type') && specitem['type'] != target_data[index].type
157
+ errors.push("data[#{index}]: type mismatch, spec '#{specitem['type']}' but '#{target_data[index].type}'")
158
+ end
159
+ #stack: stack of first data item is nonsense
160
+ if index > 0 && specitem.has_key?('stack') && specitem['stack'] != target_data[index].stack
161
+ errors.push("data[#{index}]: stack mismatch, spec '#{specitem['stack']}' but '#{target_data[index].stack}'")
162
+ end
163
+ end
164
+
165
+ return errors.empty?, errors
166
+ end
167
+
168
+ def merge_complex(cache, target)
169
+ if target.nil?
170
+ target = GrowthForecast::Complex.new({
171
+ complex: true,
172
+ service_name: @service_name, section_name: @section_name, graph_name: @graph_name,
173
+ description: '',
174
+ })
175
+ end
176
+
177
+ COMPLEX_ATTRIBUTES.each do |attr|
178
+ next unless @spec.has_key?(attr.to_s)
179
+
180
+ val = if attr == :description
181
+ replace_keywords(@spec[attr.to_s])
182
+ else
183
+ @spec[attr.to_s]
184
+ end
185
+ target.send((attr.to_s + '=').to_sym, val)
186
+ end
187
+
188
+ unless @spec.has_key?('data')
189
+ return target
190
+ end
191
+
192
+ # @spec has 'data'
193
+ spec_data_tmpl = {}
194
+ spec_data_tmpl['gmode'] = @spec['gmode'] if @spec.has_key?('gmode')
195
+ spec_data_tmpl['stack'] = @spec['stack'] if @spec.has_key?('stack')
196
+ spec_data_tmpl['type'] = @spec['type'] if @spec.has_key?('type')
197
+
198
+ target.data = target.data.dup
199
+
200
+ @spec['data'].each_with_index do |item, index|
201
+ specitem = spec_data_tmpl.merge(item)
202
+
203
+ #path
204
+ unless specitem['path']
205
+ warn "data[#{index}]: path missing"
206
+ return nil
207
+ end
208
+ replaced_path = replace_keywords(specitem['path'])
209
+ path_element = replaced_path.split('/')
210
+ unless path_element.size == 3
211
+ warn "data[#{index}]: path '#{replaced_path}' is not like SERVICE/SECTION/GRAPH"
212
+ return nil
213
+ end
214
+ specitem_graph = cache.get(*path_element)
215
+ unless specitem_graph
216
+ warn "data[#{index}]: path '#{replaced_path}' not found"
217
+ return nil
218
+ end
219
+
220
+ #data(sub graph) size
221
+ unless target.data[index]
222
+ target.data[index] = GrowthForecast::Complex::Item.new({
223
+ graph_id: specitem_graph.id,
224
+ gmode: (specitem['gmode'] || nil), # nil: default
225
+ stack: (specitem.has_key?('stack') ? specitem['stack'] : nil),
226
+ type: (specitem['type'] || nil),
227
+ })
228
+ next
229
+ end
230
+
231
+ target.data[index].graph_id = specitem_graph.id if specitem_graph.id != target.data[index].graph_id
232
+ target.data[index].gmode = specitem['gmode'] if specitem.has_key?('gmode') && specitem['gmode'] != target.data[index].gmode
233
+ target.data[index].type = specitem['type'] if specitem.has_key?('type') && specitem['type'] != target.data[index].type
234
+ target.data[index].stack = specitem['stack'] if index > 0 && specitem.has_key?('stack') && specitem['stack'] != target.data[index].stack
235
+ end
236
+
237
+ target
238
+ end
239
+ end
@@ -1,3 +1,3 @@
1
1
  class GrowthForecast
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: growthforecast
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,13 +9,14 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-01-03 00:00:00.000000000 Z
12
+ date: 2013-02-04 00:00:00.000000000 Z
13
13
  dependencies: []
14
14
  description: Client library and tool to update values, create/edit/delete graphs of
15
15
  GrowthForecast
16
16
  email:
17
17
  - tagomoris@gmail.com
18
- executables: []
18
+ executables:
19
+ - gfclient
19
20
  extensions: []
20
21
  extra_rdoc_files: []
21
22
  files:
@@ -24,12 +25,19 @@ files:
24
25
  - LICENSE
25
26
  - README.md
26
27
  - Rakefile
28
+ - bin/gfclient
29
+ - example/list
30
+ - example/spec.yaml
27
31
  - example/test1.rb
32
+ - example/testspec.yaml
28
33
  - growthforecast.gemspec
29
34
  - lib/growthforecast.rb
35
+ - lib/growthforecast/cache.rb
36
+ - lib/growthforecast/client.rb
30
37
  - lib/growthforecast/complex.rb
31
38
  - lib/growthforecast/graph.rb
32
39
  - lib/growthforecast/path.rb
40
+ - lib/growthforecast/spec.rb
33
41
  - lib/growthforecast/version.rb
34
42
  homepage: https://github.com/tagomoris/rb-growthforecast
35
43
  licenses: []