wifidiag 0.1.0
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.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +52 -0
- data/LICENSE.txt +21 -0
- data/README.md +45 -0
- data/Rakefile +6 -0
- data/app/public/data.svg +10 -0
- data/app/public/index.css +57 -0
- data/app/public/index.js +140 -0
- data/app/public/tiny.svg +1 -0
- data/app/views/index.erb +42 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/config.ru +19 -0
- data/lib/wifidiag.rb +9 -0
- data/lib/wifidiag/adapters/base.rb +12 -0
- data/lib/wifidiag/adapters/cisco_wlc.rb +51 -0
- data/lib/wifidiag/ap_data.rb +21 -0
- data/lib/wifidiag/app.rb +123 -0
- data/lib/wifidiag/client_data.rb +24 -0
- data/lib/wifidiag/collector.rb +58 -0
- data/lib/wifidiag/config.rb +29 -0
- data/lib/wifidiag/report.rb +19 -0
- data/lib/wifidiag/reporters/base.rb +9 -0
- data/lib/wifidiag/reporters/slack.rb +68 -0
- data/lib/wifidiag/version.rb +3 -0
- data/wifidiag.gemspec +29 -0
- metadata +143 -0
data/config.ru
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
require 'wifidiag'
|
3
|
+
|
4
|
+
config = Wifidiag::Config.new(
|
5
|
+
adapter: Wifidiag::Adapters::CiscoWlc.new(
|
6
|
+
host: ENV.fetch('WLC_HOST'),
|
7
|
+
community: ENV.fetch('WLC_COMMUNITY'),
|
8
|
+
),
|
9
|
+
reporters: [
|
10
|
+
ENV['WIFIDIAG_SLACK_WEBHOOK_URL'] ? Wifidiag::Reporters::Slack.new(
|
11
|
+
webhook_url: ENV['WIFIDIAG_SLACK_WEBHOOK_URL'],
|
12
|
+
) : nil,
|
13
|
+
].compact,
|
14
|
+
dummy_ip: ENV['WIFIDIAG_DUMMY_IP'],
|
15
|
+
)
|
16
|
+
|
17
|
+
config.collector.start_periodic_update(60)
|
18
|
+
|
19
|
+
run Wifidiag.app(config)
|
data/lib/wifidiag.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'wlc_snmp'
|
2
|
+
|
3
|
+
require 'wifidiag/adapters/base'
|
4
|
+
|
5
|
+
require 'wifidiag/ap_data'
|
6
|
+
require 'wifidiag/client_data'
|
7
|
+
|
8
|
+
module Wifidiag
|
9
|
+
module Adapters
|
10
|
+
class CiscoWlc
|
11
|
+
def initialize(host:, port: 161, community:)
|
12
|
+
@host = host
|
13
|
+
@port = port
|
14
|
+
@community = community
|
15
|
+
end
|
16
|
+
|
17
|
+
def collect
|
18
|
+
aps = {}
|
19
|
+
wlc.clients.map do |client|
|
20
|
+
if client.ap
|
21
|
+
ap = aps.fetch(client.ap_mac) do
|
22
|
+
aps[client.ap_mac] = ApData.new(
|
23
|
+
name: client.ap.name,
|
24
|
+
mac_address: client.ap.mac_address,
|
25
|
+
location: client.ap.location,
|
26
|
+
model: client.ap.model,
|
27
|
+
)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
ClientData.new(
|
31
|
+
mac_address: client.mac_address,
|
32
|
+
ip_address: client.ip_address,
|
33
|
+
ap: ap,
|
34
|
+
wlan_profile: client.wlan_profile,
|
35
|
+
protocol: client.protocol,
|
36
|
+
ap_mac: client.ap_mac,
|
37
|
+
uptime: client.uptime,
|
38
|
+
current_rate: client.current_rate,
|
39
|
+
supported_data_rates: client.supported_data_rates,
|
40
|
+
user: client.user,
|
41
|
+
ssid: client.ssid,
|
42
|
+
)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def wlc
|
47
|
+
@snmp ||= WlcSnmp::Client.new(host: @host, port: @port, community: @community)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Wifidiag
|
2
|
+
class ApData
|
3
|
+
def initialize(name: , mac_address: nil, **kwargs)
|
4
|
+
@name = name
|
5
|
+
@mac_address = mac_address
|
6
|
+
@additional_data = kwargs
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :name, :mac_address
|
10
|
+
attr_reader :additional_data
|
11
|
+
|
12
|
+
|
13
|
+
def to_h
|
14
|
+
{
|
15
|
+
name: name,
|
16
|
+
mac_address: mac_address,
|
17
|
+
additional_data: additional_data,
|
18
|
+
}
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/wifidiag/app.rb
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
require 'wifidiag/report'
|
5
|
+
|
6
|
+
module Wifidiag
|
7
|
+
def self.app(*args)
|
8
|
+
App.rack(*args)
|
9
|
+
end
|
10
|
+
|
11
|
+
class App < Sinatra::Base
|
12
|
+
class Boom < StandardError; end
|
13
|
+
|
14
|
+
CONTEXT_RACK_ENV_NAME = 'wifidiag.ctx'
|
15
|
+
|
16
|
+
set :root, File.expand_path(File.join(__dir__, '..', '..', 'app'))
|
17
|
+
|
18
|
+
def self.initialize_context(config)
|
19
|
+
{
|
20
|
+
config: config,
|
21
|
+
revision: self.revision(),
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.revision
|
26
|
+
path = File.join(__dir__, '..', '..', 'REVISION')
|
27
|
+
if File.exist?(path)
|
28
|
+
File.read(path).chomp
|
29
|
+
else
|
30
|
+
nil
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.rack(config={})
|
35
|
+
klass = App
|
36
|
+
|
37
|
+
context = initialize_context(config)
|
38
|
+
app = lambda { |env|
|
39
|
+
env[CONTEXT_RACK_ENV_NAME] = context
|
40
|
+
klass.call(env)
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
helpers do
|
45
|
+
def context
|
46
|
+
request.env[CONTEXT_RACK_ENV_NAME]
|
47
|
+
end
|
48
|
+
|
49
|
+
def conf
|
50
|
+
context[:config]
|
51
|
+
end
|
52
|
+
|
53
|
+
def collector
|
54
|
+
conf.collector
|
55
|
+
end
|
56
|
+
|
57
|
+
def revision
|
58
|
+
context[:revision]
|
59
|
+
end
|
60
|
+
|
61
|
+
TRUSTED_IPS = /\A127\.0\.0\.1\Z|\A(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\.|\A::1\Z|\Afd[0-9a-f]{2}:.+|\Alocalhost\Z|\Aunix\Z|\Aunix:/i
|
62
|
+
def client_ip
|
63
|
+
return conf[:dummy_ip] if conf[:dummy_ip]
|
64
|
+
@client_ip ||= begin
|
65
|
+
remote_addrs = request.get_header('REMOTE_ADDR')&.split(/,\s*/)
|
66
|
+
filtered_remote_addrs = remote_addrs.grep_v(TRUSTED_IPS)
|
67
|
+
|
68
|
+
if filtered_remote_addrs.empty? && request.get_header('HTTP_X_FORWARDED_FOR')
|
69
|
+
forwarded_ips = request.get_header('HTTP_X_FORWARDED_FOR')&.split(/,\s*/)
|
70
|
+
filtered_forwarded_ips = forwarded_ips.grep_v(TRUSTED_IPS)
|
71
|
+
|
72
|
+
filtered_forwarded_ips.empty? ? forwarded_ips.first : remote_addrs.first
|
73
|
+
else
|
74
|
+
filtered_remote_addrs.first || remote_addrs.first
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def data
|
80
|
+
begin
|
81
|
+
@data = JSON.parse(request.body.tap(&:rewind).read)
|
82
|
+
rescue JSON::ParserError
|
83
|
+
halt 400, '{"error": "invalid_payload"}'
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
|
89
|
+
configure do
|
90
|
+
enable :logging
|
91
|
+
end
|
92
|
+
|
93
|
+
get '/' do
|
94
|
+
@client = conf.collector.client_data_for_ip_address(client_ip)
|
95
|
+
erb :index
|
96
|
+
end
|
97
|
+
|
98
|
+
get '/api/self' do
|
99
|
+
content_type :json
|
100
|
+
data = conf.collector.client_data_for_ip_address(client_ip)
|
101
|
+
if data
|
102
|
+
data.to_h.to_json
|
103
|
+
else
|
104
|
+
halt 404, {error: :not_found, ip: client_ip}.to_json
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
post '/api/report' do
|
109
|
+
content_type :json
|
110
|
+
report = Report.new(
|
111
|
+
client_ip,
|
112
|
+
conf.collector.client_data_for_ip_address(client_ip),
|
113
|
+
data['data'] || {},
|
114
|
+
)
|
115
|
+
|
116
|
+
conf[:reporters].each do |x|
|
117
|
+
x.report! report
|
118
|
+
end
|
119
|
+
|
120
|
+
'{"status": "ok"}'
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Wifidiag
|
2
|
+
class ClientData
|
3
|
+
def initialize(ip_address: , mac_address: nil, ssid: nil, ap: nil, **kwargs)
|
4
|
+
@ip_address = ip_address
|
5
|
+
@mac_address = mac_address
|
6
|
+
@ssid = ssid
|
7
|
+
@additional_data = kwargs
|
8
|
+
@ap = ap
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :ip_address, :mac_address, :ssid, :ap
|
12
|
+
attr_reader :additional_data
|
13
|
+
|
14
|
+
def to_h
|
15
|
+
{
|
16
|
+
ip_address: ip_address,
|
17
|
+
mac_address: mac_address,
|
18
|
+
ssid: ssid,
|
19
|
+
ap: ap.to_h,
|
20
|
+
additional_data: additional_data,
|
21
|
+
}
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
module Wifidiag
|
4
|
+
# Collect client and AP information from +an adapter+
|
5
|
+
class Collector
|
6
|
+
def initialize(adapter)
|
7
|
+
@lock = Mutex.new
|
8
|
+
|
9
|
+
@adapter = adapter
|
10
|
+
|
11
|
+
@clients = nil
|
12
|
+
@clients_by_ip_address = nil
|
13
|
+
@clients_by_mac_address = nil
|
14
|
+
@last_update = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :adapter, :last_update
|
18
|
+
|
19
|
+
def start_periodic_update(interval) # XXX:
|
20
|
+
self.collect
|
21
|
+
|
22
|
+
Thread.new do
|
23
|
+
loop do
|
24
|
+
begin
|
25
|
+
self.collect
|
26
|
+
sleep interval
|
27
|
+
rescue Exception => e
|
28
|
+
$stderr.puts "Periodic update error: #{e.inspect}"
|
29
|
+
e.backtrace.each do |x|
|
30
|
+
$stderr.puts "\t#{x}"
|
31
|
+
end
|
32
|
+
sleep interval
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def collect
|
39
|
+
@lock.synchronize do
|
40
|
+
clients = adapter.collect()
|
41
|
+
clients_by_ip_address = clients.map { |_| [_.ip_address, _] }.to_h
|
42
|
+
clients_by_mac_address = clients.map { |_| [_.mac_address, _] }.to_h
|
43
|
+
@clients = clients
|
44
|
+
@clients_by_ip_address = clients_by_ip_address
|
45
|
+
@clients_by_mac_address = clients_by_mac_address
|
46
|
+
@last_update = Time.now
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def client_data_for_ip_address(address)
|
51
|
+
@clients_by_ip_address[address]
|
52
|
+
end
|
53
|
+
|
54
|
+
def client_data_for_mac_address(address)
|
55
|
+
@clients_by_mac_address[address]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'wifidiag/collector'
|
2
|
+
|
3
|
+
module Wifidiag
|
4
|
+
class Config
|
5
|
+
def initialize(hash)
|
6
|
+
@hash = hash
|
7
|
+
end
|
8
|
+
|
9
|
+
def [](k)
|
10
|
+
@hash[k]
|
11
|
+
end
|
12
|
+
|
13
|
+
def fetch(*args)
|
14
|
+
@hash.fetch(*args)
|
15
|
+
end
|
16
|
+
|
17
|
+
def dig(*args)
|
18
|
+
@hash.dig(*args)
|
19
|
+
end
|
20
|
+
|
21
|
+
def adapter
|
22
|
+
@hash.fetch(:adapter)
|
23
|
+
end
|
24
|
+
|
25
|
+
def collector
|
26
|
+
@collector ||= Collector.new(adapter)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Wifidiag
|
2
|
+
class Report
|
3
|
+
def initialize(client_ip, client_data, advanced_data)
|
4
|
+
@client_ip = client_ip
|
5
|
+
@client_data = client_data
|
6
|
+
@advanced_data = advanced_data
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :client_ip, :client_data, :advanced_data
|
10
|
+
|
11
|
+
def to_h
|
12
|
+
{
|
13
|
+
client_ip: client_ip,
|
14
|
+
client_data: client_data.to_h,
|
15
|
+
advanced_data: advanced_data.to_h,
|
16
|
+
}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'net/http'
|
3
|
+
require 'net/https'
|
4
|
+
require 'wifidiag/reporters/base'
|
5
|
+
|
6
|
+
module Wifidiag
|
7
|
+
module Reporters
|
8
|
+
class Slack < Base
|
9
|
+
def initialize(webhook_url:)
|
10
|
+
@webhook_url = URI.parse(webhook_url)
|
11
|
+
end
|
12
|
+
|
13
|
+
def report!(report)
|
14
|
+
Net::HTTP.post_form(
|
15
|
+
@webhook_url,
|
16
|
+
payload: {
|
17
|
+
text: "Wi-Fi diagnostic received!" + (report.advanced_data.dig('client', 'q') ? " (#{report.advanced_data['client']['q'].inspect})" : ''),
|
18
|
+
mrkdwn: true,
|
19
|
+
attachments: [
|
20
|
+
{
|
21
|
+
fallback: report.to_h.to_json,
|
22
|
+
text: "Expand to see raw JSON:\n\n\n\n\n\n```\n#{JSON.pretty_generate(report.to_h)}\n```",
|
23
|
+
mrkdwn_in: ['text'],
|
24
|
+
fields: [
|
25
|
+
report.client_data&.ssid ? {
|
26
|
+
title: 'SSID',
|
27
|
+
value: report.client_data.ssid,
|
28
|
+
short: true,
|
29
|
+
} : nil,
|
30
|
+
report.client_data&.ap ? {
|
31
|
+
title: 'AP',
|
32
|
+
value: report.client_data.ap&.name || report.client_data.ap&.mac_address || 'n/a',
|
33
|
+
short: true,
|
34
|
+
} : nil,
|
35
|
+
{
|
36
|
+
title: 'IP address',
|
37
|
+
value: report.client_ip,
|
38
|
+
short: true,
|
39
|
+
},
|
40
|
+
report.advanced_data['bandwidth']&.dig('mbps') ? {
|
41
|
+
title: 'Bandwidth',
|
42
|
+
value: "%.4f Mbps" % report.advanced_data['bandwidth']&.dig('mbps'),
|
43
|
+
short: true,
|
44
|
+
} : nil,
|
45
|
+
report.advanced_data['latency'] ? {
|
46
|
+
title: 'Latency',
|
47
|
+
value: "pkt: ok=%d ng=%d rate=%.1f%%\nrtt: min=%.2f avg=%.2f max=%.2f mdev=%.2f time=%.2f ms" \
|
48
|
+
% report.advanced_data['latency']&.values_at(
|
49
|
+
'ok', 'fail', 'rate',
|
50
|
+
'min', 'avg', 'max', 'mdev',
|
51
|
+
'time',
|
52
|
+
).map{ |_| _ == nil ? -1 : _ },
|
53
|
+
short: false,
|
54
|
+
} : nil,
|
55
|
+
report.advanced_data['client']['ua'] ? {
|
56
|
+
title: 'User agent',
|
57
|
+
value: report.advanced_data['client']['ua'],
|
58
|
+
short: false,
|
59
|
+
} : nil,
|
60
|
+
].compact,
|
61
|
+
}
|
62
|
+
]
|
63
|
+
}.to_json
|
64
|
+
)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|