keenetic 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/README.md +83 -0
- data/lib/keenetic/client.rb +361 -0
- data/lib/keenetic/configuration.rb +27 -0
- data/lib/keenetic/errors.rb +19 -0
- data/lib/keenetic/resources/base.rb +76 -0
- data/lib/keenetic/resources/devices.rb +309 -0
- data/lib/keenetic/resources/dhcp.rb +195 -0
- data/lib/keenetic/resources/internet.rb +169 -0
- data/lib/keenetic/resources/logs.rb +330 -0
- data/lib/keenetic/resources/network.rb +251 -0
- data/lib/keenetic/resources/policies.rb +171 -0
- data/lib/keenetic/resources/ports.rb +115 -0
- data/lib/keenetic/resources/routing.rb +200 -0
- data/lib/keenetic/resources/system.rb +267 -0
- data/lib/keenetic/resources/wifi.rb +376 -0
- data/lib/keenetic/version.rb +4 -0
- data/lib/keenetic.rb +99 -0
- metadata +89 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
module Keenetic
|
|
2
|
+
module Resources
|
|
3
|
+
# Manages routing policies (VPN policies, traffic routing rules).
|
|
4
|
+
#
|
|
5
|
+
# Policies allow routing specific devices through VPN tunnels or other interfaces.
|
|
6
|
+
#
|
|
7
|
+
# == API Endpoints Used
|
|
8
|
+
#
|
|
9
|
+
# === Reading Policies
|
|
10
|
+
# POST /rci/ (batch request)
|
|
11
|
+
# Body: [{"show":{"sc":{"ip":{"policy":{}}}}}]
|
|
12
|
+
#
|
|
13
|
+
# === Reading Device-Policy Assignments
|
|
14
|
+
# POST /rci/ (batch request)
|
|
15
|
+
# Body: [{"show":{"sc":{"ip":{"hotspot":{"host":{}}}}}}]
|
|
16
|
+
#
|
|
17
|
+
# == Policy Structure
|
|
18
|
+
# Policies are identified by IDs like "Policy0", "Policy1", etc.
|
|
19
|
+
# Each policy has:
|
|
20
|
+
# - description: Name prefixed with "!" (e.g., "!Latvia VPN")
|
|
21
|
+
# - permit: Array of interfaces this policy routes through
|
|
22
|
+
#
|
|
23
|
+
# == Example Response from API
|
|
24
|
+
# {
|
|
25
|
+
# "show": {
|
|
26
|
+
# "sc": {
|
|
27
|
+
# "ip": {
|
|
28
|
+
# "policy": {
|
|
29
|
+
# "Policy0": {
|
|
30
|
+
# "description": "!Latvia",
|
|
31
|
+
# "permit": [
|
|
32
|
+
# { "interface": "Wireguard0", "enabled": true },
|
|
33
|
+
# { "interface": "ISP", "enabled": false }
|
|
34
|
+
# ]
|
|
35
|
+
# }
|
|
36
|
+
# }
|
|
37
|
+
# }
|
|
38
|
+
# }
|
|
39
|
+
# }
|
|
40
|
+
# }
|
|
41
|
+
#
|
|
42
|
+
class Policies < Base
|
|
43
|
+
# Fetch all routing policies.
|
|
44
|
+
#
|
|
45
|
+
# == Keenetic API Request
|
|
46
|
+
# POST /rci/
|
|
47
|
+
# Body: [{"show":{"sc":{"ip":{"policy":{}}}}}]
|
|
48
|
+
#
|
|
49
|
+
# == Response Processing
|
|
50
|
+
# - Extracts policy data from nested response structure
|
|
51
|
+
# - Filters permit list to only enabled interfaces (enabled: true, no: not true)
|
|
52
|
+
# - Removes "!" prefix from description for clean policy name
|
|
53
|
+
#
|
|
54
|
+
# @return [Array<Hash>] List of policies with :id, :description, :name, :interfaces
|
|
55
|
+
# @example
|
|
56
|
+
# policies = client.policies.all
|
|
57
|
+
# # => [{ id: "Policy0", description: "!Latvia", name: "Latvia",
|
|
58
|
+
# # interfaces: ["Wireguard0"], interface_count: 1 }]
|
|
59
|
+
#
|
|
60
|
+
def all
|
|
61
|
+
response = client.batch([
|
|
62
|
+
{ 'show' => { 'sc' => { 'ip' => { 'policy' => {} } } } }
|
|
63
|
+
])
|
|
64
|
+
|
|
65
|
+
normalize_policies(response&.first)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Get policy assignments for all devices.
|
|
69
|
+
#
|
|
70
|
+
# == Keenetic API Request
|
|
71
|
+
# POST /rci/
|
|
72
|
+
# Body: [{"show":{"sc":{"ip":{"hotspot":{"host":{}}}}}}]
|
|
73
|
+
#
|
|
74
|
+
# == Response Structure
|
|
75
|
+
# {
|
|
76
|
+
# "show": {
|
|
77
|
+
# "sc": {
|
|
78
|
+
# "ip": {
|
|
79
|
+
# "hotspot": {
|
|
80
|
+
# "host": [
|
|
81
|
+
# { "mac": "00:11:22:33:44:55", "policy": "Policy0" },
|
|
82
|
+
# { "mac": "AA:BB:CC:DD:EE:FF", "policy": "Policy1" }
|
|
83
|
+
# ]
|
|
84
|
+
# }
|
|
85
|
+
# }
|
|
86
|
+
# }
|
|
87
|
+
# }
|
|
88
|
+
# }
|
|
89
|
+
#
|
|
90
|
+
# @return [Hash] MAC address (lowercase) => policy ID mapping
|
|
91
|
+
# @example
|
|
92
|
+
# assignments = client.policies.device_assignments
|
|
93
|
+
# # => { "00:11:22:33:44:55" => "Policy0", "aa:bb:cc:dd:ee:ff" => "Policy1" }
|
|
94
|
+
#
|
|
95
|
+
def device_assignments
|
|
96
|
+
response = client.batch([
|
|
97
|
+
{ 'show' => { 'sc' => { 'ip' => { 'hotspot' => { 'host' => {} } } } } }
|
|
98
|
+
])
|
|
99
|
+
|
|
100
|
+
extract_device_policies(response&.first)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Find a specific policy by ID.
|
|
104
|
+
#
|
|
105
|
+
# @param id [String] Policy ID (e.g., "Policy0")
|
|
106
|
+
# @return [Hash] Policy data
|
|
107
|
+
# @raise [NotFoundError] if policy not found
|
|
108
|
+
# @example
|
|
109
|
+
# policy = client.policies.find(id: 'Policy0')
|
|
110
|
+
# # => { id: "Policy0", name: "Latvia", interfaces: ["Wireguard0"], ... }
|
|
111
|
+
#
|
|
112
|
+
def find(id:)
|
|
113
|
+
policies = all
|
|
114
|
+
policy = policies.find { |p| p[:id] == id }
|
|
115
|
+
raise NotFoundError, "Policy #{id} not found" unless policy
|
|
116
|
+
policy
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
private
|
|
120
|
+
|
|
121
|
+
def normalize_policies(response)
|
|
122
|
+
return [] unless response.is_a?(Hash)
|
|
123
|
+
|
|
124
|
+
policies_data = response.dig('show', 'sc', 'ip', 'policy')
|
|
125
|
+
return [] unless policies_data.is_a?(Hash)
|
|
126
|
+
|
|
127
|
+
policies_data.map do |id, data|
|
|
128
|
+
normalize_policy(id, data)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def normalize_policy(id, data)
|
|
133
|
+
return nil unless data.is_a?(Hash)
|
|
134
|
+
|
|
135
|
+
# Extract active interfaces from permit list
|
|
136
|
+
permits = data['permit'] || []
|
|
137
|
+
active_interfaces = permits
|
|
138
|
+
.select { |p| p.is_a?(Hash) && p['enabled'] == true && p['no'] != true }
|
|
139
|
+
.map { |p| p['interface'] }
|
|
140
|
+
.compact
|
|
141
|
+
|
|
142
|
+
{
|
|
143
|
+
id: id,
|
|
144
|
+
description: data['description'] || id,
|
|
145
|
+
name: extract_policy_name(data['description'] || id),
|
|
146
|
+
interfaces: active_interfaces,
|
|
147
|
+
interface_count: active_interfaces.size
|
|
148
|
+
}
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def extract_policy_name(description)
|
|
152
|
+
# Remove leading "!" and clean up the name
|
|
153
|
+
name = description.to_s.sub(/^!/, '').strip
|
|
154
|
+
name.empty? ? 'Unnamed Policy' : name
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def extract_device_policies(response)
|
|
158
|
+
return {} unless response.is_a?(Hash)
|
|
159
|
+
|
|
160
|
+
hosts = response.dig('show', 'sc', 'ip', 'hotspot', 'host')
|
|
161
|
+
return {} unless hosts.is_a?(Array)
|
|
162
|
+
|
|
163
|
+
hosts.each_with_object({}) do |host, mapping|
|
|
164
|
+
next unless host.is_a?(Hash) && host['policy']
|
|
165
|
+
mapping[host['mac']&.downcase] = host['policy']
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
module Keenetic
|
|
2
|
+
module Resources
|
|
3
|
+
# Ports resource for accessing physical Ethernet port status.
|
|
4
|
+
#
|
|
5
|
+
# == API Endpoints Used
|
|
6
|
+
#
|
|
7
|
+
# === Reading Port Statistics
|
|
8
|
+
# GET /rci/show/interface/stat
|
|
9
|
+
# Filters: Physical ports only (GigabitEthernet*, FastEthernet*, SFP*, USB*)
|
|
10
|
+
#
|
|
11
|
+
# == Port Naming Convention
|
|
12
|
+
# - GigabitEthernet0, GigabitEthernet1, etc.: Gigabit Ethernet ports
|
|
13
|
+
# - FastEthernet0, etc.: Fast Ethernet ports (100Mbps)
|
|
14
|
+
# - SFP0: SFP port (if available)
|
|
15
|
+
#
|
|
16
|
+
# == Link Status
|
|
17
|
+
# - link: true = cable connected, false = no cable
|
|
18
|
+
# - speed: Current negotiated speed (1000, 100, 10 Mbps)
|
|
19
|
+
# - duplex: "full" or "half"
|
|
20
|
+
#
|
|
21
|
+
class Ports < Base
|
|
22
|
+
# Get all physical port statuses.
|
|
23
|
+
#
|
|
24
|
+
# == Keenetic API Request
|
|
25
|
+
# GET /rci/show/interface/stat
|
|
26
|
+
# Internally filters for physical ports only
|
|
27
|
+
#
|
|
28
|
+
# == Response Fields
|
|
29
|
+
# - id: Interface ID (e.g., "GigabitEthernet0")
|
|
30
|
+
# - port: Port number extracted from ID
|
|
31
|
+
# - type: "gigabit", "fast", "sfp", or "usb"
|
|
32
|
+
# - link: true if cable connected
|
|
33
|
+
# - speed: Negotiated speed in Mbps
|
|
34
|
+
# - duplex: "full" or "half"
|
|
35
|
+
# - rxbytes/txbytes: Traffic counters
|
|
36
|
+
# - rxerrors/txerrors: Error counters
|
|
37
|
+
# - media: Media type string
|
|
38
|
+
#
|
|
39
|
+
# @return [Array<Hash>] List of physical ports with status
|
|
40
|
+
# @example
|
|
41
|
+
# ports = client.ports.all
|
|
42
|
+
# # => [{ id: "GigabitEthernet0", port: 0, type: "gigabit", link: true, speed: 1000, ... }]
|
|
43
|
+
#
|
|
44
|
+
def all
|
|
45
|
+
response = get('/rci/show/interface/stat')
|
|
46
|
+
normalize_ports(response)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Get specific port by ID.
|
|
50
|
+
#
|
|
51
|
+
# @param id [String] Port interface ID (e.g., "GigabitEthernet0")
|
|
52
|
+
# @return [Hash, nil] Port data or nil if not found
|
|
53
|
+
# @example
|
|
54
|
+
# port = client.ports.find('GigabitEthernet0')
|
|
55
|
+
# # => { id: "GigabitEthernet0", port: 0, link: true, speed: 1000, ... }
|
|
56
|
+
#
|
|
57
|
+
def find(id)
|
|
58
|
+
all.find { |p| p[:id] == id }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def normalize_ports(response)
|
|
64
|
+
return [] unless response.is_a?(Hash)
|
|
65
|
+
|
|
66
|
+
response.filter_map do |id, data|
|
|
67
|
+
next unless physical_port?(id, data)
|
|
68
|
+
normalize_port(id, data)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def physical_port?(id, data)
|
|
73
|
+
return false unless data.is_a?(Hash)
|
|
74
|
+
|
|
75
|
+
# Physical ports are typically named GigabitEthernet0, GigabitEthernet1, etc.
|
|
76
|
+
# or SFP ports, or USB ports
|
|
77
|
+
id.match?(/^(Gigabit|Fast)?Ethernet\d+|SFP|USB/)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def normalize_port(id, data)
|
|
81
|
+
{
|
|
82
|
+
id: id,
|
|
83
|
+
port: extract_port_number(id),
|
|
84
|
+
type: extract_port_type(id),
|
|
85
|
+
link: data['link'] == true,
|
|
86
|
+
speed: data['speed'],
|
|
87
|
+
duplex: data['duplex'],
|
|
88
|
+
rxbytes: data['rxbytes'],
|
|
89
|
+
txbytes: data['txbytes'],
|
|
90
|
+
rxpackets: data['rxpackets'],
|
|
91
|
+
txpackets: data['txpackets'],
|
|
92
|
+
rxerrors: data['rxerrors'],
|
|
93
|
+
txerrors: data['txerrors'],
|
|
94
|
+
media: data['media']
|
|
95
|
+
}
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def extract_port_number(id)
|
|
99
|
+
match = id.match(/(\d+)$/)
|
|
100
|
+
match ? match[1].to_i : nil
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def extract_port_type(id)
|
|
104
|
+
case id
|
|
105
|
+
when /GigabitEthernet/ then 'gigabit'
|
|
106
|
+
when /FastEthernet/ then 'fast'
|
|
107
|
+
when /SFP/ then 'sfp'
|
|
108
|
+
when /USB/ then 'usb'
|
|
109
|
+
else 'unknown'
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
module Keenetic
|
|
2
|
+
module Resources
|
|
3
|
+
# Routing resource for managing IP routes and ARP table.
|
|
4
|
+
#
|
|
5
|
+
# == API Endpoints Used
|
|
6
|
+
#
|
|
7
|
+
# === Reading Routing Table
|
|
8
|
+
# GET /rci/show/ip/route
|
|
9
|
+
# Returns: Array of IP routes
|
|
10
|
+
#
|
|
11
|
+
# === Reading ARP Table
|
|
12
|
+
# GET /rci/show/ip/arp
|
|
13
|
+
# Returns: Array of ARP entries
|
|
14
|
+
#
|
|
15
|
+
# === Adding Static Route
|
|
16
|
+
# POST /rci/ip/route
|
|
17
|
+
# Body: { "destination": "10.0.0.0", "mask": "255.0.0.0", "gateway": "192.168.1.1", "interface": "ISP" }
|
|
18
|
+
#
|
|
19
|
+
# === Deleting Static Route
|
|
20
|
+
# POST /rci/ip/route
|
|
21
|
+
# Body: { "destination": "10.0.0.0", "mask": "255.0.0.0", "no": true }
|
|
22
|
+
#
|
|
23
|
+
class Routing < Base
|
|
24
|
+
# Get IP routing table.
|
|
25
|
+
#
|
|
26
|
+
# == Keenetic API Request
|
|
27
|
+
# GET /rci/show/ip/route
|
|
28
|
+
#
|
|
29
|
+
# == Response Structure from API
|
|
30
|
+
# [
|
|
31
|
+
# {
|
|
32
|
+
# "destination": "0.0.0.0",
|
|
33
|
+
# "mask": "0.0.0.0",
|
|
34
|
+
# "gateway": "192.168.1.1",
|
|
35
|
+
# "interface": "ISP",
|
|
36
|
+
# "metric": 0,
|
|
37
|
+
# "flags": "G",
|
|
38
|
+
# "auto": true
|
|
39
|
+
# }
|
|
40
|
+
# ]
|
|
41
|
+
#
|
|
42
|
+
# @return [Array<Hash>] List of normalized route hashes
|
|
43
|
+
# @example
|
|
44
|
+
# routes = client.routing.routes
|
|
45
|
+
# # => [{ destination: "0.0.0.0", mask: "0.0.0.0", gateway: "192.168.1.1", ... }]
|
|
46
|
+
#
|
|
47
|
+
def routes
|
|
48
|
+
response = get('/rci/show/ip/route')
|
|
49
|
+
normalize_routes(response)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Get ARP table (IP to MAC address mappings).
|
|
53
|
+
#
|
|
54
|
+
# == Keenetic API Request
|
|
55
|
+
# GET /rci/show/ip/arp
|
|
56
|
+
#
|
|
57
|
+
# == Response Structure from API
|
|
58
|
+
# [
|
|
59
|
+
# {
|
|
60
|
+
# "ip": "192.168.1.100",
|
|
61
|
+
# "mac": "AA:BB:CC:DD:EE:FF",
|
|
62
|
+
# "interface": "Bridge0",
|
|
63
|
+
# "state": "reachable"
|
|
64
|
+
# }
|
|
65
|
+
# ]
|
|
66
|
+
#
|
|
67
|
+
# @return [Array<Hash>] List of normalized ARP entries
|
|
68
|
+
# @example
|
|
69
|
+
# arp_table = client.routing.arp_table
|
|
70
|
+
# # => [{ ip: "192.168.1.100", mac: "AA:BB:CC:DD:EE:FF", interface: "Bridge0", state: "reachable" }]
|
|
71
|
+
#
|
|
72
|
+
def arp_table
|
|
73
|
+
response = get('/rci/show/ip/arp')
|
|
74
|
+
normalize_arp_entries(response)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Find a specific route by destination and mask.
|
|
78
|
+
#
|
|
79
|
+
# @param destination [String] Destination network (e.g., "10.0.0.0")
|
|
80
|
+
# @param mask [String] Network mask (e.g., "255.0.0.0")
|
|
81
|
+
# @return [Hash, nil] Route data or nil if not found
|
|
82
|
+
# @example
|
|
83
|
+
# route = client.routing.find_route(destination: "10.0.0.0", mask: "255.0.0.0")
|
|
84
|
+
# # => { destination: "10.0.0.0", mask: "255.0.0.0", gateway: "192.168.1.1", ... }
|
|
85
|
+
#
|
|
86
|
+
def find_route(destination:, mask:)
|
|
87
|
+
routes.find { |r| r[:destination] == destination && r[:mask] == mask }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Find ARP entry by IP address.
|
|
91
|
+
#
|
|
92
|
+
# @param ip [String] IP address to look up
|
|
93
|
+
# @return [Hash, nil] ARP entry or nil if not found
|
|
94
|
+
# @example
|
|
95
|
+
# entry = client.routing.find_arp_entry(ip: "192.168.1.100")
|
|
96
|
+
# # => { ip: "192.168.1.100", mac: "AA:BB:CC:DD:EE:FF", ... }
|
|
97
|
+
#
|
|
98
|
+
def find_arp_entry(ip:)
|
|
99
|
+
arp_table.find { |e| e[:ip] == ip }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Add a static route.
|
|
103
|
+
#
|
|
104
|
+
# == Keenetic API Request
|
|
105
|
+
# POST /rci/ (batch format)
|
|
106
|
+
# Body: [{"ip": {"route": {"destination": "...", "mask": "...", "gateway": "...", "interface": "..."}}}]
|
|
107
|
+
#
|
|
108
|
+
# @param destination [String] Destination network (e.g., "10.0.0.0")
|
|
109
|
+
# @param mask [String] Network mask (e.g., "255.0.0.0")
|
|
110
|
+
# @param gateway [String, nil] Next hop gateway IP address
|
|
111
|
+
# @param interface [String, nil] Output interface name
|
|
112
|
+
# @param metric [Integer, nil] Route metric (optional)
|
|
113
|
+
# @return [Array<Hash>] API response
|
|
114
|
+
#
|
|
115
|
+
# @example Add route via gateway
|
|
116
|
+
# client.routing.create_route(destination: "10.0.0.0", mask: "255.0.0.0", gateway: "192.168.1.1")
|
|
117
|
+
#
|
|
118
|
+
# @example Add route via interface
|
|
119
|
+
# client.routing.create_route(destination: "10.0.0.0", mask: "255.0.0.0", interface: "ISP")
|
|
120
|
+
#
|
|
121
|
+
def create_route(destination:, mask:, gateway: nil, interface: nil, metric: nil)
|
|
122
|
+
params = {
|
|
123
|
+
'destination' => destination,
|
|
124
|
+
'mask' => mask
|
|
125
|
+
}
|
|
126
|
+
params['gateway'] = gateway if gateway
|
|
127
|
+
params['interface'] = interface if interface
|
|
128
|
+
params['metric'] = metric if metric
|
|
129
|
+
|
|
130
|
+
client.batch([{ 'ip' => { 'route' => params } }])
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Delete a static route.
|
|
134
|
+
#
|
|
135
|
+
# == Keenetic API Request
|
|
136
|
+
# POST /rci/ (batch format)
|
|
137
|
+
# Body: [{"ip": {"route": {"destination": "...", "mask": "...", "no": true}}}]
|
|
138
|
+
#
|
|
139
|
+
# @param destination [String] Destination network
|
|
140
|
+
# @param mask [String] Network mask
|
|
141
|
+
# @return [Array<Hash>] API response
|
|
142
|
+
#
|
|
143
|
+
# @example
|
|
144
|
+
# client.routing.delete_route(destination: "10.0.0.0", mask: "255.0.0.0")
|
|
145
|
+
#
|
|
146
|
+
def delete_route(destination:, mask:)
|
|
147
|
+
client.batch([{
|
|
148
|
+
'ip' => {
|
|
149
|
+
'route' => {
|
|
150
|
+
'destination' => destination,
|
|
151
|
+
'mask' => mask,
|
|
152
|
+
'no' => true
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}])
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
private
|
|
159
|
+
|
|
160
|
+
def normalize_routes(response)
|
|
161
|
+
return [] unless response.is_a?(Array)
|
|
162
|
+
|
|
163
|
+
response.map { |route| normalize_route(route) }.compact
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def normalize_route(data)
|
|
167
|
+
return nil unless data.is_a?(Hash)
|
|
168
|
+
|
|
169
|
+
{
|
|
170
|
+
destination: data['destination'],
|
|
171
|
+
mask: data['mask'],
|
|
172
|
+
gateway: data['gateway'],
|
|
173
|
+
interface: data['interface'],
|
|
174
|
+
metric: data['metric'],
|
|
175
|
+
flags: data['flags'],
|
|
176
|
+
auto: data['auto'],
|
|
177
|
+
comment: data['comment']
|
|
178
|
+
}
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def normalize_arp_entries(response)
|
|
182
|
+
return [] unless response.is_a?(Array)
|
|
183
|
+
|
|
184
|
+
response.map { |entry| normalize_arp_entry(entry) }.compact
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def normalize_arp_entry(data)
|
|
188
|
+
return nil unless data.is_a?(Hash)
|
|
189
|
+
|
|
190
|
+
{
|
|
191
|
+
ip: data['ip'],
|
|
192
|
+
mac: data['mac'],
|
|
193
|
+
interface: data['interface'],
|
|
194
|
+
state: data['state']
|
|
195
|
+
}
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|