focuslight 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.env +13 -0
- data/.gitignore +21 -0
- data/.travis.yml +9 -0
- data/CHANGELOG.md +21 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/Procfile +3 -0
- data/Procfile-gem +3 -0
- data/README.md +162 -0
- data/Rakefile +37 -0
- data/bin/focuslight +7 -0
- data/config.ru +6 -0
- data/focuslight.gemspec +41 -0
- data/lib/focuslight.rb +6 -0
- data/lib/focuslight/cli.rb +56 -0
- data/lib/focuslight/config.rb +27 -0
- data/lib/focuslight/data.rb +258 -0
- data/lib/focuslight/graph.rb +240 -0
- data/lib/focuslight/init.rb +13 -0
- data/lib/focuslight/logger.rb +89 -0
- data/lib/focuslight/rrd.rb +393 -0
- data/lib/focuslight/validator.rb +220 -0
- data/lib/focuslight/version.rb +3 -0
- data/lib/focuslight/web.rb +614 -0
- data/lib/focuslight/worker.rb +97 -0
- data/public/css/bootstrap.min.css +7 -0
- data/public/favicon.ico +0 -0
- data/public/fonts/glyphicons-halflings-regular.eot +0 -0
- data/public/fonts/glyphicons-halflings-regular.svg +229 -0
- data/public/fonts/glyphicons-halflings-regular.ttf +0 -0
- data/public/fonts/glyphicons-halflings-regular.woff +0 -0
- data/public/js/bootstrap.min.js +7 -0
- data/public/js/jquery-1.10.2.min.js +6 -0
- data/public/js/jquery-1.10.2.min.map +0 -0
- data/public/js/jquery.storageapi.min.js +2 -0
- data/public/js/site.js +214 -0
- data/spec/spec_helper.rb +3 -0
- data/spec/syntax_spec.rb +9 -0
- data/spec/validator_predefined_rules_spec.rb +177 -0
- data/spec/validator_result_spec.rb +27 -0
- data/spec/validator_rule_spec.rb +68 -0
- data/spec/validator_spec.rb +121 -0
- data/view/add_complex.erb +143 -0
- data/view/base.erb +200 -0
- data/view/docs.erb +125 -0
- data/view/edit.erb +102 -0
- data/view/edit_complex.erb +158 -0
- data/view/index.erb +19 -0
- data/view/list.erb +22 -0
- data/view/view.erb +42 -0
- data/view/view_graph.erb +16 -0
- 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,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
|