package GLPI::Agent::Task::Inventory::Vmsystem;

use strict;
use warnings;

use parent 'GLPI::Agent::Task::Inventory::Module';

use UNIVERSAL::require;

use GLPI::Agent::Tools;
use GLPI::Agent::Tools::UUID;
use GLPI::Agent::Tools::Virtualization;

# We keep this module out of category as it is also mandatory for partial inventory

# Be sure to run after any module which can set BIOS or HARDWARE (only setting UUID)
our $runAfterIfEnabled = [ qw(
    GLPI::Agent::Task::Inventory::AIX::Bios
    GLPI::Agent::Task::Inventory::BSD::Alpha
    GLPI::Agent::Task::Inventory::BSD::i386
    GLPI::Agent::Task::Inventory::BSD::MIPS
    GLPI::Agent::Task::Inventory::BSD::SPARC
    GLPI::Agent::Task::Inventory::Generic::Dmidecode::Bios
    GLPI::Agent::Task::Inventory::Generic::Dmidecode::Hardware
    GLPI::Agent::Task::Inventory::HPUX::Bios
    GLPI::Agent::Task::Inventory::HPUX::Hardware
    GLPI::Agent::Task::Inventory::Linux::Bios
    GLPI::Agent::Task::Inventory::Linux::PowerPC::Bios
    GLPI::Agent::Task::Inventory::Linux::Hardware
    GLPI::Agent::Task::Inventory::Linux::ARM::Board
    GLPI::Agent::Task::Inventory::MacOS::Bios
    GLPI::Agent::Task::Inventory::MacOS::Hardware
    GLPI::Agent::Task::Inventory::Solaris::Bios
    GLPI::Agent::Task::Inventory::Solaris::Hardware
    GLPI::Agent::Task::Inventory::Win32::Bios
    GLPI::Agent::Task::Inventory::Win32::Hardware
)];

my @vmware_patterns = (
    'Hypervisor detected: VMware',
    'VMware vmxnet3? virtual NIC driver',
    'Vendor: VMware\s+Model: Virtual disk',
    'Vendor: VMware,\s+Model: VMware Virtual ',
    ': VMware Virtual IDE CDROM Drive'
);
my $vmware_pattern = _assemblePatterns(@vmware_patterns);

my @qemu_patterns = (
    ' QEMUAPIC ',
    'QEMU Virtual CPU',
    ': QEMU HARDDISK,',
    ': QEMU CD-ROM,',
    ': QEMU Standard PC',
    'Hypervisor detected: KVM',
    'Booting paravirtualized kernel on KVM'
);
my $qemu_pattern = _assemblePatterns(@qemu_patterns);

my @virtual_machine_patterns = (
    ': Virtual HD,',
    ': Virtual CD,'
);
my $virtual_machine_pattern = _assemblePatterns(@virtual_machine_patterns);

my @virtualbox_patterns = (
    ' VBOXBIOS ',
    ': VBOX HARDDISK,',
    ': VBOX CD-ROM,',
);
my $virtualbox_pattern = _assemblePatterns(@virtualbox_patterns);

my @xen_patterns = (
    'Hypervisor signature: xen',
    'Xen virtual console successfully installed',
    'Xen reported:',
    'Xen: \d+ - \d+',
    'xen-vbd: registered block device',
    'ACPI: [A-Z]{4} \(v\d+\s+Xen ',
);
my $xen_pattern = _assemblePatterns(@xen_patterns);

my %module_patterns = (
    '^vmxnet\s' => 'VMware',
    '^xen_\w+front\s' => 'Xen',
);

sub isEnabled {
    return 1;
}

sub doInventory {
    my (%params) = @_;

    my $inventory = $params{inventory};
    my $logger    = $params{logger};

    my $type = _getType($inventory, $logger);

    # for consistency with HVM domU
    if ($type eq 'Xen' && !$inventory->getBios('SMANUFACTURER')) {
        $inventory->setBios({
            SMANUFACTURER => 'Xen',
            SMODEL => 'PVM domU'
        });
    }

    # compute a compound identifier, as Virtuozzo uses the same identifier
    # for the host and for the guests
    if ($type eq 'Virtuozzo') {
        my $hostID  = $inventory->getHardware('UUID') || '';
        my $guestID = getFirstMatch(
            file => '/proc/self/status',
            pattern => qr/^envID:\s*(\d+)/
        ) || '';
        $inventory->setHardware({ UUID => $hostID . '-' . $guestID })
            if length($hostID) && length($guestID);

    } elsif ($type eq 'Docker') {
        # In docker, dmidecode can be run and so UUID & SSN must be overided
        my $containerid = getFirstMatch(
            file    => '/proc/1/cgroup',
            pattern => qr|/docker/([0-9a-f]{12})|,
            logger  => $params{logger}
        );

        $inventory->setHardware({ UUID => $containerid || '' });
        $inventory->setBios({ SSN  => '' });

    } elsif ($type eq "WSL") {
        my ($user, $sid) = getFirstMatch(
            command => "whoami.exe /nh /user /fo csv",
            pattern => qr|^"(.*)","(.*)"|,
            logger  => $params{logger}
        );
        if (defined($user) && defined($sid)) {
            $user =~ s/^.*\\//;
            my $distro = $ENV{"WSL_DISTRO_NAME"} // '';
            # Same UUID computing than in WSL.pm
            my $uuid = uc(create_uuid_from_name($sid."/".$distro));
            $inventory->setHardware({ UUID => $uuid }) if $uuid;
            my $hostname = "$distro on $user account";
            $inventory->setHardware({ NAME => $hostname });
        }

    } elsif (($type eq 'lxc' || ($type ne 'Physical' && !$inventory->getHardware('UUID'))) && has_file('/etc/machine-id')) {
        # Set UUID from /etc/machine-id & /etc/hostname for container like lxc
        my $machineid = getFirstLine(
            file   => '/etc/machine-id',
            logger => $params{logger}
        );
        my $hostname = getFirstLine(
            file   => '/etc/hostname',
            logger => $params{logger}
        );

        if ($machineid && $hostname) {
            $inventory->setHardware({ UUID => getVirtualUUID($machineid, $hostname) });
        }
    }

    $inventory->setHardware({
        VMSYSTEM => $type,
    });
}

sub _getType {
    my ($inventory, $logger) = @_;

    my $SMANUFACTURER = $inventory->getBios('SMANUFACTURER');
    my $SMODEL        = $inventory->getBios('SMODEL');
    if ($SMANUFACTURER) {
        return 'QEMU'    if $SMANUFACTURER =~ /QEMU/;
        return 'Hyper-V' if $SMANUFACTURER =~ /Microsoft/ && $SMODEL && $SMODEL =~ /Virtual/;
        return 'VMware'  if $SMANUFACTURER =~ /VMware/;
        return 'Xen'     if $SMANUFACTURER =~ /^Xen/;
    }
    my $BMANUFACTURER = $inventory->getBios('BMANUFACTURER');
    if ($BMANUFACTURER) {
        return 'QEMU'       if $BMANUFACTURER =~ /(QEMU|Bochs)/;
        return 'VirtualBox' if $BMANUFACTURER =~ /(VirtualBox|innotek)/;
        return 'Xen'        if $BMANUFACTURER =~ /^Xen/;
    }
    if ($SMODEL) {
        return 'VMware'          if $SMODEL =~ /VMware/;
        return 'Virtual Machine' if $SMODEL =~ /Virtual Machine/;
        return 'QEMU'            if $SMODEL =~ /KVM/;
    }
    my $BVERSION = $inventory->getBios('BVERSION');
    if ($BVERSION) {
        return 'VirtualBox'  if $BVERSION =~ /VirtualBox/;
    }
    my $MMODEL = $inventory->getBios('MMODEL');
    if ($MMODEL) {
        return 'VirtualBox'  if $MMODEL =~ /VirtualBox/;
    }
    my $VERSION = $inventory->getBios('VERSION');
    if ($VERSION) {
        return 'VirtualBox'  if $VERSION =~ /VirtualBox/;
    }
    # Can only be set by win32
    my $BIOSSERIAL = $inventory->getBios('BIOSSERIAL');
    if ($BIOSSERIAL) {
        return 'VMware'      if $BIOSSERIAL =~ /VMware/i;
    }

    # Docker

    if (has_file('/.dockerinit') || has_file('/.dockerenv')) {
        return 'Docker';
    }

    # Solaris zones
    if (OSNAME eq 'solaris' && canRun('/usr/sbin/zoneadm')) {
        if (GLPI::Agent::Tools::Solaris->require()) {
            my $zone = GLPI::Agent::Tools::Solaris::getZone();
            return 'SolarisZone' if $zone ne 'global';
        }
    }

    # Xen PV host
    if (
        has_folder('/proc/xen') ||
        getFirstMatch(
            file    => '/sys/devices/system/clocksource/clocksource0/available_clocksource',
            pattern => qr/xen/
        )
    ) {
        if (getFirstMatch(
            file    => '/proc/xen/capabilities',
            pattern => qr/control_d/
        )) {
            # dom0 host
            return 'Physical';
        } else {
            # domU PV host
            return 'Xen';
        }
    }

    my $result;

    if (canRun('/sbin/sysctl')) {
        my $handle = getFileHandle(
            command => '/sbin/sysctl -n security.jail.jailed',
            logger => $logger
        );
        my $line = <$handle>;
        close $handle;
        return 'BSDJail' if $line && $line == 1;
    }

    # loaded modules

    if (has_file('/proc/modules')) {
        my $handle = getFileHandle(
            file => '/proc/modules',
            logger => $logger
        );
        while (my $line = <$handle>) {
            foreach my $pattern (keys %module_patterns) {
                next unless $line =~ /$pattern/;
                $result = $module_patterns{$pattern};
                last;
            }
        }
        close $handle;
    }
    return $result if $result;

    # dmesg
    # dmesg can be empty or near empty on some systems (notably on Debian 8)

    my $handle;
    if (has_file('/var/log/dmesg') && FileStat('/var/log/dmesg')->size > 40) {
        $handle = getFileHandle(file => '/var/log/dmesg', logger => $logger);
    } elsif (canRun('/bin/dmesg')) {
        $handle = getFileHandle(command => '/bin/dmesg', logger => $logger);
    } elsif (canRun('/sbin/dmesg')) {
        # On OpenBSD, dmesg is in sbin
        # http://forge.fusioninventory.org/issues/402
        $handle = getFileHandle(command => '/sbin/dmesg', logger => $logger);
    }

    if ($handle) {
        $result = _matchPatterns($handle);
        close $handle;
        return $result if $result;
    }

    # scsi

    if (has_file('/proc/scsi/scsi')) {
        my $handle = getFileHandle(
            file => '/proc/scsi/scsi',
            logger => $logger
        );
        if ($handle) {
            $result = _matchPatterns($handle);
            close $handle;
            return $result if $result;
        }
    }

    # systemd based container like lxc

    if (has_file('/proc/1/environ')) {
        my $init_env = getAllLines(
            file => '/proc/1/environ',
            logger => $logger
        );
        if ($init_env) {
            $init_env =~ s/\0/\n/g;
            my $container_type = getFirstMatch(
                string  => $init_env,
                pattern => qr/^container=(\S+)/,
                logger  => $logger
            );
            return $container_type if $container_type;
        }
    }
    # OpenVZ
    if (has_file('/proc/self/status')) {
        my @selfstatus = getAllLines(
            file => '/proc/self/status',
            logger => $logger
        );
        foreach my $line (@selfstatus) {
            my ($key, $value) = split(/:/, $line);
            $result = "Virtuozzo" if $key eq 'envID' && $value > 0;
        }
    }

    # WSL
    if (has_file('/proc/sys/fs/binfmt_misc/WSLInterop')) {
        $result = "WSL";
    } elsif (canRun('lscpu') && getFirstMatch(command => 'lscpu', pattern => qr/^Hypervisor vendor:\s+(Windows Subsystem for Linux|Microsoft)/)) {
        $result = "WSL";
    } elsif (has_file('/proc/mounts') && getFirstMatch(file => '/proc/mounts', pattern => qr/^rootfs\s+\/\s+(wslfs)/)) {
        $result = "WSL";
    }

    return $result if $result;

    return 'Physical';
}

sub _assemblePatterns {
    my (@patterns) = @_;

    my $pattern = '(?:' . join('|', @patterns) . ')';
    return qr/$pattern/;
}

sub _matchPatterns {
    my ($handle) = @_;

    while (my $line = <$handle>) {
        return 'VMware'          if $line =~ $vmware_pattern;
        return 'QEMU'            if $line =~ $qemu_pattern;
        return 'Virtual Machine' if $line =~ $virtual_machine_pattern;
        return 'VirtualBox'      if $line =~ $virtualbox_pattern;
        return 'Xen'             if $line =~ $xen_pattern;
    }
}

1;
