1#! /usr/bin/perl
2
3# This script is used for generating callout lists from the MRW for devices
4# that can be accessed from the BMC.  The callouts have a location code, the
5# target name (for debug), a priority, and in some cases a MRU.  The script
6# supports I2C, FSI, SPI, FSI-I2C, and FSI-SPI devices.  The output is a JSON
7# file organized into sections for each callout type, with keys based on the
8# type.  I2c uses a bus and address, FSI uses a link, and SPI uses a bus
9# number. If FSI is combined with I2C or SPI, then the link plus the I2C/SPI
10# keys is used.  Multi-hop FSI links are indicated by a dash in between the
11# links, eg "0-1".
12#
13# An example section is:
14# "FSI":
15# {
16#   "5":
17#   {
18#      "Callouts":[
19#        {
20#           "Priority":"H"
21#           "LocationCode": "P1-C50",
22#           "MRU":"/sys-0/node-0/motherboard/proc_socket-0/module-0/power9-0",
23#           "Name":"/sys-0/node-0/motherboard/cpu0"
24#        },
25#        {
26#           "Priority":"H",
27#           "LocationCode": "P1-C42",
28#           "MRU":"/sys-0/node-0/motherboard/ebmc-card/BMC-0",
29#           "Name":"/sys-0/node-0/motherboard/ebmc-card"
30#        },
31#        {
32#           "Priority":"L",
33#           "LocationCode": "P1",
34#           "Name":"/sys-0/node-0/motherboard"
35#        }
36#     ],
37#     "Dest":"/sys-0/node-0/motherboard-0/proc_socket-0/module-0/power9-0",
38#     "Source":"/sys-0/node-0/motherboard-0/ebmc-card-connector-0/card-0/bmc-0"
39#   }
40# }
41# The Name, Dest and Source entries are MRW targets and are just for debug.
42#
43# The optional --segments argument will output a JSON file of all the bus
44# segments in the system, which is what the callouts are made from.
45
46use strict;
47use warnings;
48
49# Callout object
50# Base class for other callouts.
51# There is an object per device, so it can contain multiple
52# FRU callouts in the calloutList attribute.
53package Callout;
54sub new
55{
56    my $class = shift;
57    my $self = {
58        type => shift,
59        sourceChip => shift,
60        destChip => shift,
61        calloutList => shift,
62    };
63
64    return bless $self, $class;
65}
66
67sub sourceChip
68{
69    my $self = shift;
70    return $self->{sourceChip};
71}
72
73sub destChip
74{
75    my $self = shift;
76    return $self->{destChip};
77}
78
79sub type
80{
81    my $self = shift;
82    return $self->{type};
83}
84
85sub calloutList
86{
87    my $self = shift;
88    return $self->{calloutList};
89}
90
91# I2CCallout object for I2C callouts
92package I2CCallout;
93our @ISA = qw(Callout);
94sub new
95{
96    my ($class) = @_;
97    # source, dest, calloutList
98    my $self = $class->SUPER::new("I2C", $_[1], $_[2], $_[3]);
99    $self->{i2cBus} = $_[4];
100    $self->{i2cAddr} = $_[5];
101    return bless $self, $class;
102}
103
104sub i2cBus
105{
106    my $self = shift;
107    return $self->{i2cBus};
108}
109
110sub i2cAddress
111{
112    my $self = shift;
113    return $self->{i2cAddr};
114}
115
116# FSICallout object for FSI callouts
117package FSICallout;
118our @ISA = qw(Callout);
119sub new
120{
121    my ($class) = @_;
122    my $self = $class->SUPER::new("FSI", $_[1], $_[2], $_[3]);
123    $self->{FSILink} = $_[4];
124    bless $self, $class;
125    return $self;
126}
127
128sub fsiLink
129{
130    my $self = shift;
131    return $self->{FSILink};
132}
133
134# SPICallout object for SPI callouts
135package SPICallout;
136our @ISA = qw(Callout);
137sub new
138{
139    my ($class) = @_;
140    my $self = $class->SUPER::new("SPI", $_[1], $_[2], $_[3]);
141    $self->{SPIBus} = $_[4];
142    bless $self, $class;
143    return $self;
144}
145
146sub spiBus
147{
148    my $self = shift;
149    return $self->{SPIBus};
150}
151
152# FSII2CCallout object for FSI-I2C callouts
153# Ideally this would be derived from FSICallout, but I can't
154# get it to work.
155package FSII2CCallout;
156
157our @ISA = qw(Callout);
158sub new
159{
160    my ($class) = @_;
161    # source, dest, calloutList
162    my $self = $class->SUPER::new("FSI-I2C", $_[1], $_[2], $_[3]);
163    $self->{FSILink} = $_[4];
164    $self->{i2cBus} = $_[5];
165    $self->{i2cAddr} = $_[6];
166    return bless $self, $class;
167}
168
169sub fsiLink
170{
171    my $self = shift;
172    return $self->{FSILink};
173}
174
175sub i2cBus
176{
177    my $self = shift;
178    return $self->{i2cBus};
179}
180
181sub i2cAddress
182{
183    my $self = shift;
184    return $self->{i2cAddr};
185}
186
187#FSISPICallout object for FSI-SPI callouts
188package FSISPICallout;
189
190our @ISA = qw(Callout);
191sub new
192{
193    my ($class) = @_;
194    # source, dest, calloutList
195    my $self = $class->SUPER::new("FSI-SPI", $_[1], $_[2], $_[3]);
196    $self->{FSILink} = $_[4];
197    $self->{SPIBus} = $_[5];
198    return bless $self, $class;
199}
200
201sub fsiLink
202{
203    my $self = shift;
204    return $self->{FSILink};
205}
206
207sub spiBus
208{
209    my $self = shift;
210    return $self->{SPIBus};
211}
212
213package main;
214
215use mrw::Targets;
216use mrw::Util;
217use Getopt::Long;
218use File::Basename;
219use JSON;
220
221my $mrwFile = "";
222my $outFile = "";
223my $printSegments = 0;
224
225# Not supporting priorites A, B, or C until necessary
226my %priorities = (H => 3, M => 2, L => 1);
227
228# Segment bus types
229my %busTypes = ( I2C => 1, FSIM => 1, FSICM => 1, SPI => 1 );
230
231GetOptions(
232    "m=s" => \$mrwFile,
233    "o=s" => \$outFile,
234    "segments" => \$printSegments
235)
236    or printUsage();
237
238if (($mrwFile eq "") or ($outFile eq ""))
239{
240    printUsage();
241}
242
243# Load system MRW
244my $targets = Targets->new;
245$targets->loadXML($mrwFile);
246
247# Find all single segment buses that we care about
248my %allSegments = getPathSegments();
249
250my @callouts;
251
252# Build the single and multi segment callouts
253buildCallouts(\%allSegments, \@callouts);
254
255printJSON(\@callouts);
256
257# Write the segments to a JSON file
258if ($printSegments)
259{
260    my $outDir = dirname($outFile);
261    my $segmentsFile = "$outDir/segments.json";
262
263    open(my $fh, '>', $segmentsFile) or
264        die "Could not open file '$segmentsFile' $!";
265
266    my $json = JSON->new;
267    $json->indent(1);
268    $json->canonical(1);
269    my $text = $json->encode(\%allSegments);
270    print $fh $text;
271    close $fh;
272}
273
274# Returns a hash of all the FSI, I2C, and SPI segments in the MRW
275sub getPathSegments
276{
277    my %segments;
278    foreach my $target (sort keys %{$targets->getAllTargets()})
279    {
280        my $numConnections = $targets->getNumConnections($target);
281
282        if ($numConnections == 0)
283        {
284            next;
285        }
286
287        for (my $connIndex=0;$connIndex<$numConnections;$connIndex++)
288        {
289            my $connBusObj = $targets->getConnectionBus($target, $connIndex);
290            my $busType = $connBusObj->{bus_type};
291
292            # We only care about certain bus types
293            if (not exists $busTypes{$busType})
294            {
295                next;
296            }
297
298            my $dest = $targets->getConnectionDestination($target, $connIndex);
299
300            my %segment;
301            $segment{BusType} = $busType;
302            $segment{SourceUnit} = $target;
303            $segment{SourceChip} = getParentByClass($target, "CHIP");
304            if ($segment{SourceChip} eq "")
305            {
306                die "Warning: Could not get parent chip for source $target\n";
307            }
308
309            $segment{DestUnit} = $dest;
310            $segment{DestChip} = getParentByClass($dest, "CHIP");
311
312            # If the unit's direct parent is a connector that's OK too.
313            if ($segment{DestChip} eq "")
314            {
315                my $parent = $targets->getTargetParent($dest);
316                if ($targets->getAttribute($parent, "CLASS") eq "CONNECTOR")
317                {
318                    $segment{DestChip} = $parent;
319                }
320            }
321
322            if ($segment{DestChip} eq "")
323            {
324                die "Warning: Could not get parent chip for dest $dest\n";
325            }
326
327            my $fruPath = $targets->getBusAttribute(
328                $target, $connIndex, "FRU_PATH");
329
330            if (defined $fruPath)
331            {
332                $segment{FRUPath} = $fruPath;
333                my @callouts = getFRUPathCallouts($fruPath);
334                $segment{Callouts} = \@callouts;
335            }
336            else
337            {
338                $segment{FRUPath} = "";
339                my @empty;
340                $segment{Callouts} = \@empty;
341            }
342
343            if ($busType eq "I2C")
344            {
345                $segment{I2CBus} = $targets->getAttribute($target, "I2C_PORT");
346                $segment{I2CAddress} =
347                    hex($targets->getAttribute($dest, "I2C_ADDRESS"));
348
349                $segment{I2CBus} = $segment{I2CBus};
350
351                # Convert to the 7 bit address that linux uses
352                $segment{I2CAddress} =
353                    Util::adjustI2CAddress($segment{I2CAddress});
354            }
355            elsif ($busType eq "FSIM")
356            {
357                $segment{FSILink} =
358                    hex($targets->getAttribute($target, "FSI_LINK"));
359            }
360            elsif ($busType eq "SPI")
361            {
362                $segment{SPIBus} = $targets->getAttribute($target, "SPI_PORT");
363
364                # Seems to be in HEX sometimes
365                if ($segment{SPIBus} =~ /^0x/i)
366                {
367                    $segment{SPIBus} = hex($segment{SPIBus});
368                }
369            }
370
371            push @{$segments{$busType}}, { %segment };
372        }
373    }
374
375    return %segments;
376}
377
378#Breaks the FRU_PATH atttribute up into its component callouts.
379#It looks like:  "H:<some target>,L:<some other target>(<MRU>)"
380#Where H/L are the priorities and can be H/M/L.
381#The MRU that is in parentheses is optional and is a chip name on that
382#FRU target.
383sub getFRUPathCallouts
384{
385    my @callouts;
386    my $fruPath = shift;
387
388    my @entries = split(',', $fruPath);
389
390    for my $entry (@entries)
391    {
392        my %callout;
393        my ($priority, $path) = split(':', $entry);
394
395        # pull the MRU out of the parentheses at the end and then
396        # remove the parentheses.
397        if ($path =~ /\(.+\)$/)
398        {
399            ($callout{MRU}) = $path =~ /\((.+)\)/;
400
401            $path =~ s/\(.+\)$//;
402        }
403
404        # check if the target we read out is valid by
405        # checking for a required attribute
406        if ($targets->isBadAttribute($path, "CLASS"))
407        {
408            die "FRU Path $path not a valid target\n";
409        }
410
411        $callout{Priority} = $priority;
412        if (not exists $priorities{$priority})
413        {
414            die "Invalid priority: '$priority' on callout $path\n";
415        }
416
417        $callout{Name} = $path;
418
419        push @callouts, \%callout;
420    }
421
422    return @callouts;
423}
424
425# Returns an ancestor target based on its class
426sub getParentByClass
427{
428    my ($target, $class) = @_;
429    my $parent = $targets->getTargetParent($target);
430
431    while (defined $parent)
432    {
433        if (!$targets->isBadAttribute($parent, "CLASS"))
434        {
435            if ($class eq $targets->getAttribute($parent, "CLASS"))
436            {
437                return $parent;
438            }
439        }
440        $parent = $targets->getTargetParent($parent);
441    }
442
443    return "";
444}
445
446# Build the callout objects
447sub buildCallouts
448{
449    my ($segments, $callouts) = @_;
450
451    # Callouts for 1 segment connections directly off of the BMC.
452    buildBMCSingleSegmentCallouts($segments, $callouts);
453
454    # Callouts more than 1 segment away
455    buildMultiSegmentCallouts($segments, $callouts);
456}
457
458# Build the callout objects for devices 1 segment away.
459sub buildBMCSingleSegmentCallouts
460{
461    my ($segments, $callouts) = @_;
462
463    for my $busType (keys %$segments)
464    {
465        for my $segment (@{$$segments{$busType}})
466        {
467            my $chipType = $targets->getType($segment->{SourceChip});
468            if ($chipType eq "BMC")
469            {
470                my $callout = buildSingleSegmentCallout($segment);
471
472                if (defined $callout)
473                {
474                    push @{$callouts}, $callout;
475                }
476            }
477        }
478    }
479}
480
481# Build the callout object based on the callout type using the
482# callout list from the single segment.
483sub buildSingleSegmentCallout
484{
485    my ($segment, $callouts) = @_;
486
487    if ($segment->{BusType} eq "I2C")
488    {
489        return createI2CCallout($segment, $callouts);
490    }
491    elsif ($segment->{BusType} eq "FSIM")
492    {
493        return createFSICallout($segment, $callouts);
494    }
495    elsif ($segment->{BusType} eq "SPI")
496    {
497        return createSPICallout($segment, $callouts);
498    }
499
500    return undef;
501}
502
503# Build the callout objects for devices more than 1 segment away from
504# the BMC.  All but the last segment will always be FSI.  The FRU
505# callouts accumulate as segments are added.
506sub buildMultiSegmentCallouts
507{
508    my ($segments, $callouts) = @_;
509    my $hops = 0;
510    my $found = 1;
511
512    # Connect FSI link callouts to other FSI segments to make new callouts, and
513    # connect all FSI link callouts up with the I2C/SPI segments to make even
514    # more new callouts.  Note: Deal with I2C muxes, if they are ever modeled,
515    # when there are some.
516
517    # Each time through the loop, go out another FSI hop.
518    # Stop when no more new hops are found.
519    while ($found)
520    {
521        my @newCallouts;
522        $found = 0;
523
524        for my $callout (@$callouts)
525        {
526            if ($callout->type() ne "FSI")
527            {
528                next;
529            }
530
531            # link numbers are separated by '-'s in the link field,
532            # so 0-5 = 1 hop
533            my @numHops = $callout->fsiLink() =~ /(-)/g;
534
535            # only deal with callout objects that contain $hops hops.
536            if ($hops != scalar @numHops)
537            {
538                next;
539            }
540
541            for my $segmentType (keys %$segments)
542            {
543                for my $segment (@{$$segments{$segmentType}})
544                {
545                    # If the destination on this callout is the same
546                    # as the source of the segment, then make a new
547                    # callout that spans both.
548                    if ($callout->destChip() eq $segment->{SourceChip})
549                    {
550                        # First build the new single segment callout
551                        my $segmentCallout =
552                            buildSingleSegmentCallout($segment);
553
554                        # Now merge both callouts into one.
555                        if (defined $segmentCallout)
556                        {
557                            my $newCallout =
558                                mergeCallouts($callout, $segmentCallout);
559
560                            push @newCallouts, $newCallout;
561                            $found = 1;
562                        }
563                    }
564                }
565            }
566        }
567
568        if ($found)
569        {
570            push @{$callouts}, @newCallouts;
571        }
572
573        $hops = $hops + 1;
574    }
575}
576
577# Merge 2 callout objects into 1 that contains all of their FRU callouts.
578sub mergeCallouts
579{
580    my ($firstCallout, $secondCallout) = @_;
581
582    # This callout list will be merged/sorted later.
583    # Endpoint callouts are added first, so they will be higher
584    # in the callout list (priority permitting).
585    my @calloutList;
586    push @calloutList, @{$secondCallout->calloutList()};
587    push @calloutList, @{$firstCallout->calloutList()};
588
589    # FSI
590    if (($firstCallout->type() eq  "FSI") && ($secondCallout->type() eq "FSI"))
591    {
592        # combine the FSI links with a -
593        my $FSILink = $firstCallout->fsiLink() . "-" .
594            $secondCallout->fsiLink();
595
596        my $fsiCallout =  new FSICallout($firstCallout->sourceChip(),
597            $secondCallout->destChip(), \@calloutList, $FSILink);
598
599        return $fsiCallout;
600    }
601    # FSI-I2C
602    elsif (($firstCallout->type() eq  "FSI") &&
603        ($secondCallout->type() eq "I2C"))
604    {
605        my $i2cCallout = new FSII2CCallout($firstCallout->sourceChip(),
606            $secondCallout->destChip(), \@calloutList,
607            $firstCallout->fsiLink(),  $secondCallout->i2cBus(),
608            $secondCallout->i2cAddress());
609
610        return $i2cCallout;
611    }
612    # FSI-SPI
613    elsif (($firstCallout->type() eq  "FSI") &&
614        ($secondCallout->type() eq "SPI"))
615    {
616        my $spiCallout = new FSISPICallout($firstCallout->sourceChip(),
617            $secondCallout->destChip(), \@calloutList,
618            $firstCallout->fsiLink(), $secondCallout->spiBus());
619
620        return $spiCallout;
621    }
622
623    die "Unrecognized callouts to merge: " . $firstCallout->type() .
624    " " . $secondCallout->type() . "\n";
625}
626
627# Create an I2CCallout object
628sub createI2CCallout
629{
630    my $segment = shift;
631    my $bus = $segment->{I2CBus};
632
633    # Convert MRW BMC I2C numbering to the linux one for the BMC
634    if ($targets->getAttribute($segment->{SourceChip}, "TYPE") eq "BMC")
635    {
636        $bus = Util::adjustI2CPort($segment->{I2CBus});
637
638        if ($bus < 0)
639        {
640            die "After adjusting BMC I2C bus $segment->{I2CBus}, " .
641                "got a negative number\n";
642        }
643    }
644
645    my $i2cCallout = new I2CCallout($segment->{SourceChip},
646        $segment->{DestChip}, $segment->{Callouts}, $bus,
647        $segment->{I2CAddress});
648
649    return $i2cCallout;
650}
651
652# Create an FSICallout object
653sub createFSICallout
654{
655    my $segment = shift;
656
657    my $fsiCallout = new FSICallout($segment->{SourceChip},
658        $segment->{DestChip}, $segment->{Callouts},
659        $segment->{FSILink}, $segment);
660
661    return $fsiCallout;
662}
663
664# Create a SPICallout object
665sub createSPICallout
666{
667    my $segment = shift;
668
669    my $spiCallout = new SPICallout($segment->{SourceChip},
670        $segment->{DestChip}, $segment->{Callouts},
671        $segment->{SPIBus});
672
673    return $spiCallout;
674}
675
676# Convert all of the *Callout objects to JSON and write them to a file.
677# It will convert the callout target names to location codes and also sort
678# the callout list before doing so.
679sub printJSON
680{
681    my $callouts = shift;
682    my %output;
683
684    for my $callout (@$callouts)
685    {
686        my %c;
687        $c{Source} = $callout->sourceChip();
688        $c{Dest} = $callout->destChip();
689
690        for my $fruCallout (@{$callout->calloutList()})
691        {
692            my %entry;
693
694            if (length($fruCallout->{Name}) == 0)
695            {
696                die "Could not get target name for a callout on path:\n" .
697                 "    (" . $callout->sourceChip() . ") ->\n" .
698                 "    (" . $callout->destChip() . ")\n";
699            }
700
701            $entry{Name} = $fruCallout->{Name};
702
703            $entry{LocationCode} =
704                Util::getLocationCode($targets, $fruCallout->{Name});
705
706            # MRUs - for now just use the path + MRU name
707            if (exists $fruCallout->{MRU})
708            {
709                $entry{MRU} = $entry{Name} . "/" . $fruCallout->{MRU};
710            }
711
712            $entry{Priority} = validatePriority($fruCallout->{Priority});
713
714            push @{$c{Callouts}}, \%entry;
715        }
716
717
718        # Remove duplicate callouts and sort
719        @{$c{Callouts}} = sortCallouts(@{$c{Callouts}});
720
721        # Setup the entry based on the callout type
722        if ($callout->type() eq "I2C")
723        {
724            # The address key is in decimal, but save the hex value
725            # for easier debug.
726            $c{HexAddress} = $callout->i2cAddress();
727            my $decimal = hex($callout->i2cAddress());
728
729            $output{"I2C"}{$callout->i2cBus()}{$decimal} = \%c;
730        }
731        elsif ($callout->type() eq "SPI")
732        {
733            $output{"SPI"}{$callout->spiBus()} = \%c;
734        }
735        elsif ($callout->type() eq "FSI")
736        {
737            $output{"FSI"}{$callout->fsiLink()} = \%c;
738        }
739        elsif ($callout->type() eq "FSI-I2C")
740        {
741            $c{HexAddress} = $callout->i2cAddress();
742            my $decimal = hex($callout->i2cAddress());
743
744            $output{"FSI-I2C"}{$callout->fsiLink()}
745                {$callout->i2cBus()}{$decimal} = \%c;
746        }
747        elsif ($callout->type() eq "FSI-SPI")
748        {
749            $output{"FSI-SPI"}{$callout->fsiLink()}{$callout->spiBus()} = \%c;
750        }
751    }
752
753    open(my $fh, '>', $outFile) or die "Could not open file '$outFile' $!";
754    my $json = JSON->new->utf8;
755    $json->indent(1);
756    $json->canonical(1);
757    my $text = $json->encode(\%output);
758    print $fh $text;
759    close $fh;
760}
761
762# This will remove duplicate callouts from the input callout list, keeping
763# the highest priority value and MRU, and then also sort by priority.
764#
765# There could be duplicates when multiple single segment callouts are
766# combined into 1.
767sub sortCallouts
768{
769    my @callouts = @_;
770
771    # This will undef the duplicates, and then remove them at the end,
772    for (my $i = 0; $i < (scalar @callouts) - 1; $i++)
773    {
774        next if not defined $callouts[$i];
775
776        for (my $j = $i + 1; $j < (scalar @callouts); $j++)
777        {
778            next if not defined $callouts[$j];
779
780            if ($callouts[$i]->{LocationCode} eq $callouts[$j]->{LocationCode})
781            {
782                # Keep the highest priority value
783                $callouts[$i]->{Priority} = getHighestPriority(
784                    $callouts[$i]->{Priority}, $callouts[$j]->{Priority});
785
786                # Keep the MRU if present
787                if (defined $callouts[$j]->{MRU})
788                {
789                    $callouts[$i]->{MRU} = $callouts[$j]->{MRU};
790                }
791
792                $callouts[$j] = undef;
793            }
794        }
795    }
796
797    # removed the undefined ones
798    @callouts = grep {defined ($_)} @callouts;
799
800    # sort from highest to lowest priorities
801    @callouts = sort {
802        $priorities{$b->{Priority}} <=> $priorities{$a->{Priority}}
803    } @callouts;
804
805    return @callouts;
806}
807
808# Returns the highest priority value of the two passed in
809sub getHighestPriority
810{
811    my ($p1, $p2) = @_;
812
813    if ($priorities{$p1} > $priorities{$p2})
814    {
815        return $p1;
816    }
817    return $p2;
818}
819
820# Dies if the input priority isn't valid
821sub validatePriority
822{
823    my $priority = shift;
824
825    if (not exists $priorities{$priority})
826    {
827        die "Invalid callout priority found: $priority\n";
828    }
829
830    return $priority;
831}
832
833sub printUsage
834{
835    print "$0 -m <MRW file> -o <Output filename> [--segments] [-n]\n" .
836    "        -m <MRW file> = The MRW XML\n" .
837    "        -o <Output filename> = The output JSON\n" .
838    "        [--segments] = Optionally create a segments.json file\n";
839    exit(1);
840}
841