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