focuslight 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/.env +13 -0
  3. data/.gitignore +21 -0
  4. data/.travis.yml +9 -0
  5. data/CHANGELOG.md +21 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE.txt +22 -0
  8. data/Procfile +3 -0
  9. data/Procfile-gem +3 -0
  10. data/README.md +162 -0
  11. data/Rakefile +37 -0
  12. data/bin/focuslight +7 -0
  13. data/config.ru +6 -0
  14. data/focuslight.gemspec +41 -0
  15. data/lib/focuslight.rb +6 -0
  16. data/lib/focuslight/cli.rb +56 -0
  17. data/lib/focuslight/config.rb +27 -0
  18. data/lib/focuslight/data.rb +258 -0
  19. data/lib/focuslight/graph.rb +240 -0
  20. data/lib/focuslight/init.rb +13 -0
  21. data/lib/focuslight/logger.rb +89 -0
  22. data/lib/focuslight/rrd.rb +393 -0
  23. data/lib/focuslight/validator.rb +220 -0
  24. data/lib/focuslight/version.rb +3 -0
  25. data/lib/focuslight/web.rb +614 -0
  26. data/lib/focuslight/worker.rb +97 -0
  27. data/public/css/bootstrap.min.css +7 -0
  28. data/public/favicon.ico +0 -0
  29. data/public/fonts/glyphicons-halflings-regular.eot +0 -0
  30. data/public/fonts/glyphicons-halflings-regular.svg +229 -0
  31. data/public/fonts/glyphicons-halflings-regular.ttf +0 -0
  32. data/public/fonts/glyphicons-halflings-regular.woff +0 -0
  33. data/public/js/bootstrap.min.js +7 -0
  34. data/public/js/jquery-1.10.2.min.js +6 -0
  35. data/public/js/jquery-1.10.2.min.map +0 -0
  36. data/public/js/jquery.storageapi.min.js +2 -0
  37. data/public/js/site.js +214 -0
  38. data/spec/spec_helper.rb +3 -0
  39. data/spec/syntax_spec.rb +9 -0
  40. data/spec/validator_predefined_rules_spec.rb +177 -0
  41. data/spec/validator_result_spec.rb +27 -0
  42. data/spec/validator_rule_spec.rb +68 -0
  43. data/spec/validator_spec.rb +121 -0
  44. data/view/add_complex.erb +143 -0
  45. data/view/base.erb +200 -0
  46. data/view/docs.erb +125 -0
  47. data/view/edit.erb +102 -0
  48. data/view/edit_complex.erb +158 -0
  49. data/view/index.erb +19 -0
  50. data/view/list.erb +22 -0
  51. data/view/view.erb +42 -0
  52. data/view/view_graph.erb +16 -0
  53. metadata +345 -0
@@ -0,0 +1,220 @@
1
+ require "focuslight"
2
+
3
+ module Focuslight
4
+ module Validator
5
+ # Validator.validate(params, {
6
+ # :request_param_key_name => { # single key, single value
7
+ # :default => default_value,
8
+ # :rule => [
9
+ # Validator.rule(:not_null),
10
+ # Validator.rule(:int_range, 0..10),
11
+ # ],
12
+ # },
13
+ # :array_value_key_name => { # single key, array value
14
+ # :array => true
15
+ # :size => 1..10 # default is unlimited (empty also allowed)
16
+ # # default cannot be used
17
+ # :rule => [ ... ]
18
+ # }
19
+ # # ...
20
+ # [:param1, :param2, :param3] => {
21
+ # # default cannot be used
22
+ # :rule => Validator::Rule.new(->(p1, p2, p3){ ... }, "error_message")
23
+ # },
24
+ # }
25
+ def self.validate(params, spec)
26
+ result = Result.new
27
+ spec.each do |key, specitem|
28
+ if key.is_a?(Array)
29
+ validate_multi_key(result, params, key, specitem)
30
+ elsif specitem[:array]
31
+ validate_array(result, params, key, specitem)
32
+ else
33
+ validate_single(result, params, key, specitem)
34
+ end
35
+ end
36
+ result
37
+ end
38
+
39
+ def self.validate_single(result, params, key_arg, spec)
40
+ key = key_arg.to_sym
41
+
42
+ value = params[key]
43
+ if spec.has_key?(:default) && value.nil?
44
+ value = spec[:default]
45
+ end
46
+ if spec[:excludable] && value.nil?
47
+ result[key] = nil
48
+ return
49
+ end
50
+
51
+ rules = [spec[:rule]].flatten.compact
52
+
53
+ errors = []
54
+ valid = true
55
+ formatted = value
56
+
57
+ rules.each do |rule|
58
+ if rule.check(value)
59
+ formatted = rule.format(value)
60
+ else
61
+ result.error(key, rule.message)
62
+ valid = false
63
+ end
64
+ end
65
+
66
+ if valid
67
+ result[key] = formatted
68
+ end
69
+ end
70
+
71
+ def self.validate_array(result, params, key_arg, spec)
72
+ key = key_arg.to_sym
73
+
74
+ values = params[key]
75
+ if spec.has_key?(:default)
76
+ raise ArgumentError, "array parameter cannot have :default"
77
+ end
78
+ if spec[:excludable] && value.nil?
79
+ result[key] = []
80
+ end
81
+
82
+ if spec.has_key?(:size)
83
+ if (values.nil? || values.size == 0) && !spec[:size].include?(0)
84
+ result.error(key, "not allowed for empty")
85
+ return
86
+ end
87
+ if !spec[:size].include?(values.size)
88
+ result.error(key, "doesn't have values specified: #{spec[:size]}")
89
+ return
90
+ end
91
+ end
92
+
93
+ unless values.is_a?(Array)
94
+ values = [values]
95
+ end
96
+
97
+ rules = [spec[:rule]].flatten.compact
98
+
99
+ error_values = []
100
+ valid = true
101
+ formatted_values = []
102
+
103
+ values.each do |value|
104
+ errors = []
105
+ formatted = nil
106
+ rules.each do |rule|
107
+ if rule.check(value)
108
+ formatted = rule.format(value)
109
+ else
110
+ result.error(key, rule.message)
111
+ valid = false
112
+ end
113
+ end
114
+ error_values += errors
115
+ formatted_values.push(formatted) if formatted
116
+ end
117
+
118
+ if valid
119
+ result[key] = formatted_values
120
+ end
121
+ end
122
+
123
+ def self.validate_multi_key(result, params, keys, spec)
124
+ values = keys.map{|key| params[key.to_sym]}
125
+ if spec.has_key?(:default)
126
+ raise ArgumentError, "multi key validation spec cannot have :default"
127
+ end
128
+
129
+ rules = [spec[:rule]].flatten.compact
130
+ errors = []
131
+
132
+ rules.each do |rule|
133
+ unless rule.check(*values)
134
+ result.error(keys.map{|s| s.to_s}.join(','), rule.message)
135
+ end
136
+ end
137
+ end
138
+
139
+ class Rule
140
+ attr_reader :message
141
+
142
+ def initialize(checker, invalid_message, formatter=nil)
143
+ @checker = checker
144
+ @message = invalid_message
145
+ @formatter = formatter
146
+ end
147
+
148
+ def check(*values)
149
+ @checker.(*values)
150
+ end
151
+
152
+ def format(value)
153
+ if @formatter && @formatter.is_a?(Symbol)
154
+ value.send(@formatter)
155
+ elsif @formatter
156
+ @formatter.(value)
157
+ else
158
+ value
159
+ end
160
+ end
161
+ end
162
+
163
+ def self.rule(type, *args)
164
+ args.flatten!
165
+ case type
166
+ when :not_blank
167
+ Rule.new(->(v){not v.nil? and not v.strip.empty?}, "missing or blank", :strip)
168
+ when :choice
169
+ Rule.new(->(v){args.include?(v)}, "invalid value")
170
+ when :int
171
+ Rule.new(->(v){v =~ /^-?\d+$/}, "invalid integer", :to_i)
172
+ when :uint
173
+ Rule.new(->(v){v =~ /^\d+$/}, "invalid integer (>= 0)", :to_i)
174
+ when :natural
175
+ Rule.new(->(v){v =~ /^\d+$/ && v.to_i >= 1}, "invalid integer (>= 1)", :to_i)
176
+ when :float, :double, :real
177
+ Rule.new(->(v){v =~ /^\-?(\d+\.?\d*|\.\d+)(e[+-]\d+)?$/}, "invalid floating point num", :to_f)
178
+ when :int_range
179
+ Rule.new(->(v){args.first.include?(v.to_i)}, "invalid number in range #{args.first}", :to_i)
180
+ when :bool
181
+ Rule.new(->(v){v =~ /^(0|1|true|false)$/i}, "invalid bool value", ->(v){!!(v =~ /^(1|true)$/i)})
182
+ when :regexp
183
+ Rule.new(->(v){v =~ args.first}, "invalid input for pattern #{args.first.source}")
184
+ when :lambda
185
+ Rule.new(*args)
186
+ else
187
+ raise ArgumentError, "unknown validator rule: #{type}"
188
+ end
189
+ end
190
+
191
+ class Result
192
+ attr_reader :errors
193
+
194
+ def initialize
195
+ @errors = {}
196
+ @params = {}
197
+ end
198
+
199
+ def hash
200
+ @params.dup
201
+ end
202
+
203
+ def [](name)
204
+ @params[name.to_sym]
205
+ end
206
+
207
+ def []=(name, value)
208
+ @params[name.to_sym] = value
209
+ end
210
+
211
+ def error(param, message)
212
+ @errors[param.to_sym] = "#{param}: " + message
213
+ end
214
+
215
+ def has_error?
216
+ not @errors.empty?
217
+ end
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,3 @@
1
+ module Focuslight
2
+ VERSION = "0.1.1"
3
+ end
@@ -0,0 +1,614 @@
1
+ require "focuslight"
2
+ require "focuslight/config"
3
+ require "focuslight/logger"
4
+ require "focuslight/data"
5
+ require "focuslight/rrd"
6
+
7
+ require "focuslight/validator"
8
+
9
+ require "time"
10
+ require "cgi"
11
+
12
+ require "sinatra/base"
13
+ require "sinatra/json"
14
+ require "erubis"
15
+
16
+ class Focuslight::Web < Sinatra::Base
17
+ include Focuslight::Logger
18
+
19
+ set :dump_errors, true
20
+ set :public_folder, File.join(__dir__, '..', '..', 'public')
21
+ set :views, File.join(__dir__, '..', '..', 'view')
22
+ set :erb, escape_html: true
23
+
24
+ ### TODO: both of static method and helper method
25
+ def self.rule(*args)
26
+ Focuslight::Validator.rule(*args)
27
+ end
28
+
29
+ configure do
30
+ datadir = Focuslight::Config.get(:datadir)
31
+ FileUtils.mkdir_p(datadir)
32
+ end
33
+
34
+ helpers Sinatra::JSON
35
+ helpers do
36
+ def url_for(url_fragment, mode=nil, options = nil)
37
+ if mode.is_a? Hash
38
+ options = mode
39
+ mode = nil
40
+ end
41
+
42
+ if mode.nil?
43
+ mode = :path_only
44
+ end
45
+
46
+ mode = mode.to_sym unless mode.is_a? Symbol
47
+ optstring = nil
48
+
49
+ if options.is_a? Hash
50
+ optstring = '?' + options.map { |k,v| "#{k}=#{URI.escape(v.to_s, /[^#{URI::PATTERN::UNRESERVED}]/)}" }.join('&')
51
+ end
52
+
53
+ case mode
54
+ when :path_only
55
+ base = request.script_name
56
+ when :full
57
+ scheme = request.scheme
58
+ if (scheme == 'http' && request.port == 80 ||
59
+ scheme == 'https' && request.port == 443)
60
+ port = ""
61
+ else
62
+ port = ":#{request.port}"
63
+ end
64
+ base = "#{scheme}://#{request.host}#{port}#{request.script_name}"
65
+ else
66
+ raise TypeError, "Unknown url_for mode #{mode.inspect}"
67
+ end
68
+ "#{base}#{url_fragment}#{optstring}"
69
+ end
70
+
71
+ def urlencode(str)
72
+ CGI.escape(str)
73
+ end
74
+
75
+ def validate(*args)
76
+ Focuslight::Validator.validate(*args)
77
+ end
78
+
79
+ def rule(*args)
80
+ Focuslight::Validator.rule(*args)
81
+ end
82
+
83
+ def data
84
+ @data ||= Focuslight::Data.new
85
+ end
86
+
87
+ def number_type_rule
88
+ type = data().number_type
89
+ if type == Float
90
+ Focuslight::Validator.rule(:real)
91
+ elsif type == Integer
92
+ Focuslight::Validator.rule(:int)
93
+ elsif type == Bignum
94
+ Focuslight::Validator.rule(:int)
95
+ else
96
+ raise "unknown number_type #{data().number_type}"
97
+ end
98
+ end
99
+
100
+ def rrd
101
+ @rrd ||= Focuslight::RRD.new
102
+ end
103
+
104
+ # short interval update is always enabled in focuslight
105
+ ## TODO: option to disable?
106
+
107
+ def delete(graph)
108
+ if graph.complex?
109
+ data().remove_complex(graph.id)
110
+ else
111
+ rrd().remove(graph)
112
+ data().remove(graph.id)
113
+ end
114
+ parts = [:service, :section].map{|s| urlencode(graph.send(s))}
115
+ {error: 0, location: url_for("/list/%s/%s" % parts)}
116
+ end
117
+
118
+ def pathinfo(params)
119
+ items = []
120
+ return items unless params[:service_name]
121
+
122
+ items << params[:service_name]
123
+ return items unless params[:section_name]
124
+
125
+ items << params[:section_name]
126
+ return items unless params[:graph_name]
127
+
128
+ items << params[:graph_name]
129
+ return items unless params[:t]
130
+
131
+ items << params[:t]
132
+ items
133
+ end
134
+
135
+ def linkpath(ary, prefix='/list')
136
+ [prefix, ary.map{|v| urlencode(v)}].join('/')
137
+ end
138
+
139
+ def format_number(num)
140
+ # 12345678 => "12,345,678"
141
+ num.to_s.reverse.chars.each_slice(3).map{|slice| slice.reverse.join}.reverse.join(',')
142
+ end
143
+
144
+ def selected?(real, option)
145
+ real == option ? 'selected' : ''
146
+ end
147
+ end
148
+
149
+ module Stash
150
+ def stash
151
+ @stash ||= {}
152
+ end
153
+ end
154
+
155
+ before { request.extend Stash }
156
+
157
+ set(:graph) do |type|
158
+ condition do
159
+ graph = case type
160
+ when :simple
161
+ if params[:graph_id]
162
+ data().get_by_id(params[:graph_id].to_i)
163
+ else
164
+ data().get(params[:service_name], params[:section_name], params[:graph_name])
165
+ end
166
+ when :complex
167
+ if params[:complex_id]
168
+ data().get_complex_by_id(params[:complex_id].to_i)
169
+ else
170
+ data().get_complex(params[:service_name], params[:section_name], params[:graph_name])
171
+ end
172
+ else
173
+ raise "graph type is invalid: #{type}"
174
+ end
175
+ halt 404 unless graph
176
+ request.stash[:graph] = graph
177
+ end
178
+ end
179
+
180
+ get '/docs' do
181
+ erb :docs, layout: :base, locals: { pathinfo: [nil, nil, nil, nil, :docs] }
182
+ end
183
+
184
+ get '/' do
185
+ services = []
186
+ data().get_services.each do |service|
187
+ services << {:name => service, :sections => data().get_sections(service)}
188
+ end
189
+ erb :index, layout: :base, locals: { pathinfo: pathinfo(params), services: services }
190
+ end
191
+
192
+ get '/list/:service_name' do
193
+ services = []
194
+ sections = data().get_sections(params[:service_name])
195
+ services << { name: params[:service_name], sections: sections }
196
+ erb :index, layout: :base, :locals => { pathinfo: pathinfo(params), services: services }
197
+ end
198
+
199
+ not_specified_or_not_whitespece = {
200
+ rule: rule(:lambda, ->(v){ v.nil? || !v.strip.empty? }, "invalid name(whitespace only)", ->(v){ v && v.strip })
201
+ }
202
+ graph_view_spec = {
203
+ service_name: not_specified_or_not_whitespece,
204
+ section_name: not_specified_or_not_whitespece,
205
+ graph_name: not_specified_or_not_whitespece,
206
+ t: { default: 'd', rule: rule(:choice, 'd', 'h', 'm', 'sh', 'sd') }
207
+ }
208
+
209
+ get '/list/:service_name/:section_name' do
210
+ req_params = validate(params, graph_view_spec)
211
+ graphs = data().get_graphs(req_params[:service_name], req_params[:section_name])
212
+ pi = pathinfo(req_params.hash)
213
+ erb :list, layout: :base, locals: { pathinfo: pi, params: req_params.hash, graphs: graphs }
214
+ end
215
+
216
+ get '/view_graph/:service_name/:section_name/:graph_name', :graph => :simple do
217
+ req_params = validate(params, graph_view_spec)
218
+ pi = pathinfo(req_params.hash)
219
+ erb :view_graph, layout: :base, locals: { pathinfo: pi, params: req_params.hash, graphs: [ request.stash[:graph] ], view_complex: false }
220
+ end
221
+
222
+ get '/view_complex/:service_name/:section_name/:graph_name', :graph => :complex do
223
+ req_params = validate(params, graph_view_spec)
224
+ pi = pathinfo(req_params.hash)
225
+ erb :view_graph, layout: :base, locals: { pathinfo: pi, params: req_params.hash, graphs: [ request.stash[:graph] ], view_complex: true }
226
+ end
227
+
228
+ get '/edit/:service_name/:section_name/:graph_name', :graph => :simple do
229
+ erb :edit, layout: :base, locals: { pathinfo: [nil,nil,nil,nil,:edit], graph: request.stash[:graph] }
230
+ end
231
+
232
+ post '/edit/:service_name/:section_name/:graph_name', :graph => :simple do
233
+ edit_graph_spec = {
234
+ service_name: { rule: rule(:not_blank) },
235
+ section_name: { rule: rule(:not_blank) },
236
+ graph_name: { rule: rule(:not_blank) },
237
+ description: { default: '' },
238
+ sort: { rule: [ rule(:not_blank), rule(:int_range, 0..19) ] },
239
+ adjust: { default: '*', rule: [ rule(:not_blank), rule(:choice, '*', '/') ] },
240
+ adjustval: { default: '1', rule: [ rule(:not_blank), rule(:natural) ] },
241
+ unit: { default: '' },
242
+ color: { rule: [ rule(:not_blank), rule(:regexp, /^#[0-9a-f]{6}$/i) ] },
243
+ type: { rule: [ rule(:not_blank), rule(:choice, 'AREA', 'LINE1', 'LINE2') ] },
244
+ llimit: { rule: [ rule(:not_blank), number_type_rule() ] },
245
+ ulimit: { rule: [ rule(:not_blank), number_type_rule() ] },
246
+ }
247
+ req_params = validate(params, edit_graph_spec)
248
+
249
+ if req_params.has_error?
250
+ json({error: 1, messages: req_params.errors})
251
+ else
252
+ data().update_graph(request.stash[:graph].id, req_params.hash)
253
+ edit_path = "/view_graph/%s/%s/%s" % [:service_name,:section_name,:graph_name].map{|s| urlencode(req_params[s])}
254
+ json({error: 0, location: url_for(edit_path)})
255
+ end
256
+ end
257
+
258
+ post '/delete/:service_name/:section_name' do
259
+ graphs = data().get_graphs(params[:service_name], params[:section_name])
260
+ graphs.each do |graph|
261
+ if graph.complex?
262
+ data().remove_complex(graph.id)
263
+ else
264
+ data().remove(graph.id)
265
+ rrd().remove(graph)
266
+ end
267
+ end
268
+ service_path = "/list/%s" % [ urlencode(params[:service_name]) ]
269
+ json({ error: 0, location: url_for(service_path) })
270
+ end
271
+
272
+ post '/delete/:service_name/:section_name/:graph_name', :graph => :simple do
273
+ delete(request.stash[:graph]).to_json
274
+ end
275
+
276
+ get '/add_complex' do
277
+ graphs = data().get_all_graph_name
278
+ erb :add_complex, layout: :base, locals: { pathinfo: [nil, nil, nil, nil, :add_complex], params: params, graphs: graphs }
279
+ end
280
+
281
+ complex_graph_request_spec_generator = ->(type2s_num){
282
+ {
283
+ service_name: { rule: rule(:not_blank) },
284
+ section_name: { rule: rule(:not_blank) },
285
+ graph_name: { rule: rule(:not_blank) },
286
+ description: { default: '' },
287
+ sumup: { rule: [ rule(:not_blank), rule(:int_range, 0..1) ] },
288
+ sort: { rule: [ rule(:not_blank), rule(:int_range, 0..19) ] },
289
+ 'type-1'.to_sym => { rule: [ rule(:not_blank), rule(:choice, 'AREA', 'LINE1', 'LINE2') ] },
290
+ 'path-1'.to_sym => { rule: [ rule(:not_blank), rule(:natural) ] },
291
+ 'type-2'.to_sym => {
292
+ array: true, size: (type2s_num..type2s_num),
293
+ rule: [ rule(:not_blank), rule(:choice, 'AREA', 'LINE1', 'LINE2') ],
294
+ },
295
+ 'path-2'.to_sym => {
296
+ array: true, size: (type2s_num..type2s_num),
297
+ rule: [ rule(:not_blank), rule(:natural) ],
298
+ },
299
+ 'stack-2'.to_sym => {
300
+ array: true, size: (type2s_num..type2s_num),
301
+ rule: [ rule(:not_blank), rule(:bool) ],
302
+ },
303
+ }
304
+ }
305
+
306
+ post '/add_complex' do
307
+ type2s = params['type-2'.to_sym]
308
+ type2s_num = type2s && (! type2s.empty?) ? type2s.size : 1
309
+
310
+ specs = complex_graph_request_spec_generator.(type2s_num)
311
+ additional = {
312
+ [:service_name, :section_name, :graph_name] => {
313
+ rule: rule(:lambda, ->(service,section,graph){ data().get_complex(service,section,graph).nil? }, "duplicate graph path")
314
+ },
315
+ }
316
+ specs.update(additional)
317
+ req_params = validate(params, specs)
318
+
319
+ if req_params.has_error?
320
+ json({error: 1, messages: req_params.errors})
321
+ else
322
+ data().create_complex(req_params[:service_name], req_params[:section_name], req_params[:graph_name], req_params.hash)
323
+ created_path = "/list/%s/%s" % [:service_name,:section_name].map{|s| urlencode(req_params[s])}
324
+ json({error: 0, location: url_for(created_path)})
325
+ end
326
+ end
327
+
328
+ get '/edit_complex/:complex_id', :graph => :complex do
329
+ graphs = data().get_all_graph_name
330
+ graph_dic = Hash[ graphs.map{|g| [g[:id], g]} ]
331
+ erb :edit_complex, layout: :base, locals: { pathinfo: [nil, nil, nil, nil, :edit_complex], complex: request.stash[:graph], graphs: graphs, dic: graph_dic }
332
+ end
333
+
334
+ post '/edit_complex/:complex_id', :graph => :complex do
335
+ type2s = params['type-2'.to_sym]
336
+ type2s_num = type2s && (! type2s.empty?) ? type2s.size : 1
337
+
338
+ specs = complex_graph_request_spec_generator.(type2s_num)
339
+ current_graph_id = request.stash[:graph].id
340
+ additional = {
341
+ [:service_name, :section_name, :graph_name] => {
342
+ rule: rule(:lambda, ->(service,section,graph){
343
+ graph = data().get_complex(service,section,graph)
344
+ graph.nil? || graph.id == current_graph_id
345
+ }, "graph path must be unique")
346
+ },
347
+ }
348
+ specs.update(additional)
349
+ req_params = validate(params, specs)
350
+
351
+ if req_params.has_error?
352
+ json({error: 1, messages: req_params.errors})
353
+ else
354
+ data().update_complex(request.stash[:graph].id, req_params.hash)
355
+ created_path = "/list/%s/%s" % [:service_name,:section_name].map{|s| urlencode(req_params[s])}
356
+ json({error: 0, location: url_for(created_path)})
357
+ end
358
+ end
359
+
360
+ post '/delete_complex/:complex_id', :graph => :complex do
361
+ delete(request.stash[:graph]).to_json
362
+ end
363
+
364
+ graph_rendering_request_spec = {
365
+ service_name: not_specified_or_not_whitespece,
366
+ section_name: not_specified_or_not_whitespece,
367
+ graph_name: not_specified_or_not_whitespece,
368
+ complex: not_specified_or_not_whitespece,
369
+ t: { default: 'd', rule: rule(:choice, 'd', 'h', 'm', 'sh', 'sd') },
370
+ from: {
371
+ default: (Time.now - 86400*8).strftime('%Y/%m/%d %T'),
372
+ rule: rule(:lambda, ->(v){ Time.parse(v) rescue false }, "invalid time format"),
373
+ },
374
+ to: {
375
+ default: Time.now.strftime('%Y/%m/%d %T'),
376
+ rule: rule(:lambda, ->(v){ Time.parse(v) rescue false }, "invalid time format"),
377
+ },
378
+ width: { default: '390', rule: rule(:natural) },
379
+ height: { default: '110', rule: rule(:natural) },
380
+ graphonly: { default: 'false', rule: rule(:bool) },
381
+ logarithmic: { default: 'false', rule: rule(:bool) },
382
+ background_color: { default: 'f3f3f3', rule: rule(:regexp, /^[0-9a-f]{6}([0-9a-f]{2})?$/i) },
383
+ canvas_color: { default: 'ffffff', rule: rule(:regexp, /^[0-9a-f]{6}([0-9a-f]{2})?$/i) },
384
+ font_color: { default: '000000', rule: rule(:regexp, /^[0-9a-f]{6}([0-9a-f]{2})?$/i) },
385
+ frame_color: { default: '000000', rule: rule(:regexp, /^[0-9a-f]{6}([0-9a-f]{2})?$/i) },
386
+ axis_color: { default: '000000', rule: rule(:regexp, /^[0-9a-f]{6}([0-9a-f]{2})?$/i) },
387
+ shadea_color: { default: 'cfcfcf', rule: rule(:regexp, /^[0-9a-f]{6}([0-9a-f]{2})?$/i) },
388
+ shadeb_color: { default: '9e9e9e', rule: rule(:regexp, /^[0-9a-f]{6}([0-9a-f]{2})?$/i) },
389
+ border: { default: '3', rule: rule(:uint) },
390
+ legend: { default: 'true', rule: rule(:bool) },
391
+ notitle: { default: 'false', rule: rule(:bool) },
392
+ xgrid: { default: '' },
393
+ ygrid: { default: '' },
394
+ upper_limit: { default: '' },
395
+ lower_limit: { default: '' },
396
+ rigid: { default: 'false', rule: rule(:bool) },
397
+ sumup: { default: 'false', rule: rule(:bool) },
398
+ step: { excludable: true, rule: rule(:uint) },
399
+ cf: { default: 'AVERAGE', rule: rule(:choice, 'AVERAGE', 'MAX') }
400
+ }
401
+
402
+ get '/complex/graph/:service_name/:section_name/:graph_name', :graph => :complex do
403
+ req_params = validate(params, graph_rendering_request_spec)
404
+
405
+ data = []
406
+ request.stash[:graph].data_rows.each do |row|
407
+ g = data().get_by_id(row[:graph_id])
408
+ g.c_type = row[:type]
409
+ g.stack = row[:stack]
410
+ data << g
411
+ end
412
+
413
+ graph_img = rrd().graph(data, req_params.hash)
414
+ [200, {'Content-Type' => 'image/png'}, graph_img]
415
+ end
416
+
417
+ get '/complex/xport/:service_name/:section_name/:graph_name', :graph => :complex do
418
+ req_params = validate(params, graph_rendering_request_spec)
419
+
420
+ data = []
421
+ request.stash[:graph].data_rows.each do |row|
422
+ g = data().get_by_id(row[:graph_id])
423
+ g.c_type = row[:type]
424
+ g.stack = row[:stack]
425
+ data << g
426
+ end
427
+
428
+ json(rrd().export(data, req_params.hash))
429
+ end
430
+
431
+ get '/graph/:service_name/:section_name/:graph_name', :graph => :simple do
432
+ req_params = validate(params, graph_rendering_request_spec)
433
+ graph_img = rrd().graph(request.stash[:graph], req_params.hash)
434
+ [200, {'Content-Type' => 'image/png'}, graph_img]
435
+ end
436
+
437
+ get '/xport/:service_name/:section_name/:graph_name', :graph => :simple do
438
+ req_params = validate(params, graph_rendering_request_spec)
439
+ json(rrd().export(request.stash[:graph], req_params.hash))
440
+ end
441
+
442
+ get '/graph/:complex' do
443
+ req_params = validate(params, graph_rendering_request_spec)
444
+
445
+ data = []
446
+ req_params[:complex].split(':').each_slice(4).each do |type, id, stack|
447
+ g = data().get_by_id(id)
448
+ next unless g
449
+ g.c_type = type
450
+ g.stack = !!(stack =~ /^(1|true)$/i)
451
+ data << g
452
+ end
453
+ graph_img = rrd().graph(data, req_params.hash)
454
+ [200, {'Content-Type' => 'image/png'}, graph_img]
455
+ end
456
+
457
+ get '/xport/:complex' do
458
+ req_params = validate(params, graph_rendering_request_spec)
459
+
460
+ data = []
461
+ req_params[:complex].split(':').each_slice(4).each do |type, id, stack|
462
+ g = data().get_by_id(id)
463
+ next unless g
464
+ g.c_type = type
465
+ g.stack = !!(stack =~ /^(1|true)$/i)
466
+ data << g
467
+ end
468
+
469
+ json(rrd().export(data, req_params.hash))
470
+ end
471
+
472
+ get '/api/:service_name/:section_name/:graph_name', :graph => :simple do
473
+ json(request.stash[:graph].to_hash)
474
+ end
475
+
476
+ post '/api/:service_name/:section_name/:graph_name' do
477
+ api_graph_post_spec = {
478
+ service_name: { rule: rule(:not_blank) },
479
+ section_name: { rule: rule(:not_blank) },
480
+ graph_name: { rule: rule(:not_blank) },
481
+ number: { rule: [ rule(:not_blank), number_type_rule() ] },
482
+ mode: { default: 'gauge', rule: rule(:choice, 'count', 'gauge', 'modified', 'derive') },
483
+ color: { default: '', rule: rule(:regexp, /^(|#[0-9a-f]{6})$/i) },
484
+ description: { default: '' },
485
+ }
486
+ req_params = validate(params, api_graph_post_spec)
487
+
488
+ if req_params.has_error?
489
+ halt json({ error: 1, messages: req_params.errors })
490
+ end
491
+
492
+ graph = nil
493
+ graph = data().update(
494
+ req_params[:service_name], req_params[:section_name], req_params[:graph_name],
495
+ req_params[:number], req_params[:mode], req_params[:color]
496
+ )
497
+ unless req_params[:description].empty?
498
+ data().update_graph_description(graph.id, req_params[:description])
499
+ end
500
+ json({ error: 0, data: graph.to_hash })
501
+ end
502
+
503
+ # graph4json => Focuslight::Graph#to_hash
504
+ # graph4internal => Focuslight::Graph.hash2request(hash)
505
+
506
+ # alias to /api/:service_name/:section_name/:graph_name
507
+ get '/json/graph/:service_name/:section_name/:graph_name', :graph => :simple do
508
+ json(request.stash[:graph].to_hash)
509
+ end
510
+
511
+ get '/json/complex/:service_name/:section_name/:graph_name', :graph => :complex do
512
+ json(request.stash[:graph].to_hash)
513
+ end
514
+
515
+ # alias to /delete/:service_name/:section_name/:graph_name
516
+ post '/json/delete/graph/:service_name/:section_name/:graph_name', :graph => :simple do
517
+ delete(request.stash[:graph]).to_json
518
+ end
519
+
520
+ post '/json/delete/graph/:graph_id', :graph => :simple do
521
+ delete(request.stash[:graph]).to_json
522
+ end
523
+
524
+ post '/json/delete/complex/:service_name/:section_name/:graph_name', :graph => :complex do
525
+ delete(request.stash[:graph]).to_json
526
+ end
527
+
528
+ post '/json/delete/complex/:complex_id', :graph => :complex do
529
+ delete(request.stash[:graph]).to_json
530
+ end
531
+
532
+ get '/json/graph/:graph_id', :graph => :simple do
533
+ json(request.stash[:graph].to_hash)
534
+ end
535
+
536
+ get '/json/complex/:complex_id', :graph => :complex do
537
+ json(request.stash[:graph].to_hash)
538
+ end
539
+
540
+ get '/json/list/graph' do
541
+ json(data().get_all_graph_name()) #TODO return type?
542
+ end
543
+
544
+ get '/json/list/complex' do
545
+ json(data().get_all_complex_graph_name()) #TODO return type?
546
+ end
547
+
548
+ get '/json/list/all' do
549
+ json( (data().get_all_graph_all() + data().get_all_complex_graph_all()).map(&:to_hash) )
550
+ end
551
+
552
+ # TODO in create/edit, validations about json object properties, sub graph id existense, ....
553
+ post '/json/create/complex' do
554
+ spec = JSON.parse(request.body.read || '{}', symbolize_names: true)
555
+
556
+ exists_simple = data().get(spec[:service_name], spec[:section_name], spec[:graph_name])
557
+ exists_complex = data().get_complex(spec[:service_name], spec[:section_name], spec[:graph_name])
558
+ if exists_simple || exists_complex
559
+ halt 409, "Invalid target: graph path already exists: #{spec[:service_name]}/#{spec[:section_name]}/#{spec[:graph_name]}"
560
+ end
561
+
562
+ if spec[:data].nil? || spec[:data].size < 2
563
+ halt 400, "Invalid argument: data (sub graph list (size >= 2)) required"
564
+ end
565
+
566
+ spec[:complex] = true
567
+ spec[:description] ||= ''
568
+ spec[:sumup] ||= false
569
+ spec[:sort] ||= 19
570
+
571
+ spec[:data].each do |data|
572
+ data[:type] ||= 'AREA'
573
+ data[:stack] = true unless data.has_key?(:stack)
574
+ end
575
+
576
+ internal = Focuslight::Graph.hash2request(spec)
577
+ data().create_complex(spec[:service_name], spec[:section_name], spec[:graph_name], internal)
578
+ section_path = "/list/%s/%s" % [:service_name,:section_name].map{|s| urlencode(spec[s])}
579
+ json({ error: 0, location: url_for(section_path) })
580
+ end
581
+
582
+ # post '/json/edit/{type:(?:graph|complex)}/:id' => sub {
583
+ post '/json/edit/:type/:id' do
584
+ graph = case params[:type]
585
+ when 'graph'
586
+ data().get_by_id( params[:id] )
587
+ when 'complex'
588
+ data().get_complex_by_id( params[:id] )
589
+ else
590
+ nil
591
+ end
592
+ unless graph
593
+ halt 404
594
+ end
595
+
596
+ spec = JSON.parse(request.body.read || '{}', symbolize_names: true)
597
+ id = spec.delete(:id) || graph.id
598
+
599
+ if spec.has_key?(:data)
600
+ spec[:data].each do |data|
601
+ data[:type] ||= 'AREA'
602
+ data[:stack] = true unless data.has_key?(:stack)
603
+ end
604
+ end
605
+
606
+ internal = Focuslight::Graph.hash2request(spec)
607
+ if graph.complex?
608
+ data().update_complex(graph.id, internal)
609
+ else
610
+ data().update_graph(graph.id, internal)
611
+ end
612
+ json({ error: 0 })
613
+ end
614
+ end