$local_volumes->{$volid}->{ref} = $attr->{referenced_in_config} ? 'config' : 'snapshot';
$local_volumes->{$volid}->{ref} = 'storage' if $attr->{is_unused};
+ $local_volumes->{$volid}->{ref} = 'generated' if $attr->{is_tpmstate};
$local_volumes->{$volid}->{is_vmstate} = $attr->{is_vmstate} ? 1 : 0;
$local_volumes->{$volid}->{migration_mode} = 'online';
} elsif ($self->{running} && $ref eq 'generated') {
# offline migrate the cloud-init ISO and don't regenerate on VM start
+ #
+ # tpmstate will also be offline migrated first, and in case of
+ # live migration then updated by QEMU/swtpm if necessary
$local_volumes->{$volid}->{migration_mode} = 'offline';
} else {
$local_volumes->{$volid}->{migration_mode} = 'offline';
PVE::QemuConfig->foreach_volume($conf, sub {
my ($key, $drive) = @_;
- return if $key eq 'efidisk0'; # skip efidisk, will be handled later
+ # skip special disks, will be handled later
+ return if $key eq 'efidisk0';
+ return if $key eq 'tpmstate0';
my $volid = $drive->{file};
return if !defined($local_volumes->{$volid}); # only update sizes for local volumes
if (defined($conf->{efidisk0})) {
PVE::QemuServer::update_efidisk_size($conf);
}
+
+ # TPM state might have an irregular filesize, to avoid problems on transfer
+ # we always assume the static size of 4M to allocate on the target
+ if (defined($conf->{tpmstate0})) {
+ PVE::QemuServer::update_tpmstate_size($conf);
+ }
}
sub filter_local_volumes {
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) = @_;
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) = @_;
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};
# 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/;
$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+$/;
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)) {
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";
+ }
};
};
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)) {
$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);
my $volid = $d->{volid};
my $path = PVE::Storage::path($storecfg, $volid);
- # for live-restore we only want to preload the efidisk
- next if $options->{live} && $virtdev ne 'efidisk0';
+ # 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',
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");
$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);
}
}
} 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"
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) = @_;
};
PVE::JSONSchema::register_standard_option("pve-qm-virtio", $virtiodesc);
-my $alldrive_fmt = {
- %drivedesc_base,
- %iothread_fmt,
- %model_fmt,
- %queues_fmt,
- %scsiblock_fmt,
- %ssd_fmt,
- %wwn_fmt,
-};
-
my $efidisk_fmt = {
volume => { alias => 'file' },
file => {
PVE::JSONSchema::register_standard_option("pve-qm-efidisk", $efidisk_desc);
+my %tpmversion_fmt = (
+ version => {
+ type => 'string',
+ enum => [qw(v1.2 v2.0)],
+ description => "The TPM interface version. v2.0 is newer and should be "
+ . "preferred. Note that this cannot be changed later on.",
+ optional => 1,
+ default => 'v2.0',
+ },
+);
+my $tpmstate_fmt = {
+ volume => { alias => 'file' },
+ file => {
+ type => 'string',
+ format => 'pve-volume-id-or-qm-path',
+ default_key => 1,
+ format_description => 'volume',
+ description => "The drive's backing volume.",
+ },
+ size => {
+ type => 'string',
+ format => 'disk-size',
+ format_description => 'DiskSize',
+ description => "Disk size. This is purely informational and has no effect.",
+ optional => 1,
+ },
+ %tpmversion_fmt,
+};
+my $tpmstate_desc = {
+ optional => 1,
+ type => 'string', format => $tpmstate_fmt,
+ description => "Configure a Disk for storing TPM state. " .
+ $ALLOCATION_SYNTAX_DESC . " Note that SIZE_IN_GiB is ignored here " .
+ "and that the default size of 4 MiB will always be used instead. The " .
+ "format is also fixed to 'raw'.",
+};
+use constant TPMSTATE_DISK_SIZE => 4 * 1024 * 1024;
+
+my $alldrive_fmt = {
+ %drivedesc_base,
+ %iothread_fmt,
+ %model_fmt,
+ %queues_fmt,
+ %scsiblock_fmt,
+ %ssd_fmt,
+ %wwn_fmt,
+ %tpmversion_fmt,
+};
+
my $unused_fmt = {
volume => { alias => 'file' },
file => {
}
$drivedesc_hash->{efidisk0} = $efidisk_desc;
+$drivedesc_hash->{tpmstate0} = $tpmstate_desc;
for (my $i = 0; $i < $MAX_UNUSED_DISKS; $i++) {
$drivedesc_hash->{"unused$i"} = $unuseddesc;
(map { "scsi$_" } (0 .. ($MAX_SCSI_DISKS - 1))),
(map { "virtio$_" } (0 .. ($MAX_VIRTIO_DISKS - 1))),
(map { "sata$_" } (0 .. ($MAX_SATA_DISKS - 1))),
- 'efidisk0');
+ 'efidisk0',
+ 'tpmstate0');
}
sub is_valid_drivename {
if (!$volume->{included}) {
$self->loginfo("exclude disk '$name' '$volid' ($volume->{reason})");
next;
- } elsif ($self->{vm_was_running} && $volume_config->{iothread}) {
- if (!PVE::QemuServer::Machine::runs_at_least_qemu_version($vmid, 4, 0, 1)) {
- die "disk '$name' '$volid' (iothread=on) can't use backup feature with running QEMU " .
- "version < 4.0.1! Either set backup=no for this drive or upgrade QEMU and restart VM\n";
- }
+ } elsif ($self->{vm_was_running} && $volume_config->{iothread} &&
+ !PVE::QemuServer::Machine::runs_at_least_qemu_version($vmid, 4, 0, 1)) {
+ die "disk '$name' '$volid' (iothread=on) can't use backup feature with running QEMU " .
+ "version < 4.0.1! Either set backup=no for this drive or upgrade QEMU and restart VM\n";
} else {
my $log = "include disk '$name' '$volid'";
if (defined(my $size = $volume_config->{size})) {
qmdevice => "drive-$ds",
};
+ if ($ds eq 'tpmstate0') {
+ # TPM drive only exists for backup, which is reflected in the name
+ $diskinfo->{qmdevice} = 'drive-tpmstate0-backup';
+ $task->{tpmpath} = $path;
+ }
+
if (-b $path) {
$diskinfo->{type} = 'block';
} else {
};
};
+my $attach_tpmstate_drive = sub {
+ my ($self, $task, $vmid) = @_;
+
+ return if !$task->{tpmpath};
+
+ # unconditionally try to remove the tpmstate-named drive - it only exists
+ # for backing up, and avoids errors if left over from some previous event
+ eval { PVE::QemuServer::qemu_drivedel($vmid, "tpmstate0-backup"); };
+
+ $self->loginfo('attaching TPM drive to QEMU for backup');
+
+ my $drive = "file=$task->{tpmpath},if=none,read-only=on,id=drive-tpmstate0-backup";
+ my $ret = PVE::QemuServer::Monitor::hmp_cmd($vmid, "drive_add auto \"$drive\"");
+ die "attaching TPM drive failed\n" if $ret !~ m/OK/s;
+};
+
+my $detach_tpmstate_drive = sub {
+ my ($task, $vmid) = @_;
+ return if !$task->{tpmpath} || !PVE::QemuServer::check_running($vmid);
+ eval { PVE::QemuServer::qemu_drivedel($vmid, "tpmstate0-backup"); };
+};
+
sub archive_pbs {
my ($self, $task, $vmid) = @_;
$master_keyfile = undef; # skip rest of master key handling below
}
+ $attach_tpmstate_drive->($self, $task, $vmid);
+
my $fs_frozen = $self->qga_fs_freeze($task, $vmid);
my $params = {
die "interrupted by signal\n";
};
+ $attach_tpmstate_drive->($self, $task, $vmid);
+
my $outfh;
if ($opts->{stdout}) {
$outfh = $opts->{stdout};
sub cleanup {
my ($self, $task, $vmid) = @_;
+ $detach_tpmstate_drive->($task, $vmid);
+
if ($self->{qmeventd_fh}) {
close($self->{qmeventd_fh});
}