Repository navigation
Expand file tree
/
Copy pathlf_interop_port_reset_test.py
More file actions
executable file
·2810 lines (2603 loc) · 124 KB
/
Copy pathlf_interop_port_reset_test.py
File metadata and controls
executable file
·2810 lines (2603 loc) · 124 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python3
"""
NAME: lf_interop_port_reset_test.py
PURPOSE:
The LANforge interop port reset test enables users to use real Wi-Fi stations and connect them to the
Access Point (AP) being tested. It then disconnects and reconnects a given number of stations at
different time intervals. This test helps evaluate how well the AP handles a dynamic and busy network environment
with devices joining and leaving the network at random times.
EXAMPLE:
# To run port-reset test on specified real devices (android, laptops)
./lf_interop_port_reset_test.py --lanforge_ip 192.168.200.192 --upstream_port 192.168.1.161 --dut TestDut --ssid OpenWifi
--passwd OpenWifi --encryp psk2 --iterations 10 --reset_interval 5 --android_releases 11
# To run port-reset test on specified real devices with only coordinates
./lf_interop_port_reset_test.py --lanforge_ip 192.168.207.78 --upstream_port eth1 --dut AP --ssid "NETGEAR_2G_wpa2" --encryp psk2 --passwd Password@123
--iterations 2 --reset_interval 5 --robot_test --coordinate 4,3 --robot_ip 192.168.200.169 --device_list ubuntu24
# To run port-reset test on specified real devices with only coordinates and rotations
./lf_interop_port_reset_test.py --lanforge_ip 192.168.207.78 --upstream_port eth1 --dut AP --ssid "NETGEAR_2G_wpa2" --encryp psk2 --passwd Password@123
--iterations 2 --reset_interval 5 --robot_test --coordinate 4,3 --rotation 30,45 --robot_ip 192.168.200.169 --device_list ubuntu24
SCRIPT_CLASSIFICATION: Toggling, Report Generation, Each Reset Wi-Fi Messages
SCRIPT_CATEGORIES: Interop Port-Reset Test
NOTES:
The primary objective of this script is to automate the process of toggling Wi-Fi on real devices with the
InterOp Application, evaluating their performance with an access point. It achieves this by simulating multiple
Wi-Fi resets as specified by the user.
* Currently the script will work for the REAL CLIENTS (android with version 11+, laptop devices).
STATUS: Functional
VERIFIED_ON: 28-OCT-2023,
GUI Version: 5.4.7
Kernel Version: 6.2.16+
LICENSE:
Free to distribute and modify. LANforge systems must be licensed.
Copyright (C) 2020-2026 Candela Technologies Inc
INCLUDE_IN_README: False
"""
import json
import re
import sys
import os
import importlib
import argparse
import time
import datetime
# When you use from datetime import datetime, you are making the
# datetime class directly accessible in your code without
# having to prefix it with the module name
from datetime import datetime # noqa: F811
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.ticker import MaxNLocator
import logging
from lf_base_robo import RobotClass
import asyncio
if sys.version_info[0] != 3:
print("This script requires Python3")
exit()
sys.path.append(os.path.join(os.path.abspath(__file__ + "../../../")))
interop_modify = importlib.import_module("py-scripts.lf_interop_modify")
base = importlib.import_module("py-scripts.lf_base_interop_profile")
lf_csv = importlib.import_module("py-scripts.lf_csv")
realm = importlib.import_module("py-json.realm")
Realm = realm.Realm
lf_report_pdf = importlib.import_module("py-scripts.lf_report")
lf_graph = importlib.import_module("py-scripts.lf_graph")
logger = logging.getLogger(__name__)
lf_logger_config = importlib.import_module("py-scripts.lf_logger_config")
class InteropPortReset(Realm):
def __init__(
self,
lanforge_ip,
lanforge_port=8080,
dut=None,
ssid=None,
passwd=None,
encryption=None,
iterations=None,
upstream_port=None,
reset_interval_sec=None,
device_list=None,
android_releases=None,
forget_network=True,
dowebgui=False,
result_dir=None,
test_name=None,
robot_test=False,
robot_ip=None,
robot_port=None,
coordinate=None,
rotation=None,
get_live_view=False,
total_floors=0,
):
"""Build the test object from CLI settings and prepare the report helpers.
InteropPortReset("192.168.244.97", ssid="Authtesting", iterations=10)"""
super().__init__(lfclient_host=lanforge_ip, lfclient_port=8080)
self.total_connects = []
self.total_assoc_rejections = []
self.total_assoc_attempts = []
self.total_scans = []
self.total_disconnects = []
self.total_resets = []
self.graph_image_name = ""
self.all_selected_devices = []
self.all_laptops = []
self.user_query = []
self.available_device_list = []
self.final_selected_android_list = []
self.adb_device_list = []
self.windows_list = []
self.linux_list = []
self.mac_list = []
self.encrypt_value = 0
self.lanforge_ip = lanforge_ip
self.lanforge_port = lanforge_port
self.android_serials = []
self.dut_name = dut
self.ssid = ssid
self.passwd = passwd
self.encryption = encryption
self.upstream_port = upstream_port
self.iterations = iterations
self.reset_interval_sec = reset_interval_sec
self.device_list = device_list
self.forget_network = forget_network
self.result_dir = result_dir
self.dowebgui = dowebgui
self.test_name = test_name
self.result_df = {}
self.port_reset_data = {}
self.robot_test = robot_test
self.coordinate_df = {}
self.get_live_view = get_live_view
self.total_floors = total_floors
self.robo_test_stopped = False
self.robot_ip = robot_ip
self.robot_port = robot_port
self.coordinate = coordinate
self.rotation = rotation
self.rotation_enabled = False
self.coordinate_list = coordinate.split(",")
self.rotation_list = rotation.split(",")
self.current_coordinate = None
self.current_angle = None
self.android_releases = android_releases
self.android_user_names = []
self.lf_report = lf_report_pdf.lf_report(
_path="" if not self.dowebgui else self.result_dir,
_results_dir_name="Interop_port_reset_test",
_output_html="port_reset_test.html",
_output_pdf="port_reset_test.pdf",
)
self.report_path = self.lf_report.get_report_path()
self.base_interop_profile = base.RealDevice(
manager_ip=self.lanforge_ip,
server_ip=self.upstream_port,
ssid_5g=self.ssid,
encryption_5g=self.encryption,
passwd_5g=self.passwd,
disconnect_devices=self.forget_network,
reboot=False,
selected_bands=["5g"],
)
self.utility = base.UtilityInteropWifi(host_ip=self.lanforge_ip)
def json_get_with_retry(self, url, wait_time=40, poll_interval=5, debug_=False):
"""Take a LANforge URL and return its JSON response, or None on failure.
"/port/1/1/eth1?fields=ip" -> {"interface": {"ip": "192.168.1.1"}}"""
start_time = time.time()
response = self.json_get(url, debug_=debug_)
while response is None and (time.time() - start_time) < wait_time:
logging.warning(
f"GET {url} returned no response from LANforge; retrying..."
)
time.sleep(poll_interval)
response = self.json_get(url, debug_=debug_)
if response is None:
logging.error(
f"GET {url} returned no response from LANforge after waiting "
f"{wait_time} seconds."
)
return response
def change_port_to_ip(self, upstream_port):
# Resolves an ethernet port name (e.g. "eth1") to its IP via LANforge, used
# for --upstream_port. This is only called once, at test start, and we cannot
# proceed without a resolved manager IP, so retry for a while and then
# abort instead of silently continuing with an unresolved port name.
"""Take an upstream port name and return its IP address as a string.
"eth1" -> "192.168.1.1"; an IP passed in comes back unchanged"""
if upstream_port.count(".") != 3:
shelf, resource, port_name, _ = self.name_to_eid(upstream_port)
response = self.json_get_with_retry(
f"/port/{shelf}/{resource}/{port_name}?fields=ip"
)
try:
upstream_port = response["interface"]["ip"]
except (TypeError, KeyError) as e:
logging.error(
f"Could not resolve upstream port '{upstream_port}' to an IP; LANforge "
f"response is not in expected format ({e}). Data received: {response}"
)
logging.critical(
"Aborting the test: a valid manager IP is required to proceed."
)
exit(1)
logging.info(f"Upstream port IP {upstream_port}")
else:
logging.info(f"Upstream port IP {upstream_port}")
return upstream_port
def selecting_devices_from_available(self):
"""Pick the devices to test and store the selection.
Fills self.final_selected_android_list, e.g. ["1.10", "1.12"]"""
if self.device_list is None:
configurable_devices = (
self.base_interop_profile.query_all_devices_to_configure_wifi()
)
else:
configurable_devices = (
self.base_interop_profile.query_all_devices_to_configure_wifi(
device_list=self.device_list.split(",")
)
)
asyncio.run(
self.base_interop_profile.configure_wifi(
configurable_devices[0]
+ configurable_devices[1]
+ configurable_devices[2]
)
)
self.real_station_list = self.base_interop_profile.station_list
logger.info(self.real_station_list)
device_details = self.base_interop_profile.devices_data
if len(self.real_station_list) == 0:
logging.error(
"There are no real devices in this testbed. Aborting the test."
)
# Added for the purpose to stop webui test when there are no selected devices available in lanforge.
raise RuntimeError(
"There are no real devices in this testbed. Aborting the test."
)
logging.info(f"{self.real_station_list}")
for station_name in self.real_station_list:
if station_name not in device_details:
logger.error("Real Station not in devices data")
raise ValueError("Real station not in devices data")
android_list = self.base_interop_profile.android_list
self.interop = base.BaseInteropWifi(
manager_ip=self.lanforge_ip,
port=self.lanforge_port,
ssid=self.ssid,
passwd=self.passwd,
encryption=self.encryption,
release=self.android_releases,
screen_size_prcnt=0.4,
_debug_on=False,
_exit_on_error=False,
)
supported_resource_ids = self.interop.supported_devices_resource_id
self.final_selected_android_list = []
for device_name in supported_resource_ids.keys():
if device_name != "":
if any(device_name in item for item in android_list):
self.final_selected_android_list.append(
supported_resource_ids[device_name]
)
logging.info(
f"Final Android Serial Numbers List: {self.final_selected_android_list}"
)
def create_log_file(self, json_list, file_name="empty.json"):
"""Write wifi messages as JSON under the report Wifi_Messages folder.
([{...}], "reset_0_log.json") -> Wifi_Messages/reset_0_log.json"""
json_string = json.dumps(json_list)
new_folder = os.path.join(self.report_path, "Wifi_Messages")
if not (os.path.exists(new_folder) and os.path.isdir(new_folder)):
os.makedirs(new_folder)
file_path = f"{self.report_path}/Wifi_Messages/{file_name}"
with open(file_path, "w") as file:
file.write(json_string)
def remove_files_with_duplicate_names(self, folder_path):
"""Delete every file in a folder whose name was already seen.
"<report>/Wifi_Messages/" -> keeps the first of each name"""
file_names = {}
for root, _, files in os.walk(folder_path):
for file in files:
file_path = os.path.join(root, file)
file_name = os.path.basename(file_path)
if file_name in file_names:
os.remove(file_path)
logging.info(f"Removed duplicate file: {file_path}")
else:
file_names[file_name] = file_path
def get_last_wifi_msg_timestamp(self):
"""Return the newest wifi message time-stamp, or "NA" if unreadable.
-> "1787389179894", epoch ms, the baseline for one reset"""
response = self.json_get_with_retry("/wifi-msgs/last/1", debug_=True)
try:
last_timestamp = response["wifi-messages"]["time-stamp"]
except (TypeError, KeyError) as e:
logging.error(
f"LANforge response is not in expected format ({e}). Data received: {response}"
)
logging.warning(
"Could not establish a wifi-msgs baseline timestamp; falling back to 'NA' for this reset."
)
return "NA"
logging.info(f"Last WiFi Message Time Stamp: {last_timestamp}")
return last_timestamp
def tokenize_wifi_msg(self, text):
"""Take a wifi message string and return its tokens as a list of strings.
"en0: STA is connected." -> ["en0", "STA", "is", "connected"]"""
tokens = []
for token in re.split(r"[\s:]+", text.strip()):
token = token.strip(".,;")
if token:
tokens.append(token)
return tokens
def count_wifi_msg_matches(
self, wifi_messages=None, message_keys=None, device_eid=None, match_text=None
):
"""Count one device wifi messages containing every word of match_text.
(msgs, keys, "1.29.en0", "STA is connected") -> 2"""
matches = []
eid_parts = device_eid.split(".")
port_name = eid_parts[2]
resource_id = eid_parts[0] + "." + eid_parts[1]
for message_key, index in zip(message_keys, range(len(message_keys))):
wifi_msg_text = wifi_messages[index][message_key]["text"]
message_resource = wifi_messages[index][message_key]["resource"]
if type(wifi_msg_text) is str:
message_tokens = self.tokenize_wifi_msg(
wifi_messages[index][message_key]["text"]
)
if port_name is None:
logging.info(
f"Device {port_name} is None device name not existed in wifi messages..."
)
else:
if message_resource == resource_id:
if port_name in message_tokens:
if match_text in message_tokens:
matches.append("YES")
else:
match_tokens = self.tokenize_wifi_msg(match_text)
if all(
token in message_tokens for token in match_tokens
):
matches.append("YES")
else:
if f"IFNAME={port_name}" in message_tokens:
if match_text in message_tokens:
matches.append("YES")
else:
match_tokens = self.tokenize_wifi_msg(match_text)
if all(
token in message_tokens
for token in match_tokens
):
matches.append("YES")
else: # if wifi_msg_text is list
for message_line in wifi_msg_text:
message_tokens = self.tokenize_wifi_msg(message_line)
if port_name is not None:
if message_resource == resource_id:
if port_name in message_tokens:
if match_text in message_tokens:
matches.append("YES")
else:
match_tokens = self.tokenize_wifi_msg(match_text)
if all(
token in message_tokens
for token in match_tokens
):
matches.append("YES")
else:
if f"IFNAME={port_name}" in message_tokens:
if match_text in message_tokens:
matches.append("YES")
else:
match_tokens = self.tokenize_wifi_msg(
match_text
)
if all(
token in message_tokens
for token in match_tokens
):
matches.append("YES")
match_count = matches.count("YES")
return match_count
def collect_device_metrics(
self,
device_metrics=None,
device_eid=None,
since_time=None,
file_name="dummy.json",
iteration=None,
):
"""Count one device wifi events since a time-stamp and return metrics.
-> {"1.29.en0": {"Connected": 1, "Disconnected": 1, ...}}"""
wifi_msgs_response = self.json_get_with_retry(
"/wifi-msgs/since=time/" + str(since_time), debug_=True
)
try:
wifi_messages = wifi_msgs_response["wifi-messages"]
except (TypeError, KeyError) as e:
logging.error(
f"LANforge response is not in expected format ({e}) while fetching "
f"wifi-msgs for device {device_eid}. Data received: {wifi_msgs_response}"
)
logging.warning(
f"Defaulting device {device_eid} stats to 0 for this reset and continuing."
)
for metric_name in (
"ConnectAttempt",
"Disconnected",
"Scanning",
"Association Rejection",
"Connected",
):
device_metrics[str(device_eid)][metric_name] = 0
device_metrics[str(device_eid)][
"Remarks"
] = "Data unavailable - LANforge API error, stats defaulted to 0"
device_metrics[str(device_eid)]["cx time (us)"] = "NA"
self.write_iteration_csvs(device_metrics, iteration)
return device_metrics
logging.info(
f"Counting the DISCONNECTIONS, SCANNING, ASSOC ATTEMPTS, ASSOC RECJECTIONS, CONNECTS for device {device_eid}"
)
if type(wifi_messages) is not list:
logging.info(
f"Device {device_eid} : Getting wifi messages for only single time-stamp. Converting into List."
)
wifi_messages = [
{
f"{wifi_messages['resource']}.{wifi_messages['time-stamp']}": wifi_messages
}
]
self.create_log_file(json_list=wifi_messages, file_name=file_name)
self.remove_files_with_duplicate_names(
folder_path=f"{self.report_path}/Wifi_Messages/"
)
message_keys = []
for index in range(len(wifi_messages)):
message_keys.append(list(wifi_messages[index].keys())[0])
# android (flag) check for clustered lanforge cases
is_android = False
adb_response = self.json_get_with_retry("/adb/")
try:
adb_devices = adb_response["devices"]
except (TypeError, KeyError) as e:
logging.error(
f"LANforge response is not in expected format ({e}) while fetching "
f"/adb/ devices. Data received: {adb_response}"
)
logging.warning(
f"Could not fetch adb device list; relying only on the '1.1.' prefix heuristic for {device_eid}."
)
adb_devices = []
for device_data in adb_devices:
device_name, _ = list(device_data.keys())[0], list(device_data.values())[0]
if device_eid in device_name:
is_android = True
break
if "1.1." in device_eid or is_android:
# disconnects
android_disconnect_count = self.count_wifi_msg_matches(
wifi_messages=wifi_messages,
message_keys=message_keys,
device_eid=device_eid,
match_text="Terminating...",
)
if android_disconnect_count > 1 or android_disconnect_count == 0:
device_state = self.utility.get_device_state(device=device_eid)
if device_state == "COMPLETED":
logging.info("The Device %s is in connected state." % device_eid)
android_disconnect_count = 0
else:
logging.info(
"The Device %s is not in connected state." % device_eid
)
android_disconnect_count = 1
logging.info(
f"Disconnect Count For Android: {android_disconnect_count}"
)
logging.info(
"Final Disconnect count for %s: %s"
% (device_eid, android_disconnect_count)
)
device_metrics[device_eid]["Disconnected"] = android_disconnect_count
# scanning count
android_scan_count = self.count_wifi_msg_matches(
wifi_messages=wifi_messages,
message_keys=message_keys,
device_eid=device_eid,
match_text="SCAN-STARTED",
)
logging.info(
"Final Scanning Count for %s: %s" % (device_eid, android_scan_count)
)
device_metrics[str(device_eid)]["Scanning"] = android_scan_count
# association attempts
android_association_attempt = self.count_wifi_msg_matches(
wifi_messages=wifi_messages,
message_keys=message_keys,
device_eid=device_eid,
match_text="ASSOCIATING",
)
logging.info(
"Final Association Attempts Count for %s: %s"
% (device_eid, android_association_attempt)
)
device_metrics[str(device_eid)][
"ConnectAttempt"
] = android_association_attempt
# association rejections
android_association_rejection = self.count_wifi_msg_matches(
wifi_messages=wifi_messages,
message_keys=message_keys,
device_eid=device_eid,
match_text="ASSOC_REJECT",
)
logging.info(
"Final Association Rejection Count for %s: %s"
% (device_eid, android_association_rejection)
)
device_metrics[str(device_eid)][
"Association Rejection"
] = android_association_rejection
# connections
android_connected_count = self.count_wifi_msg_matches(
wifi_messages=wifi_messages,
message_keys=message_keys,
device_eid=device_eid,
match_text="CTRL-EVENT-CONNECTED",
)
# Double-checking & adding remarks if any
if android_connected_count > 1 or android_connected_count == 0:
device_ssid = self.utility.get_device_ssid(device=device_eid)
if device_ssid == self.ssid:
logging.info(
"The Device %s is connected to expected ssid" % device_eid
)
android_connected_count = 1
else:
logging.info(
"**** The Device is not connected to the expected ssid ****"
)
android_connected_count = 0
logging.info(f"Connected Count for Android: {android_connected_count}")
logging.info(
"Final Connected Count for %s: %s"
% (device_eid, android_connected_count)
)
device_metrics[str(device_eid)]["Connected"] = android_connected_count
remarks = "NA"
device_metrics[str(device_eid)]["Remarks"] = remarks
if android_association_attempt > android_connected_count:
android_association_rejection = (
android_association_attempt - android_connected_count
)
device_metrics[str(device_eid)][
"Association Rejection"
] = android_association_rejection
if android_connected_count > 0:
_, resource, serial = device_eid.split(".")
resource_id_response = self.json_get_with_retry(
"/adb/1/{}/{}?fields=resource-id".format(resource, serial)
)
port_response = None
try:
resource_id = resource_id_response["devices"]["resource-id"]
port_response = self.json_get_with_retry(
"port/1/{}/wlan0?fields=cx time (us)".format(
resource_id.split(".")[1]
)
)
cx_time_us = port_response["interface"]["cx time (us)"]
device_metrics[str(device_eid)]["cx time (us)"] = cx_time_us
except (TypeError, KeyError) as e:
logging.error(
f"Could not fetch cx time (us) for device {device_eid}; LANforge "
f"response is not in expected format ({e}). resource-id response: {resource_id_response}, "
f"port response: {port_response}"
)
device_metrics[str(device_eid)]["cx time (us)"] = "NA"
else:
device_metrics[str(device_eid)]["cx time (us)"] = "NA"
else:
if device_eid in self.windows_list: # for windows
win_disconnect_count = self.count_wifi_msg_matches(
wifi_messages=wifi_messages,
message_keys=message_keys,
device_eid=device_eid,
match_text="Wireless security stopped.",
)
# Double-checking the disconnect count with another key msg
if win_disconnect_count == 0:
win_disconnect_count = self.count_wifi_msg_matches(
wifi_messages=wifi_messages,
message_keys=message_keys,
device_eid=device_eid,
match_text="WLAN AutoConfig service has successfully disconnected from a wireless network",
)
logging.info(
"Final Disconnect count for %s: %s"
% (device_eid, win_disconnect_count)
)
device_metrics[device_eid]["Disconnected"] = win_disconnect_count
win_scan_count = self.count_wifi_msg_matches(
wifi_messages=wifi_messages,
message_keys=message_keys,
device_eid=device_eid,
match_text="service started",
)
logging.info(
"Final Scanning Count for %s: %s" % (device_eid, win_scan_count)
)
device_metrics[str(device_eid)]["Scanning"] = win_scan_count
win_association_attempt = self.count_wifi_msg_matches(
wifi_messages=wifi_messages,
message_keys=message_keys,
device_eid=device_eid,
match_text="association started.",
)
logging.info(
"Final Association Attempts Count for %s: %s"
% (device_eid, win_association_attempt)
)
device_metrics[str(device_eid)][
"ConnectAttempt"
] = win_association_attempt
win_association_rejection = self.count_wifi_msg_matches(
wifi_messages=wifi_messages,
message_keys=message_keys,
device_eid=device_eid,
match_text="failed to connect",
)
logging.info(
"Final Association Rejection Count for %s: %s"
% (device_eid, win_association_rejection)
)
device_metrics[str(device_eid)][
"Association Rejection"
] = win_association_rejection
win_connected_count = self.count_wifi_msg_matches(
wifi_messages=wifi_messages,
message_keys=message_keys,
device_eid=device_eid,
match_text="connected",
)
win_connection_state_unverified = False
# assoc-rejection based logic
if win_association_rejection:
actual_connects = (
win_association_attempt - win_association_rejection
)
if actual_connects == win_connected_count:
win_connected_count = win_connected_count
else:
win_connected_count = actual_connects
else:
if win_association_attempt == win_connected_count:
win_connected_count = win_connected_count
else:
# Double-checking
if win_connected_count > 1 or win_connected_count == 0:
eid_parts = device_eid.split(".")
port_response = self.json_get_with_retry(
f"port/{eid_parts[0]}/{eid_parts[1]}/{eid_parts[2]}?fields=ssid,ip"
)
try:
if (
port_response["interface"]["ssid"] == self.ssid
and port_response["interface"]["ip"] != "0.0.0.0"
):
win_connected_count = 1
else:
win_connected_count = 0
except (TypeError, KeyError) as e:
logging.error(
f"Could not verify connection state for device "
f"{device_eid}; LANforge response is not in expected format ({e}). Data "
f"received: {port_response}"
)
win_connected_count = 0
win_connection_state_unverified = True
logging.info(
"Final Connected Count for %s: %s"
% (device_eid, win_connected_count)
)
device_metrics[str(device_eid)]["Connected"] = win_connected_count
if win_association_attempt > win_connected_count:
win_association_rejection = (
win_association_attempt - win_connected_count
)
device_metrics[str(device_eid)][
"Association Rejection"
] = win_association_rejection
remarks = "NA"
if win_disconnect_count == 0 and win_connected_count == 1:
remarks = "No Disconnections are seen but Client is UP and connected to user given SSID."
elif win_disconnect_count >= 1 and win_connected_count == 0:
remarks = "The Disconnections are seen but Client did not connected to user given SSID."
if win_connection_state_unverified:
remarks = "Connection state unverified - LANforge API error while double-checking connect count"
device_metrics[str(device_eid)]["Remarks"] = remarks
if win_connected_count > 0:
eid_parts = device_eid.split(".")
port_response = self.json_get_with_retry(
f"port/{eid_parts[0]}/{eid_parts[1]}/{eid_parts[2]}?fields=cx time (us)"
)
try:
cx_time_us = port_response["interface"]["cx time (us)"]
device_metrics[str(device_eid)]["cx time (us)"] = cx_time_us
except (TypeError, KeyError) as e:
logging.error(
f"Could not fetch cx time (us) for device {device_eid}; "
f"LANforge response is not in expected format ({e}). Data received: {port_response}"
)
device_metrics[str(device_eid)]["cx time (us)"] = "NA"
else:
device_metrics[str(device_eid)]["cx time (us)"] = "NA"
elif device_eid in self.mac_list: # for macOS
mac_disconnect_count = self.count_wifi_msg_matches(
wifi_messages=wifi_messages,
message_keys=message_keys,
device_eid=device_eid,
match_text="disconnected",
)
logging.info(
"Final Disconnect count for %s: %s"
% (device_eid, mac_disconnect_count)
)
device_metrics[device_eid]["Disconnected"] = mac_disconnect_count
mac_connected_count = self.count_wifi_msg_matches(
wifi_messages=wifi_messages,
message_keys=message_keys,
device_eid=device_eid,
match_text="STA is connected",
)
logging.info(
"Final Connected count for %s: %s"
% (device_eid, mac_connected_count)
)
device_metrics[str(device_eid)]["Connected"] = mac_connected_count
device_metrics[str(device_eid)]["Scanning"] = 0
device_metrics[str(device_eid)]["ConnectAttempt"] = 0
device_metrics[str(device_eid)]["Association Rejection"] = 0
remarks = "NA"
if mac_disconnect_count == 0 and mac_connected_count >= 1:
remarks = "No Disconnections are seen but Client is UP and connected to user given SSID."
elif mac_disconnect_count >= 1 and mac_connected_count == 0:
remarks = "The Disconnections are seen but Client did not connected to user given SSID."
device_metrics[str(device_eid)]["Remarks"] = remarks
if mac_connected_count > 0:
eid_parts = device_eid.split(".")
port_response = self.json_get_with_retry(
f"port/{eid_parts[0]}/{eid_parts[1]}/{eid_parts[2]}?fields=cx time (us)"
)
try:
cx_time_us = port_response["interface"]["cx time (us)"]
device_metrics[str(device_eid)]["cx time (us)"] = cx_time_us
except (TypeError, KeyError) as e:
logging.error(
f"Could not fetch cx time (us) for device {device_eid}; "
f"LANforge response is not in expected format ({e}). Data received: {port_response}"
)
device_metrics[str(device_eid)]["cx time (us)"] = "NA"
else:
device_metrics[str(device_eid)]["cx time (us)"] = "NA"
else: # other means (for linux)
linux_disconnect_count = self.count_wifi_msg_matches(
wifi_messages=wifi_messages,
message_keys=message_keys,
device_eid=device_eid,
match_text="disconnected",
)
# Double-checking the disconnect count with another key msg
if linux_disconnect_count == 0:
linux_disconnect_count = self.count_wifi_msg_matches(
wifi_messages=wifi_messages,
message_keys=message_keys,
device_eid=device_eid,
match_text="<3>CTRL-EVENT-DSCP-POLICY clear_all",
)
logging.info(
"Final Disconnect count for %s: %s"
% (device_eid, linux_disconnect_count)
)
device_metrics[device_eid]["Disconnected"] = linux_disconnect_count
linux_scan_count = self.count_wifi_msg_matches(
wifi_messages=wifi_messages,
message_keys=message_keys,
device_eid=device_eid,
match_text="<3>CTRL-EVENT-SCAN-STARTED",
)
if linux_scan_count == 0:
linux_scan_count = self.count_wifi_msg_matches(
wifi_messages=wifi_messages,
message_keys=message_keys,
device_eid=device_eid,
match_text="scan started",
)
logging.info(
"Final Scanning Count for %s: %s" % (device_eid, linux_scan_count)
)
device_metrics[str(device_eid)]["Scanning"] = linux_scan_count
linux_association_attempt = self.count_wifi_msg_matches(
wifi_messages=wifi_messages,
message_keys=message_keys,
device_eid=device_eid,
match_text="<3>Trying to associate with",
)
logging.info(
"Final Association Attempts Count for %s: %s"
% (device_eid, linux_association_attempt)
)
device_metrics[str(device_eid)][
"ConnectAttempt"
] = linux_association_attempt
linux_association_rejection = self.count_wifi_msg_matches(
wifi_messages=wifi_messages,
message_keys=message_keys,
device_eid=device_eid,
match_text="NoneValue",
)
logging.info(
"Final Association Rejection Count for %s: %s"
% (device_eid, linux_association_rejection)
)
device_metrics[str(device_eid)][
"Association Rejection"
] = linux_association_rejection
linux_connected_count = self.count_wifi_msg_matches(
wifi_messages=wifi_messages,
message_keys=message_keys,
device_eid=device_eid,
match_text="<3>CTRL-EVENT-CONNECTED",
)
linux_connection_state_unverified = False
# assoc-rejection based logic
if linux_association_rejection:
actual_connects = (
linux_association_attempt - linux_association_rejection
)
if actual_connects == linux_connected_count:
linux_connected_count = linux_connected_count
else:
linux_connected_count = actual_connects
else:
if linux_association_attempt == linux_connected_count:
linux_connected_count = linux_connected_count
else:
# Double-checking & adding remarks if any
if linux_connected_count > 1 or linux_connected_count == 0:
eid_parts = device_eid.split(".")
port_response = self.json_get_with_retry(
f"port/{eid_parts[0]}/{eid_parts[1]}/{eid_parts[2]}?fields=ssid,ip"
)
try:
if (
port_response["interface"]["ssid"] == self.ssid
and port_response["interface"]["ip"] != "0.0.0.0"
):
linux_connected_count = 1
else:
linux_connected_count = 0
except (TypeError, KeyError) as e:
logging.error(
f"Could not verify connection state for device "
f"{device_eid}; LANforge response is not in expected format ({e}). Data "
f"received: {port_response}"
)
linux_connected_count = 0
linux_connection_state_unverified = True
logging.info(
"Final Connected Count for %s: %s"
% (device_eid, linux_connected_count)
)
device_metrics[str(device_eid)]["Connected"] = linux_connected_count
if linux_association_attempt > linux_connected_count:
linux_association_rejection = (
linux_association_attempt - linux_connected_count
)
device_metrics[str(device_eid)][
"Association Rejection"
] = linux_association_rejection
remarks = "NA"
if linux_disconnect_count == 0 and linux_connected_count == 1:
remarks = "No Disconnections are seen but Client is UP and connected to user given SSID."
elif linux_disconnect_count >= 1 and linux_connected_count == 0:
remarks = "The Disconnections are seen but Client did not connected to user given SSID."
if linux_connection_state_unverified:
remarks = "Connection state unverified - LANforge API error while double-checking connect count"
device_metrics[str(device_eid)]["Remarks"] = remarks
if linux_connected_count > 0:
eid_parts = device_eid.split(".")
port_response = self.json_get_with_retry(
f"port/{eid_parts[0]}/{eid_parts[1]}/{eid_parts[2]}?fields=cx time (us)"
)
try:
cx_time_us = port_response["interface"]["cx time (us)"]
device_metrics[str(device_eid)]["cx time (us)"] = cx_time_us
except (TypeError, KeyError) as e:
logging.error(
f"Could not fetch cx time (us) for device {device_eid}; "
f"LANforge response is not in expected format ({e}). Data received: {port_response}"
)
device_metrics[str(device_eid)]["cx time (us)"] = "NA"
else:
device_metrics[str(device_eid)]["cx time (us)"] = "NA"
self.normalize_reset_metrics(device_metrics[str(device_eid)])
logging.info("device_metrics " + str(device_metrics))
self.write_iteration_csvs(device_metrics, iteration)
return device_metrics
@staticmethod
def normalize_reset_metrics(metrics):
"""Report one disconnect/reconnect outcome per commanded reset.
Multiple OS messages may describe the same transition. Scan and
association counters remain event counts, including retries.
"""
for key in ("Connected", "Disconnected"):
metrics[key] = int((metrics.get(key) or 0) > 0)
previous_remark = metrics.get("Remarks") or ""
if "unverified" in previous_remark.lower() or "unavailable" in previous_remark.lower():
return
connect_attempts = metrics.get("ConnectAttempt", 0) or 0
if metrics["Connected"] and connect_attempts in (1, 2):
remark = "Client reconnected after reset. Association attempt(s) were observed."
elif metrics["Connected"]:
remark = "Client reconnected after reset."
if not metrics["Disconnected"]:
remark += " No disconnect message was observed."
if metrics.get("Association Rejection", 0):
remark += " Association failures were reported."
elif metrics["Disconnected"]:
remark = "Disconnect observed; no successful reconnection was confirmed."
if metrics.get("Association Rejection", 0):
remark += " Association failures were reported."
else:
remark = "No disconnect or successful reconnection was confirmed."
if metrics.get("Association Rejection", 0):
remark += " Association failures were reported."
metrics["Remarks"] = remark
def report_client_names(self, device_names, user_names):
"""Pair each client's resource ID with its host/user name.
Android eids use the Interop tab's own resource (e.g. "1.1"), not the