enfcli 4.0.0 → 5.0.0.pre.alpha
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.circleci/Dockerfile +2 -2
- data/.circleci/config.yml +5 -0
- data/Gemfile.lock +38 -26
- data/Makefile +7 -0
- data/README.md +52 -7
- data/enfcli.gemspec +28 -26
- data/format.sh +9 -0
- data/lib/enfapi.rb +184 -237
- data/lib/enfapi/dns.rb +95 -0
- data/lib/enfapi/firewall.rb +37 -0
- data/lib/enfapi/user.rb +75 -0
- data/lib/enfcli.rb +211 -111
- data/lib/enfcli/commands/captive.rb +518 -157
- data/lib/enfcli/commands/user.rb +208 -160
- data/lib/enfcli/commands/xcr.rb +151 -119
- data/lib/enfcli/commands/xdns.rb +65 -55
- data/lib/enfcli/commands/xfw.rb +37 -37
- data/lib/enfcli/commands/xiam.rb +87 -80
- data/lib/enfcli/version.rb +2 -2
- data/lib/enfthor.rb +38 -14
- metadata +65 -5
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
#
|
4
|
-
# Copyright 2018-
|
4
|
+
# Copyright 2018-2020 Xaptum,Inc
|
5
5
|
#
|
6
6
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
7
|
# you may not use this file except in compliance with the License.
|
@@ -15,21 +15,22 @@
|
|
15
15
|
# See the License for the specific language governing permissions and
|
16
16
|
# limitations under the License.
|
17
17
|
#
|
18
|
-
require
|
19
|
-
require
|
20
|
-
require
|
21
|
-
require
|
18
|
+
require "enfthor"
|
19
|
+
require "enfapi"
|
20
|
+
require "json"
|
21
|
+
require "erb"
|
22
22
|
|
23
23
|
module EnfCli
|
24
24
|
module Cmd
|
25
25
|
class Captive < EnfThor
|
26
|
-
desc
|
27
|
-
|
28
|
-
method_option :name, default: nil, type: :string, banner:
|
29
|
-
desc:
|
30
|
-
method_option :domain, default: nil, type: :string, banner:
|
31
|
-
aliases:
|
32
|
-
method_option :ssid, default: nil, type: :string, banner:
|
26
|
+
desc "list-wifi-configurations",
|
27
|
+
"List wifi configuration information for all or the matching records."
|
28
|
+
method_option :name, default: nil, type: :string, banner: "NAME",
|
29
|
+
desc: "where NAME will match the user-given name."
|
30
|
+
method_option :domain, default: nil, type: :string, banner: "DOMAIN",
|
31
|
+
aliases: "-d"
|
32
|
+
method_option :ssid, default: nil, type: :string, banner: "SSID"
|
33
|
+
|
33
34
|
def list_wifi_configurations
|
34
35
|
try_with_rescue_in_session do
|
35
36
|
## TODO: V1 is only listing all of the wifi configurations, it is not
|
@@ -40,15 +41,16 @@ module EnfCli
|
|
40
41
|
# display the data
|
41
42
|
display_wifi_configs wifi_configs
|
42
43
|
else
|
43
|
-
say
|
44
|
+
say "No WiFi configurations found."
|
44
45
|
end
|
45
46
|
end
|
46
47
|
end
|
47
48
|
|
48
|
-
desc
|
49
|
+
desc "create-wifi-configuration", "Create a new wifi configuration."
|
49
50
|
method_option :'wifi-config-file', type: :string, required: true,
|
50
|
-
banner:
|
51
|
-
desc:
|
51
|
+
banner: "<file>",
|
52
|
+
desc: "<file> is JSON file with parameters required for configuring the router card."
|
53
|
+
|
52
54
|
def create_wifi_configuration
|
53
55
|
try_with_rescue_in_session do
|
54
56
|
json_file_name = options[:'wifi-config-file']
|
@@ -65,20 +67,21 @@ module EnfCli
|
|
65
67
|
end
|
66
68
|
end
|
67
69
|
|
68
|
-
desc
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
method_option :'wifi-id', defaut: nil, type: :string, banner:
|
73
|
-
desc:
|
74
|
-
method_option :version, defaut: nil, type: :string, banner:
|
75
|
-
desc:
|
76
|
-
method_option :'profile-id', defaut: nil, type: :string, banner:
|
77
|
-
desc:
|
78
|
-
method_option :'device-id', defaut: nil, type: :string, banner:
|
79
|
-
desc:
|
80
|
-
method_option :'wifi-name', defaut: nil, type: :string, banner:
|
81
|
-
desc:
|
70
|
+
desc "get-wifi-configuration",
|
71
|
+
"Get details of the specified wifi configuration. Exactly one of: " \
|
72
|
+
"--wifi-id, --profile-id, --device-id, or --wifi-name must be " \
|
73
|
+
"specified."
|
74
|
+
method_option :'wifi-id', defaut: nil, type: :string, banner: "WIFI-ID",
|
75
|
+
desc: "WIFI-ID is the UUID of the wifi record"
|
76
|
+
method_option :version, defaut: nil, type: :string, banner: "VERSION",
|
77
|
+
desc: "Optionally used with --wifi-id"
|
78
|
+
method_option :'profile-id', defaut: nil, type: :string, banner: "PROFILE-ID",
|
79
|
+
desc: "PROFILE-ID is the UUID of the profile."
|
80
|
+
method_option :'device-id', defaut: nil, type: :string, banner: "DEVICE-ID",
|
81
|
+
desc: "DEVICE-ID is the UUID of the device."
|
82
|
+
method_option :'wifi-name', defaut: nil, type: :string, banner: "WIFI-NAME",
|
83
|
+
desc: "Result matches any wifi configuration that contains the substring WIFI-NAME"
|
84
|
+
|
82
85
|
def get_wifi_configuration
|
83
86
|
try_with_rescue_in_session do
|
84
87
|
## TODO - Currently, this only handles wifi-id, add the others to v2
|
@@ -95,31 +98,30 @@ module EnfCli
|
|
95
98
|
num_query_opts += 1 if device_id
|
96
99
|
num_query_opts += 1 if wifi_name
|
97
100
|
|
98
|
-
|
99
|
-
raise 'ERROR: Exactly one of: --wifi-id, --profile-id, --device-id, or --wifi-name must be specified.' if num_query_opts != 1
|
101
|
+
raise "ERROR: Exactly one of: --wifi-id, --profile-id, --device-id, or --wifi-name must be specified." if num_query_opts != 1
|
100
102
|
|
101
103
|
if wifi_id
|
102
104
|
wifi_config = EnfApi::Captive.instance.get_wifi_configuration wifi_id, version
|
103
105
|
|
104
106
|
# display the data
|
105
107
|
display_wifi_detail wifi_config
|
106
|
-
|
107
108
|
end
|
108
109
|
end
|
109
110
|
end
|
110
111
|
|
111
|
-
desc
|
112
|
-
|
112
|
+
desc "update-wifi-configuration",
|
113
|
+
"Update an existing wifi configuration."
|
113
114
|
method_option :'wifi-id', type: :string,
|
114
115
|
required: true,
|
115
|
-
banner:
|
116
|
-
desc:
|
116
|
+
banner: "WIFI-ID",
|
117
|
+
desc: "WIFI-ID is the UUID of the wifi profile"
|
117
118
|
method_option :'wifi-config-file', type: :string,
|
118
119
|
required: true,
|
119
|
-
banner:
|
120
|
-
desc:
|
121
|
-
|
122
|
-
|
120
|
+
banner: "<file>",
|
121
|
+
desc: "<file> is JSON file with " \
|
122
|
+
"parameters required for " \
|
123
|
+
"configuring the router card."
|
124
|
+
|
123
125
|
def update_wifi_configuration
|
124
126
|
try_with_rescue_in_session do
|
125
127
|
json_file_name = options[:'wifi-config-file']
|
@@ -133,29 +135,29 @@ module EnfCli
|
|
133
135
|
end
|
134
136
|
end
|
135
137
|
|
138
|
+
desc "create-device",
|
139
|
+
"Add a new device to the database. This is only available to Xaptum administrators"
|
140
|
+
method_option :'device-id', type: :string, required: true, banner: "SERIAL-NUM",
|
141
|
+
desc: "SERIAL-NUM is the serial number of the device."
|
142
|
+
method_option :'device-name', type: :string, default: nil, banner: "DEVICE-NAME",
|
143
|
+
desc: "User-defined name for the device."
|
144
|
+
method_option :'mac-addr1', type: :string, default: nil, banner: "MAC-ADDR1",
|
145
|
+
desc: "MAC address 1 (wifi address on a wifi-enabled device)"
|
146
|
+
method_option :'mac-addr2', type: :string, default: nil, banner: "MAC-ADDR2",
|
147
|
+
desc: "MAC address 2"
|
148
|
+
method_option :'mac-addr3', type: :string, default: nil, banner: "MAC-ADDR3",
|
149
|
+
desc: "MAC address 3"
|
150
|
+
method_option :'mac-addr4', type: :string, default: nil, banner: "MAC-ADDR4",
|
151
|
+
desc: "MAC address 4"
|
152
|
+
method_option :'profile-id', type: :string, default: nil, banner: "PROFILE_ID",
|
153
|
+
desc: "UUID of the profile that the device will use. The profile must already exist."
|
154
|
+
method_option :model, type: :string, default: nil, banner: "MODEL",
|
155
|
+
desc: "Model identifier of the device"
|
136
156
|
|
137
|
-
desc 'create-device',
|
138
|
-
'Add a new device to the database. This is only available to Xaptum administrators'
|
139
|
-
method_option :'device-id', type: :string, required: true, banner: 'SERIAL-NUM',
|
140
|
-
desc: 'SERIAL-NUM is the serial number of the device.'
|
141
|
-
method_option :'device-name', type: :string, default: nil, banner: 'DEVICE-NAME',
|
142
|
-
desc: 'User-defined name for the device.'
|
143
|
-
method_option :'mac-addr1', type: :string, default: nil, banner: 'MAC-ADDR1',
|
144
|
-
desc: 'MAC address 1 (wifi address on a wifi-enabled device)'
|
145
|
-
method_option :'mac-addr2', type: :string, default: nil, banner: 'MAC-ADDR2',
|
146
|
-
desc: 'MAC address 2'
|
147
|
-
method_option :'mac-addr3', type: :string, default: nil, banner: 'MAC-ADDR3',
|
148
|
-
desc: 'MAC address 3'
|
149
|
-
method_option :'mac-addr4', type: :string, default: nil, banner: 'MAC-ADDR4',
|
150
|
-
desc: 'MAC address 4'
|
151
|
-
method_option :'profile-id', type: :string, default: nil, banner: 'PROFILE_ID',
|
152
|
-
desc: 'UUID of the profile that the device will use. The profile must already exist.'
|
153
|
-
method_option :model, type: :string, default: nil, banner: 'MODEL',
|
154
|
-
desc: 'Model identifier of the device'
|
155
157
|
def create_device
|
156
158
|
try_with_rescue_in_session do
|
157
159
|
new_device_hash = {
|
158
|
-
serial_number: options[:'device-id']
|
160
|
+
serial_number: options[:'device-id'],
|
159
161
|
}
|
160
162
|
|
161
163
|
mac1 = options[:'mac-addr1']
|
@@ -192,10 +194,11 @@ module EnfCli
|
|
192
194
|
end
|
193
195
|
end
|
194
196
|
|
195
|
-
desc
|
196
|
-
|
197
|
-
method_option :network, default: nil, type: :string, banner:
|
198
|
-
desc:
|
197
|
+
desc "list-devices",
|
198
|
+
"List basic device information for all devices matching the option specified."
|
199
|
+
method_option :network, default: nil, type: :string, banner: "NETWORK",
|
200
|
+
desc: "NETWORK is the ipv6 subnet used by the device."
|
201
|
+
|
199
202
|
def list_devices
|
200
203
|
try_with_rescue_in_session do
|
201
204
|
network = options[:network]
|
@@ -205,11 +208,12 @@ module EnfCli
|
|
205
208
|
end
|
206
209
|
end
|
207
210
|
|
208
|
-
desc
|
209
|
-
|
211
|
+
desc "get-device",
|
212
|
+
"Get details of the specified device."
|
210
213
|
method_option :'device-id', required: true, type: :string,
|
211
|
-
banner:
|
212
|
-
desc:
|
214
|
+
banner: "DEVICE-ID",
|
215
|
+
desc: "DEVICE-ID is either the device serial number or its ipv6 address."
|
216
|
+
|
213
217
|
def get_device
|
214
218
|
try_with_rescue_in_session do
|
215
219
|
device_id = ERB::Util::url_encode(options[:'device-id'])
|
@@ -220,14 +224,15 @@ module EnfCli
|
|
220
224
|
end
|
221
225
|
end
|
222
226
|
|
223
|
-
desc
|
224
|
-
|
225
|
-
method_option :'device-id', type: :string, required: true, banner:
|
226
|
-
desc:
|
227
|
-
method_option :'device-name', type: :string, default: nil, banner:
|
228
|
-
|
229
|
-
method_option :'profile-id', type: :string, default: nil, banner:
|
230
|
-
|
227
|
+
desc "update-device",
|
228
|
+
"Update an existing device record with the values specified."
|
229
|
+
method_option :'device-id', type: :string, required: true, banner: "DEVICE-ID",
|
230
|
+
desc: "DEVICE-ID is either the device serial number or its ipv6 address."
|
231
|
+
method_option :'device-name', type: :string, default: nil, banner: "DEVICE-NAME",
|
232
|
+
desc: "User-defined name for the device."
|
233
|
+
method_option :'profile-id', type: :string, default: nil, banner: "PROFILE_ID",
|
234
|
+
desc: "UUID of the profile that the device will use. The profile must already exist."
|
235
|
+
|
231
236
|
def update_device
|
232
237
|
try_with_rescue_in_session do
|
233
238
|
id = ERB::Util::url_encode(options[:'device-id'])
|
@@ -248,11 +253,12 @@ module EnfCli
|
|
248
253
|
end
|
249
254
|
end
|
250
255
|
|
251
|
-
desc
|
252
|
-
|
256
|
+
desc "get-device-status",
|
257
|
+
"Get the latest status of the specified device."
|
253
258
|
method_option :'device-id', required: true, type: :string,
|
254
|
-
banner:
|
255
|
-
desc:
|
259
|
+
banner: "DEVICE-ID",
|
260
|
+
desc: "DEVICE-ID is either the device serial number or its ipv6 address."
|
261
|
+
|
256
262
|
def get_device_status
|
257
263
|
try_with_rescue_in_session do
|
258
264
|
device_id = ERB::Util::url_encode(options[:'device-id'])
|
@@ -262,22 +268,27 @@ module EnfCli
|
|
262
268
|
end
|
263
269
|
end
|
264
270
|
|
265
|
-
desc
|
266
|
-
|
271
|
+
desc "create-profile",
|
272
|
+
"Create a new profile."
|
267
273
|
method_option :'profile-name', type: :string, required: true,
|
268
|
-
banner:
|
269
|
-
desc:
|
274
|
+
banner: "PROFILE-NAME",
|
275
|
+
desc: "PROFILE-NAME is the user-given name."
|
270
276
|
method_option :'device-mode', type: :string, required: true,
|
271
|
-
banner:
|
272
|
-
desc:
|
273
|
-
method_option :'wifi-id', type: :string, default: nil, banner:
|
274
|
-
desc:
|
277
|
+
banner: "DEVICE-MODE",
|
278
|
+
desc: "secure-host or passthrough"
|
279
|
+
method_option :'wifi-id', type: :string, default: nil, banner: "WIFI-ID",
|
280
|
+
desc: "WIFI-ID is the UUID of the wifi record that the profile will use. wifi record must already exist"
|
281
|
+
method_option :'update-id', type: :string, default: nil,
|
282
|
+
banner: "UPDATE-ID",
|
283
|
+
desc: "UPDATE-ID is the UUID of the firmware update record. " \
|
284
|
+
"The firmware-update record must already exist."
|
285
|
+
|
275
286
|
def create_profile
|
276
287
|
try_with_rescue_in_session do
|
277
288
|
profile_name = options[:'profile-name']
|
278
289
|
new_profile_hash = {
|
279
290
|
name: profile_name,
|
280
|
-
config: { mode: options[:'device-mode'] }
|
291
|
+
config: { mode: options[:'device-mode'] },
|
281
292
|
}
|
282
293
|
|
283
294
|
wifi = options[:'wifi-id']
|
@@ -286,16 +297,23 @@ module EnfCli
|
|
286
297
|
new_profile_hash[:config][:wifi][:id] = wifi
|
287
298
|
end
|
288
299
|
|
300
|
+
fw_update = options[:'update-id']
|
301
|
+
if fw_update
|
302
|
+
new_profile_hash[:config][:firmware] = {}
|
303
|
+
new_profile_hash[:config][:firmware][:id] = fw_update if fw_update
|
304
|
+
end
|
305
|
+
|
289
306
|
# send the POST to create a new profile
|
290
307
|
profile = EnfApi::Captive.instance.create_profile new_profile_hash
|
291
308
|
display_profile profile
|
292
309
|
end
|
293
310
|
end
|
294
311
|
|
295
|
-
desc
|
296
|
-
|
297
|
-
method_option :name, default: nil, type: :string, banner:
|
298
|
-
desc:
|
312
|
+
desc "list-profiles",
|
313
|
+
"List the existing profiles."
|
314
|
+
method_option :name, default: nil, type: :string, banner: "NAME",
|
315
|
+
desc: "where NAME will match all or part of the user-given profile name."
|
316
|
+
|
299
317
|
def list_profiles
|
300
318
|
try_with_rescue_in_session do
|
301
319
|
query_name = options[:name]
|
@@ -304,14 +322,14 @@ module EnfCli
|
|
304
322
|
end
|
305
323
|
end
|
306
324
|
|
307
|
-
desc
|
308
|
-
|
309
|
-
method_option :'profile-id', required: true, type: :string,
|
310
|
-
banner:
|
311
|
-
desc:
|
312
|
-
# TODO - server doesn't support version yet.
|
313
|
-
# method_option :version, default: nil, type: :integer, banner: 'VERSION',
|
314
|
-
# desc: 'Get a specific version.'
|
325
|
+
desc "get-profile",
|
326
|
+
"Get full detail listing of the profile."
|
327
|
+
method_option :'profile-id', required: true, type: :string,
|
328
|
+
banner: "PROFILE-ID",
|
329
|
+
desc: "PROFILE-ID is the UUID of the profile."
|
330
|
+
# TODO - server doesn't support version yet.
|
331
|
+
# method_option :version, default: nil, type: :integer, banner: 'VERSION',
|
332
|
+
# desc: 'Get a specific version.'
|
315
333
|
def get_profile
|
316
334
|
try_with_rescue_in_session do
|
317
335
|
profile = EnfApi::Captive.instance.get_profile options[:'profile-id']
|
@@ -319,44 +337,274 @@ module EnfCli
|
|
319
337
|
end
|
320
338
|
end
|
321
339
|
|
322
|
-
desc
|
323
|
-
|
324
|
-
|
325
|
-
method_option :'profile-id', required: true, type: :string,
|
326
|
-
banner:
|
327
|
-
desc:
|
340
|
+
desc "update-profile",
|
341
|
+
"Update a previously-created profile specified by PROFILE-ID. At " \
|
342
|
+
"least one property must be changed."
|
343
|
+
method_option :'profile-id', required: true, type: :string,
|
344
|
+
banner: "PROFILE-ID",
|
345
|
+
desc: "UUID of the device profile."
|
328
346
|
method_option :'profile-name', type: :string, default: nil,
|
329
|
-
banner:
|
330
|
-
desc:
|
347
|
+
banner: "PROFILE-NAME",
|
348
|
+
desc: "PROFILE-NAME is the user-given name."
|
331
349
|
method_option :'device-mode', type: :string, default: nil,
|
332
|
-
banner:
|
333
|
-
desc:
|
334
|
-
method_option :'wifi-id', type: :string, default: nil, banner:
|
335
|
-
desc:
|
350
|
+
banner: "DEVICE-MODE",
|
351
|
+
desc: "secure-host or passthrough"
|
352
|
+
method_option :'wifi-id', type: :string, default: nil, banner: "WIFI-ID",
|
353
|
+
desc: "WIFI-ID is the UUID of the wifi record that the profile will use. wifi record must already exist"
|
354
|
+
method_option :'update-id', type: :string, default: nil,
|
355
|
+
banner: "UPDATE-ID",
|
356
|
+
desc: "UPDATE-ID is the UUID of the firmware update record. " \
|
357
|
+
"The firmware-update record must already exist."
|
358
|
+
|
336
359
|
def update_profile
|
337
360
|
try_with_rescue_in_session do
|
338
361
|
id = options[:'profile-id']
|
339
362
|
name = options[:'profile-name']
|
340
363
|
mode = options[:'device-mode']
|
341
364
|
wifi_id = options[:'wifi-id']
|
365
|
+
fw_update = options[:'update-id']
|
342
366
|
|
343
|
-
raise "At least one option needs to change." if name == nil && mode == nil && wifi_id == nil
|
367
|
+
raise "At least one option needs to change." if name == nil && mode == nil && wifi_id == nil && fw_update == nil
|
344
368
|
|
345
369
|
update_hash = {}
|
346
370
|
update_hash[:name] = name if name
|
347
|
-
update_hash[:config] = {} if wifi_id || mode
|
371
|
+
update_hash[:config] = {} if wifi_id || mode || fw_update
|
348
372
|
update_hash[:config][:mode] = mode if mode
|
349
373
|
if wifi_id
|
350
374
|
update_hash[:config][:wifi] = {}
|
351
375
|
update_hash[:config][:wifi][:id] = wifi_id
|
352
376
|
end
|
353
377
|
|
378
|
+
if fw_update
|
379
|
+
update_hash[:config][:firmware] = {}
|
380
|
+
update_hash[:config][:firmware][:id] = fw_update
|
381
|
+
end
|
354
382
|
profile = EnfApi::Captive.instance.update_profile id, update_hash
|
355
383
|
display_profile profile
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
desc "list-firmware-images",
|
388
|
+
"Lists all of the available versions of the firmware"
|
389
|
+
|
390
|
+
def list_firmware_images
|
391
|
+
try_with_rescue_in_session do
|
392
|
+
data = EnfApi::Captive.instance.list_firmware_images
|
393
|
+
display_firmware_image_list data
|
394
|
+
end
|
395
|
+
end
|
396
|
+
|
397
|
+
desc "upload-firmware-image",
|
398
|
+
"Uploads a new firmware image. Each version will have multiple " \
|
399
|
+
"images -- one for each type of hardware. \nNOTE: This version " \
|
400
|
+
"doesn't actually upload the file, it merely informs the server of " \
|
401
|
+
"its existance."
|
402
|
+
method_option :'image-file', required: true, type: :string,
|
403
|
+
banner: "<file>", desc: "<file> is the firmware binary image."
|
404
|
+
method_option :version, required: true, type: :string,
|
405
|
+
banner: "VERSION",
|
406
|
+
desc: "VERSION is the release version."
|
407
|
+
method_option :'image-name', type: :string, default: nil, banner: "NAME",
|
408
|
+
desc: "NAME is the name to associate with the firmware " \
|
409
|
+
"image. This is usually the generated filename. " \
|
410
|
+
"Defaults to the base name of <file>"
|
411
|
+
|
412
|
+
def upload_firmware_image
|
413
|
+
try_with_rescue_in_session do
|
414
|
+
filename = options[:'image-file']
|
415
|
+
version = options[:version]
|
416
|
+
image_name = options[:'image-name']
|
417
|
+
|
418
|
+
temp_img_name = File.basename(filename)
|
419
|
+
image_name ||= temp_img_name
|
420
|
+
|
421
|
+
raise "image-name does not match image-file" if temp_img_name != image_name
|
422
|
+
|
423
|
+
# This version PUTs an empty body
|
424
|
+
resp = EnfApi::Captive.instance.upload_firmware_image version, image_name
|
425
|
+
|
426
|
+
if (resp.code == 200)
|
427
|
+
say "Upload complete."
|
428
|
+
else
|
429
|
+
say "Upload failed with code #{resp.code}"
|
430
|
+
end
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|
434
|
+
desc "get-firmware-info",
|
435
|
+
"Prints details of an existing firmware version."
|
436
|
+
method_option :version, required: true, type: :string,
|
437
|
+
banner: "VERSION", desc: "VERSION is the release version."
|
438
|
+
|
439
|
+
def get_firmware_info
|
440
|
+
try_with_rescue_in_session do
|
441
|
+
version = options[:version]
|
356
442
|
|
443
|
+
fw_info = EnfApi::Captive.instance.get_firmware_info version
|
444
|
+
|
445
|
+
display_firmware_detail fw_info
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
449
|
+
desc "create-schedule",
|
450
|
+
"Creates a schedule object that will be used to determine when a " \
|
451
|
+
"device or group of devices may be upgraded. The schedule will be " \
|
452
|
+
"uploaded from a JSON-formatted file."
|
453
|
+
method_option :schedule, required: true, type: :string,
|
454
|
+
banner: "<file>",
|
455
|
+
desc: "<file> is a JSON file containing the desired schedule."
|
456
|
+
|
457
|
+
def create_schedule
|
458
|
+
try_with_rescue_in_session do
|
459
|
+
filename = options[:schedule]
|
460
|
+
|
461
|
+
# reading the whole file - shouldn't get more than a few KB
|
462
|
+
content = File.read filename
|
463
|
+
sched_hash = JSON.parse(content)
|
464
|
+
|
465
|
+
resp_data = EnfApi::Captive.instance.create_schedule sched_hash
|
466
|
+
display_schedule_detail resp_data
|
357
467
|
end
|
358
468
|
end
|
359
469
|
|
470
|
+
desc "list-schedules",
|
471
|
+
"Prints a summary list of all existing schedules."
|
472
|
+
|
473
|
+
def list_schedules
|
474
|
+
try_with_rescue_in_session do
|
475
|
+
sched_list = EnfApi::Captive.instance.list_schedules
|
476
|
+
display_schedule_list sched_list
|
477
|
+
end
|
478
|
+
end
|
479
|
+
|
480
|
+
desc "get-schedule",
|
481
|
+
"Prints the detail of a specific, existing schedule."
|
482
|
+
method_option :'schedule-id', type: :string, required: true, banner: "ID",
|
483
|
+
desc: "ID is the system-assigned UUID of the schedule " \
|
484
|
+
"to retrieve."
|
485
|
+
|
486
|
+
def get_schedule
|
487
|
+
try_with_rescue_in_session do
|
488
|
+
sched_id = options[:'schedule-id']
|
489
|
+
sched_data = EnfApi::Captive.instance.get_schedule sched_id
|
490
|
+
display_schedule_detail sched_data
|
491
|
+
end
|
492
|
+
end
|
493
|
+
|
494
|
+
desc "update-schedule",
|
495
|
+
"Modifies an existing schedule by uploading an updated schedule in " \
|
496
|
+
"the form of a JSON file."
|
497
|
+
method_option :'schedule-id', type: :string, required: true, banner: "ID",
|
498
|
+
desc: "ID is the UUID of the schedule to update."
|
499
|
+
method_option :schedule, type: :string, required: true, bannder: "<file>",
|
500
|
+
desc: "<file> is a JSON file containing the desired schedule."
|
501
|
+
|
502
|
+
def update_schedule
|
503
|
+
try_with_rescue_in_session do
|
504
|
+
sched_id = options[:'schedule-id']
|
505
|
+
filename = options[:schedule]
|
506
|
+
|
507
|
+
# read in whole schedule file
|
508
|
+
content = File.read filename
|
509
|
+
sched_hash = JSON.parse(content)
|
510
|
+
|
511
|
+
resp_data = EnfApi::Captive.instance.update_schedule sched_id, sched_hash
|
512
|
+
display_schedule_detail resp_data
|
513
|
+
end
|
514
|
+
end
|
515
|
+
|
516
|
+
desc "delete-schedule",
|
517
|
+
"Deletes the specified, existing schedule."
|
518
|
+
method_option :'schedule-id', type: :string, required: true, banner: "ID",
|
519
|
+
desc: "ID is the system-assigned UUID of the schedule " \
|
520
|
+
"to delete."
|
521
|
+
|
522
|
+
def delete_schedule
|
523
|
+
try_with_rescue_in_session do
|
524
|
+
sched_id = options[:'schedule-id']
|
525
|
+
|
526
|
+
# get the name of the schedule
|
527
|
+
resp = EnfApi::Captive.instance.get_schedule(sched_id)
|
528
|
+
name = resp[:name]
|
529
|
+
|
530
|
+
resp = EnfApi::Captive.instance.delete_schedule sched_id
|
531
|
+
|
532
|
+
if (resp.code == 200)
|
533
|
+
say "Successfully deleted schedule named: #{name}"
|
534
|
+
elsif (resp.code == 409)
|
535
|
+
say "Schedule #{name} is being used by pending updates and cannot be deleted."
|
536
|
+
else
|
537
|
+
say "Failed to delete schedule #{name} with code #{resp.code}"
|
538
|
+
end
|
539
|
+
end
|
540
|
+
end
|
541
|
+
|
542
|
+
desc "list-firmware-updates",
|
543
|
+
"Lists the existing firmware update tasks."
|
544
|
+
|
545
|
+
def list_firmware_updates
|
546
|
+
try_with_rescue_in_session do
|
547
|
+
updates_list = EnfApi::Captive.instance.list_firmware_updates
|
548
|
+
display_updates_list updates_list
|
549
|
+
end
|
550
|
+
end
|
551
|
+
|
552
|
+
desc "get-firmware-update",
|
553
|
+
"Prints the details of the specified firmware update task."
|
554
|
+
method_option :'update-id', type: :string, required: true, banner: "ID",
|
555
|
+
desc: "ID is the UUID of the firmware update task"
|
556
|
+
|
557
|
+
def get_firmware_update
|
558
|
+
try_with_rescue_in_session do
|
559
|
+
update_id = options[:'update-id']
|
560
|
+
|
561
|
+
update_data = EnfApi::Captive.instance.get_firmware_update update_id
|
562
|
+
display_update_detail update_data
|
563
|
+
end
|
564
|
+
end
|
565
|
+
|
566
|
+
desc "create-firmware-update",
|
567
|
+
"Creates a firmware-update task that the system will use to update " \
|
568
|
+
"router-card firmware as prescribed in the specified JSON file."
|
569
|
+
method_option :update, type: :string, required: true, banner: "<file>",
|
570
|
+
desc: "<file> is a JSON file containing the details of " \
|
571
|
+
"the update task."
|
572
|
+
|
573
|
+
def create_firmware_update
|
574
|
+
try_with_rescue_in_session do
|
575
|
+
filename = options[:update]
|
576
|
+
|
577
|
+
# reading the whole file - shouldn't get more than a few KB
|
578
|
+
content = File.read filename
|
579
|
+
update_hash = JSON.parse(content)
|
580
|
+
|
581
|
+
resp_data = EnfApi::Captive.instance.create_firmware_update update_hash
|
582
|
+
display_update_detail resp_data
|
583
|
+
end
|
584
|
+
end
|
585
|
+
|
586
|
+
desc "modify-firmware-update",
|
587
|
+
"Modifies an existing firmware-update task with the information " \
|
588
|
+
"contained in the specified JSON file"
|
589
|
+
method_option :'update-id', type: :string, required: true, banner: "ID",
|
590
|
+
desc: "ID is the UUID of the firmware update task to be modified."
|
591
|
+
method_option :update, type: :string, required: true, banner: "<file>",
|
592
|
+
desc: "<file> is a JSON file containing the details of " \
|
593
|
+
"the update task."
|
594
|
+
|
595
|
+
def modify_firmware_update
|
596
|
+
try_with_rescue_in_session do
|
597
|
+
update_id = options[:'update-id']
|
598
|
+
filename = options[:update]
|
599
|
+
|
600
|
+
# reading the whole file - shouldn't get more than a few KB
|
601
|
+
content = File.read filename
|
602
|
+
update_hash = JSON.parse(content)
|
603
|
+
|
604
|
+
resp_data = EnfApi::Captive.instance.modify_firmware_update update_id, update_hash
|
605
|
+
display_update_detail resp_data
|
606
|
+
end
|
607
|
+
end
|
360
608
|
|
361
609
|
#########################################################################
|
362
610
|
#
|
@@ -367,7 +615,7 @@ module EnfCli
|
|
367
615
|
# Displays the wifi configuration summary list.
|
368
616
|
# TODO - Does non-Xaptum-admin columns only - add option to do admin list
|
369
617
|
def display_wifi_configs(configs)
|
370
|
-
headings = [
|
618
|
+
headings = ["ID", "Wifi Name", "Config Vers."]
|
371
619
|
rows = configs.map do |hash|
|
372
620
|
[hash[:id], hash[:name], hash[:version]]
|
373
621
|
end
|
@@ -376,25 +624,24 @@ module EnfCli
|
|
376
624
|
end
|
377
625
|
|
378
626
|
# Display a single wifi configuration in detail
|
379
|
-
def display_wifi_detail(wifi_data, full_listing=true)
|
627
|
+
def display_wifi_detail(wifi_data, full_listing = true, tabs = 0)
|
628
|
+
indent = " " * tabs
|
380
629
|
name = wifi_data[:name]
|
381
630
|
wifi_id = wifi_data[:id]
|
382
631
|
desc = wifi_data[:description]
|
383
632
|
nets = wifi_data[:networks]
|
384
633
|
|
385
|
-
say "
|
386
|
-
say "
|
387
|
-
say "Description : #{desc}", nil, true if desc
|
634
|
+
say indent + "Name : #{name}", nil, true
|
635
|
+
say indent + "Wifi ID : #{wifi_id}", nil, true
|
636
|
+
say indent + "Description : #{desc}", nil, true if desc
|
388
637
|
if full_listing
|
389
638
|
say "WiFi Networks :"
|
390
639
|
if nets
|
391
640
|
nets.each do |wifi_net|
|
392
|
-
display_wifi_net(wifi_net, 1)
|
641
|
+
display_wifi_net(wifi_net, tabs + 1)
|
393
642
|
end
|
394
643
|
end
|
395
|
-
|
396
644
|
end
|
397
|
-
|
398
645
|
end
|
399
646
|
|
400
647
|
def display_wifi_net(wifi_net, tabs)
|
@@ -412,7 +659,7 @@ module EnfCli
|
|
412
659
|
display_ipv6_addr(wifi_net[:IPv6], tabs + 1)
|
413
660
|
end
|
414
661
|
|
415
|
-
def display_ipv4_addr
|
662
|
+
def display_ipv4_addr(ipv4, tabs)
|
416
663
|
indent = " " * tabs
|
417
664
|
if ipv4.instance_of? String
|
418
665
|
say indent + "IPv4 : #{ipv4}", nil, true
|
@@ -435,7 +682,7 @@ module EnfCli
|
|
435
682
|
end
|
436
683
|
end
|
437
684
|
|
438
|
-
def display_ipv6_addr
|
685
|
+
def display_ipv6_addr(ipv6, tabs)
|
439
686
|
indent = " " * tabs
|
440
687
|
if ipv6.instance_of? String
|
441
688
|
say indent + "IPv6 : #{ipv6}", nil, true
|
@@ -458,8 +705,6 @@ module EnfCli
|
|
458
705
|
end
|
459
706
|
end
|
460
707
|
|
461
|
-
|
462
|
-
|
463
708
|
#
|
464
709
|
# display the device info.
|
465
710
|
# device_data is a hash matching the json structure
|
@@ -469,8 +714,8 @@ module EnfCli
|
|
469
714
|
ctl_addr = device_data[:control_address]
|
470
715
|
dev_addr = device_data[:device_address] || "\n"
|
471
716
|
mac_addrs = device_data[:mac_address]
|
472
|
-
firmware = device_data[:firmware_version] ||
|
473
|
-
model = device_data[:model] ||
|
717
|
+
firmware = device_data[:firmware_version] || "< not available >"
|
718
|
+
model = device_data[:model] || "< not available >"
|
474
719
|
profile = device_data[:profile]
|
475
720
|
status = device_data[:status]
|
476
721
|
|
@@ -485,18 +730,18 @@ module EnfCli
|
|
485
730
|
mac4 = mac_addrs[:'4']
|
486
731
|
|
487
732
|
if !mac1
|
488
|
-
say
|
733
|
+
say "Mac Address : < not available >"
|
489
734
|
elsif !mac2 && !mac3 && !mac4
|
490
735
|
say "Mac Address : #{mac1}", nil, true
|
491
736
|
else
|
492
|
-
say
|
737
|
+
say "Mac Address :"
|
493
738
|
say " 1 : #{mac1}", nil, true
|
494
739
|
say " 2 : #{mac2}", nil, true if mac2
|
495
740
|
say " 3 : #{mac3}", nil, true if mac3
|
496
741
|
say " 4 : #{mac4}", nil, true if mac4
|
497
742
|
end
|
498
743
|
else
|
499
|
-
say
|
744
|
+
say "Mac Address : < not available >"
|
500
745
|
end
|
501
746
|
|
502
747
|
say "Firmware Version : #{firmware}", nil, true
|
@@ -508,19 +753,19 @@ module EnfCli
|
|
508
753
|
say "Profile : < not available >"
|
509
754
|
end
|
510
755
|
display_device_status_summary status
|
511
|
-
say
|
756
|
+
say " ", nil, true
|
512
757
|
end
|
513
758
|
|
514
|
-
#
|
759
|
+
#
|
515
760
|
# display the status summary
|
516
761
|
#
|
517
|
-
def display_device_status_summary
|
762
|
+
def display_device_status_summary(device_status)
|
518
763
|
if device_status
|
519
|
-
mode = device_status[:router_mode] || device_status[:mode] ||
|
764
|
+
mode = device_status[:router_mode] || device_status[:mode] || "< not available >"
|
520
765
|
|
521
766
|
wifi = device_status[:wifi]
|
522
767
|
|
523
|
-
say
|
768
|
+
say "Status :"
|
524
769
|
say " Router Mode : #{mode}", nil, true
|
525
770
|
|
526
771
|
if wifi
|
@@ -539,21 +784,21 @@ module EnfCli
|
|
539
784
|
# status is a hash matching the json structure
|
540
785
|
#
|
541
786
|
def display_device_status(device_status)
|
542
|
-
sn = device_status[:serial_number] ||
|
543
|
-
mode = device_status[:router_mode] || device_status[:mode] ||
|
787
|
+
sn = device_status[:serial_number] || "< not available >"
|
788
|
+
mode = device_status[:router_mode] || device_status[:mode] || "< not available >"
|
544
789
|
|
545
|
-
uptime = device_status[:uptime] ||
|
546
|
-
refresh = device_status[:refresh_time] ||
|
790
|
+
uptime = device_status[:uptime] || "< not available >"
|
791
|
+
refresh = device_status[:refresh_time] || "< not available >"
|
547
792
|
|
548
793
|
wifi = device_status[:wifi]
|
549
794
|
if wifi
|
550
|
-
connected = wifi[:connected] ||
|
795
|
+
connected = wifi[:connected] || "< not available >"
|
551
796
|
ssid = wifi[:SSID]
|
552
797
|
ipv4 = wifi[:IPv4_addresses]
|
553
798
|
ipv6 = wifi[:IPv6_addresses]
|
554
799
|
wifi_config = wifi[:config]
|
555
800
|
else
|
556
|
-
connected =
|
801
|
+
connected = "< not available >"
|
557
802
|
ssid = nil
|
558
803
|
ipv4 = nil
|
559
804
|
ipv6 = nil
|
@@ -564,7 +809,7 @@ module EnfCli
|
|
564
809
|
say "Router Mode : #{mode}", nil, true
|
565
810
|
say "Uptime (in seconds) : #{uptime}", nil, true
|
566
811
|
say "Status refresh time : #{refresh}", nil, true
|
567
|
-
say "WIFI
|
812
|
+
say "WIFI status :"
|
568
813
|
say " connected : #{connected}", nil, true
|
569
814
|
say " SSID : #{ssid}" if ssid
|
570
815
|
if ipv4 && !ipv4.empty?
|
@@ -581,17 +826,27 @@ module EnfCli
|
|
581
826
|
end
|
582
827
|
end
|
583
828
|
|
584
|
-
|
829
|
+
say "WIFI configuration :"
|
830
|
+
display_wifi_detail(wifi_config, false, 1) if wifi_config
|
831
|
+
|
832
|
+
fw_status = device_status[:firmware]
|
833
|
+
if fw_status
|
834
|
+
image_name = fw_status[:image_name] || "< not available >"
|
835
|
+
image_state = fw_status[:state] || " < not available >"
|
836
|
+
say "Firmware Status :"
|
837
|
+
say " image name : #{image_name}", nil, true
|
838
|
+
say " operating state : #{image_state}", nil, true
|
839
|
+
end
|
585
840
|
|
586
|
-
say
|
841
|
+
say " ", nil, true
|
587
842
|
end
|
588
843
|
|
589
844
|
#
|
590
845
|
# display the devices summary list
|
591
846
|
#
|
592
847
|
def display_device_list(devices)
|
593
|
-
headings = [
|
594
|
-
|
848
|
+
headings = ["Serial No", "Dev Name", "Dev Addr", "Router Mode",
|
849
|
+
"Connected", "SSID"]
|
595
850
|
rows = devices.map do |hash|
|
596
851
|
status = hash[:status]
|
597
852
|
if status
|
@@ -601,16 +856,15 @@ module EnfCli
|
|
601
856
|
mode = status[:router_mode] || status[:mode]
|
602
857
|
|
603
858
|
[hash[:serial_number], hash[:device_name], hash[:device_address],
|
604
|
-
|
859
|
+
mode, connected, ssid]
|
605
860
|
else
|
606
861
|
[hash[:serial_number], hash[:device_name], hash[:device_address],
|
607
|
-
|
862
|
+
nil, nil, nil]
|
608
863
|
end
|
609
864
|
end
|
610
865
|
# [hash[:serial_number], hash[:device_name], hash[:device_address],
|
611
866
|
# hash[:status][:router_mode], hash[:status][:wifi][:connected],
|
612
867
|
# hash[:status][:wifi][:SSID]]
|
613
|
-
|
614
868
|
render_table(headings, rows)
|
615
869
|
end
|
616
870
|
|
@@ -618,13 +872,22 @@ module EnfCli
|
|
618
872
|
# Display profile detail
|
619
873
|
#
|
620
874
|
def display_profile(profile, summary = false)
|
621
|
-
indent = summary ?
|
875
|
+
indent = summary ? " " : ""
|
876
|
+
|
877
|
+
firmware = profile[:config][:firmware]
|
878
|
+
fw_id = "< not configured >"
|
879
|
+
if firmware
|
880
|
+
fw_id = firmware[:id]
|
881
|
+
fw_version = firmware[:version]
|
882
|
+
end
|
622
883
|
|
623
884
|
say indent + "Name : #{profile[:name]}", nil, true
|
624
885
|
say indent + "Profile ID : #{profile[:id]}", nil, true
|
625
886
|
say indent + "Configuration version : #{profile[:config][:version]}", nil, true
|
887
|
+
say indent + "Firmware Update ID : #{fw_id}", nil, true
|
626
888
|
unless summary
|
627
|
-
say "
|
889
|
+
say indent + " Update version : #{fw_version}", nil, true if fw_version
|
890
|
+
say indent + "Mode : #{profile[:config][:mode]}", nil, true
|
628
891
|
display_wifi_summary profile[:config][:wifi]
|
629
892
|
end
|
630
893
|
end
|
@@ -632,7 +895,7 @@ module EnfCli
|
|
632
895
|
#
|
633
896
|
# Display summary of the WIFI configuration info
|
634
897
|
#
|
635
|
-
def display_wifi_summary
|
898
|
+
def display_wifi_summary(wifi)
|
636
899
|
if wifi
|
637
900
|
say "Wifi config :"
|
638
901
|
say " id : #{wifi[:id]}", nil, true
|
@@ -645,22 +908,120 @@ module EnfCli
|
|
645
908
|
else
|
646
909
|
say "Wifi config : < not configured >"
|
647
910
|
end
|
648
|
-
|
649
911
|
end
|
650
912
|
|
651
913
|
#
|
652
914
|
# display the profile summary list
|
653
915
|
#
|
654
916
|
def display_profile_list(profiles)
|
655
|
-
headings = [
|
917
|
+
headings = ["Profile Name", "Profile ID", "Version", "Mode", "Wifi ID", "Firmware Update ID"]
|
656
918
|
rows = profiles.map do |hash|
|
657
|
-
|
919
|
+
config = hash[:config]
|
920
|
+
fw_id = config[:firmware] ? config[:firmware][:id] : "none"
|
921
|
+
[hash[:name], hash[:id], config[:version], config[:mode],
|
922
|
+
config[:wifi][:id], fw_id]
|
658
923
|
end
|
924
|
+
render_table(headings, rows)
|
925
|
+
end
|
659
926
|
|
927
|
+
#
|
928
|
+
# Displays the list of firmware releases
|
929
|
+
#
|
930
|
+
def display_firmware_image_list(images)
|
931
|
+
headings = ["Firmware Version"]
|
932
|
+
rows = images.map do |element|
|
933
|
+
[element[:version]]
|
934
|
+
end
|
935
|
+
render_table(headings, rows)
|
936
|
+
end
|
937
|
+
|
938
|
+
#
|
939
|
+
# Displays firmware detail
|
940
|
+
#
|
941
|
+
def display_firmware_detail(fw_info)
|
942
|
+
version = fw_info[:version] || "< not available >"
|
943
|
+
images = fw_info[:images]
|
944
|
+
|
945
|
+
say "Release version: #{version}"
|
946
|
+
display_image_list images
|
947
|
+
end
|
948
|
+
|
949
|
+
#
|
950
|
+
# Displays the list of images available for a release version.
|
951
|
+
#
|
952
|
+
def display_image_list(images)
|
953
|
+
headings = ["Image Name", "Hardware Model", "Update Type", "SHA256"]
|
954
|
+
rows = images.map do |hash|
|
955
|
+
[hash[:name], hash[:model], hash[:type], hash[:sha256]]
|
956
|
+
end
|
957
|
+
render_table(headings, rows)
|
958
|
+
end
|
959
|
+
|
960
|
+
#
|
961
|
+
# Displays the full SCHEDULE object detail
|
962
|
+
#
|
963
|
+
def display_schedule_detail(sched_data)
|
964
|
+
name = sched_data[:name]
|
965
|
+
id = sched_data[:id]
|
966
|
+
domain = sched_data[:domain]
|
967
|
+
times = sched_data[:times]
|
968
|
+
|
969
|
+
say "Schedule Name : #{name}", nil, true
|
970
|
+
say "Shedule ID : #{id}", nil, true
|
971
|
+
say "domain : #{domain}", nil, true
|
972
|
+
say "Times:", nil, true
|
973
|
+
display_time_list(times)
|
974
|
+
end
|
975
|
+
|
976
|
+
#
|
977
|
+
# Displays the list of times in a schedule as a table
|
978
|
+
#
|
979
|
+
def display_time_list(times)
|
980
|
+
headings = ["Name", "year", "Month", "Date", "Weekday", "Hour",
|
981
|
+
"Minute", "id"]
|
982
|
+
rows = times.map do |hash|
|
983
|
+
[hash[:name], hash[:year], hash[:month], hash[:day_of_month],
|
984
|
+
hash[:day_of_week], hash[:hour], hash[:minute], hash[:id]]
|
985
|
+
end
|
986
|
+
render_table(headings, rows)
|
987
|
+
end
|
988
|
+
|
989
|
+
#
|
990
|
+
# Displays a list of schedules
|
991
|
+
#
|
992
|
+
def display_schedule_list(sched_list)
|
993
|
+
headings = ["Name", "ID"]
|
994
|
+
rows = sched_list.map do |hash|
|
995
|
+
[hash[:name], hash[:id]]
|
996
|
+
end
|
660
997
|
render_table(headings, rows)
|
661
998
|
end
|
662
999
|
|
1000
|
+
#
|
1001
|
+
# Displays a list of firmware updates
|
1002
|
+
#
|
1003
|
+
def display_updates_list(updates_list)
|
1004
|
+
headings = ["Update ID", "Update Version"]
|
1005
|
+
rows = updates_list.map do |hash|
|
1006
|
+
[hash[:id], hash[:version]]
|
1007
|
+
end
|
1008
|
+
render_table(headings, rows)
|
1009
|
+
end
|
663
1010
|
|
1011
|
+
#
|
1012
|
+
# Displays detail of a specific firmware update task
|
1013
|
+
#
|
1014
|
+
def display_update_detail(update_data)
|
1015
|
+
id = update_data[:id]
|
1016
|
+
sched_id = update_data[:schedule_id]
|
1017
|
+
version = update_data[:version]
|
1018
|
+
percent = update_data[:update_percentage]
|
1019
|
+
|
1020
|
+
say "Update Task ID : #{id}"
|
1021
|
+
say "Schedule ID : #{sched_id}"
|
1022
|
+
say "Firmware Version : #{version}"
|
1023
|
+
say "Percent of devices to update : #{percent}"
|
1024
|
+
end
|
664
1025
|
end
|
665
1026
|
end
|
666
1027
|
end
|