focuslight 0.1.1

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.
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