#!/usr/bin/perl
package OVS;

use strict;
use warnings;
use autodie ':all';

use base 'Net::DBus::Object';

use Net::DBus::Exporter 'org.opensuse.os_autoinst.switch';
require IPC::System::Simple;
use IPC::Open3;
use Symbol 'gensym';

sub new {
    my $class   = shift;
    my $service = shift;
    my $self    = $class->SUPER::new($service, '/switch');
    bless $self, $class;
    $self->init_switch;
    return $self;
}

sub init_switch {
    my $self = shift;

    $self->{BRIDGE} = $ENV{OS_AUTOINST_USE_BRIDGE};
    $self->{BRIDGE} //= 'br0';

    until (-e "/sys/class/net/$self->{BRIDGE}") {
        print "Waiting for bridge '$self->{BRIDGE}' to be created and configured...\n";
        sleep 1;
    }
    system('ovs-vsctl', 'br-exists', $self->{BRIDGE});

    for (my $timeout = $ENV{OS_AUTOINST_OPENVSWITCH_INIT_TIMEOUT} // 60; $timeout > 0; --$timeout) {
        my $bridge_conf = `ip addr show $self->{BRIDGE}`;
        $self->{MAC} = $1 if $bridge_conf =~ /ether\s+(([0-9a-f]{2}:){5}[0-9a-f]{2})\s/;
        $self->{IP}  = $1 if $bridge_conf =~ /inet\s+(([0-9]+.){3}[0-9]+\/[0-9]+)\s/;
        last if $self->{IP};
        print "Waiting for IP on bridge '$self->{BRIDGE}', ${timeout}s left ...\n";
        sleep 1;
    }

    die "can't parse bridge local port MAC" unless $self->{MAC};
    die "can't parse bridge local port IP"  unless $self->{IP};

    my $local_ip       = $ENV{OS_AUTOINST_BRIDGE_LOCAL_IP}       // '10.0.2.2';
    my $netmask        = $ENV{OS_AUTOINST_BRIDGE_NETMASK}        // 15;
    my $rewrite_target = $ENV{OS_AUTOINST_BRIDGE_REWRITE_TARGET} // '10.1.0.0';
    # we also need a hex-converted form of the rewrite target, thanks
    # https://www.perlmonks.org/?node_id=704295
    my $rewrite_target_hex = unpack('H*', pack('C*', split('\.', $rewrite_target)));

    # the VM have unique MAC that differs in the last 16 bits (see /usr/lib/os-autoinst/backend/qemu.pm)
    # the IP can conflict across vlans
    # to allow connection from VM  to host os-autoinst ($local_ip), we have to do some IP translation
    # we use simple scheme, e.g.:
    # MAC 52:54:00:12:XX:YY -> IP 10.1.XX.YY

    # br0 has IP $local_ip and netmask $netmask. E.g. '/15' covers 10.0.2.2 and 10.1.0.0 ranges
    # this should be also configured permanently in /etc/sysconfig/network
    die "bridge local port IP is expected to be $local_ip/$netmask" unless $self->{IP} eq "$local_ip/$netmask";

    # openflow rules don't survive reboot so they must be installed on each startup
    for my $rule (
        # openflow ports:
        #  LOCAL = br0
        #  1,2,3 ... tap devices

        # default: normal action
        'table=0,priority=0,action=normal',

        # reply packets from local port are handled by learned rules in table 1
        'table=0,priority=1,in_port=LOCAL,actions=resubmit(,1)',


        # arp e.g. 10.0.2.2 - learn rule for handling replies, rewrite ARP sender IP to e.g. 10.1.x.x range and send to local
        # the learned rule rewrites ARP target to the original IP and sends the packet to the original port
        "table=0,priority=100,dl_type=0x0806,nw_dst=$local_ip,actions=" .
'learn(table=1,priority=100,in_port=LOCAL,dl_type=0x0806,NXM_OF_ETH_DST[]=NXM_OF_ETH_SRC[],load:NXM_OF_ARP_SPA[]->NXM_OF_ARP_TPA[],output:NXM_OF_IN_PORT[]),' .
        "load:0x$rewrite_target_hex->NXM_OF_ARP_SPA[],move:NXM_OF_ETH_SRC[0..$netmask]->NXM_OF_ARP_SPA[0..$netmask]," .
        'local',

        # tcp to $self->{MAC} syn - learn rule for handling replies, rewrite source IP to e.g. 10.1.x.x range and send to local
        # the learned rule rewrites DST to the original IP and sends the packet to the original port
        "table=0,priority=100,dl_type=0x0800,tcp_flags=+syn-ack,dl_dst=$self->{MAC},actions=" .
'learn(table=1,priority=100,in_port=LOCAL,dl_type=0x0800,NXM_OF_ETH_DST[]=NXM_OF_ETH_SRC[],load:NXM_OF_IP_SRC[]->NXM_OF_IP_DST[],output:NXM_OF_IN_PORT[]),' .
        "mod_nw_src:$rewrite_target,move:NXM_OF_ETH_SRC[0..$netmask]->NXM_OF_IP_SRC[0..$netmask]," .
        'local',

        # tcp to $self->{MAC} other - rewrite source IP to e.g. 10.1.x.x range and send to local
        "table=0,priority=99,dl_type=0x0800,dl_dst=$self->{MAC},actions=" .
        "mod_nw_src:$rewrite_target,move:NXM_OF_ETH_SRC[0..$netmask]->NXM_OF_IP_SRC[0..$netmask],local",
      )
    {
        system('ovs-ofctl', 'add-flow', $self->{BRIDGE}, $rule);
    }
}

# Check if tap and vlan are in the correct format.
sub _ovs_check {
    my $tap    = shift;
    my $vlan   = shift;
    my $bridge = shift;
    my $return_output;
    my $return_code = 1;

    if ($tap !~ /^tap[0-9]+$/) {
        $return_output = "'$tap' does not fit the naming scheme";
        return ($return_code, $return_output);
    }
    if ($vlan !~ /^[0-9]+$/) {
        $return_output = "'$vlan' does not fit the naming scheme (only numbers)";
        return ($return_code, $return_output);
    }

    my $check_bridge = `ovs-vsctl port-to-br $tap`;
    chomp $check_bridge;
    if ($check_bridge ne $bridge) {
        $return_output = "'$tap' is not connected to bridge '$bridge'";
        return ($return_code, $return_output);
    }

    return 0;
}

sub _cmd {
    my @args = @_;
    my ($wtr, $rdr, $err);
    $err = gensym;

    # We need open3 because otherwise STDERR is captured by systemd.
    # In such way we collect the error and send it back in the dbus call as well.
    my $ovs_vsctl_pid = open3($wtr, $rdr, $err, @args);

    my @ovs_vsctl_output = <$rdr>;
    my @ovs_vsctl_error  = <$err>;
    waitpid($ovs_vsctl_pid, 0);
    my $return_code = $?;

    return $return_code, "@ovs_vsctl_error", "@ovs_vsctl_output";
}

sub check_min_ovs_version {
    my $min_ver = shift;
    my $out     = `ovs-vsctl --version`;
    return if ($out !~ /\(Open vSwitch\)\s+(\d+\.\d+\.\d+)/m);
    my @ver     = split(/\./, $1);
    my @min_ver = split(/\./, $min_ver);
    return if (@ver != @min_ver);

    return sprintf("%04d%04d%04d", @ver) >= sprintf("%04d%04d%04d", @min_ver);
}

dbus_method("set_vlan", ["string", "uint32"], ["int32", "string"]);
sub set_vlan {
    my $self = shift;
    my $tap  = shift;
    my $vlan = shift;
    my $return_output;
    my $return_code = 1;
    my $ovs_vsctl_error;
    my $ovs_vsctl_output;
    my @cmd;

    ($return_code, $return_output) = _ovs_check($tap, $vlan, $self->{BRIDGE});

    unless ($return_code == 0) {
        print STDERR $return_output . "\n";
        return ($return_code, $return_output);
    }

    @cmd = ('ovs-vsctl', 'set', 'port', $tap, "tag=$vlan");
    if (check_min_ovs_version('2.8.1')) {
        push(@cmd, 'vlan_mode=dot1q-tunnel');
    }

    # Connect tap device to given vlan
    ($return_code, $ovs_vsctl_error, $ovs_vsctl_output) = _cmd(@cmd);

    print STDERR $ovs_vsctl_error if length($ovs_vsctl_error) > 0;
    print $ovs_vsctl_output       if length($ovs_vsctl_output) > 0;
    return $return_code, $ovs_vsctl_error unless $return_code == 0;

    $return_code = system('ip', 'link', 'set', $tap, 'up');
    return $return_code, $return_code != 0 ? "Failed to set $tap up " : '';
}

dbus_method("unset_vlan", ["string", "uint32"], ["int32", "string"]);
sub unset_vlan {
    my $self = shift;
    my $tap  = shift;
    my $vlan = shift;
    my $return_output;
    my $return_code = 1;
    my $ovs_vsctl_error;
    my $ovs_vsctl_output;

    ($return_code, $return_output) = _ovs_check($tap, $vlan, $self->{BRIDGE});

    unless ($return_code == 0) {
        print STDERR $return_output . "\n";
        return ($return_code, $return_output);
    }

    # Remove tap device to given vlan
    ($return_code, $ovs_vsctl_error, $ovs_vsctl_output) = _cmd('ovs-vsctl', 'remove', 'port', $tap, 'tag', $vlan);

    print STDERR $ovs_vsctl_error if length($ovs_vsctl_error) > 0;
    print $ovs_vsctl_output       if length($ovs_vsctl_output) > 0;
    return $return_code, $return_code != 0 ? $ovs_vsctl_error : '';
}

dbus_method("show", [], ["int32", "string"]);
sub show {
    my $self = shift;

    my ($return_code, undef, $ovs_vsctl_output) = _cmd('ovs-vsctl', 'show');

    return $return_code, $ovs_vsctl_output;
}

################################################################################
package main;
use strict;

use Net::DBus;
use Net::DBus::Reactor;

my $bus = Net::DBus->system;

my $service = $bus->export_service("org.opensuse.os_autoinst.switch");
my $object  = OVS->new($service);

Net::DBus::Reactor->main->run;

exit 0;
