-
Notifications
You must be signed in to change notification settings - Fork 1
/
xmcacli.pl
1531 lines (1312 loc) · 60.1 KB
/
xmcacli.pl
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/perl
my $Version = "1.17";
my $Debug = 0;
# Written by Ludovico Stevens (lstevens@extremenetworks.com)
#
# Version history:
# 0.01 - Initial
# 0.03 - After launch, selected entries are deselceted automatically
# - Containing window entry box now has a history pull down
# - Added -n argument to launch terminals in transparent mode
# 0.04 - Fixed issue with -t argument not implementing the wait timer between 1st and subsequent tabs
# 1.00 - Added -q argument to provide an override graphQl file (to work with different versions of XMC which have incompatible API keys..)
# - Hitting Quit button during Fetch was hanging the application
# - JSON device data structure is now flattened to eliminate the inconsistent 'extraData' sub key, which becomes 'deviceData' in XMC8.2
# 1.01 - Enhanced to also work on MAC OS distribution
# 1.02 - Added ISW to list of devices to launch ACLI in interact mode
# - Version now shows in window title
# 1.03 - Issues with threads on MAC OS; no longer sets stack_size on MAC OS
# - If no XMC server details and filtering criteria were provided in xmcacli.ini then the Sysname column was missing in app
# - Input field backgrounds now set to white for correct rendering on MAC OS
# 1.04 - Correction in syntax display
# - Added new 'Extreme Access Series' family which is how XMC classifies XA1400 since VOSS8.1
# 1.05 - Added new 'Unified Switching VOSS' & 'Unified Switching EXOS' families used in XMC for 5520 unified hardware
# - Added support for external acli.spawn file; now acligui can be theoretically launched and
# customized on any OS; in practice this adds support for Linux
# - Transparent mode (-n) now has a checkbox in the GUI
# 1.06 - HTTP timeout changed from 5 to 20 seconds
# - HTTP timeout can now be changed in the xmcacli.ini file
# 1.07 - Sockets are not loaded in transparent mode (-n)
# - Acli.spawn key <ACLI-PL-PATH> was not replaced with "acli.pl" but just "acli"
# 1.08 - Update to work with XIQ-SE 21.9 which no longer calls itself XMC on server responses
# 1.09 - Update to -s switch syntax
# 1.10 - Corrections to debug function
# 1.11 - Switch -s is normally suppressed if the -n switch is set; but is now not suppressed if the -s
# switch is set to 0 (disable sockets)
# 1.12 - Added new 'Universal Platform Fabric Engine' & 'Universal Platform Switch Engine' families used in XIQ-SE 22.3 for
# universal hardware running VOSS8.6 or EXOS31.6 or later
# 1.13 - The login credentials are now always double quoted when launching ACLI in case the password might contain special
# characters like *,&,etc..
# 1.14 - Added new 'Universal Platform Fabric Engine' & 'Universal Platform Switch Engine' families used in XIQ-SE 22.3 for
# universal hardware running VOSS8.6 or EXOS31.6 or later - was not added in 1.12
# 1.15 - Site filter pull down was matching other sites if other sites had longer names containing the selected site
# - Site filter pull down was causing application to crash with "Tk_FreeCursor received unknown cursor argument"
# if a site was selected and then the site filter pull down was used to select a site
# - Setting the Logging or Working directory from GUI, the directory chooser now starts from the directory which was
# specified with command line switches or from the very top "This PC"
# - Last Logging or Working directory selected from directory chooser is remembered and offered as default in subsequent
# executions of the script
# 1.16 - Added new 'XIQ Native' XIQ-SE family since ACLI now supports Cloud APs (HiveOS)
# 1.17 - Added missing '200 Series' XIQ-SE family for Series200 support
#############################
# STANDARD MODULES #
#############################
use strict;
use warnings;
no warnings 'threads'; # Prevents errors on console window about thread terminated abnormally when quiting the application
use threads;
use threads::shared;
use Getopt::Std;
use Cwd;
use File::Basename;
use File::Spec;
use Tk;
use Tk::Tree;
use Tk::ProgressBar;
use Tk::BrowseEntry;
use Tk::DoubleClick;
use Time::HiRes qw( sleep );
use LWP::UserAgent;
use HTTP::Request;
use Cpanel::JSON::XS; # Can't use JSON, as it uses JSON::XS as backend, which does not work with threads
use Config::INI::Reader::Ordered;
if ($^O eq "MSWin32") {
unless (eval "require Win32::Process") { die "Cannot find module Win32::Process" }
import Win32::Process qw( NORMAL_PRIORITY_CLASS );
# http://www.perlmonks.org/?node_id=874944 / On MSWin32 minimum becomes 8192, but not setting would use 16Meg
# However we stay with default stack_size on MAC OS as otherwise we get all sorts of errors
threads->set_stack_size(8192);
}
#use Data::Dumper;
############################
# GLOBAL VARIABLES #
############################
my $ThreadSleepTimer = 0.5; # Sleep timer for worker thread, between checking if $Shared_flag has been set
my $MaxWindowSessions = 20; # Maximum number of ConsoleZ tabs we want to open in same window
my $ThreadCheckInterval = 150; # Time to wait between checking status of httpThread
my $HttpTimeout = 20; # Timeout to use by LWP::UserAgent
my $IPentryBoxWidth = 50; # Size of text box with IP list
my $CredentialBoxWidth = 25; # Size of username & password text boxes
my $WindowNameBoxWidth = 30; # Size of window name text box
my $WorkDirBoxWidth = 61; # Size of working directory text box
my $SocketNamesWidth = 61; # Size of socket names text box
my $RunScriptWidth = 61; # Size of run script text box
my $ProgressBarMax = 100; # Progress bar on 100%
my $ProgressBarBit = 2; # Granularity of progress bar increases
my $DefaultSortColumn = 0; # Sets default sort column (0 = no sort)
my $HistoryDepth = 15; # Maximum number of entries we will list in XMC server history recall pull down (can be modified via xmcacli.ini)
my %XmcDeviceFamilyInteract = ( # This hash holds the XMC deviceDisplayFamily values for which we want to launch acli in interact mode
# For any device not in these families, acli will be launched with the -n flag set, for transparent mode
'VSP Series' => 1,
'ERS Series' => 1,
'Summit Series' => 1,
'WLAN Series' => 1, # WLAN9100
'ISW-Series' => 1,
'Extreme Access Series' => 1, # XA1400
'Unified Switching VOSS' => 1, # 5520-VOSS
'Unified Switching EXOS' => 1, # 5520-EXOS
'Universal Platform Fabric Engine' => 1, # 5520,5420,5320
'Universal Platform Switch Engine' => 1, # 5520,5420,5320
'XIQ Native' => 1, # HiveOS
'200 Series' => 1, # Series200
);
my ($ScriptName, $ScriptDir) = File::Basename::fileparse(File::Spec->rel2abs($0));
my $ConsoleWinTitle = "ACLI Terminal Launched Sessions";
my $ConsoleAcliProfile = 'ACLI';
my $RunScriptExtensions = [
["Run Scripts", ['.run', '.src', '']],
["All files", '*']
];
my $Ofh = \*STDOUT; # Default debug Output File Handle
our ($opt_d, $opt_f, $opt_g, $opt_h, $opt_i, $opt_m, $opt_n, $opt_p, $opt_q, $opt_s, $opt_t, $opt_u, $opt_w); #Getopts switches
my $IniFileName = 'xmcacli.ini';
my $GraphQlFile = 'xmcacli.graphql';
my $XmcHistoryFile = 'xmcacli.hist';
my $WindowHistoryFile = 'xmcacli.whist';
my $LastWorkingLoggingDir = 'xmcacli.lastdir',
my $AcliSpawnFile = 'acli.spawn';
my $AcliDir = '/.acli';
my (@AcliFilePath, $RunFilePath);
if (defined(my $path = $ENV{'ACLI'})) {
push(@AcliFilePath, File::Spec->canonpath($path));
$RunFilePath = File::Spec->canonpath($path);
}
elsif (defined($path = $ENV{'HOME'})) {
push(@AcliFilePath, File::Spec->canonpath($path.$AcliDir));
$RunFilePath = File::Spec->canonpath($path.$AcliDir);
}
elsif (defined($path = $ENV{'USERPROFILE'})) {
push(@AcliFilePath, File::Spec->canonpath($path.$AcliDir));
$RunFilePath = File::Spec->canonpath($path.$AcliDir);
}
push(@AcliFilePath, File::Spec->canonpath($ScriptDir)); # Last resort, script directory
############################
# THREAD SHARED VARIABLES #
############################
my $Shared_flag :shared = 0; # When set to 1, httpWorkerThread fetches data
my %Shared_xmcServer :shared; # Hash with info about XMC IP,port,credentials
my @Shared_thrdError : shared; # When $Shared_flag reset to 0 by thread, this will hold the error, if thread failed
my $Shared_JsonOutput : shared; # When $Shared_flag reset to 0 by thread, this will hold JSON output data, if the thread succeeded
#############################
# FUNCTIONS #
#############################
sub printSyntax {
printf "%s version %s%s\n\n", $ScriptName, $Version, ($Debug ? " running on $^O perl version $]" : "");
print "Usage:\n";
print " $ScriptName [-fgimnpqstuw] [<XMC server/IP[:port]>]\n\n";
print " <XMC server/IP[:port]>: Extreme Management Center IP address or hostname & port number\n";
print " -f <site-wildcard> : Filter entries on Site wildcard\n";
print " -g <record-grep> : Filter entries pattern match across any column data\n";
print " -h : Help and usage (this output)\n";
print " -i <log-dir> : Path to use when logging to file\n";
print " -m <script> : Once connected execute script (if no path included will use \@run search paths)\n";
print " -n : Launch terminals in transparent mode (no auto-detect & interact)\n";
print " -p ssh|telnet : Protocol to use; can be either SSH or Telnet (case insensitive)\n";
print " -q <graphql-file> : Override of default xmcacli.graphql file; must be placed in same path\n";
print " -s <sockets> : List of socket names for terminals to listen on (0 to disable sockets)\n";
print " -t <window-title> : Sets the containing window title into which all connections will be opened\n";
print " -u user[:<pwd>] : Specify XMC username[& password] to use\n";
print " -w <work-dir> : Working directory to use\n";
exit 1;
}
sub debugMsg { # Takes 4 args: debug-level, string1 [, ref-to-string [, string2] ]
if (shift() & $Debug) {
my ($string1, $stringRef, $string2) = @_;
my $refPrint = '';
if (defined $stringRef) {
if (!defined $$stringRef) {
$refPrint = '%stringRef UNDEFINED%';
}
elsif (length($$stringRef) && $string1 =~ /0x$/) {
$refPrint = unpack("H*", $$stringRef);
}
else {
$refPrint = $$stringRef;
}
}
$string2 = '' unless defined $string2;
print $Ofh $string1, $refPrint, $string2;
}
}
sub quit { # Quit printing script name + error message
my ($retval, $quitmsg) = @_;
print "\n$ScriptName: ",$quitmsg,"\n" if $quitmsg;
# Clean up and exit
exit $retval;
}
sub errorMsg { # Display invalid IP message to user
my ($tk, $title, $message) = @_;
# From command line
quit(1, $message) unless defined $tk;
# From Gui, don't exit, give popup
$tk->{mw}->messageBox(
-title => $title,
-icon => 'info',
-type => 'OK',
-message => $message,
);
return; # Invalid
}
sub readAcliSpawnFile { # Reads in acli.spawn file with command to execute based on local OS
my $tk = shift;
my $spawnFile;
foreach my $path (@AcliFilePath) {
if (-e "$path/$AcliSpawnFile") {
$spawnFile = "$path/$AcliSpawnFile";
last;
}
}
return errorMsg($tk, "File not found", "Unable to locate ACLI spawn file $AcliSpawnFile") unless defined $spawnFile;
open(SPAWN, '<', $spawnFile) or
return errorMsg($tk, "Cannot open file", "Unable to open ACLI spawn file " . File::Spec->canonpath($spawnFile));
my $lineNumber = 0;
my %execTemplate;
while (<SPAWN>) {
chomp;
$lineNumber++;
next if /^\s*$/; # skip empty lines
next if /^\s*#/; # skip comment lines
next unless s/^(\S+)\s+//; # Grab 1st word (OS)
next unless $1 eq $^O;
debugMsg(1,"ACLI.SPAWN: Entry for OS $^O found in line ", \$lineNumber, "\n");
%execTemplate = (timer1 => 0, timer2 => 0);
s/^(?:(\d{1,4}):)?(\d{1,4})\s+// && do {
$execTemplate{timer1} = defined $1 ? $1 : $2;
$execTemplate{timer2} = $2;
debugMsg(1,"ACLI.SPAWN: Timer1 = ", \$execTemplate{timer1}, " / Timer2 = $execTemplate{timer2}\n");
};
next unless s/^(\S+)\s+//; # Grab 2nd word (executable)
$execTemplate{executable} = $1;
$execTemplate{arguments} = $_;
debugMsg(1,"ACLI.SPAWN: Executable = ", \$execTemplate{executable}, "\n");
debugMsg(1,"ACLI.SPAWN: Arguments = ", \$execTemplate{arguments}, "\n");
}
close SPAWN;
unless (defined $execTemplate{executable} && defined $execTemplate{arguments}) {
print "Error: Unable to extract $^O executable + arguments from ACLI spawn file ", File::Spec->canonpath($AcliSpawnFile), "\n";
return;
}
return \%execTemplate;
}
sub substituteExecArgs { # Substitutes values into the arguments template obtained from acli.spawn file for the OS at hand
my ($template, $windowName, $instanceName, $tabName, $cwd, $acliProfile, $acliPath, $acliPlPath, $acliArgs) = @_;
# Replace values
$template =~ s/<WINDOW-NAME>/$windowName/g if defined $windowName;
$template =~ s/<INSTANCE-NAME>/$instanceName/g if defined $instanceName;
$template =~ s/<TAB-NAME>/$tabName/g if defined $tabName;
$template =~ s/<CWD>/$cwd/g if defined $cwd;
$template =~ s/<ACLI-PROFILE>/$acliProfile/g if defined $acliProfile;
$template =~ s/<ACLI-PATH>/$acliPath/g if defined $acliPath;
$template =~ s/<ACLI-PL-PATH>/$acliPlPath/g if defined $acliPlPath;
$template =~ s/<ACLI-ARGS>/$acliArgs/g if defined $acliArgs;
# Remove unused value markers
$template =~ s/(?:-[a-z]|--\w[\w-]+)(?:\s+|=)\"<[A-Z-]+>\"\s*//g; # Double quoted with preceding -switch
$template =~ s/(?:-[a-z]|--\w[\w-]+)(?:\s+|=)\'<[A-Z-]+>\'\s*//g; # Single quoted with preceding -switch
$template =~ s/(?:-[a-z]|--\w[\w-]+)(?:\s+|=)<[A-Z-]+>\s*//g; # Non quoted with preceding -switch
$template =~ s/\"<[A-Z-]+>\"\s*//g; # Double quoted with no preceding -switch
$template =~ s/\'<[A-Z-]+>\'\s*//g; # Single quoted with no preceding -switch
$template =~ s/<[A-Z-]+>\s*//g; # Non quoted with no preceding -switch
return $template;
}
sub exeIsRunning { # Checks whether the provided exe file is already running on the system
my $exeFile = File::Basename::fileparse(shift);
my $exeWinTitle = shift;
debugMsg(1, "exeIsRunning checking file $exeFile with Window Title: ", \$exeWinTitle, "\n");
my $tasklist = `tasklist /FI "IMAGENAME eq $exeFile" /FI "WINDOWTITLE eq $exeWinTitle"`;
debugMsg(1, "exeIsRunning tasklist :>", \$tasklist, "<\n");
return 1 if $tasklist =~ /^$exeFile/m;
return 0; # Otherwise
}
sub launchConsole { # Spawn entry into ConsoleZ; use Win32/Process instead of exec to avoid annoying DOS box
my ($tk, $displayData, $launchValues) = @_;
my $waitTimer;
my $containingWindow = length $launchValues->{Window} ? $launchValues->{Window} : $ConsoleWinTitle;
my $execTemplate = readAcliSpawnFile($tk);
return unless defined $execTemplate;
foreach my $device ( @{$displayData->{sortedDevices}} ) {
next unless $device->{selected};
$device->{selected} = 0; # Deselect it
my $acliArgs = '';
$acliArgs .= '-d 7 ' if $Debug;
$acliArgs .= '-n ' if $launchValues->{Transparent} || !$XmcDeviceFamilyInteract{$device->{deviceDisplayFamily}};
$acliArgs .= "-w \\\"$launchValues->{WorkDir}\\\" " if defined $launchValues->{WorkDir};
$acliArgs .= "-i \\\"$launchValues->{LogDir}\\\" " if defined $launchValues->{LogDir};
$acliArgs .= "-s \\\"$launchValues->{Sockets}\\\" " if defined $launchValues->{Sockets} && ($acliArgs !~ /-n/ || $launchValues->{Sockets} eq '0');
$acliArgs .= "-m \\\"$launchValues->{RunScript}\\\" " if defined $launchValues->{RunScript};
my $profile = $device->{profileName}; # Get device profile
if (defined $profile && defined $displayData->{profiles}->{$profile}->{userName}) { # We have a username
my $credentials = $displayData->{profiles}->{$profile}->{userName} . (defined $displayData->{profiles}->{$profile}->{loginPassword} ? ':'.$displayData->{profiles}->{$profile}->{loginPassword} : '');
if ($launchValues->{'Protocol'} eq 'SSH') {
$acliArgs .= "-l \\\"$credentials\\\" $device->{ip}";
}
else { # TELNET
$acliArgs .= "\\\"$credentials\@$device->{ip}\\\"";
}
}
my $executeArgs = substituteExecArgs( # Substitutes values into the executable arguments template
$execTemplate->{arguments}, # Template
$containingWindow, # <WINDOW-NAME>
$containingWindow, # <INSTANCE-NAME>
(defined $device->{sysName} ? $device->{sysName} : $device->{ip}), # <TAB-NAME>
File::Spec->rel2abs(cwd), # <CWD>
$ConsoleAcliProfile, # <ACLI-PROFILE>
$ScriptDir . 'acli', # <ACLI-PATH>
$ScriptDir . 'acli.pl', # <ACLI-PL-PATH>
$acliArgs, # <ACLI-ARGS>
);
debugMsg(1,"launchNewTerm / execuatable = ", \$execTemplate->{executable}, "\n");
debugMsg(1,"launchNewTerm / arguments = ", \$executeArgs, "\n");
# Perform sleep delay if applicable
if (defined $waitTimer) { # We never sleep on 1st instance launch
my $sleepTime;
if ($waitTimer) { # 2nd launch
if ($^O eq "MSWin32" && exeIsRunning($execTemplate->{executable}, $containingWindow) ) {
$sleepTime = $execTemplate->{timer2} / 1000;
debugMsg(1,"launchNewTerm / Sleep time 2nd launch (exe already running) = ", \$sleepTime, "\n");
}
else { # On MSWin32 make timer1 conditional on Console.exe not already running
$sleepTime = $execTemplate->{timer1} / 1000;
debugMsg(1,"launchNewTerm / Sleep time 2nd launch = ", \$sleepTime, "\n");
}
$waitTimer = 0; # Only do this once
}
else { # 3rd and beyond launches
$sleepTime = $execTemplate->{timer2} / 1000;
debugMsg(1,"launchNewTerm / Sleep time 3rd and above launch = ", \$sleepTime, "\n");
}
sleep $sleepTime if defined $sleepTime;
}
else {
$waitTimer = 1; # Force wait time on 2nd launch
}
if ($^O eq "MSWin32") { # Windows
my $processObj;
(my $executable = $execTemplate->{executable}) =~ s/\%([^\%]+)\%/defined $ENV{$1} ? $ENV{$1} : $1/ge;
debugMsg(1,"launchNewTerm / execuatable after resolving %ENV = ", \$executable, "\n");
Win32::Process::Create($processObj, $executable, $executeArgs, 0, &NORMAL_PRIORITY_CLASS, File::Spec->rel2abs(cwd));
return errorMsg($tk, "Failed to Launch", "Error launching $device->{ip} with ACLI Terminal") unless $processObj;
}
else { # Any other OS (MAC-OS and Linux...)
my $executable = join(' ', $execTemplate->{executable}, $executeArgs);
debugMsg(1,"launchNewTerm / execuatable after joining = ", \$executable, "\n");
my $retVal = system($executable);
return errorMsg($tk, "Failed to Launch", "Error launching $device->{ip} with ACLI Terminal") if $retVal;
}
}
}
sub findFile { # Searches our paths for specified file
my $fileName = shift;
my $filePath;
# Determine which file to work with
foreach my $path (@AcliFilePath) {
if (-e "$path/$fileName") {
$filePath = "$path/$fileName";
last;
}
}
return $filePath;
}
sub readIniFile { # Reads in acli.ini file
my ($xmcValues, $displayData) = @_;
my $iniFile = findFile($IniFileName);
quit(1, "Cannot find INI file $IniFileName") unless defined $iniFile;
debugMsg(1, "readIniFile - read in file : ", \$iniFile, "\n");
my $iniData = Config::INI::Reader::Ordered->read_file($iniFile);
my $xmcInfo = (shift @{$iniData})->[1] if $iniData->[0][0] eq '_'; # Remove the XMC hostname/credentials fields if present
unshift(@$iniData, [undef, {display => "Tree selection"}]); # Pre-pend our 1st header column, always present
$displayData->{headers} = $iniData;
$xmcValues->{xmcServer} = $xmcInfo->{xmcServer} if defined $xmcInfo->{xmcServer};
$xmcValues->{xmcUsername} = $xmcInfo->{xmcUsername} if defined $xmcInfo->{xmcUsername};
$xmcValues->{xmcPassword} = $xmcInfo->{xmcPassword} if defined $xmcInfo->{xmcPassword};
$HttpTimeout = $xmcInfo->{httpTimeout} if defined $xmcInfo->{httpTimeout}; # Override global
$HistoryDepth = $xmcInfo->{historyDepth} if defined $xmcInfo->{historyDepth}; # Override global
$displayData->{siteFilter} = $xmcInfo->{siteFilter} if defined $xmcInfo->{siteFilter};
$displayData->{grepFilter} = $xmcInfo->{grepFilter} if defined $xmcInfo->{grepFilter};
}
sub readHistoryFile { # Read the xmcacli.hist file
my $historyFile = shift;
my $histFile = findFile($historyFile);
unless (defined $histFile) { # If the file does not yet exist...
debugMsg(1, "readHistoryFile - history file ", \$historyFile, " does not exist\n");
return;
}
# Read the file into our array
open(HISTORY, '<', $histFile) or do {
debugMsg(1, "readHistoryFile - cannot open file to read : ", \$histFile, "\n");
return; # Same, if we can't open it
};
flock(HISTORY, 1); # 1 = LOCK_SH; Put a shared lock on the file (wait to read if it's being changed)
my @history;
while (<HISTORY>) {
chomp;
next unless length;
push(@history, $_);
}
close HISTORY;
debugMsg(1, "readHistoryFile - read in file : ", \$histFile, "\n");
return \@history;
}
sub writeHistoryFile { # Write a new xmcacli.hist file
my ($historyFile, $history) = @_;
unless (-e $AcliFilePath[0] && -d $AcliFilePath[0]) { # Create base directory if not existing
mkdir $AcliFilePath[0] or return;
debugMsg(1, "writeHistoryFile - created directory:\n ", \$AcliFilePath[0], "\n");
}
my $histFile = join('', $AcliFilePath[0], '/', $historyFile);
open(HISTORY, '>', $histFile) or do {
debugMsg(1, "writeHistoryFile - cannot open file to write : ", \$histFile, "\n");
return;
};
flock(HISTORY, 2); # 2 = LOCK_EX; Put an exclusive lock on the file as we are modifying it
my $count;
foreach (@$history) {
print HISTORY "$_\n";
last if ++$count == $HistoryDepth;
}
close HISTORY;
debugMsg(1, "writeHistoryFile - updated history file : ", \$histFile, "\n");
}
sub httpWorkerThread { # This is the actual http thread which handles GraphQL queries to XMC; it runs all the time and is triggered via shared variables
my ($graphQlFile, $dataSet, $nbiUrl, %nbi_call, $lwp, $request, $response, $history);
$SIG{'KILL'} = sub { die; }; # Signal handler for thread if killed by gui
JOB: while (1) { # Loop forever
# Wait for signal from GUI
sleep $ThreadSleepTimer until $Shared_flag;
debugMsg(1, "httpWorkerThread - starting\n");
# Empty both shared data structures
@Shared_thrdError = ();
$Shared_JsonOutput = '';
# Determine which acli.graphql file to work with
$graphQlFile = findFile($Shared_xmcServer{xmcGraphQl});
unless (defined $graphQlFile) {
@Shared_thrdError = ("No GraphQl query file", "Unable to locate file $Shared_xmcServer{xmcGraphQl}");
$Shared_flag = 0;
next JOB;
}
debugMsg(1, "httpWorkerThread - read in file : ", \$graphQlFile, "\n");
# Read in GraphQL dataset
open(GRAPHQL, '<', $graphQlFile) or do {
@Shared_thrdError = ("Cannot read GraphQl query file", "Unable to read GraphQL query file $Shared_xmcServer{xmcGraphQl}");
$Shared_flag = 0;
next JOB;
};
$dataSet = '';
while (<GRAPHQL>) {
next if /^$/; # Skip empty lines
next if /^#/; # Skip comment lines
$dataSet .= $_;
}
close GRAPHQL;
# Prepare GraphQL query
%nbi_call = (
operationName => undef,
query => $dataSet,
variables => undef,
);
# Prepare URL for HTTP POST
$nbiUrl = 'https://' . $Shared_xmcServer{xmcServer} . '/nbi/graphql';
debugMsg(1, "httpWorkerThread - url = ", \$nbiUrl, "\n");
# Create HTTP client
$lwp = LWP::UserAgent->new(
timeout => $HttpTimeout,
ssl_opts => {
verify_hostname => 0, # disable check called host name <=> CN
SSL_verify_mode => 0x00, # disable certificate validation
},
);
# Set the user-agent HTTP field to reflect this script/version + libwww-perl/version
$lwp->agent("$ScriptName/$Version" . $lwp->agent);
# Setup HTTP Headers to use
$lwp->default_header(
'Accept' => 'application/json',
'Accept-Encoding' => 'gzip, deflate, br',
'Connection' => 'keep-alive',
'Content-type' => 'application/json',
'Cache-Control' => 'no-cache',
'Pragma' => 'no-cache',
);
# Create HTTP Request
$request = HTTP::Request->new( POST => $nbiUrl );
$request->content( encode_json(\%nbi_call) );
$request->authorization_basic($Shared_xmcServer{xmcUsername}, $Shared_xmcServer{xmcPassword});
# Send HTTP Request to XMC server and fetch response
$response = $lwp->request( $request );
#print $response->as_string;
# Verify response for errors or success
if (!$response->is_success) { # HTTP Request failed
@Shared_thrdError = ("HTTP Request failed", $response->status_line);
}
elsif (!defined $response->header("Server")
|| ($response->header("Server") ne "Extreme Management Center" && $response->header("Server") ne "ExtremeCloudIQSiteEngine")
) { # We are not talking with an XMC server
@Shared_thrdError = ("Invalid HTTP Server", "Server is not Extreme Management Center");
}
elsif (!defined $response->header("Server-Version") || version->parse($response->header("Server-Version")) < version->parse("8.1.2")) { # We are talking with an XMC server which does not support GraphQL
@Shared_thrdError = ("No GraphQL support", "Extreme Management Center needs to be version 8.1.2 or higher to support GraphQL queries");
}
else { # Send the JSON output back
$Shared_JsonOutput = $response->content;
# It is not possible to do the decode_json here, as the hash structure returned becomes impossible to share back with the main gui thread
# Update the XMC history file with this XMC server, only if the fetch was successful
$history = readHistoryFile($XmcHistoryFile); # Read in history file
@$history = grep($_ ne $Shared_xmcServer{xmcServer}, @$history) if defined $history; # Filter out this XMC server if it was already present
unshift(@$history, $Shared_xmcServer{xmcServer}); # Add this XMC server at top of the list
writeHistoryFile($XmcHistoryFile, $history); # Re-write the file
}
# Reset shared flag to 0; ensures that thread will wait again at next loop cycle
$Shared_flag = 0;
} # Loop forever
}
sub flattenData { # This function flattend the $json->{data}->{network}->{devices} data to a single level; basically the extraData/deviceData sub key is ironed out
my ($tk, $arrayRef) = @_;
my ($dataLossError, $suffixAddedError);
foreach my $device (@$arrayRef) {
foreach my $key (keys %$device) {
next unless ref($device->{$key}) eq 'HASH'; # Skip local keys (or keys which are not a hash)
# Nested keys, we move to upper context
foreach my $nestedKey (keys %{$device->{$key}}) { # Sub-keys
if ( ref($device->{$key}->{$nestedKey}) ) { # If nested key has a 2nd level of hash/array, this will be lost..
$dataLossError = 1; # Generate an error once completed
next; # Skip
}
my $newKey = $nestedKey; # Assume we can just move the key up one level
my $suffix = ''; # Assume no suffix needed
while (exists $device->{$newKey . $suffix}) { # We have a key clash; there is already a key with the same name as the nested one
$suffix = 1 unless length $suffix; # Init to 1 (becomes 2 below)
$suffix++;
}
if ($suffix) { # If we had to add a suffix to the new key
$newKey .= $suffix;
$suffixAddedError = 1;
}
$device->{$newKey} = $device->{$key}->{$nestedKey}; # Move the key up a level
}
delete $device->{$key}; # Now remove the nested hash
}
}
errorMsg($tk, "Unexpected JSON returned", "JSON response from server contains more than one level of nested data; some data was lost while flattening the structure") if $dataLossError;
errorMsg($tk, "Unexpected JSON returned", "Nested data in JSON response from server contains keys which clash with the base level keys; some keys had a numerical suffix append while flattening the structure") if $suffixAddedError;
return $arrayRef;
}
sub profileHash { # This function re-arranges the XMC admin profiles array structure into a hash structure where the profile name is the key
my $arrayRef = shift;
my $hashRef = {};
foreach my $profile (@$arrayRef) {
$hashRef->{$profile->{profileName}} = $profile->{authCred};
}
return $hashRef;
}
sub recordSite { # Enters path into data structure and returns list of paths (may include parent ones) which had to be added and which will require adding to HList widget
my ($device, $sites, $sitePath, $siteFilter) = @_;
my @siteChain = split('/', $sitePath);
my $filterMatch = length $siteFilter ? 0 : 1;
my @newSites;
my $path = '';
my $prunedPath = '/';
while (my $branch = shift(@siteChain)) {
$filterMatch = 1 if length $siteFilter && $branch =~ /$siteFilter/i;
unless ($filterMatch) { # No match
$prunedPath .= $branch . '/';
next
}
$path .= length $path ? '/'.$branch : $branch;
unless ($sites->{$path}) { # New path
$sites->{$path}->{name} = $branch;
$sites->{$path}->{state} = scalar @siteChain ? 'normal' : 'disabled';
debugMsg(1, "recordSite - add parent site = ", \$path, "\n");
push(@newSites, $path);
}
}
$device->{prunedPath} = $prunedPath;
debugMsg(1, "recordSite - prunedPath = ", \$prunedPath, "\n");
debugMsg(1, "recordSite - returnPath = ", \$path, "\n");
return ($path, \@newSites);
}
sub convert_time { # Stolen here : https://neilang.com/articles/converting-seconds-into-a-readable-format-in-perl/
my $time = shift; # XMC time is in hunderds of a sec
my $timeSecs = int($time / 100); # Total sysuptime in secs
my $days = int($timeSecs / 86400);
$timeSecs -= ($days * 86400);
my $hours = int($timeSecs / 3600);
$timeSecs -= ($hours * 3600);
my $minutes = int($timeSecs / 60);
my $seconds = $timeSecs % 60;
$days = $days < 1 ? '' : $days .'d ';
$hours = $hours < 1 ? '' : $hours .'h ';
$minutes = $minutes < 1 ? '' : $minutes . 'm ';
$time = $days . $hours . $minutes . $seconds . 's';
return $time;
}
sub filterDevice { # Given a device, determines whether the device should be displayed based on siteFilter and/or grepFilter, if these are set
# If device is to be skipped, returns undef; if device is to be listed, returns a list ref with values to display
my ($displayData, $device) = @_;
# Immediately skip device if siteFilter is set and it has no match in the device sitePath
if (length $displayData->{siteFilter} && $device->{sitePath} !~ /\/$displayData->{siteFilter}(?:\/|$)/i) {
debugMsg(1, "filterDevice - device filtered due to siteFilter >", \$displayData->{siteFilter}, "<\n");
$device->{selected} = undef; # undef = entry filtered and not displayed
return
}
my @valuesList;
my $grepMatch = length $displayData->{grepFilter} ? 0 : 1; # If we have grepFilter set, the device will need to match on at least one field below; if not, the entry will be passed on anyway
for my $h (1 .. $#{$displayData->{headers}}) { # Skip 1st header, as that is just the tree view
my $hdr = $displayData->{headers}->[$h]->[0]; # Name of field (as returned by XMC)
my $hdrInfo = $displayData->{headers}->[$h]->[1]; # Hash with how we display the header {display} and how we format the value from XMC {type}
my $displayValue;
my $value = $device->{$hdr};
if (defined $value) { # Format it
if ($hdrInfo->{type} eq 'Time') {
$displayValue = convert_time($value);
}
elsif ($hdrInfo->{type} eq 'Flag') {
$displayValue = $value ? 'True' : 'False';
}
elsif ($hdrInfo->{type} eq 'YesNo') {
$displayValue = $value ? 'Yes' : 'No';
}
else { # we assume String, DotDecimal, Number
$displayValue = $value;
}
}
debugMsg(1, "filterDevice - value $h = ", \$displayValue, "\n");
push(@valuesList, $displayValue);
next unless defined $displayValue;
$grepMatch = 1 if !$grepMatch && $displayValue =~ /$displayData->{grepFilter}/i; # See if value gives us a match
}
# If $grepMatch is not set this means that a grepFilter was set and it did not match any of the device fields; so we skip the device
unless ($grepMatch) {
debugMsg(1, "filterDevice - device filtered due to grepFilter >", \$displayData->{grepFilter}, "<\n");
$device->{selected} = undef; # undef = entry filtered and not displayed
return
}
# If we get here, then the device is to be listed; return its values
$device->{selected} = 0 unless defined $device->{selected}; # defined value = entry is to be displayed
return \@valuesList;
}
sub doubleClickTree { # Handle double click performed on a branch of the tree
my ($tk, $displayData) = @_;
my $pathSelected = $tk->{mwTree}->infoAnchor; # Pre-pend the pruned path (if siteFilter is in effect) or '/' otherwise
return unless defined $pathSelected; # Not sure when this happens, but sometime it does...
debugMsg(1, "doubleClickTree - pathSelected = ", \$pathSelected, "\n");
# First determine if switches under path are all selected, or not
my $allSelected = 1; # Assume yes
foreach my $device ( @{$displayData->{devices}} ) {
next unless $device->{prunedPath} . $pathSelected eq $device->{sitePath};
if (defined $device->{selected} && $device->{selected} == 0) { # Device is diplayed (not filtered out by siteFilter or grepFilter) and not selected
$allSelected = 0; # Conclude no
last;
}
}
debugMsg(1, "doubleClickTree - allSelected = ", \$allSelected, "\n");
if ($allSelected) { # All selected
# Deselect all
foreach my $device ( @{$displayData->{devices}} ) {
next unless defined $device->{selected}; # If entry not displayed, skip
next unless $device->{prunedPath} . $pathSelected eq $device->{sitePath};
$device->{selected} = 0 if defined $device->{selected}; # Unselect, if displayed
debugMsg(1, "doubleClickTree - deselecting device = ", \$device->{ip}, "\n");
}
}
else { # Some selected & some not, or none selected
# Select all
foreach my $device ( @{$displayData->{devices}} ) {
next unless defined $device->{selected}; # If entry not displayed, skip
next unless $device->{prunedPath} . $pathSelected eq $device->{sitePath};
$device->{selected} = 1 if defined $device->{selected}; # Select, if displayed
debugMsg(1, "doubleClickTree - selecting device = ", \$device->{ip}, "\n");
}
}
}
sub compareDigitList { # Does a compare between 2 lists of digits
my ($aList, $bList) = @_;
my $result = 0;
for (my $i = 0; $i <= $#$aList || $i <= $#$bList; $i++) {
if (defined $aList->[$i] && defined $bList->[$i]) {
$result = $aList->[$i] <=> $bList->[$i];
}
else {
$result = defined $aList->[$i] ? 1 : -1;
}
last if $result;
}
return $result;
}
sub byHeader { # Sort function to arrange devices according to one of the elements of info (headers)
my ($displayData, $hdrIdx) = @_;
my ($a_field, $b_field, $direction, $result);
my $hdr = $displayData->{headers}->[$hdrIdx]->[0]; # Name of field (as returned by XMC)
my $hdrInfo = $displayData->{headers}->[$hdrIdx]->[1]; # Hash with how we need to perform sort
$a_field = $a->{$hdr};
$a_field = '' unless defined $a_field;
$b_field = $b->{$hdr};
$b_field = '' unless defined $b_field;
$direction = $displayData->{sortHeaders}->{$hdr};
if ($hdrInfo->{type} eq 'DotDecimal') {
$a_field =~ s/[^\d\._]//g; # Version numbers sometimes preceded by 'v'; betas sometimes have a 'b'
$b_field =~ s/[^\d\._]//g; # Take out any non-digit / valid separator characters
my @a_digits = split(/[\._]/, $a_field);
my @b_digits = split(/[\._]/, $b_field);
$result = compareDigitList(\@a_digits, \@b_digits);
}
elsif ($hdrInfo->{type} eq 'Time' || $hdrInfo->{type} eq 'Number') {
$result = lc($a_field) <=> lc($b_field);
}
else { # we assume String
$result = lc($a_field) cmp lc($b_field);
}
return $direction * $result;
}
sub bySitePath { # Sort function to arrange devices accorfing to the depth of their sitePath
my ($displayData) = @_;
my $a_pathDepth = scalar( split('/', $a->{sitePath}) );
my $b_pathDepth = scalar( split('/', $b->{sitePath}) );
return $a_pathDepth <=> $b_pathDepth;
}
sub updateDeviceData { # Populates the GUI window with the data extracted from XMC
my ($tk, $displayData, $sortHdrIdx) = @_;
my $sortHdr = defined $sortHdrIdx ? $displayData->{headers}->[$sortHdrIdx]->[0] : undef;
return unless @{$displayData->{devices}}; # If we have no data, simply come out
if ( Tk::Exists($tk->{mwTree}) ) { # Widget already exists, remove it and delete it
$tk->{mwTree}->packForget;
#$tk->{mwTree}->destroy; # We don't do this anymore because it was bombing with "Tk_FreeCursor received unknown cursor argument"
# when selecting a site folder, and then using the Site Filter input to select a site (bug29)
# See: https://github.com/eserte/perl-tk/pull/40
}
# We have to create a new widget, as the -columns can only be set on creation
$tk->{mwTree} = $tk->{mwMiddleFrame}->ScrlTree(
-itemtype => 'text',
-separator => '/',
-scrollbars => "se",
-selectmode => 'browse',
#-command => [\&doubleClickTree, $tk, $displayData], # Has issues on window resize (stops working); using Tk::DoubleClick instead
-columns => scalar @{$displayData->{headers}},
-header => 1,
);
# Display the headers
for my $h (0 .. $#{$displayData->{headers}}) {
my $hdrInfo = $displayData->{headers}->[$h]->[1]; # Hash with how we display the header {display} and how we format the value from XMC {type}
$tk->{mwTree}->headerCreate(
$h,
-itemtype => 'window',
-borderwidth => -2,
-widget => $tk->{mwTree}->Button(
-anchor => 'center',
-text => $hdrInfo->{display},
-command => [\&updateDeviceData, $tk, $displayData, $h],
-state => ($h == 0 ? 'disabled' : 'normal'),
),
);
}
# Sort the device records according to any selected header column
if (defined $sortHdr) { # User clicked on one of the headers
if (!defined $displayData->{sortHeaders}->{$sortHdr} || $displayData->{sortHeaders}->{$sortHdr} == -1) { # First time, or last sort was reverse
$displayData->{sortHeaders}->{$sortHdr} = 1; # Do a normal sort
}
else { # Last sort was normal sort
$displayData->{sortHeaders}->{$sortHdr} = -1; # Do a reverse sort
}
@{$displayData->{sortedDevices}} = sort { byHeader($displayData, $sortHdrIdx) } @{$displayData->{devices}};
}
else { # We just show the records in the order in which they are
$displayData->{sortedDevices} = $displayData->{devices};
}
# The above sort was across all site paths; but for parent site branches, which have devices and child branches,
# we want the devices to be added to the widget before the branches, otherwise it looks ugly.
# This means adding devices with path X before adding devices with path X/Y
# So we do another sort, this time on the sitePath
my @deviceDisplayOrder = sort { bySitePath($displayData) } @{$displayData->{sortedDevices}};
# Display the records
$displayData->{sites} = {}; # Clear this before starting
foreach my $d ( 0 .. $#deviceDisplayOrder ) {
my $device = $deviceDisplayOrder[$d];
debugMsg(1, "\nupdateDeviceData - device = ", \$device->{ip}, "\n");
# Check if this device is to be shown, based on siteFilter and grepFilter, if these are set
next unless my $valuesList = filterDevice($displayData, $device);
# Process the device sitePath
my $sitePath = $device->{sitePath};
$sitePath =~ s/^\///; # Remove leading \; HList does not like it
$sitePath .= "/&$d"; # We use the array index as switch leaf path name and we prepend it with & which ensures no clash with XMC site names
my $parentList;
($sitePath, $parentList) = recordSite($device, $displayData->{sites}, $sitePath, $displayData->{siteFilter});
# Create the device sitePath as well as parents, if these were not already created before
foreach my $site (@$parentList) {
$tk->{mwTree}->add(
$site,
-text => $displayData->{sites}->{$site}->{name},
-state => $displayData->{sites}->{$site}->{state},
);
}
# Create the tree entry (1st column)
$tk->{mwTree}->itemCreate(
$sitePath,
0, # Column 0
-itemtype => 'window',
-widget => $tk->{mwTree}->Checkbutton(
-anchor => 'w',
-variable => \$device->{selected},
-borderwidth => $^O eq "MSWin32" ? 0 : 1
),
);
# Create the entries for the values (2nd to last columns)
foreach my $i (0 .. $#{$valuesList}) {
$tk->{mwTree}->itemCreate(
$sitePath,
$i + 1,
-text => $valuesList->[$i],
);
}
}
$tk->{mwTree}->pack( -fill => 'both', -expand => 1 );
$tk->{mwTree}->autosetmode();
$tk->{mwTree}->update;
# Bind mouse double click event to ScrlTree widget
#$tk->{mwTree}->bind('<Double-Button-1>' => sub { doubleClickTree($tk, $displayData) } ); # Process double-click
# Binding <Double-Button-1> with ScrlTree/HList widget has issues on window resize (stops working); so using Tk::DoubleClick instead
Tk::DoubleClick::bind_clicks(
$tk->{mwTree},
sub{}, # Single callback -> do nothing
[\&doubleClickTree, $tk, $displayData], # Double callback -> this is what we want
-delay => 500,
-button => 'left',
);
}
sub updateSiteFilterListBox { # From freshly fetched data, we extract all the branches for the sitePaths we see
my ($tk, $displayData) = @_;
my %branches;
foreach my $device ( @{$displayData->{devices}} ) {
my @siteChain = split('/', $device->{sitePath});
foreach my $branch (@siteChain) {
next unless length $branch;
$branches{$branch} = 1;
}
}
my @branches = sort { $a cmp $b } keys %branches;
$tk->{enSiteFilt}->configure( -choices => \@branches );
}
sub updateXmcHistoryListBox { # The thread will have updated the xmcacli.hist file; so need to refresh the Gui list box
my ($tk, $xmcData) = @_;
$xmcData->{xmcHistory} = readHistoryFile($XmcHistoryFile);
$tk->{enXmcIp}->configure( -choices => $xmcData->{xmcHistory} );
}
sub checkThread { # This function is used to communicate between the httpWorkingThread and the tk Gui Mainloop
my ($tk, $displayData, $xmcData) = @_;
# Update progress bar
$tk->{progressPercent} += $ProgressBarBit;
# If worker thread has not finished come out
return if $Shared_flag;
# Worker thread has completed; disable running myself again
$tk->{mwRepeatId}->cancel;
# Check what the outcome was
my $success;
if (@Shared_thrdError) { # We got an error back
errorMsg($tk, @Shared_thrdError);
}
elsif ($Shared_JsonOutput) { # We got JSON data back
# Decode the JSON output