#!/usr/bin/perl
#
# Module: vyatta-bridge-fdb
#
# **** License ****
# Copyright (c) 2018-2019, AT&T Intellectual Property.
# All rights reserved.
#
# Copyright (c) 2015 by Brocade Communications Systems, Inc.
# All rights reserved.
#
# SPDX-License-Identifier: GPL-2.0-only
#
# **** End License ****
#
# Syntax:
#    vyatta-bridge-fdb <hwaddr> [--verbose]
#
# Will update/delete a fdb entry for <hwaddr>
# based on config changes.
#

use strict;
use warnings;
use JSON;

use lib '/opt/vyatta/share/perl5/';

use Vyatta::Configd;
use Vyatta::SwitchConfig qw(get_default_switchports get_physical_switches);
use Vyatta::Bridge qw(get_cfg_bridge_ports);
use Vyatta::Interface;
use Getopt::Long;

our $VERSION = 1.00;

my $action;
my $addr;
my $verbose;

GetOptions(
    'action=s' => \$action,
    'mac=s'    => \$addr,
    'verbose'  => \$verbose,
) or usage();

usage() unless defined($addr);
if ( $action eq "validate" ) {
    check_bridge_fdb($addr);
} elsif ( $action eq "update" ) {
    end_bridge_fdb($addr);
}

exit 0;

#
# Subroutines
#

sub usage {
    printf("Usage for vyatta-bridge-static-fdb\n");
    printf("  --action=update --mac=<hwaddr> [--verbose]\n");
    printf("  --action=validate --mac=<hwaddr> [--verbose]\n");
    exit 1;
}

#
# Check for a valid VLAN ID. Ensure that the supplied switch port is a
# member of the defined VLAN, i.e. is the VLAN ID one of the VLANs
# listed in the switch-group or one of the default VLANs listed in the
# switch.
#
sub check_vlan {
    my ( $ifname, $ifpath, $vid, $cfg ) = @_;

    my $path   = "$ifpath switch-group";
    my $swname = $cfg->returnValue( $path . " switch" );
    $path .= " port-parameters vlan-parameters";

    my $pvid = $cfg->returnValue( $path . " primary-vlan-id" );
    return 1 if defined($pvid) and $pvid == $vid;

    my @vlans = $cfg->returnValues( $path . " vlans" );
    return 1 if ( grep { $_ == $vid } @vlans );

    my @switches = ();
    if ( defined($swname) ) {
        @switches = ($swname);
    } else {
        #
        # No explicit switch-port configuration, assume the port is an
        # implicit member of the hardware switch(es)
        #
        foreach my $swid ( get_physical_switches() ) {
            push @switches, "sw$swid";
        }
    }

    foreach $swname (@switches) {
        $path =
          "interfaces switch $swname default-port-parameters vlan-parameters";
        $pvid = $cfg->returnValue( $path . " primary-vlan-id" );
        return 1 if defined($pvid) and $pvid == $vid;

        @vlans = $cfg->returnValues( $path . " vlans" );
        return 1 if ( grep { $_ == $vid } @vlans );
    }

    return 0;
}

#
# Figure out if the interface is a switch or bridge group member.
#
sub get_port_type {
    my ($ifname) = @_;

    my @ports = get_cfg_bridge_ports("bridge");
    return "bridge" if ( grep { $_ eq $ifname } (@ports) );

    #
    # On a switch platform check the list of implicitly and explicitly
    # defined ports
    #
    @ports = get_cfg_bridge_ports("switch");
    my @defswports = get_default_switchports();
    return "switch" if ( grep { $_ eq $ifname } ( @ports, @defswports ) );
    return undef;
}

sub check_bridge_fdb {
    my ($hwaddr) = @_;

    my $path   = "protocols static bridge-mac $hwaddr";
    my $cfg    = new Vyatta::Config;
    my $ifname = $cfg->returnValue( $path . " interface" );
    my $vlanid = $cfg->returnValue( $path . " vlan" );
    #
    # Mandatory attribute so the name should always exist... but is it
    # a genuine interface?
    #
    return unless defined($ifname);
    my $intf = Vyatta::Interface->new($ifname);
    die "Interface \"$ifname\" does not exist\n"
      unless $intf && $intf->exists();

    my $porttype = get_port_type($ifname);
    die "Interface \"$ifname\" is not a bridge or switch port\n"
      if !defined($porttype);

    if ( $porttype eq "bridge" ) {
        die "VLAN \"$vlanid\" not valid for bridge port \"$ifname\"\n"
          if defined($vlanid);
        return;
    }

    if ( $porttype eq "switch" ) {
        die "VLAN required for switch port \"$ifname\"\n"
          if !defined($vlanid);

        die "Interface \"$ifname\" is not a member of VLAN \"$vlanid\"\n"
          if !check_vlan( $ifname, $intf->{path}, $vlanid, $cfg );
    }
}

sub delete_bridge_fdb {
    my ( $hwaddr, $ifname, $vlan ) = @_;

    my $cmd = "bridge fdb del $hwaddr dev $ifname";
    $cmd = $cmd . " vlan $vlan master" if defined $vlan;

    # We expect the interface to exist and to be member of bridge group
    if ( system($cmd) != 0 ) {
        warn "$cmd failed\n";
    } elsif ($verbose) {
        print "$cmd\n";
    }
    return 0;
}

sub add_bridge_fdb {
    my ( $hwaddr, $ifname, $vlan ) = @_;

    my $cmd = "bridge fdb replace $hwaddr dev $ifname";
    $cmd = $cmd . " static vlan $vlan master" if defined $vlan;

    if ( system($cmd) != 0 ) {
        warn "$cmd failed\n";
    } elsif ($verbose) {
	printf "$cmd\n";
    }
    return 0;
}

sub end_bridge_fdb {
    my ( $hwaddr ) = @_;
    my $oldifname;
    my $oldvlan;
    my $ifname;
    my $vlan;

    foreach my $br ( @{ decode_json(`bridge -j fdb`) } ) {
        if ($br->{mac} eq $hwaddr) {
            $oldifname = $br->{ifname};
            $oldvlan = $br->{vlan};
            # Try to find an entry with vlan
            last if defined $oldvlan;
        }
    }

    my $cpath = 'protocols static bridge-mac '.$hwaddr;
    my $client = Vyatta::Configd::Client->new();
    if ($client->node_exists($Vyatta::Configd::Client::AUTO, $cpath)) {
        my $tree = $client->tree_get_full_hash($cpath);
        $ifname = $tree->{interface};
        $vlan = $tree->{vlan};
    }

    if ($verbose) {
        printf "$hwaddr if %s -> %s vlan %s -> %s\n",
          defined($oldifname) ? $oldifname : "N/A",
          defined($ifname)    ? $ifname    : "N/A",
          defined($oldvlan)   ? $oldvlan   : "N/A",
          defined($vlan)      ? $vlan      : "N/A";
    }

    if (!defined($ifname)) {
        # Delete the complete interface
        delete_bridge_fdb( $hwaddr, $oldifname, $oldvlan );
    } elsif (defined($oldvlan) && !defined($vlan)) {
        # Delete just the vlan
        delete_bridge_fdb( $hwaddr, $oldifname, $oldvlan );
        add_bridge_fdb( $hwaddr, $ifname, $vlan );
    } elsif (defined($oldifname)) {
        # Delete the existing interface first
        delete_bridge_fdb( $hwaddr, $oldifname, $oldvlan );
        add_bridge_fdb( $hwaddr, $ifname, $vlan);
    } else {
        # Fresh entry. Update interface with vlan, if provided
        add_bridge_fdb( $hwaddr, $ifname, $vlan );
    }

    return 0;
}
