]> git.proxmox.com Git - qemu-server.git/blobdiff - PVE/QemuServer.pm
fix #3075: add TPM v1.2 and v2.0 support via swtpm
[qemu-server.git] / PVE / QemuServer.pm
index 3c3da5930a39436608552086ca7c134f62a1ba12..0ca5e00e4cdba11c8280f74a6106ea194318d52f 100644 (file)
@@ -1143,7 +1143,8 @@ PVE::JSONSchema::register_format('pve-qm-bootdev', \&verify_bootdev);
 sub verify_bootdev {
     my ($dev, $noerr) = @_;
 
-    return $dev if PVE::QemuServer::Drive::is_valid_drivename($dev) && $dev !~ m/^efidisk/;
+    my $special = $dev =~ m/^efidisk/ || $dev =~ m/^tpmstate/;
+    return $dev if PVE::QemuServer::Drive::is_valid_drivename($dev) && !$special;
 
     my $check = sub {
        my ($base) = @_;
@@ -1618,8 +1619,9 @@ sub print_drive_commandline_full {
     # io_uring with cache mode writeback or writethrough on krbd will hang...
     my $rbd_no_io_uring = $scfg && $scfg->{type} eq 'rbd' && $scfg->{krbd} && !$cache_direct;
 
-    # io_uring with cache mode writeback or writethrough on LVM will hang...
-    my $lvm_no_io_uring = $scfg && $scfg->{type} eq 'lvm' && !$cache_direct;
+    # io_uring with cache mode writeback or writethrough on LVM will hang, without cache only
+    # sometimes, just plain disable...
+    my $lvm_no_io_uring = $scfg && $scfg->{type} eq 'lvm';
 
     if (!$drive->{aio}) {
        if ($io_uring && !$rbd_no_io_uring && !$lvm_no_io_uring) {
@@ -2171,16 +2173,19 @@ sub destroy_vm {
        });
     }
 
+    my $volids = {};
     my $remove_owned_drive = sub {
        my ($ds, $drive) = @_;
        return if drive_is_cdrom($drive, 1);
 
        my $volid = $drive->{file};
        return if !$volid || $volid =~ m|^/|;
+       return if $volids->{$volid};
 
        my ($path, $owner) = PVE::Storage::path($storecfg, $volid);
        return if !$path || !$owner || ($owner != $vmid);
 
+       $volids->{$volid} = 1;
        eval { PVE::Storage::vdisk_free($storecfg, $volid) };
        warn "Could not remove disk '$volid', check manually: $@" if $@;
     };
@@ -2199,6 +2204,8 @@ sub destroy_vm {
        $remove_owned_drive->('vmstate', $drive);
     }
 
+    PVE::QemuConfig->foreach_volume_full($conf->{pending}, $include_opts, $remove_owned_drive);
+
     if ($purge_unreferenced) { # also remove unreferenced disk
        my $vmdisks = PVE::Storage::vdisk_list($storecfg, undef, $vmid, undef, 'images');
        PVE::Storage::foreach_volid($vmdisks, sub {
@@ -2960,6 +2967,90 @@ sub audio_devs {
     return $devs;
 }
 
+sub get_tpm_paths {
+    my ($vmid) = @_;
+    return {
+       socket => "/var/run/qemu-server/$vmid.swtpm",
+       pid => "/var/run/qemu-server/$vmid.swtpm.pid",
+    };
+}
+
+sub add_tpm_device {
+    my ($vmid, $devices, $conf) = @_;
+
+    return if !$conf->{tpmstate0};
+
+    my $paths = get_tpm_paths($vmid);
+
+    push @$devices, "-chardev", "socket,id=tpmchar,path=$paths->{socket}";
+    push @$devices, "-tpmdev", "emulator,id=tpmdev,chardev=tpmchar";
+    push @$devices, "-device", "tpm-tis,tpmdev=tpmdev";
+}
+
+sub start_swtpm {
+    my ($storecfg, $vmid, $tpmdrive, $migration) = @_;
+
+    return if !$tpmdrive;
+
+    my $state;
+    my $tpm = parse_drive("tpmstate0", $tpmdrive);
+    my ($storeid, $volname) = PVE::Storage::parse_volume_id($tpm->{file}, 1);
+    if ($storeid) {
+       $state = PVE::Storage::map_volume($storecfg, $tpm->{file});
+    } else {
+       $state = $tpm->{file};
+    }
+
+    my $paths = get_tpm_paths($vmid);
+
+    # during migration, we will get state from remote
+    #
+    if (!$migration) {
+       # run swtpm_setup to create a new TPM state if it doesn't exist yet
+       my $setup_cmd = [
+           "swtpm_setup",
+           "--tpmstate",
+           "file://$state",
+           "--createek",
+           "--create-ek-cert",
+           "--create-platform-cert",
+           "--lock-nvram",
+           "--config",
+           "/etc/swtpm_setup.conf", # do not use XDG configs
+           "--runas",
+           "0", # force creation as root, error if not possible
+           "--not-overwrite", # ignore existing state, do not modify
+       ];
+
+       push @$setup_cmd, "--tpm2" if $tpm->{version} eq 'v2.0';
+       # TPM 2.0 supports ECC crypto, use if possible
+       push @$setup_cmd, "--ecc" if $tpm->{version} eq 'v2.0';
+
+       run_command($setup_cmd, outfunc => sub {
+           print "swtpm_setup: $1\n";
+       });
+    }
+
+    my $emulator_cmd = [
+       "swtpm",
+       "socket",
+       "--tpmstate",
+       "backend-uri=file://$state,mode=0600",
+       "--ctrl",
+       "type=unixio,path=$paths->{socket},mode=0600",
+       "--pid",
+       "file=$paths->{pid}",
+       "--terminate", # terminate on QEMU disconnect
+       "--daemon",
+    ];
+    push @$emulator_cmd, "--tpm2" if $tpm->{version} eq 'v2.0';
+    run_command($emulator_cmd, outfunc => sub { print $1; });
+
+    # return untainted PID of swtpm daemon so it can be killed on error
+    file_read_firstline($paths->{pid}) =~ m/(\d+)/;
+    return $1;
+}
+
 sub vga_conf_has_spice {
     my ($vga) = @_;
 
@@ -3461,6 +3552,8 @@ sub config_to_command {
        push @$devices, @$audio_devs;
     }
 
+    add_tpm_device($vmid, $devices, $conf);
+
     my $sockets = 1;
     $sockets = $conf->{smp} if $conf->{smp}; # old style - no longer iused
     $sockets = $conf->{sockets} if  $conf->{sockets};
@@ -3657,6 +3750,8 @@ sub config_to_command {
 
        # ignore efidisk here, already added in bios/fw handling code above
        return if $drive->{interface} eq 'efidisk';
+       # similar for TPM
+       return if $drive->{interface} eq 'tpmstate';
 
        $use_virtio = 1 if $ds =~ m/^virtio/;
 
@@ -4025,42 +4120,36 @@ sub vm_deviceunplug {
     die "can't unplug bootdisk '$deviceid'\n" if grep {$_ eq $deviceid} @$bootdisks;
 
     if ($deviceid eq 'tablet' || $deviceid eq 'keyboard') {
-
        qemu_devicedel($vmid, $deviceid);
-
     } elsif ($deviceid =~ m/^usb\d+$/) {
-
        die "usb hotplug currently not reliable\n";
        # when unplugging usb devices this way, there may be remaining usb
        # controllers/hubs so we disable it for now
        #qemu_devicedel($vmid, $deviceid);
        #qemu_devicedelverify($vmid, $deviceid);
-
     } elsif ($deviceid =~ m/^(virtio)(\d+)$/) {
+       my $device = parse_drive($deviceid, $conf->{$deviceid});
 
-        qemu_devicedel($vmid, $deviceid);
-        qemu_devicedelverify($vmid, $deviceid);
-        qemu_drivedel($vmid, $deviceid);
-       qemu_iothread_del($conf, $vmid, $deviceid);
-
+       qemu_devicedel($vmid, $deviceid);
+       qemu_devicedelverify($vmid, $deviceid);
+       qemu_drivedel($vmid, $deviceid);
+       qemu_iothread_del($vmid, $deviceid, $device);
     } elsif ($deviceid =~ m/^(virtioscsi|scsihw)(\d+)$/) {
-
        qemu_devicedel($vmid, $deviceid);
        qemu_devicedelverify($vmid, $deviceid);
-       qemu_iothread_del($conf, $vmid, $deviceid);
-
     } elsif ($deviceid =~ m/^(scsi)(\d+)$/) {
+       my $device = parse_drive($deviceid, $conf->{$deviceid});
 
-        qemu_devicedel($vmid, $deviceid);
-        qemu_drivedel($vmid, $deviceid);
+       qemu_devicedel($vmid, $deviceid);
+       qemu_drivedel($vmid, $deviceid);
        qemu_deletescsihw($conf, $vmid, $deviceid);
 
+       qemu_iothread_del($vmid, "virtioscsi$device->{index}", $device)
+           if $conf->{scsihw} && ($conf->{scsihw} eq 'virtio-scsi-single');
     } elsif ($deviceid =~ m/^(net)(\d+)$/) {
-
-        qemu_devicedel($vmid, $deviceid);
-        qemu_devicedelverify($vmid, $deviceid);
-        qemu_netdevdel($vmid, $deviceid);
-
+       qemu_devicedel($vmid, $deviceid);
+       qemu_devicedelverify($vmid, $deviceid);
+       qemu_netdevdel($vmid, $deviceid);
     } else {
        die "can't unplug device '$deviceid'\n";
     }
@@ -4084,7 +4173,7 @@ sub qemu_devicedel {
 }
 
 sub qemu_iothread_add {
-    my($vmid, $deviceid, $device) = @_;
+    my ($vmid, $deviceid, $device) = @_;
 
     if ($device->{iothread}) {
        my $iothreads = vm_iothreads_list($vmid);
@@ -4093,13 +4182,8 @@ sub qemu_iothread_add {
 }
 
 sub qemu_iothread_del {
-    my($conf, $vmid, $deviceid) = @_;
+    my ($vmid, $deviceid, $device) = @_;
 
-    my $confid = $deviceid;
-    if ($deviceid =~ m/^(?:virtioscsi|scsihw)(\d+)$/) {
-       $confid = 'scsi' . $1;
-    }
-    my $device = parse_drive($confid, $conf->{$confid});
     if ($device->{iothread}) {
        my $iothreads = vm_iothreads_list($vmid);
        qemu_objectdel($vmid, "iothread-$deviceid") if $iothreads->{"iothread-$deviceid"};
@@ -4107,7 +4191,7 @@ sub qemu_iothread_del {
 }
 
 sub qemu_objectadd {
-    my($vmid, $objectid, $qomtype) = @_;
+    my ($vmid, $objectid, $qomtype) = @_;
 
     mon_cmd($vmid, "object-add", id => $objectid, "qom-type" => $qomtype);
 
@@ -4115,7 +4199,7 @@ sub qemu_objectadd {
 }
 
 sub qemu_objectdel {
-    my($vmid, $objectid) = @_;
+    my ($vmid, $objectid) = @_;
 
     mon_cmd($vmid, "object-del", id => $objectid);
 
@@ -4138,7 +4222,7 @@ sub qemu_driveadd {
 }
 
 sub qemu_drivedel {
-    my($vmid, $deviceid) = @_;
+    my ($vmid, $deviceid) = @_;
 
     my $ret = PVE::QemuServer::Monitor::hmp_cmd($vmid, "drive_del drive-$deviceid");
     $ret =~ s/^\s+//;
@@ -4187,7 +4271,7 @@ sub qemu_findorcreatescsihw {
     my $scsihwid="$controller_prefix$controller";
     my $devices_list = vm_devices_list($vmid);
 
-    if(!defined($devices_list->{$scsihwid})) {
+    if (!defined($devices_list->{$scsihwid})) {
        vm_deviceplug($storecfg, $conf, $vmid, $scsihwid, $device, $arch, $machine_type);
     }
 
@@ -4210,7 +4294,7 @@ sub qemu_deletescsihw {
     foreach my $opt (keys %{$devices_list}) {
        if (is_valid_drivename($opt)) {
            my $drive = parse_drive($opt, $conf->{$opt});
-           if($drive->{interface} eq 'scsi' && $drive->{index} < (($maxdev-1)*($controller+1))) {
+           if ($drive->{interface} eq 'scsi' && $drive->{index} < (($maxdev-1)*($controller+1))) {
                return 1;
            }
        }
@@ -4529,6 +4613,9 @@ sub foreach_volid {
        $volhash->{$volid}->{is_vmstate} //= 0;
        $volhash->{$volid}->{is_vmstate} = 1 if $key eq 'vmstate';
 
+       $volhash->{$volid}->{is_tpmstate} //= 0;
+       $volhash->{$volid}->{is_tpmstate} = 1 if $key eq 'tpmstate0';
+
        $volhash->{$volid}->{is_unused} //= 0;
        $volhash->{$volid}->{is_unused} = 1 if $key =~ /^unused\d+$/;
 
@@ -4726,7 +4813,7 @@ sub vmconfig_hotplug_pending {
                vmconfig_update_net($storecfg, $conf, $hotplug_features->{network},
                                    $vmid, $opt, $value, $arch, $machine_type);
            } elsif (is_valid_drivename($opt)) {
-               die "skip\n" if $opt eq 'efidisk0';
+               die "skip\n" if $opt eq 'efidisk0' || $opt eq 'tpmstate0';
                # some changes can be done without hotplug
                my $drive = parse_drive($opt, $value);
                if (drive_is_cloudinit($drive)) {
@@ -5170,7 +5257,9 @@ sub vm_start_nolock {
        $conf = PVE::QemuConfig->load_config($vmid); # update/reload
     }
 
-    PVE::QemuServer::Cloudinit::generate_cloudinitconfig($conf, $vmid);
+    # don't regenerate the ISO if the VM is started as part of a live migration
+    # this way we can reuse the old ISO with the correct config
+    PVE::QemuServer::Cloudinit::generate_cloudinitconfig($conf, $vmid) if !$migratedfrom;
 
     my $defaults = load_defaults();
 
@@ -5344,8 +5433,17 @@ sub vm_start_nolock {
        PVE::Tools::run_fork sub {
            PVE::Systemd::enter_systemd_scope($vmid, "Proxmox VE VM $vmid", %properties);
 
+           my $tpmpid;
+           if (my $tpm = $conf->{tpmstate0}) {
+               # start the TPM emulator so QEMU can connect on start
+               $tpmpid = start_swtpm($storecfg, $vmid, $tpm, $migratedfrom);
+           }
+
            my $exitcode = run_command($cmd, %run_params);
-           die "QEMU exited with code $exitcode\n" if $exitcode;
+           if ($exitcode) {
+               kill 'TERM', $tpmpid if $tpmpid;
+               die "QEMU exited with code $exitcode\n";
+           }
        };
     };
 
@@ -5545,6 +5643,14 @@ sub vm_stop_cleanup {
        if (!$keepActive) {
            my $vollist = get_vm_volumes($conf);
            PVE::Storage::deactivate_volumes($storecfg, $vollist);
+
+           if (my $tpmdrive = $conf->{tpmstate0}) {
+               my $tpm = parse_drive("tpmstate0", $tpmdrive);
+               my ($storeid, $volname) = PVE::Storage::parse_volume_id($tpm->{file}, 1);
+               if ($storeid) {
+                   PVE::Storage::unmap_volume($storecfg, $tpm->{file});
+               }
+           }
        }
 
        foreach my $ext (qw(mon qmp pid vnc qga)) {
@@ -6082,7 +6188,7 @@ sub restore_update_config_line {
        $net->{macaddr} = PVE::Tools::random_ether_addr($dc->{mac_prefix}) if $net->{macaddr};
        $netstr = print_net($net);
        $res .= "$id: $netstr\n";
-    } elsif ($line =~ m/^((ide|scsi|virtio|sata|efidisk)\d+):\s*(\S+)\s*$/) {
+    } elsif ($line =~ m/^((ide|scsi|virtio|sata|efidisk|tpmstate)\d+):\s*(\S+)\s*$/) {
        my $virtdev = $1;
        my $value = $3;
        my $di = parse_drive($virtdev, $value);
@@ -6396,13 +6502,13 @@ sub restore_proxmox_backup_archive {
            my $d = $virtdev_hash->{$virtdev};
            next if $d->{is_cloudinit}; # no need to restore cloudinit
 
-           # for live-restore we only want to preload the efidisk
-           next if $options->{live} && $virtdev ne 'efidisk0';
-
+           # this fails if storage is unavailable
            my $volid = $d->{volid};
-
            my $path = PVE::Storage::path($storecfg, $volid);
 
+           # for live-restore we only want to preload the efidisk and TPM state
+           next if $options->{live} && $virtdev ne 'efidisk0' && $virtdev ne 'tpmstate0';
+
            my $pbs_restore_cmd = [
                '/usr/bin/pbs-restore',
                '--repository', $repo,
@@ -6476,7 +6582,9 @@ sub restore_proxmox_backup_archive {
        my $conf = PVE::QemuConfig->load_config($vmid);
        die "cannot do live-restore for template\n" if PVE::QemuConfig->is_template($conf);
 
-       delete $devinfo->{'drive-efidisk0'}; # this special drive is already restored before start
+       # these special drives are already restored before start
+       delete $devinfo->{'drive-efidisk0'};
+       delete $devinfo->{'drive-tpmstate0-backup'};
        pbs_live_restore($vmid, $conf, $storecfg, $devinfo, $repo, $keyfile, $pbs_backup_name);
 
        PVE::QemuConfig->remove_lock($vmid, "create");
@@ -7310,6 +7418,8 @@ sub clone_disk {
            $size = PVE::QemuServer::Cloudinit::CLOUDINIT_DISK_SIZE;
        } elsif ($drivename eq 'efidisk0') {
            $size = get_efivars_size($conf);
+       } elsif ($drivename eq 'tpmstate0') {
+           $size = PVE::QemuServer::Drive::TPMSTATE_DISK_SIZE;
        } else {
            ($size) = PVE::Storage::volume_size_info($storecfg, $drive->{file}, 10);
        }
@@ -7350,6 +7460,8 @@ sub clone_disk {
            }
        } else {
 
+           die "cannot move TPM state while VM is running\n" if $drivename eq 'tpmstate0';
+
            my $kvmver = get_running_qemu_version ($vmid);
            if (!min_version($kvmver, 2, 7)) {
                die "drive-mirror with iothread requires qemu version 2.7 or higher\n"
@@ -7420,6 +7532,14 @@ sub update_efidisk_size {
     return;
 }
 
+sub update_tpmstate_size {
+    my ($conf) = @_;
+
+    my $disk = PVE::QemuServer::parse_drive('tpmstate0', $conf->{tpmstate0});
+    $disk->{size} = PVE::QemuServer::Drive::TPMSTATE_DISK_SIZE;
+    $conf->{tpmstate0} = print_drive($disk);
+}
+
 sub create_efidisk($$$$$) {
     my ($storecfg, $storeid, $vmid, $fmt, $arch) = @_;