mayu 0.1.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +5 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +69 -0
- data/LICENSE.txt +21 -0
- data/README.md +206 -0
- data/Rakefile +6 -0
- data/bin/mayu-association-collector +84 -0
- data/config.ru +41 -0
- data/lib/mayu.rb +8 -0
- data/lib/mayu/ap.rb +43 -0
- data/lib/mayu/app.rb +246 -0
- data/lib/mayu/association.rb +34 -0
- data/lib/mayu/cisco_wlc_collector.rb +102 -0
- data/lib/mayu/device.rb +23 -0
- data/lib/mayu/loader.rb +168 -0
- data/lib/mayu/map.rb +42 -0
- data/lib/mayu/periodic_loader.rb +55 -0
- data/lib/mayu/relation.rb +50 -0
- data/lib/mayu/renderer.rb +43 -0
- data/lib/mayu/stores/base.rb +44 -0
- data/lib/mayu/stores/concat.rb +41 -0
- data/lib/mayu/stores/file.rb +25 -0
- data/lib/mayu/stores/memory.rb +21 -0
- data/lib/mayu/stores/s3.rb +40 -0
- data/lib/mayu/user.rb +41 -0
- data/lib/mayu/user_completer.rb +30 -0
- data/lib/mayu/version.rb +3 -0
- data/mayu.gemspec +34 -0
- data/script/console +14 -0
- data/script/setup +8 -0
- metadata +176 -0
data/lib/mayu/loader.rb
ADDED
@@ -0,0 +1,168 @@
|
|
1
|
+
require 'mayu/association'
|
2
|
+
require 'mayu/user'
|
3
|
+
require 'mayu/device'
|
4
|
+
require 'mayu/map'
|
5
|
+
require 'mayu/ap'
|
6
|
+
|
7
|
+
require 'mayu/user_completer'
|
8
|
+
|
9
|
+
module Mayu
|
10
|
+
class Loader
|
11
|
+
def initialize(store:, user_completer: nil)
|
12
|
+
@store = store
|
13
|
+
@cached_user_completer = user_completer
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :store
|
17
|
+
|
18
|
+
def load
|
19
|
+
objects
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
def objects
|
24
|
+
@objects ||= store.get
|
25
|
+
end
|
26
|
+
|
27
|
+
###
|
28
|
+
|
29
|
+
def associations
|
30
|
+
@associations ||= objects.fetch(:associations).map do |_|
|
31
|
+
Association.load(_).tap do |association|
|
32
|
+
association._ap_finder = method(:find_ap)
|
33
|
+
association._user_finder = method(:find_user)
|
34
|
+
association._device_finder = method(:find_device_by_mac)
|
35
|
+
association._ensure_relation_finders_satisfied!
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def association_by_mac
|
41
|
+
@association_by_mac = associations.map do |_|
|
42
|
+
[_.mac, _]
|
43
|
+
end.to_h
|
44
|
+
end
|
45
|
+
|
46
|
+
def association_by_ip
|
47
|
+
@association_by_ip = associations.map do |_|
|
48
|
+
[_.ip, _]
|
49
|
+
end.to_h
|
50
|
+
end
|
51
|
+
|
52
|
+
def associations_by_ap
|
53
|
+
@association_by_ap = associations.group_by do |_|
|
54
|
+
_.ap_key.to_s
|
55
|
+
end.to_h
|
56
|
+
end
|
57
|
+
|
58
|
+
def find_association_by_mac(mac)
|
59
|
+
association_by_mac[mac]
|
60
|
+
end
|
61
|
+
|
62
|
+
def find_association_by_ip(ip)
|
63
|
+
association_by_ip[ip]
|
64
|
+
end
|
65
|
+
|
66
|
+
def find_associations_by_ap(k)
|
67
|
+
associations_by_ap.fetch(k.to_s, [])
|
68
|
+
end
|
69
|
+
|
70
|
+
###
|
71
|
+
|
72
|
+
def users
|
73
|
+
@users ||= objects.fetch(:users).map do |_|
|
74
|
+
User.load(_).tap do |user|
|
75
|
+
user._devices_finder = method(:find_devices_by_user)
|
76
|
+
user._ensure_relation_finders_satisfied!
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def user_by_key
|
82
|
+
@user_by_key ||= users.map do |_|
|
83
|
+
[_.key.to_sym, _]
|
84
|
+
end.to_h
|
85
|
+
end
|
86
|
+
|
87
|
+
def find_user(k)
|
88
|
+
user_by_key[k.to_sym]
|
89
|
+
end
|
90
|
+
|
91
|
+
def user_completer
|
92
|
+
@user_completer ||= @cached_user_completer ? @cached_user_completer.update(users) : UserCompleter.new(users)
|
93
|
+
end
|
94
|
+
|
95
|
+
def suggest_users(query)
|
96
|
+
user_completer.query(query)
|
97
|
+
end
|
98
|
+
|
99
|
+
###
|
100
|
+
|
101
|
+
def devices
|
102
|
+
@devices ||= objects.fetch(:devices).map do |_|
|
103
|
+
Device.load(_).tap do |device|
|
104
|
+
device._user_finder = method(:find_user)
|
105
|
+
device._association_finder = proc { find_association_by_mac(device.mac) }
|
106
|
+
device._ensure_relation_finders_satisfied!
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def device_by_mac
|
112
|
+
@device_by_mac ||= devices.reverse_each.map do |device|
|
113
|
+
[device.mac, device]
|
114
|
+
end.to_h
|
115
|
+
end
|
116
|
+
|
117
|
+
def devices_by_user
|
118
|
+
@devices_by_user ||= devices.reverse_each.group_by do |device|
|
119
|
+
device.user_key
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def find_device_by_mac(k)
|
124
|
+
device_by_mac[k]
|
125
|
+
end
|
126
|
+
|
127
|
+
def find_devices_by_user(k)
|
128
|
+
devices_by_user.fetch(k, [])
|
129
|
+
end
|
130
|
+
|
131
|
+
###
|
132
|
+
|
133
|
+
def maps
|
134
|
+
@maps ||= objects.fetch(:maps).map do |k, v|
|
135
|
+
[k, Map.load(v).tap do |map|
|
136
|
+
map.key = k
|
137
|
+
map._aps_finder = proc { find_aps_by_map(k).values }
|
138
|
+
map._ensure_relation_finders_satisfied!
|
139
|
+
end]
|
140
|
+
end.to_h
|
141
|
+
end
|
142
|
+
|
143
|
+
def find_map(k)
|
144
|
+
maps[k.to_sym]
|
145
|
+
end
|
146
|
+
|
147
|
+
###
|
148
|
+
|
149
|
+
def aps
|
150
|
+
@aps ||= objects.fetch(:aps).map do |k,v|
|
151
|
+
[k, Ap.load(v).tap do |ap|
|
152
|
+
ap.key = k
|
153
|
+
ap._map_finder = method(:find_map)
|
154
|
+
ap._associations_finder = proc { find_associations_by_ap(k) }
|
155
|
+
ap._ensure_relation_finders_satisfied!
|
156
|
+
end]
|
157
|
+
end.to_h
|
158
|
+
end
|
159
|
+
|
160
|
+
def find_ap(k)
|
161
|
+
aps[k.to_sym]
|
162
|
+
end
|
163
|
+
|
164
|
+
def find_aps_by_map(key)
|
165
|
+
aps.select { |k,v| v.map_key.to_s == key.to_s }
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
data/lib/mayu/map.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
module Mayu
|
2
|
+
Map = Struct.new(:key, :name, :url, :fg_color, :bg_color, :highlight_color, keyword_init: true) do
|
3
|
+
include Mayu::Relation
|
4
|
+
|
5
|
+
def self.load(obj)
|
6
|
+
new(**obj)
|
7
|
+
end
|
8
|
+
|
9
|
+
relates :aps
|
10
|
+
|
11
|
+
def associations
|
12
|
+
@associations ||= aps.flat_map(&:associations)
|
13
|
+
end
|
14
|
+
def devices
|
15
|
+
@devices ||= aps.flat_map(&:devices)
|
16
|
+
end
|
17
|
+
def users
|
18
|
+
@users ||= aps.flat_map(&:users)
|
19
|
+
end
|
20
|
+
|
21
|
+
def associations_count
|
22
|
+
associations.size
|
23
|
+
end
|
24
|
+
def devices_count
|
25
|
+
devices.size
|
26
|
+
end
|
27
|
+
def users_count
|
28
|
+
users.size
|
29
|
+
end
|
30
|
+
|
31
|
+
def as_json
|
32
|
+
{
|
33
|
+
key: key,
|
34
|
+
name: name,
|
35
|
+
url: url,
|
36
|
+
fg_color: fg_color,
|
37
|
+
bg_color: bg_color,
|
38
|
+
highlight_color: highlight_color,
|
39
|
+
}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'mayu/loader'
|
2
|
+
require 'thread'
|
3
|
+
|
4
|
+
module Mayu
|
5
|
+
class PeriodicLoader
|
6
|
+
SHUTDOWN_KEY = :mayu_periodic_loader_shutdown
|
7
|
+
def initialize(interval:, **options)
|
8
|
+
@loaded_at = nil
|
9
|
+
@loader = nil
|
10
|
+
@interval = interval
|
11
|
+
@options = options
|
12
|
+
@lock = Mutex.new
|
13
|
+
@thread = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :loaded_at, :loader
|
17
|
+
|
18
|
+
def start
|
19
|
+
return if @thread
|
20
|
+
@lock.synchronize do
|
21
|
+
return if @thread
|
22
|
+
reload
|
23
|
+
@thread = Thread.new(&method(:thread))
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def shutdown
|
28
|
+
@lock.synchronize do
|
29
|
+
if @thread
|
30
|
+
@thread[SHUTDOWN_KEY] = true
|
31
|
+
end
|
32
|
+
@thread = nil
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def reload
|
37
|
+
@loaded_at = Time.now
|
38
|
+
new_loader = Loader.new(user_completer: @loader&.user_completer, **@options).load
|
39
|
+
@loader = new_loader
|
40
|
+
end
|
41
|
+
|
42
|
+
def thread
|
43
|
+
loop do
|
44
|
+
return if @thread[SHUTDOWN_KEY]
|
45
|
+
@lock.synchronize do
|
46
|
+
reload
|
47
|
+
end
|
48
|
+
sleep @interval
|
49
|
+
rescue Exception => e
|
50
|
+
$stderr.puts e.full_message
|
51
|
+
sleep @interval
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Mayu
|
2
|
+
module Relation
|
3
|
+
def self.included(klass)
|
4
|
+
klass.extend ClassMethods
|
5
|
+
end
|
6
|
+
|
7
|
+
def _ensure_relation_finders_satisfied!
|
8
|
+
self.class.relations.map do |k|
|
9
|
+
unless instance_variable_get(:"@_#{k}_finder")
|
10
|
+
raise "#{k.class}: #{k.inspect} not satisfied"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
def relations
|
17
|
+
@_relations ||= []
|
18
|
+
end
|
19
|
+
def relates(name)
|
20
|
+
self.relations << name
|
21
|
+
|
22
|
+
key_exist = self.instance_methods.include?(:"#{name}_key")
|
23
|
+
self.class_eval(<<-EOF, __FILE__, __LINE__ + 1)
|
24
|
+
def _#{name}_finder=(x)
|
25
|
+
@_#{name}_finder = x
|
26
|
+
end
|
27
|
+
|
28
|
+
def _#{name}
|
29
|
+
@_#{name}
|
30
|
+
end
|
31
|
+
|
32
|
+
def _#{name}=(x)
|
33
|
+
key = #{key_exist ? "#{name}_key" : 'nil'}
|
34
|
+
@_#{name}_key = key
|
35
|
+
@_#{name} = x
|
36
|
+
end
|
37
|
+
|
38
|
+
def #{name}
|
39
|
+
raise "#{name} finder not set" unless @_#{name}_finder
|
40
|
+
key = #{key_exist ? "#{name}_key" : 'nil'}
|
41
|
+
if @_#{name}_key && @_#{name}_key != key
|
42
|
+
@_#{name} = nil
|
43
|
+
end
|
44
|
+
self._#{name} ||= @_#{name}_finder[key]
|
45
|
+
end
|
46
|
+
EOF
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Mayu
|
4
|
+
|
5
|
+
class Renderer
|
6
|
+
def initialize(spec)
|
7
|
+
@spec = spec
|
8
|
+
end
|
9
|
+
|
10
|
+
def render(obj, spec: @spec)
|
11
|
+
r = nil
|
12
|
+
case obj
|
13
|
+
when nil
|
14
|
+
return nil
|
15
|
+
when Array
|
16
|
+
return obj.map { |_| render(_, spec: spec) }
|
17
|
+
when Hash
|
18
|
+
r = obj
|
19
|
+
else
|
20
|
+
case
|
21
|
+
when obj.respond_to?(:as_json)
|
22
|
+
r = obj.as_json
|
23
|
+
when !spec.empty?
|
24
|
+
r = {}
|
25
|
+
else
|
26
|
+
return obj
|
27
|
+
end
|
28
|
+
end
|
29
|
+
next_specs, keys = [spec].flatten.partition { |_| Hash === _ }
|
30
|
+
aref = Hash === obj
|
31
|
+
r2 = r.transform_values do |v|
|
32
|
+
render(v, spec: {})
|
33
|
+
end
|
34
|
+
keys.each do |k|
|
35
|
+
r[k] = render(r.fetch(k) { aref ? obj.fetch(k) { obj.send(k) } : obj.send(k) }, spec: {})
|
36
|
+
end
|
37
|
+
(next_specs.inject(&:merge) || {}).each do |k, ns|
|
38
|
+
r[k] = render(r.fetch(k) { aref ? obj.fetch(k) { obj.send(k) } : obj.send(k) }, spec: ns)
|
39
|
+
end
|
40
|
+
r2.merge(r)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'time'
|
2
|
+
module Mayu
|
3
|
+
module Stores
|
4
|
+
class Base
|
5
|
+
def initialize(**options)
|
6
|
+
@options = options
|
7
|
+
end
|
8
|
+
|
9
|
+
def put(obj)
|
10
|
+
raise NotImplementedError
|
11
|
+
end
|
12
|
+
|
13
|
+
def get
|
14
|
+
raise NotImplementedError
|
15
|
+
end
|
16
|
+
|
17
|
+
def for_json(obj)
|
18
|
+
case obj
|
19
|
+
when Array
|
20
|
+
obj.map{ |_| for_json(_) }
|
21
|
+
when Hash
|
22
|
+
obj.transform_values { |v| v.is_a?(Time) ? v.xmlschema : for_json(v) }
|
23
|
+
else
|
24
|
+
obj
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def from_json(obj)
|
29
|
+
case obj
|
30
|
+
when Array
|
31
|
+
obj.map do |v|
|
32
|
+
from_json(v)
|
33
|
+
end
|
34
|
+
when Hash
|
35
|
+
obj.map do |k,v|
|
36
|
+
[k, k.to_s.end_with?('_at') ? (v && Time.xmlschema(v)) : from_json(v)]
|
37
|
+
end.to_h
|
38
|
+
else
|
39
|
+
obj
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'mayu/stores/base'
|
2
|
+
|
3
|
+
module Mayu
|
4
|
+
module Stores
|
5
|
+
class Concat < Base
|
6
|
+
def initialize(stores:)
|
7
|
+
@stores = stores
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :stores
|
11
|
+
|
12
|
+
def put(obj)
|
13
|
+
raise NotImplementedError, "this store is read only"
|
14
|
+
end
|
15
|
+
|
16
|
+
def get
|
17
|
+
objs = @stores.map(&:get)
|
18
|
+
keys = objs.flat_map(&:keys).uniq
|
19
|
+
|
20
|
+
r = {}
|
21
|
+
objs.each do |i|
|
22
|
+
keys.each do |k|
|
23
|
+
next unless i[k]
|
24
|
+
case r[k]
|
25
|
+
when Array
|
26
|
+
r[k] ||= []
|
27
|
+
r[k] += i[k]
|
28
|
+
when Hash
|
29
|
+
r[k] ||= {}
|
30
|
+
r[k] = r[k].merge(i[k])
|
31
|
+
else
|
32
|
+
r[k] = i[k]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
r
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|