#!/usr/bin/perl
package OVS;
use strict;

use base 'Net::DBus::Object';
use Net::DBus::Exporter 'org.opensuse.os_autoinst.switch';
require IPC::System::Simple;
use autodie ':all';
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';

    #the bridge must be already created and configured
    system('ovs-vsctl', 'br-exists', $self->{BRIDGE});

    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/;

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


    # 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 (10.0.2.2), we have to do some IP translation
    # we use simple scheme:
    # MAC 52:54:00:12:XX:YY -> IP 10.1.XX.YY

    # br0 has IP 10.0.2.2 and netmask /15 that covers 10.0.0.0 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 10.0.2.2/15" unless $self->{IP} eq '10.0.2.2/15';

    # 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 10.0.2.2 - learn rule for handling replies, rewrite ARP sender IP to 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=10.0.2.2,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:0xa010000->NXM_OF_ARP_SPA[],move:NXM_OF_ETH_SRC[0..15]->NXM_OF_ARP_SPA[0..15],' .                                                                            #
        'local',

        # tcp to $self->{MAC} syn - learn rule for handling replies, rewrite source IP to 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:10.1.0.0,move:NXM_OF_ETH_SRC[0..15]->NXM_OF_IP_SRC[0..15],' .                                                                                        #
        'local',

        # tcp to $self->{MAC} other - rewrite source IP to 10.1.x.x range and send to local
        "table=0,priority=99,dl_type=0x0800,dl_dst=$self->{MAC},actions=" .                                                                                              #
        'mod_nw_src:10.1.0.0,move:NXM_OF_ETH_SRC[0..15]->NXM_OF_IP_SRC[0..15],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";
}

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;

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

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

    # Connect tap device to given vlan
    ($return_code, $ovs_vsctl_error, $ovs_vsctl_output) = _cmd( 'ovs-vsctl', 'set', '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, $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;
