keenetic 0.1.0 → 0.2.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 +4 -4
- data/LICENSE.txt +21 -0
- data/README.md +28 -0
- data/lib/keenetic/client.rb +49 -0
- data/lib/keenetic/resources/config.rb +53 -0
- data/lib/keenetic/resources/hotspot.rb +282 -0
- data/lib/keenetic/resources/routes.rb +432 -0
- data/lib/keenetic/version.rb +1 -2
- data/lib/keenetic.rb +20 -0
- metadata +7 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3fa7552177bec3d537db352ed748bcd24e07bee6156272569a121b7cb18c6a2f
|
|
4
|
+
data.tar.gz: e48b1c10d4cc62466ebd5e34b200f399e28e8261e0e1a83a1a4b1ee86efc4ee3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f801f8df36437d0f2ea76f6765beaeee3a68750b2974b8b3fc4eed9145be5a5c123940245b56566c690ef906d791eaf8223ae3f016b4bb0472c1a2a3476f77df
|
|
7
|
+
data.tar.gz: 984b84f49fb83117ab0c9c89b60a0b88718ccbbd92258c69987be8103bec4d2fd66554c4413c001e45167396735c634a578e839c35cce74b0124d2d3c829f930
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Anton
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
CHANGED
|
@@ -48,6 +48,34 @@ client.internet.speed
|
|
|
48
48
|
|
|
49
49
|
# Ports
|
|
50
50
|
client.ports.all
|
|
51
|
+
|
|
52
|
+
# Static Routes
|
|
53
|
+
client.routes.all
|
|
54
|
+
client.routes.add(host: '1.2.3.4', interface: 'Wireguard0', comment: 'VPN host')
|
|
55
|
+
client.routes.add(network: '10.0.0.0/24', interface: 'Wireguard0', comment: 'VPN network')
|
|
56
|
+
client.routes.add_batch([
|
|
57
|
+
{ host: '1.2.3.4', interface: 'Wireguard0', comment: 'Host 1' },
|
|
58
|
+
{ network: '10.0.0.0/24', interface: 'Wireguard0', comment: 'Network 1' }
|
|
59
|
+
])
|
|
60
|
+
client.routes.delete(host: '1.2.3.4')
|
|
61
|
+
client.routes.delete(network: '10.0.0.0/24')
|
|
62
|
+
|
|
63
|
+
# Hotspot / Policies
|
|
64
|
+
client.hotspot.policies # all IP policies
|
|
65
|
+
client.hotspot.hosts # all registered hosts
|
|
66
|
+
client.hotspot.set_host_policy(mac: 'AA:BB:CC:DD:EE:FF', policy: 'Policy0')
|
|
67
|
+
client.hotspot.set_host_policy(mac: 'AA:BB:CC:DD:EE:FF', policy: nil) # remove policy
|
|
68
|
+
|
|
69
|
+
# Configuration
|
|
70
|
+
client.system_config.save # save to flash
|
|
71
|
+
client.system_config.download # download startup-config.txt
|
|
72
|
+
|
|
73
|
+
# Raw RCI Access (for custom commands)
|
|
74
|
+
client.rci({ 'show' => { 'system' => {} } })
|
|
75
|
+
client.rci([
|
|
76
|
+
{ 'show' => { 'system' => {} } },
|
|
77
|
+
{ 'show' => { 'version' => {} } }
|
|
78
|
+
])
|
|
51
79
|
```
|
|
52
80
|
|
|
53
81
|
## Error Handling
|
data/lib/keenetic/client.rb
CHANGED
|
@@ -102,6 +102,55 @@ module Keenetic
|
|
|
102
102
|
@logs ||= Resources::Logs.new(self)
|
|
103
103
|
end
|
|
104
104
|
|
|
105
|
+
# @return [Resources::Routes] Static routes resource
|
|
106
|
+
def routes
|
|
107
|
+
@routes ||= Resources::Routes.new(self)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# @return [Resources::Hotspot] Hotspot hosts and policies resource
|
|
111
|
+
def hotspot
|
|
112
|
+
@hotspot ||= Resources::Hotspot.new(self)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# @return [Resources::Config] Configuration management resource
|
|
116
|
+
def system_config
|
|
117
|
+
@system_config ||= Resources::Config.new(self)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Execute arbitrary RCI command(s).
|
|
121
|
+
#
|
|
122
|
+
# Provides raw access to the Keenetic RCI (Remote Command Interface).
|
|
123
|
+
# Use this for custom commands not covered by the gem's resources.
|
|
124
|
+
#
|
|
125
|
+
# == Keenetic API
|
|
126
|
+
# POST http://<host>/rci/
|
|
127
|
+
# Content-Type: application/json
|
|
128
|
+
# Body: Array or Hash of RCI commands
|
|
129
|
+
#
|
|
130
|
+
# @param body [Hash, Array<Hash>] RCI command(s) to execute
|
|
131
|
+
# @return [Hash, Array, nil] Parsed JSON response
|
|
132
|
+
#
|
|
133
|
+
# @example Execute single command
|
|
134
|
+
# client.rci({ 'show' => { 'system' => {} } })
|
|
135
|
+
# # => { "show" => { "system" => { ... } } }
|
|
136
|
+
#
|
|
137
|
+
# @example Execute batch commands
|
|
138
|
+
# client.rci([
|
|
139
|
+
# { 'show' => { 'system' => {} } },
|
|
140
|
+
# { 'show' => { 'version' => {} } }
|
|
141
|
+
# ])
|
|
142
|
+
# # => [{ "show" => { "system" => { ... } } }, { "show" => { "version" => { ... } } }]
|
|
143
|
+
#
|
|
144
|
+
# @example Execute write command
|
|
145
|
+
# client.rci([
|
|
146
|
+
# { 'ip' => { 'hotspot' => { 'host' => { 'mac' => 'aa:bb:cc:dd:ee:ff', 'permit' => true } } } }
|
|
147
|
+
# ])
|
|
148
|
+
#
|
|
149
|
+
def rci(body)
|
|
150
|
+
commands = body.is_a?(Array) ? body : [body]
|
|
151
|
+
batch(commands)
|
|
152
|
+
end
|
|
153
|
+
|
|
105
154
|
# Make a GET request to the router API.
|
|
106
155
|
#
|
|
107
156
|
# == Keenetic API
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module Keenetic
|
|
2
|
+
module Resources
|
|
3
|
+
# Manages router configuration operations.
|
|
4
|
+
#
|
|
5
|
+
# == API Endpoints Used
|
|
6
|
+
#
|
|
7
|
+
# === Save Configuration
|
|
8
|
+
# POST /rci/ (batch format)
|
|
9
|
+
# Body: [{"system": {"configuration": {"save": {}}}}]
|
|
10
|
+
# Saves current configuration to persistent storage
|
|
11
|
+
#
|
|
12
|
+
# === Download Configuration
|
|
13
|
+
# GET /ci/startup-config.txt
|
|
14
|
+
# Returns: Plain text configuration file
|
|
15
|
+
#
|
|
16
|
+
class Config < Base
|
|
17
|
+
# Save current configuration to persistent storage.
|
|
18
|
+
#
|
|
19
|
+
# == Keenetic API Request
|
|
20
|
+
# POST /rci/ (batch format)
|
|
21
|
+
# Body: [{"system": {"configuration": {"save": {}}}}]
|
|
22
|
+
#
|
|
23
|
+
# Configuration changes are typically auto-saved, but this method
|
|
24
|
+
# forces an immediate save to flash storage.
|
|
25
|
+
#
|
|
26
|
+
# @return [Array<Hash>] API response
|
|
27
|
+
# @example
|
|
28
|
+
# client.config.save
|
|
29
|
+
# # => [{ "system" => { "configuration" => { "save" => {} } } }]
|
|
30
|
+
#
|
|
31
|
+
def save
|
|
32
|
+
client.batch([{ 'system' => { 'configuration' => { 'save' => {} } } }])
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Download the startup configuration file.
|
|
36
|
+
#
|
|
37
|
+
# == Keenetic API Request
|
|
38
|
+
# GET /ci/startup-config.txt
|
|
39
|
+
#
|
|
40
|
+
# Returns the full router configuration as a text file.
|
|
41
|
+
# This is the same format used for backup/restore operations.
|
|
42
|
+
#
|
|
43
|
+
# @return [String] Configuration file content
|
|
44
|
+
# @example
|
|
45
|
+
# config_text = client.config.download
|
|
46
|
+
# File.write('router-backup.txt', config_text)
|
|
47
|
+
#
|
|
48
|
+
def download
|
|
49
|
+
get('/ci/startup-config.txt')
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
module Keenetic
|
|
2
|
+
module Resources
|
|
3
|
+
# Manages hotspot hosts and IP policies.
|
|
4
|
+
#
|
|
5
|
+
# == API Endpoints Used
|
|
6
|
+
#
|
|
7
|
+
# === Reading Policies
|
|
8
|
+
# POST /rci/ (batch format)
|
|
9
|
+
# Body: [{"show": {"sc": {"ip": {"policy": {}}}}}]
|
|
10
|
+
# Returns: All IP policies from configuration
|
|
11
|
+
#
|
|
12
|
+
# === Reading Hosts
|
|
13
|
+
# POST /rci/ (batch format)
|
|
14
|
+
# Body: [
|
|
15
|
+
# {"show": {"sc": {"ip": {"hotspot": {"host": {}}}}}},
|
|
16
|
+
# {"show": {"ip": {"hotspot": {}}}}
|
|
17
|
+
# ]
|
|
18
|
+
# Returns: Configuration hosts and runtime hosts
|
|
19
|
+
#
|
|
20
|
+
# === Setting Host Policy
|
|
21
|
+
# POST /rci/ (batch format)
|
|
22
|
+
# Body: [
|
|
23
|
+
# {"webhelp": {"event": {"push": {"data": "..."}}}},
|
|
24
|
+
# {"ip": {"hotspot": {"host": {"mac": "...", "permit": true, "policy": "..."}}}},
|
|
25
|
+
# {"system": {"configuration": {"save": {}}}}
|
|
26
|
+
# ]
|
|
27
|
+
#
|
|
28
|
+
class Hotspot < Base
|
|
29
|
+
# Get all IP policies.
|
|
30
|
+
#
|
|
31
|
+
# == Keenetic API Request
|
|
32
|
+
# POST /rci/ (batch format)
|
|
33
|
+
# Body: [{"show": {"sc": {"ip": {"policy": {}}}}}]
|
|
34
|
+
#
|
|
35
|
+
# == Response Structure from API
|
|
36
|
+
# [
|
|
37
|
+
# {
|
|
38
|
+
# "id": "Policy0",
|
|
39
|
+
# "description": "VPN Policy",
|
|
40
|
+
# "global": false,
|
|
41
|
+
# "interface": [
|
|
42
|
+
# { "name": "Wireguard0", "priority": 100 }
|
|
43
|
+
# ]
|
|
44
|
+
# }
|
|
45
|
+
# ]
|
|
46
|
+
#
|
|
47
|
+
# @return [Array<Hash>] List of normalized policy hashes
|
|
48
|
+
# @example
|
|
49
|
+
# policies = client.hotspot.policies
|
|
50
|
+
# # => [{ id: "Policy0", description: "VPN Policy", global: false, interfaces: [...] }]
|
|
51
|
+
#
|
|
52
|
+
def policies
|
|
53
|
+
response = client.batch([{ 'show' => { 'sc' => { 'ip' => { 'policy' => {} } } } }])
|
|
54
|
+
policies_data = extract_policies_from_response(response)
|
|
55
|
+
normalize_policies(policies_data)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Get all registered hosts with their policies.
|
|
59
|
+
#
|
|
60
|
+
# == Keenetic API Request
|
|
61
|
+
# POST /rci/ (batch format)
|
|
62
|
+
# Body: [
|
|
63
|
+
# {"show": {"sc": {"ip": {"hotspot": {"host": {}}}}}},
|
|
64
|
+
# {"show": {"ip": {"hotspot": {}}}}
|
|
65
|
+
# ]
|
|
66
|
+
#
|
|
67
|
+
# Returns merged data from configuration (static settings) and runtime (current status).
|
|
68
|
+
#
|
|
69
|
+
# @return [Array<Hash>] List of normalized host hashes
|
|
70
|
+
# @example
|
|
71
|
+
# hosts = client.hotspot.hosts
|
|
72
|
+
# # => [{ mac: "AA:BB:CC:DD:EE:FF", name: "My Device", policy: "Policy0", permit: true, ... }]
|
|
73
|
+
#
|
|
74
|
+
def hosts
|
|
75
|
+
response = client.batch([
|
|
76
|
+
{ 'show' => { 'sc' => { 'ip' => { 'hotspot' => { 'host' => {} } } } } },
|
|
77
|
+
{ 'show' => { 'ip' => { 'hotspot' => {} } } }
|
|
78
|
+
])
|
|
79
|
+
|
|
80
|
+
config_hosts = extract_config_hosts(response)
|
|
81
|
+
runtime_hosts = extract_runtime_hosts(response)
|
|
82
|
+
|
|
83
|
+
merge_hosts(config_hosts, runtime_hosts)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Set or remove policy for a host.
|
|
87
|
+
#
|
|
88
|
+
# == Keenetic API Request
|
|
89
|
+
# POST /rci/ (batch format)
|
|
90
|
+
# Body: [
|
|
91
|
+
# {"webhelp": {"event": {"push": {"data": "{\"type\":\"configuration_change\",\"value\":{\"url\":\"/policies/policy-consumers\"}}"}}}},
|
|
92
|
+
# {"ip": {"hotspot": {"host": {"mac": "...", "permit": true, "policy": "..."}}}},
|
|
93
|
+
# {"system": {"configuration": {"save": {}}}}
|
|
94
|
+
# ]
|
|
95
|
+
#
|
|
96
|
+
# @param mac [String] Client MAC address (required)
|
|
97
|
+
# @param policy [String, nil] Policy name (e.g., "Policy0"), or nil to remove policy
|
|
98
|
+
# @param permit [Boolean] Whether to permit the host (default: true)
|
|
99
|
+
# @return [Array<Hash>] API response
|
|
100
|
+
#
|
|
101
|
+
# @example Assign policy to host
|
|
102
|
+
# client.hotspot.set_host_policy(mac: "AA:BB:CC:DD:EE:FF", policy: "Policy0")
|
|
103
|
+
#
|
|
104
|
+
# @example Remove policy from host
|
|
105
|
+
# client.hotspot.set_host_policy(mac: "AA:BB:CC:DD:EE:FF", policy: nil)
|
|
106
|
+
#
|
|
107
|
+
# @example Assign policy with deny access
|
|
108
|
+
# client.hotspot.set_host_policy(mac: "AA:BB:CC:DD:EE:FF", policy: "Policy0", permit: false)
|
|
109
|
+
#
|
|
110
|
+
def set_host_policy(mac:, policy:, permit: true)
|
|
111
|
+
raise ArgumentError, 'MAC address is required' if mac.nil? || mac.to_s.strip.empty?
|
|
112
|
+
|
|
113
|
+
normalized_mac = mac.downcase
|
|
114
|
+
|
|
115
|
+
host_params = {
|
|
116
|
+
'mac' => normalized_mac,
|
|
117
|
+
'permit' => permit
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if policy.nil? || policy.to_s.strip.empty?
|
|
121
|
+
# Remove policy assignment
|
|
122
|
+
host_params['policy'] = { 'no' => true }
|
|
123
|
+
else
|
|
124
|
+
host_params['policy'] = policy
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
commands = [
|
|
128
|
+
webhelp_event,
|
|
129
|
+
{ 'ip' => { 'hotspot' => { 'host' => host_params } } },
|
|
130
|
+
save_config_command
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
client.batch(commands)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Find a specific policy by ID.
|
|
137
|
+
#
|
|
138
|
+
# @param id [String] Policy ID (e.g., "Policy0")
|
|
139
|
+
# @return [Hash, nil] Policy data or nil if not found
|
|
140
|
+
# @example
|
|
141
|
+
# policy = client.hotspot.find_policy(id: "Policy0")
|
|
142
|
+
# # => { id: "Policy0", description: "VPN Policy", ... }
|
|
143
|
+
#
|
|
144
|
+
def find_policy(id:)
|
|
145
|
+
policies.find { |p| p[:id] == id }
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Find a specific host by MAC address.
|
|
149
|
+
#
|
|
150
|
+
# @param mac [String] MAC address (case-insensitive)
|
|
151
|
+
# @return [Hash, nil] Host data or nil if not found
|
|
152
|
+
# @example
|
|
153
|
+
# host = client.hotspot.find_host(mac: "AA:BB:CC:DD:EE:FF")
|
|
154
|
+
# # => { mac: "AA:BB:CC:DD:EE:FF", name: "My Device", policy: "Policy0", ... }
|
|
155
|
+
#
|
|
156
|
+
def find_host(mac:)
|
|
157
|
+
hosts.find { |h| h[:mac]&.downcase == mac.downcase }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
private
|
|
161
|
+
|
|
162
|
+
def extract_policies_from_response(response)
|
|
163
|
+
return [] unless response.is_a?(Array) && response.first.is_a?(Hash)
|
|
164
|
+
|
|
165
|
+
response.dig(0, 'show', 'sc', 'ip', 'policy') || []
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def extract_config_hosts(response)
|
|
169
|
+
return [] unless response.is_a?(Array) && response[0].is_a?(Hash)
|
|
170
|
+
|
|
171
|
+
hosts = response.dig(0, 'show', 'sc', 'ip', 'hotspot', 'host') || []
|
|
172
|
+
hosts.is_a?(Array) ? hosts : [hosts]
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def extract_runtime_hosts(response)
|
|
176
|
+
return [] unless response.is_a?(Array) && response[1].is_a?(Hash)
|
|
177
|
+
|
|
178
|
+
hosts = response.dig(1, 'show', 'ip', 'hotspot', 'host') || []
|
|
179
|
+
hosts.is_a?(Array) ? hosts : [hosts]
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def normalize_policies(policies_data)
|
|
183
|
+
return [] unless policies_data.is_a?(Array)
|
|
184
|
+
|
|
185
|
+
policies_data.map { |policy| normalize_policy(policy) }.compact
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def normalize_policy(data)
|
|
189
|
+
return nil unless data.is_a?(Hash)
|
|
190
|
+
|
|
191
|
+
interfaces = data['interface'] || []
|
|
192
|
+
interfaces = [interfaces] unless interfaces.is_a?(Array)
|
|
193
|
+
|
|
194
|
+
{
|
|
195
|
+
id: data['id'],
|
|
196
|
+
description: data['description'],
|
|
197
|
+
global: normalize_boolean(data['global']),
|
|
198
|
+
interfaces: interfaces.map { |iface| normalize_policy_interface(iface) }.compact
|
|
199
|
+
}
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def normalize_policy_interface(data)
|
|
203
|
+
return nil unless data.is_a?(Hash)
|
|
204
|
+
|
|
205
|
+
{
|
|
206
|
+
name: data['name'],
|
|
207
|
+
priority: data['priority']
|
|
208
|
+
}
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def merge_hosts(config_hosts, runtime_hosts)
|
|
212
|
+
# Create lookup from runtime hosts by MAC
|
|
213
|
+
runtime_by_mac = runtime_hosts.each_with_object({}) do |host, lookup|
|
|
214
|
+
next unless host.is_a?(Hash) && host['mac']
|
|
215
|
+
|
|
216
|
+
lookup[host['mac'].upcase] = host
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Process config hosts and merge with runtime data
|
|
220
|
+
all_hosts = config_hosts.map do |config_host|
|
|
221
|
+
next unless config_host.is_a?(Hash) && config_host['mac']
|
|
222
|
+
|
|
223
|
+
mac = config_host['mac'].upcase
|
|
224
|
+
runtime_host = runtime_by_mac.delete(mac) || {}
|
|
225
|
+
normalize_host(config_host, runtime_host)
|
|
226
|
+
end.compact
|
|
227
|
+
|
|
228
|
+
# Add any remaining runtime-only hosts
|
|
229
|
+
runtime_by_mac.values.each do |runtime_host|
|
|
230
|
+
all_hosts << normalize_host({}, runtime_host)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
all_hosts
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def normalize_host(config_data, runtime_data)
|
|
237
|
+
config_data ||= {}
|
|
238
|
+
runtime_data ||= {}
|
|
239
|
+
|
|
240
|
+
mac = config_data['mac'] || runtime_data['mac']
|
|
241
|
+
return nil unless mac
|
|
242
|
+
|
|
243
|
+
{
|
|
244
|
+
mac: mac.upcase,
|
|
245
|
+
name: config_data['name'] || runtime_data['name'] || runtime_data['hostname'],
|
|
246
|
+
hostname: runtime_data['hostname'],
|
|
247
|
+
ip: runtime_data['ip'],
|
|
248
|
+
interface: runtime_data['interface'],
|
|
249
|
+
via: runtime_data['via'],
|
|
250
|
+
policy: config_data['policy'],
|
|
251
|
+
permit: normalize_boolean(config_data['permit']),
|
|
252
|
+
deny: normalize_boolean(config_data['deny']),
|
|
253
|
+
schedule: config_data['schedule'],
|
|
254
|
+
active: normalize_boolean(runtime_data['active']),
|
|
255
|
+
registered: normalize_boolean(runtime_data['registered']),
|
|
256
|
+
access: runtime_data['access'],
|
|
257
|
+
rxbytes: runtime_data['rxbytes'],
|
|
258
|
+
txbytes: runtime_data['txbytes'],
|
|
259
|
+
uptime: runtime_data['uptime'],
|
|
260
|
+
first_seen: runtime_data['first-seen'],
|
|
261
|
+
last_seen: runtime_data['last-seen']
|
|
262
|
+
}
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def webhelp_event
|
|
266
|
+
{
|
|
267
|
+
'webhelp' => {
|
|
268
|
+
'event' => {
|
|
269
|
+
'push' => {
|
|
270
|
+
'data' => '{"type":"configuration_change","value":{"url":"/policies/policy-consumers"}}'
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def save_config_command
|
|
278
|
+
{ 'system' => { 'configuration' => { 'save' => {} } } }
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
module Keenetic
|
|
2
|
+
module Resources
|
|
3
|
+
# Manages static routes configuration on the router.
|
|
4
|
+
#
|
|
5
|
+
# == API Endpoints Used
|
|
6
|
+
#
|
|
7
|
+
# === Reading Static Routes
|
|
8
|
+
# POST /rci/ (batch format)
|
|
9
|
+
# Body: [{"show": {"sc": {"ip": {"route": {}}}}}]
|
|
10
|
+
# Returns: Static routes from router configuration
|
|
11
|
+
#
|
|
12
|
+
# === Adding Static Routes
|
|
13
|
+
# POST /rci/ (batch format)
|
|
14
|
+
# Body: [
|
|
15
|
+
# {"webhelp": {"event": {"push": {"data": "..."}}}},
|
|
16
|
+
# {"ip": {"route": {...}}},
|
|
17
|
+
# {"system": {"configuration": {"save": {}}}}
|
|
18
|
+
# ]
|
|
19
|
+
#
|
|
20
|
+
# === Deleting Static Routes
|
|
21
|
+
# POST /rci/ (batch format)
|
|
22
|
+
# Body: [
|
|
23
|
+
# {"webhelp": {"event": {"push": {"data": "..."}}}},
|
|
24
|
+
# {"ip": {"route": {"no": true, ...}}},
|
|
25
|
+
# {"system": {"configuration": {"save": {}}}}
|
|
26
|
+
# ]
|
|
27
|
+
#
|
|
28
|
+
# == CIDR Support
|
|
29
|
+
# The gem automatically converts CIDR notation to subnet masks:
|
|
30
|
+
# host: "10.0.0.0/24" -> network: "10.0.0.0", mask: "255.255.255.0"
|
|
31
|
+
# host: "1.2.3.4/32" -> host: "1.2.3.4"
|
|
32
|
+
#
|
|
33
|
+
class Routes < Base
|
|
34
|
+
# CIDR prefix to subnet mask mapping
|
|
35
|
+
CIDR_TO_MASK = {
|
|
36
|
+
8 => '255.0.0.0',
|
|
37
|
+
9 => '255.128.0.0',
|
|
38
|
+
10 => '255.192.0.0',
|
|
39
|
+
11 => '255.224.0.0',
|
|
40
|
+
12 => '255.240.0.0',
|
|
41
|
+
13 => '255.248.0.0',
|
|
42
|
+
14 => '255.252.0.0',
|
|
43
|
+
15 => '255.254.0.0',
|
|
44
|
+
16 => '255.255.0.0',
|
|
45
|
+
17 => '255.255.128.0',
|
|
46
|
+
18 => '255.255.192.0',
|
|
47
|
+
19 => '255.255.224.0',
|
|
48
|
+
20 => '255.255.240.0',
|
|
49
|
+
21 => '255.255.248.0',
|
|
50
|
+
22 => '255.255.252.0',
|
|
51
|
+
23 => '255.255.254.0',
|
|
52
|
+
24 => '255.255.255.0',
|
|
53
|
+
25 => '255.255.255.128',
|
|
54
|
+
26 => '255.255.255.192',
|
|
55
|
+
27 => '255.255.255.224',
|
|
56
|
+
28 => '255.255.255.240',
|
|
57
|
+
29 => '255.255.255.248',
|
|
58
|
+
30 => '255.255.255.252',
|
|
59
|
+
31 => '255.255.255.254',
|
|
60
|
+
32 => '255.255.255.255'
|
|
61
|
+
}.freeze
|
|
62
|
+
|
|
63
|
+
# Get all static routes from router configuration.
|
|
64
|
+
#
|
|
65
|
+
# == Keenetic API Request
|
|
66
|
+
# POST /rci/ (batch format)
|
|
67
|
+
# Body: [{"show": {"sc": {"ip": {"route": {}}}}}]
|
|
68
|
+
#
|
|
69
|
+
# == Response Structure from API
|
|
70
|
+
# [
|
|
71
|
+
# {
|
|
72
|
+
# "network": "10.0.0.0",
|
|
73
|
+
# "mask": "255.255.255.0",
|
|
74
|
+
# "interface": "Wireguard0",
|
|
75
|
+
# "gateway": "",
|
|
76
|
+
# "auto": true,
|
|
77
|
+
# "reject": false,
|
|
78
|
+
# "comment": "VPN route"
|
|
79
|
+
# }
|
|
80
|
+
# ]
|
|
81
|
+
#
|
|
82
|
+
# @return [Array<Hash>] List of normalized static route hashes
|
|
83
|
+
# @example
|
|
84
|
+
# routes = client.routes.all
|
|
85
|
+
# # => [{ network: "10.0.0.0", mask: "255.255.255.0", interface: "Wireguard0", ... }]
|
|
86
|
+
#
|
|
87
|
+
def all
|
|
88
|
+
response = client.batch([{ 'show' => { 'sc' => { 'ip' => { 'route' => {} } } } }])
|
|
89
|
+
routes_data = extract_routes_from_response(response)
|
|
90
|
+
normalize_routes(routes_data)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Add a single static route.
|
|
94
|
+
#
|
|
95
|
+
# == Keenetic API Request
|
|
96
|
+
# POST /rci/ (batch format)
|
|
97
|
+
# Body: [
|
|
98
|
+
# {"webhelp": {"event": {"push": {"data": "{\"type\":\"configuration_change\",\"value\":{\"url\":\"/staticRoutes\"}}"}}}},
|
|
99
|
+
# {"ip": {"route": {"host|network": "...", "mask": "...", "interface": "...", ...}}},
|
|
100
|
+
# {"system": {"configuration": {"save": {}}}}
|
|
101
|
+
# ]
|
|
102
|
+
#
|
|
103
|
+
# @param host [String, nil] Single host IP (e.g., "1.2.3.4" or "1.2.3.4/32"), mutually exclusive with network/mask
|
|
104
|
+
# @param network [String, nil] Network address (e.g., "10.0.0.0" or "10.0.0.0/24")
|
|
105
|
+
# @param mask [String, nil] Subnet mask (e.g., "255.255.255.0"), required with network unless CIDR notation used
|
|
106
|
+
# @param interface [String] Interface name (required, e.g., "Wireguard0")
|
|
107
|
+
# @param comment [String] Route description (required)
|
|
108
|
+
# @param gateway [String] Gateway address (optional, default "")
|
|
109
|
+
# @param auto [Boolean] Auto-enable route (optional, default true)
|
|
110
|
+
# @param reject [Boolean] Reject route (optional, default false)
|
|
111
|
+
# @return [Array<Hash>] API response
|
|
112
|
+
# @raise [ArgumentError] if required parameters are missing or invalid
|
|
113
|
+
#
|
|
114
|
+
# @example Add host route
|
|
115
|
+
# client.routes.add(host: "1.2.3.4", interface: "Wireguard0", comment: "VPN host")
|
|
116
|
+
#
|
|
117
|
+
# @example Add network route with CIDR
|
|
118
|
+
# client.routes.add(network: "10.0.0.0/24", interface: "Wireguard0", comment: "VPN network")
|
|
119
|
+
#
|
|
120
|
+
# @example Add network route with explicit mask
|
|
121
|
+
# client.routes.add(network: "10.0.0.0", mask: "255.255.255.0", interface: "Wireguard0", comment: "VPN network")
|
|
122
|
+
#
|
|
123
|
+
def add(host: nil, network: nil, mask: nil, interface:, comment:, gateway: '', auto: true, reject: false)
|
|
124
|
+
route_params = build_route_params(
|
|
125
|
+
host: host,
|
|
126
|
+
network: network,
|
|
127
|
+
mask: mask,
|
|
128
|
+
interface: interface,
|
|
129
|
+
comment: comment,
|
|
130
|
+
gateway: gateway,
|
|
131
|
+
auto: auto,
|
|
132
|
+
reject: reject
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
commands = [
|
|
136
|
+
webhelp_event,
|
|
137
|
+
{ 'ip' => { 'route' => route_params } },
|
|
138
|
+
save_config_command
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
response = client.batch(commands)
|
|
142
|
+
validate_route_response(response)
|
|
143
|
+
response
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Add multiple static routes in a single request.
|
|
147
|
+
#
|
|
148
|
+
# == Keenetic API Request
|
|
149
|
+
# POST /rci/ (batch format)
|
|
150
|
+
# Body: [
|
|
151
|
+
# {"webhelp": {"event": {"push": {"data": "..."}}}},
|
|
152
|
+
# {"ip": {"route": {...}}},
|
|
153
|
+
# {"ip": {"route": {...}}},
|
|
154
|
+
# ...
|
|
155
|
+
# {"system": {"configuration": {"save": {}}}}
|
|
156
|
+
# ]
|
|
157
|
+
#
|
|
158
|
+
# @param routes [Array<Hash>] Array of route parameter hashes (same format as #add)
|
|
159
|
+
# @return [Array<Hash>] API response
|
|
160
|
+
# @raise [ArgumentError] if routes is empty or any route has invalid parameters
|
|
161
|
+
#
|
|
162
|
+
# @example Add multiple routes
|
|
163
|
+
# client.routes.add_batch([
|
|
164
|
+
# { host: "1.2.3.4", interface: "Wireguard0", comment: "Host 1" },
|
|
165
|
+
# { network: "10.0.0.0/24", interface: "Wireguard0", comment: "Network 1" }
|
|
166
|
+
# ])
|
|
167
|
+
#
|
|
168
|
+
def add_batch(routes)
|
|
169
|
+
raise ArgumentError, 'Routes array cannot be empty' if routes.nil? || routes.empty?
|
|
170
|
+
|
|
171
|
+
commands = [webhelp_event]
|
|
172
|
+
|
|
173
|
+
routes.each do |route|
|
|
174
|
+
route_params = build_route_params(
|
|
175
|
+
host: route[:host],
|
|
176
|
+
network: route[:network],
|
|
177
|
+
mask: route[:mask],
|
|
178
|
+
interface: route[:interface],
|
|
179
|
+
comment: route[:comment],
|
|
180
|
+
gateway: route[:gateway] || '',
|
|
181
|
+
auto: route.fetch(:auto, true),
|
|
182
|
+
reject: route.fetch(:reject, false)
|
|
183
|
+
)
|
|
184
|
+
commands << { 'ip' => { 'route' => route_params } }
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
commands << save_config_command
|
|
188
|
+
|
|
189
|
+
response = client.batch(commands)
|
|
190
|
+
validate_batch_response(response, routes.size)
|
|
191
|
+
response
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Delete a single static route.
|
|
195
|
+
#
|
|
196
|
+
# == Keenetic API Request
|
|
197
|
+
# POST /rci/ (batch format)
|
|
198
|
+
# Body: [
|
|
199
|
+
# {"webhelp": {"event": {"push": {"data": "..."}}}},
|
|
200
|
+
# {"ip": {"route": {"no": true, "host|network": "...", "mask": "..."}}},
|
|
201
|
+
# {"system": {"configuration": {"save": {}}}}
|
|
202
|
+
# ]
|
|
203
|
+
#
|
|
204
|
+
# @param host [String, nil] Single host IP (e.g., "1.2.3.4")
|
|
205
|
+
# @param network [String, nil] Network address (e.g., "10.0.0.0")
|
|
206
|
+
# @param mask [String, nil] Subnet mask (e.g., "255.255.255.0")
|
|
207
|
+
# @return [Array<Hash>] API response
|
|
208
|
+
# @raise [ArgumentError] if neither host nor network is provided
|
|
209
|
+
#
|
|
210
|
+
# @example Delete host route
|
|
211
|
+
# client.routes.delete(host: "1.2.3.4")
|
|
212
|
+
#
|
|
213
|
+
# @example Delete network route with CIDR
|
|
214
|
+
# client.routes.delete(network: "10.0.0.0/24")
|
|
215
|
+
#
|
|
216
|
+
# @example Delete network route with explicit mask
|
|
217
|
+
# client.routes.delete(network: "10.0.0.0", mask: "255.255.255.0")
|
|
218
|
+
#
|
|
219
|
+
def delete(host: nil, network: nil, mask: nil)
|
|
220
|
+
route_params = build_delete_params(host: host, network: network, mask: mask)
|
|
221
|
+
|
|
222
|
+
commands = [
|
|
223
|
+
webhelp_event,
|
|
224
|
+
{ 'ip' => { 'route' => route_params } },
|
|
225
|
+
save_config_command
|
|
226
|
+
]
|
|
227
|
+
|
|
228
|
+
client.batch(commands)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Delete multiple static routes in a single request.
|
|
232
|
+
#
|
|
233
|
+
# == Keenetic API Request
|
|
234
|
+
# POST /rci/ (batch format)
|
|
235
|
+
# Body: [
|
|
236
|
+
# {"webhelp": {"event": {"push": {"data": "..."}}}},
|
|
237
|
+
# {"ip": {"route": {"no": true, ...}}},
|
|
238
|
+
# {"ip": {"route": {"no": true, ...}}},
|
|
239
|
+
# ...
|
|
240
|
+
# {"system": {"configuration": {"save": {}}}}
|
|
241
|
+
# ]
|
|
242
|
+
#
|
|
243
|
+
# @param routes [Array<Hash>] Array of route identifiers (host or network/mask)
|
|
244
|
+
# @return [Array<Hash>] API response
|
|
245
|
+
# @raise [ArgumentError] if routes is empty
|
|
246
|
+
#
|
|
247
|
+
# @example Delete multiple routes
|
|
248
|
+
# client.routes.delete_batch([
|
|
249
|
+
# { host: "1.2.3.4" },
|
|
250
|
+
# { network: "10.0.0.0/24" }
|
|
251
|
+
# ])
|
|
252
|
+
#
|
|
253
|
+
def delete_batch(routes)
|
|
254
|
+
raise ArgumentError, 'Routes array cannot be empty' if routes.nil? || routes.empty?
|
|
255
|
+
|
|
256
|
+
commands = [webhelp_event]
|
|
257
|
+
|
|
258
|
+
routes.each do |route|
|
|
259
|
+
route_params = build_delete_params(
|
|
260
|
+
host: route[:host],
|
|
261
|
+
network: route[:network],
|
|
262
|
+
mask: route[:mask]
|
|
263
|
+
)
|
|
264
|
+
commands << { 'ip' => { 'route' => route_params } }
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
commands << save_config_command
|
|
268
|
+
|
|
269
|
+
client.batch(commands)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Convert CIDR notation to subnet mask.
|
|
273
|
+
#
|
|
274
|
+
# @param cidr [Integer, String] CIDR prefix length (8-32)
|
|
275
|
+
# @return [String] Subnet mask in dotted notation
|
|
276
|
+
# @raise [ArgumentError] if CIDR is invalid
|
|
277
|
+
#
|
|
278
|
+
# @example
|
|
279
|
+
# Keenetic::Resources::Routes.cidr_to_mask(24)
|
|
280
|
+
# # => "255.255.255.0"
|
|
281
|
+
#
|
|
282
|
+
def self.cidr_to_mask(cidr)
|
|
283
|
+
prefix = cidr.to_i
|
|
284
|
+
raise ArgumentError, "Invalid CIDR prefix: #{cidr}" unless CIDR_TO_MASK.key?(prefix)
|
|
285
|
+
|
|
286
|
+
CIDR_TO_MASK[prefix]
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
private
|
|
290
|
+
|
|
291
|
+
def extract_routes_from_response(response)
|
|
292
|
+
return [] unless response.is_a?(Array) && response.first.is_a?(Hash)
|
|
293
|
+
|
|
294
|
+
response.dig(0, 'show', 'sc', 'ip', 'route') || []
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def normalize_routes(routes_data)
|
|
298
|
+
return [] unless routes_data.is_a?(Array)
|
|
299
|
+
|
|
300
|
+
routes_data.map { |route| normalize_route(route) }.compact
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def normalize_route(data)
|
|
304
|
+
return nil unless data.is_a?(Hash)
|
|
305
|
+
|
|
306
|
+
{
|
|
307
|
+
network: data['network'],
|
|
308
|
+
mask: data['mask'],
|
|
309
|
+
host: data['host'],
|
|
310
|
+
interface: data['interface'],
|
|
311
|
+
gateway: data['gateway'],
|
|
312
|
+
comment: data['comment'],
|
|
313
|
+
auto: normalize_boolean(data['auto']),
|
|
314
|
+
reject: normalize_boolean(data['reject'])
|
|
315
|
+
}
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def build_route_params(host:, network:, mask:, interface:, comment:, gateway:, auto:, reject:)
|
|
319
|
+
raise ArgumentError, 'Interface is required' if interface.nil? || interface.to_s.strip.empty?
|
|
320
|
+
raise ArgumentError, 'Comment is required' if comment.nil? || comment.to_s.strip.empty?
|
|
321
|
+
|
|
322
|
+
params = {}
|
|
323
|
+
|
|
324
|
+
if host && !host.to_s.strip.empty?
|
|
325
|
+
parsed = parse_cidr(host)
|
|
326
|
+
if parsed[:cidr] == 32 || parsed[:cidr].nil?
|
|
327
|
+
params['host'] = parsed[:address]
|
|
328
|
+
else
|
|
329
|
+
# If CIDR is not /32, treat as network
|
|
330
|
+
params['network'] = parsed[:address]
|
|
331
|
+
params['mask'] = self.class.cidr_to_mask(parsed[:cidr])
|
|
332
|
+
end
|
|
333
|
+
elsif network && !network.to_s.strip.empty?
|
|
334
|
+
parsed = parse_cidr(network)
|
|
335
|
+
params['network'] = parsed[:address]
|
|
336
|
+
params['mask'] = parsed[:cidr] ? self.class.cidr_to_mask(parsed[:cidr]) : mask
|
|
337
|
+
raise ArgumentError, 'Mask is required for network routes without CIDR notation' if params['mask'].nil?
|
|
338
|
+
else
|
|
339
|
+
raise ArgumentError, 'Either host or network must be provided'
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
params['interface'] = interface
|
|
343
|
+
params['comment'] = comment
|
|
344
|
+
params['gateway'] = gateway.to_s
|
|
345
|
+
params['auto'] = auto
|
|
346
|
+
params['reject'] = reject
|
|
347
|
+
|
|
348
|
+
params
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def build_delete_params(host:, network:, mask:)
|
|
352
|
+
params = { 'no' => true }
|
|
353
|
+
|
|
354
|
+
if host && !host.to_s.strip.empty?
|
|
355
|
+
parsed = parse_cidr(host)
|
|
356
|
+
if parsed[:cidr] == 32 || parsed[:cidr].nil?
|
|
357
|
+
params['host'] = parsed[:address]
|
|
358
|
+
else
|
|
359
|
+
params['network'] = parsed[:address]
|
|
360
|
+
params['mask'] = self.class.cidr_to_mask(parsed[:cidr])
|
|
361
|
+
end
|
|
362
|
+
elsif network && !network.to_s.strip.empty?
|
|
363
|
+
parsed = parse_cidr(network)
|
|
364
|
+
params['network'] = parsed[:address]
|
|
365
|
+
params['mask'] = parsed[:cidr] ? self.class.cidr_to_mask(parsed[:cidr]) : mask
|
|
366
|
+
raise ArgumentError, 'Mask is required for network routes without CIDR notation' if params['mask'].nil?
|
|
367
|
+
else
|
|
368
|
+
raise ArgumentError, 'Either host or network must be provided'
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
params
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def parse_cidr(address)
|
|
375
|
+
return { address: address, cidr: nil } unless address.to_s.include?('/')
|
|
376
|
+
|
|
377
|
+
parts = address.to_s.split('/')
|
|
378
|
+
{ address: parts[0], cidr: parts[1].to_i }
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def webhelp_event
|
|
382
|
+
{
|
|
383
|
+
'webhelp' => {
|
|
384
|
+
'event' => {
|
|
385
|
+
'push' => {
|
|
386
|
+
'data' => '{"type":"configuration_change","value":{"url":"/staticRoutes"}}'
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def save_config_command
|
|
394
|
+
{ 'system' => { 'configuration' => { 'save' => {} } } }
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def validate_route_response(response)
|
|
398
|
+
return unless response.is_a?(Array)
|
|
399
|
+
|
|
400
|
+
# Response index 1 is the route command response
|
|
401
|
+
route_response = response[1]
|
|
402
|
+
return unless route_response.is_a?(Hash)
|
|
403
|
+
|
|
404
|
+
status = route_response.dig('ip', 'route', 'status')
|
|
405
|
+
return unless status.is_a?(Array) && status.first.is_a?(Hash)
|
|
406
|
+
|
|
407
|
+
if status.first['status'] == 'error'
|
|
408
|
+
error_msg = status.first['message'] || 'Unknown error'
|
|
409
|
+
raise ApiError.new("Failed to add route: #{error_msg}")
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def validate_batch_response(response, routes_count)
|
|
414
|
+
return unless response.is_a?(Array)
|
|
415
|
+
|
|
416
|
+
# Route responses start at index 1 (after webhelp event)
|
|
417
|
+
(1..routes_count).each do |i|
|
|
418
|
+
route_response = response[i]
|
|
419
|
+
next unless route_response.is_a?(Hash)
|
|
420
|
+
|
|
421
|
+
status = route_response.dig('ip', 'route', 'status')
|
|
422
|
+
next unless status.is_a?(Array) && status.first.is_a?(Hash)
|
|
423
|
+
|
|
424
|
+
if status.first['status'] == 'error'
|
|
425
|
+
error_msg = status.first['message'] || 'Unknown error'
|
|
426
|
+
raise ApiError.new("Failed to add route at index #{i - 1}: #{error_msg}")
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
end
|
data/lib/keenetic/version.rb
CHANGED
data/lib/keenetic.rb
CHANGED
|
@@ -13,6 +13,9 @@ require_relative 'keenetic/resources/policies'
|
|
|
13
13
|
require_relative 'keenetic/resources/dhcp'
|
|
14
14
|
require_relative 'keenetic/resources/routing'
|
|
15
15
|
require_relative 'keenetic/resources/logs'
|
|
16
|
+
require_relative 'keenetic/resources/routes'
|
|
17
|
+
require_relative 'keenetic/resources/hotspot'
|
|
18
|
+
require_relative 'keenetic/resources/config'
|
|
16
19
|
|
|
17
20
|
# Keenetic Router API Client
|
|
18
21
|
#
|
|
@@ -57,6 +60,23 @@ require_relative 'keenetic/resources/logs'
|
|
|
57
60
|
# # Ports
|
|
58
61
|
# client.ports.all # Physical port statuses
|
|
59
62
|
#
|
|
63
|
+
# # Static Routes
|
|
64
|
+
# client.routes.all # All static routes
|
|
65
|
+
# client.routes.add(...) # Add static route
|
|
66
|
+
# client.routes.delete(...) # Delete static route
|
|
67
|
+
#
|
|
68
|
+
# # Hotspot / Policies
|
|
69
|
+
# client.hotspot.policies # All IP policies
|
|
70
|
+
# client.hotspot.hosts # All hosts with policies
|
|
71
|
+
# client.hotspot.set_host_policy(mac: '...', policy: '...')
|
|
72
|
+
#
|
|
73
|
+
# # Configuration
|
|
74
|
+
# client.system_config.save # Save configuration
|
|
75
|
+
# client.system_config.download # Download startup config
|
|
76
|
+
#
|
|
77
|
+
# # Raw RCI Access
|
|
78
|
+
# client.rci({ ... }) # Execute arbitrary RCI commands
|
|
79
|
+
#
|
|
60
80
|
# == Error Handling
|
|
61
81
|
#
|
|
62
82
|
# begin
|
metadata
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: keenetic
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
|
-
- Anton
|
|
7
|
+
- Anton Zaytsev
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
@@ -45,24 +45,28 @@ executables: []
|
|
|
45
45
|
extensions: []
|
|
46
46
|
extra_rdoc_files: []
|
|
47
47
|
files:
|
|
48
|
+
- LICENSE.txt
|
|
48
49
|
- README.md
|
|
49
50
|
- lib/keenetic.rb
|
|
50
51
|
- lib/keenetic/client.rb
|
|
51
52
|
- lib/keenetic/configuration.rb
|
|
52
53
|
- lib/keenetic/errors.rb
|
|
53
54
|
- lib/keenetic/resources/base.rb
|
|
55
|
+
- lib/keenetic/resources/config.rb
|
|
54
56
|
- lib/keenetic/resources/devices.rb
|
|
55
57
|
- lib/keenetic/resources/dhcp.rb
|
|
58
|
+
- lib/keenetic/resources/hotspot.rb
|
|
56
59
|
- lib/keenetic/resources/internet.rb
|
|
57
60
|
- lib/keenetic/resources/logs.rb
|
|
58
61
|
- lib/keenetic/resources/network.rb
|
|
59
62
|
- lib/keenetic/resources/policies.rb
|
|
60
63
|
- lib/keenetic/resources/ports.rb
|
|
64
|
+
- lib/keenetic/resources/routes.rb
|
|
61
65
|
- lib/keenetic/resources/routing.rb
|
|
62
66
|
- lib/keenetic/resources/system.rb
|
|
63
67
|
- lib/keenetic/resources/wifi.rb
|
|
64
68
|
- lib/keenetic/version.rb
|
|
65
|
-
homepage: https://github.com/
|
|
69
|
+
homepage: https://github.com/antonzaytsev/keenetic-ruby
|
|
66
70
|
licenses:
|
|
67
71
|
- MIT
|
|
68
72
|
metadata:
|