growthforecast 0.0.1 → 0.0.2

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.
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: []