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 +153 -3
- data/bin/gfclient +76 -0
- data/example/list +6 -0
- data/example/spec.yaml +149 -0
- data/example/testspec.yaml +72 -0
- data/lib/growthforecast.rb +25 -6
- data/lib/growthforecast/cache.rb +46 -0
- data/lib/growthforecast/client.rb +142 -0
- data/lib/growthforecast/spec.rb +239 -0
- data/lib/growthforecast/version.rb +1 -1
- metadata +11 -3
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
|
-
|
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
|
data/bin/gfclient
ADDED
@@ -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
|
data/example/spec.yaml
ADDED
@@ -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'
|
data/lib/growthforecast.rb
CHANGED
@@ -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
|
176
|
-
|
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=
|
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
|
-
|
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
|
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.
|
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-
|
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: []
|