# KVM guest - identify host

John Simpson `<jms1@jms1.net>` 2018-07-10

Last updated 2019-07-28

This document is specific to cases where a Linux-based KVM host, and its Linux guests, are both managed by Puppet.

# Background

**There is no way for a KVM guest to directly identify the KVM host on which it's running.**

There *is*, however, a way for a host to write an arbitrary file within the guest. If it writes a file in the guest's `/etc/facter/facts.d/` directory which contains the host's hostname ... *problem solved*.

In theory.

What I have below *mostly* works, except for SELinux on the guest.

Although if the script writes the file under `/tmp/`, SELinux doesn't interfere (the policies installed by CentOS allow any process to write files under `/tmp/`.) So this *could* be made to work, if we add a custom fact which reads and parses a file under `/tmp/`, although it would be a lot cleaner if the script were able to write the file under `/etc/facter/facts.d/` directly.

# Requirements

## Guest

```
  include stdlib

  if ( $facts['virtual'] == 'kvm' ) {
    package { 'qemu-guest-agent' :
      ensure => present ,
    }
    service { 'qemu-guest-agent' :
      ensure  => running ,
      enable  => true ,
      require => Package['qemu-guest-agent'] ,
    }
    file_line { 'qemu-ga-blacklist' :
      ensure             => present ,
      path               => '/etc/sysconfig/qemu-ga' ,
      match              => '^BLACKLIST_RPC=' ,
      line               => 'BLACKLIST_RPC=' ,
      append_on_no_match => true ,
      notify             => Service['qemu-guest-agent'] ,
    }

    if ( $facts['os']['selinux']['enabled'] ) {

      $workdir = '/usr/local/qemu-selinux'
      $modname = 'qemu_ga_write_facts'

      file { $workdir :
        ensure  => directory ,
        owner   => 'root' ,
        group   => 'root' ,
        mode    => '0755' ,
      }

      file { '"${workdir}/Makefile' :
        ensure  => file ,
        owner   => 'root' ,
        group   => 'root' ,
        mode    => '0644' ,
        content => template( "${module_name}${workdir}/Makefile.erb" ) ,
        notify  => Exec['qemu-selinux-make'] ,
      }

      file { '${workdir}/${modname}.te' :
        ensure  => file ,
        owner   => 'root' ,
        group   => 'root' ,
        mode    => '0644' ,
        content => template( "${module_name}${workdir}/${modname}.te.erb" ) ,
        notify  => Exec['qemu-selinux-make'] ,
      }

      exec { 'qemu-selinux-make' :
        cwd         => $workdir ,
        path        => '/usr/bin:/bin' ,
        command     => 'make' ,
        refreshonly => true ,
        before      => Selmodule['${modname}'] ,
      }

      selmodule { ${modname} :
        ensure        => present ,
        selmodulepath => '${workdir}/${modname}.pp' ,
        syncversion   => true ,
        require       => Package['qemu-guest-agent'] ,
        notify        => Service['qemu-guest-agent'] ,
      }
    }
  }
```

### `${modname}.te.erb`

Doesn't seem to work when writing to `/etc/facter/facts.d/` or `/dev/shm/` ... when it fails, the AVC error showing up in `/var/log/audit/audit.log` matches the rule created here (and by `audit2allow`) so there's no reason why it *shouldn't* be working...

```
module qemu_ga_write_facts 0.0.4 ;

require {
    type    etc_t ;
    type    tmpfs_t ;
    type    virt_qemu_ga_t ;
    class   file            { create getattr lock open read write add_name } ;
}

allow virt_qemu_ga_t etc_t:file   { create getattr lock open read write add_name } ;
allow virt_qemu_ga_t tmpfs_t:file { create getattr lock open read write add_name } ;
```

**TODO:** find that extra setting to make selinux log ALL rejections, even the ones marked "noaudit" or "dontaudit"? -jms1 2019-04-01)

### `Makefile.erb`

```
# Makefile for ${modname} SELinux module

%.pp: %.te
	checkmodule -M -m -o $(basename $@).mod $<
	semodule_package -m $(basename $@).mod -o $@

########################################

all: ${modname}.pp

install: all
	semodule -i ${modname}.pp

clean:
	rm *.mod *.pp
```

## Host

Needs to be a KVM host (duh)

```
(root@squanch) # virsh qemu-agent-command jnagios.eng.voalte.net '{"execute":"guest-info"}' \
  | jq -r -M '.return.supported_commands[]|select(.name=="guest-file-write").enabled'
false
```

This script seems to work, howeer it requires SELinux to be disabled on the guest.

```perl
#!/usr/bin/perl -w
#
# set-kvm-host-info
# John Simpson <jms1@voalte.com> 2018-07-10
#
# Runs on a KVM host. Creates a Puppet custom fact file on one or more guests
# running on that host, which allow the guest to identify which host it's
# running on.

require 5.005 ;
use strict ;

#use Data::Dumper ;
use Getopt::Std ;
use JSON ;
use MIME::Base64 ;
use Sys::Hostname ;

my %opt         = () ;

my $host        = hostname() ;
my %guest_id    = () ;
my %guest_stat  = () ;
my @do_guests   = () ;

my $target_file = '/etc/facter/facts.d/kvm_host.txt' ;

###############################################################################

sub usage(;$)
{
    my $msg = ( shift || '' ) ;

    print <<EOF ;
$0 [-h] [guest [...]]

Create a custom fact on one or more KVM guests, running on this host.

If no guest names are specified, the script will operate on all guests.

EOF

    if ( $msg ne '' )
    {
        print $msg ;
        exit 1 ;
    }
    exit 0 ;
}

###############################################################################

sub blueline($)
{
    printf "\033[0;1;37;44m%s\033[K\033[0m\n" , $_[0] ;
}

sub greenline($)
{
    printf "\033[0;1;37;42m%s\033[K\033[0m\n" , $_[0] ;
}

sub redline($)
{
    printf "\033[0;1;37;41m%s\033[K\033[0m\n" , $_[0] ;
}

###############################################################################

sub list_guests()
{
    %guest_id   = () ;
    %guest_stat = () ;
    @do_guests  = () ;

    open ( L , "virsh list |" )
        or die "ERROR: 'virsh list': $!\n" ;
    while ( my $line = <L> )
    {
        if ( $line =~ m|^\s*(\d+)\s+(\S+)\s+(.+)| )
        {
            my ( $id , $name , $status ) = ( $1 , $2 , $3 ) ;
            $guest_id{$name} = $id ;
            $guest_stat{$name} = $status ;
            push( @do_guests , $name ) ;
        }
    }
    close L ;
}

###############################################################################
#
# Build and execute a qemu-agent-command
# - $guest  = guest to which command will be sent
# - $cmd    = command to be executed
# - $args   = arguments, should be a reference to a hash if present

sub agent_cmd($$;$)
{
    my $guest = shift ;
    my $cmd   = shift ;
    my $args  = ( shift || undef ) ;

    my %j = ( 'execute' => $cmd ) ;

    if ( defined $args )
    {
        if ( ref($args) eq 'HASH' )
        {
            $j{'arguments'} = $args ;
        }
        else
        {
            redline "agent_cmd(): \$args is not a hashref" ;
            return undef ;
        }
    }

    my $js = encode_json( \%j ) ;

    print <<EOF ;
agent_cmd()
- guest '$guest'
- cmd   '$cmd'
- args  '$args'
= js    '$js'

EOF

    my $cl = "virsh qemu-agent-command $guest --timeout 5 '$js'" ;
    greenline $cl ;

    my $output = '' ;
    open( I , "$cl 2>&1 |" )
        or die "ERROR: $!\n" ;
    while ( my $line = <I> )
    {
        $output .= $line ;
    }
    close I ;

    if ( $output =~ m|^error:|is )
    {
        $output =~ s|\s+$||s ;
        redline $output ;
        print "\n" ;
        return undef ;
    }

    return $output ;
}

###############################################################################
###############################################################################
###############################################################################

getopts( 'ah' , \%opt ) ;
$opt{'h'} && usage() ;

list_guests() ;

unless ( $opt{'a'} )
{
    @do_guests = @ARGV ;
}

if ( $#do_guests < 0 )
{
    usage "ERROR: no guest names specified\n" ;
}

for my $g ( @do_guests )
{
    unless ( exists( $guest_id{$g} ) )
    {
        usage "ERROR: guest '$g' does not exist on this host\n" ;
    }
}

###############################################################################
#
# Process each guest

for my $g ( @do_guests )
{
    blueline "Processing $g" ;

    ########################################
    # Make sure the guest has a working qemu-guest-agent and allows file writes

    my $info = agent_cmd( $g , 'guest-info' ) ;
    next unless ( defined $info ) ;

    my $ri = decode_json( $info ) ;
    my $ver = $ri->{'return'}->{'version'} ;
    print "version: '$ver'\n" ;

    my $sc = $ri->{'return'}->{'supported_commands'} ;
    my %cmd_enabled = () ;
    for my $k ( @$sc )
    {
        $cmd_enabled{$k->{'name'}} = $k->{'enabled'} ;
    }

    my $ok = 1 ;
    for my $k ( qw ( guest-file-open guest-file-write guest-file-close ) )
    {
        unless ( $cmd_enabled{$k} eq 'true' )
        {
            redline "ERROR: guest agent does not allow $k" ;
            $ok = 0 ;
        }
    }
    next unless ( $ok ) ;

    ########################################
    # Build the file's content

    my $content_txt = "kvm_host=$host\n" ;
    my $content_b64 = encode_base64( $content_txt , '' ) ;

    ########################################
    # Open the file on the guest

    my %args = ( 'path' => $target_file , 'mode' => 'w+' ) ;
    my $rv = agent_cmd( $g , 'guest-file-open' , \%args ) ;
    next unless ( defined $rv ) ;
    my $rrv = decode_json( $rv ) ;
    my $fh = $rrv->{'return'} ;

    ########################################
    # Write the data

    my %args = ( 'handle' => $fh , 'buf-b64' => $content_b64 ) ;
    $rv = agent_cmd( $g , 'guest-file-write' , \%args ) ;
    if ( defined $rv )
    {
        my $rrv = decode_json( $rv ) ;
        my $bytes = $rrv->{'return'}->{'count'} ;
        my $eof   = $rrv->{'return'}->{'eof'} ;
        print "wrote $bytes bytes, eof=$eof\n" ;
    }

    ########################################
    # Close the file

    my %args = ( 'handle' => $fh ) ;

    $rv = agent_cmd( $g , 'guest-file-close' , \%args ) ;
    next unless ( defined $rv ) ;
}
```
