use UUID;
use POSIX;
use IO::Socket::IP;
+use URI::Escape;
use PVE::Cluster qw (cfs_read_file cfs_write_file);;
use PVE::SafeSyslog;
use PVE::Network;
use PVE::Firewall;
use PVE::API2::Firewall::VM;
+use PVE::API2::Qemu::Agent;
BEGIN {
if (!$ENV{PVE_GENERATING_DOCS}) {
my $volid = $drive->{file};
- if (!$volid || $volid eq 'none') {
+ if (!$volid || ($volid eq 'none' || $volid eq 'cloudinit')) {
+ # nothing to check
+ } elsif ($volid =~ m/^(([^:\s]+):)?(cloudinit)$/) {
# nothing to check
} elsif ($isCDROM && ($volid eq 'cdrom')) {
$rpcenv->check($authuser, "/", ['Sys.Console']);
if (!$volid || $volid eq 'none' || $volid eq 'cdrom') {
delete $disk->{size};
$res->{$ds} = PVE::QemuServer::print_drive($vmid, $disk);
+ } elsif ($volid =~ m!^(?:([^/:\s]+):)?cloudinit$!) {
+ my $storeid = $1 || $default_storage;
+ die "no storage ID specified (and no default storage)\n" if !$storeid;
+ my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
+ my $name = "vm-$vmid-cloudinit";
+ my $fmt = undef;
+ if ($scfg->{path}) {
+ $name .= ".qcow2";
+ $fmt = 'qcow2';
+ }else{
+ $fmt = 'raw';
+ }
+ # FIXME: Reasonable size? qcow2 shouldn't grow if the space isn't used anyway?
+ my $cloudinit_iso_size = 5; # in MB
+ my $volid = PVE::Storage::vdisk_alloc($storecfg, $storeid, $vmid,
+ $fmt, $name, $cloudinit_iso_size*1024);
+ $disk->{file} = $volid;
+ $disk->{media} = 'cdrom';
+ push @$vollist, $volid;
+ delete $disk->{format}; # no longer needed
+ $res->{$ds} = PVE::QemuServer::print_drive($vmid, $disk);
} elsif ($volid =~ $NEW_DISK_RE) {
my ($storeid, $size) = ($2 || $default_storage, $3);
die "no storage ID specified (and no default storage)\n" if !$storeid;
'vmstatestorage' => 1,
};
+my $cloudinitoptions = {
+ cipassword => 1,
+ citype => 1,
+ ciuser => 1,
+ nameserver => 1,
+ searchdomain => 1,
+ sshkeys => 1,
+};
+
my $check_vm_modify_config_perm = sub {
my ($rpcenv, $authuser, $vmid, $pool, $key_list) = @_;
$rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.PowerMgmt']);
} elsif ($diskoptions->{$opt}) {
$rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Disk']);
- } elsif ($opt =~ m/^net\d+$/) {
+ } elsif ($cloudinitoptions->{$opt} || ($opt =~ m/^(?:net|ipconfig)\d+$/)) {
$rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Network']);
} else {
# catches usb\d+, hostpci\d+, args, lock, etc.
type => 'array',
items => {
type => "object",
- properties => {},
+ properties => $PVE::QemuServer::vmstatus_return_properties,
},
links => [ { rel => 'child', href => "{vmid}" } ],
},
next if !$rpcenv->check($authuser, "/vms/$vmid", [ 'VM.Audit' ], 1);
my $data = $vmstatus->{$vmid};
- $data->{vmid} = int($vmid);
push @$res, $data;
}
type => 'string', format => 'pve-poolid',
description => "Add the VM to the specified pool.",
},
+ bwlimit => {
+ description => "Override i/o bandwidth limit (in KiB/s).",
+ optional => 1,
+ type => 'integer',
+ minimum => '0',
+ },
+ start => {
+ optional => 1,
+ type => 'boolean',
+ default => 0,
+ description => "Start VM after it was created successfully.",
+ },
}),
},
returns => {
my $vmid = extract_param($param, 'vmid');
my $archive = extract_param($param, 'archive');
+ my $is_restore = !!$archive;
my $storage = extract_param($param, 'storage');
my $pool = extract_param($param, 'pool');
+ my $bwlimit = extract_param($param, 'bwlimit');
+
+ my $start_after_create = extract_param($param, 'start');
+
my $filename = PVE::QemuConfig->config_file($vmid);
my $storecfg = PVE::Storage::config();
+ if (defined(my $ssh_keys = $param->{sshkeys})) {
+ $ssh_keys = URI::Escape::uri_unescape($ssh_keys);
+ PVE::Tools::validate_ssh_public_keys($ssh_keys);
+ }
+
PVE::Cluster::check_cfs_quorum();
if (defined($pool)) {
}
}
- my $restorefn = sub {
- my $vmlist = PVE::Cluster::get_vmlist();
- if ($vmlist->{ids}->{$vmid}) {
- my $current_node = $vmlist->{ids}->{$vmid}->{node};
- if ($current_node eq $node) {
- my $conf = PVE::QemuConfig->load_config($vmid);
-
- PVE::QemuConfig->check_protection($conf, "unable to restore VM $vmid");
+ my $emsg = $is_restore ? "unable to restore VM $vmid -" : "unable to create VM $vmid -";
- die "unable to restore vm $vmid - config file already exists\n"
- if !$force;
+ eval { PVE::QemuConfig->create_and_lock_config($vmid, $force) };
+ die "$emsg $@" if $@;
- die "unable to restore vm $vmid - vm is running\n"
- if PVE::QemuServer::check_running($vmid);
+ my $restorefn = sub {
+ my $conf = PVE::QemuConfig->load_config($vmid);
- die "unable to restore vm $vmid - vm is a template\n"
- if PVE::QemuConfig->is_template($conf);
+ PVE::QemuConfig->check_protection($conf, $emsg);
- } else {
- die "unable to restore vm $vmid - already existing on cluster node '$current_node'\n";
- }
- }
+ die "$emsg vm is running\n" if PVE::QemuServer::check_running($vmid);
+ die "$emsg vm is a template\n" if PVE::QemuConfig->is_template($conf);
my $realcmd = sub {
PVE::QemuServer::restore_archive($archive, $vmid, $authuser, {
storage => $storage,
pool => $pool,
- unique => $unique });
+ unique => $unique,
+ bwlimit => $bwlimit, });
PVE::AccessControl::add_vm_to_pool($vmid, $pool) if $pool;
+
+ if ($start_after_create) {
+ eval { PVE::API2::Qemu->vm_start({ vmid => $vmid, node => $node }) };
+ warn $@ if $@;
+ }
};
# ensure no old replication state are exists
PVE::ReplicationState::delete_guest_states($vmid);
- return $rpcenv->fork_worker('qmrestore', $vmid, $authuser, $realcmd);
+ return PVE::QemuConfig->lock_config_full($vmid, 1, $realcmd);
};
my $createfn = sub {
-
- # test after locking
- PVE::Cluster::check_vmid_unused($vmid);
-
# ensure no old replication state are exists
PVE::ReplicationState::delete_guest_states($vmid);
my $conf = $param;
+ my ($arch, undef) = PVE::QemuServer::get_basic_machine_info($conf);
+
eval {
$vollist = &$create_disks($rpcenv, $authuser, $conf, $storecfg, $vmid, $pool, $param, $storage);
$conf->{smbios1} = PVE::QemuServer::generate_smbios1_uuid();
}
+ if ((!defined($conf->{vmgenid}) || $conf->{vmgenid} eq '1') && $arch ne 'aarch64') {
+ $conf->{vmgenid} = PVE::QemuServer::generate_uuid();
+ }
+
PVE::QemuConfig->write_config($vmid, $conf);
};
eval { PVE::Storage::vdisk_free($storecfg, $volid); };
warn $@ if $@;
}
- die "create failed - $err";
+ die "$emsg $err";
}
PVE::AccessControl::add_vm_to_pool($vmid, $pool) if $pool;
};
- return $rpcenv->fork_worker('qmcreate', $vmid, $authuser, $realcmd);
+ PVE::QemuConfig->lock_config_full($vmid, 1, $realcmd);
+
+ if ($start_after_create) {
+ print "Execute autostart\n";
+ eval { PVE::API2::Qemu->vm_start({vmid => $vmid, node => $node}) };
+ warn $@ if $@;
+ }
};
- return PVE::QemuConfig->lock_config_full($vmid, 1, $archive ? $restorefn : $createfn);
+ my ($code, $worker_name);
+ if ($is_restore) {
+ $worker_name = 'qmrestore';
+ $code = sub {
+ eval { $restorefn->() };
+ if (my $err = $@) {
+ eval { PVE::QemuConfig->remove_lock($vmid, 'create') };
+ warn $@ if $@;
+ die $err;
+ }
+ };
+ } else {
+ $worker_name = 'qmcreate';
+ $code = sub {
+ eval { $createfn->() };
+ if (my $err = $@) {
+ eval {
+ my $conffile = PVE::QemuConfig->config_file($vmid);
+ unlink($conffile)
+ or die "failed to remove config file: $@\n";
+ };
+ warn $@ if $@;
+ die $err;
+ }
+ };
+ }
+
+ return $rpcenv->fork_worker($worker_name, $vmid, $authuser, $code);
}});
__PACKAGE__->register_method({
path => '{vmid}/firewall',
});
+__PACKAGE__->register_method ({
+ subclass => "PVE::API2::Qemu::Agent",
+ path => '{vmid}/agent',
+});
+
__PACKAGE__->register_method({
name => 'rrd',
path => '{vmid}/rrd',
},
},
returns => {
+ description => "The current VM configuration.",
type => "object",
- properties => {
+ properties => PVE::QemuServer::json_config_properties({
digest => {
type => 'string',
description => 'SHA1 digest of configuration file. This can be used to prevent concurrent modifications.',
}
- },
+ }),
},
code => sub {
my ($param) = @_;
delete $conf->{pending};
+ # hide cloudinit password
+ if ($conf->{cipassword}) {
+ $conf->{cipassword} = '**********';
+ }
+
return $conf;
}});
$item->{value} = $conf->{$opt} if defined($conf->{$opt});
$item->{pending} = $conf->{pending}->{$opt} if defined($conf->{pending}->{$opt});
$item->{delete} = ($pending_delete_hash->{$opt} ? 2 : 1) if exists $pending_delete_hash->{$opt};
+
+ # hide cloudinit password
+ if ($opt eq 'cipassword') {
+ $item->{value} = '**********' if defined($item->{value});
+ # the trailing space so that the pending string is different
+ $item->{pending} = '********** ' if defined($item->{pending});
+ }
push @$res, $item;
}
next if defined($conf->{$opt});
my $item = { key => $opt };
$item->{pending} = $conf->{pending}->{$opt};
+
+ # hide cloudinit password
+ if ($opt eq 'cipassword') {
+ $item->{pending} = '**********' if defined($item->{pending});
+ }
push @$res, $item;
}
my $background_delay = extract_param($param, 'background_delay');
+ if (defined(my $cipassword = $param->{cipassword})) {
+ # Same logic as in cloud-init (but with the regex fixed...)
+ $param->{cipassword} = PVE::Tools::encrypt_pw($cipassword)
+ if $cipassword !~ /^\$(?:[156]|2[ay])(\$.+){2}/;
+ }
+
my @paramarr = (); # used for log message
foreach my $key (sort keys %$param) {
- push @paramarr, "-$key", $param->{$key};
+ my $value = $key eq 'cipassword' ? '<hidden>' : $param->{$key};
+ push @paramarr, "-$key", $value;
}
my $skiplock = extract_param($param, 'skiplock');
my $force = extract_param($param, 'force');
+ if (defined(my $ssh_keys = $param->{sshkeys})) {
+ $ssh_keys = URI::Escape::uri_unescape($ssh_keys);
+ PVE::Tools::validate_ssh_public_keys($ssh_keys);
+ }
+
die "no options specified\n" if !$delete_str && !$revert_str && !scalar(keys %$param);
my $storecfg = PVE::Storage::config();
# add macaddr
my $net = PVE::QemuServer::parse_net($param->{$opt});
$param->{$opt} = PVE::QemuServer::print_net($net);
+ } elsif ($opt eq 'vmgenid') {
+ if ($param->{$opt} eq '1') {
+ $param->{$opt} = PVE::QemuServer::generate_uuid();
+ }
}
}
if (PVE::QemuServer::is_valid_drivename($opt)) {
my $drive = PVE::QemuServer::parse_drive($opt, $param->{$opt});
+ # FIXME: cloudinit: CDROM or Disk?
if (PVE::QemuServer::drive_is_cdrom($drive)) { # CDROM
$rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.CDROM']);
} else {
vmid => get_standard_option('pve-vmid'),
},
},
- returns => { type => 'object' },
+ returns => {
+ type => 'object',
+ properties => {
+ %$PVE::QemuServer::vmstatus_return_properties,
+ ha => {
+ description => "HA manager service status.",
+ type => 'object',
+ },
+ spice => {
+ description => "Qemu VGA configuration supports spice.",
+ type => 'boolean',
+ optional => 1,
+ },
+ agent => {
+ description => "Qemu GuestAgent enabled in config.",
+ type => 'boolean',
+ optional => 1,
+ },
+ },
+ },
code => sub {
my ($param) = @_;
$status->{ha} = PVE::HA::Config::get_service_status("vm:$param->{vmid}");
$status->{spice} = 1 if PVE::QemuServer::vga_conf_has_spice($conf->{vga});
-
- $status->{agent} = 1 if $conf->{agent};
+ $status->{agent} = 1 if (PVE::QemuServer::parse_guest_agent($conf)->{enabled});
return $status;
}});
properties => {
node => get_standard_option('pve-node'),
vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
- newid => get_standard_option('pve-vmid', { description => 'VMID for the clone.' }),
+ newid => get_standard_option('pve-vmid', {
+ completion => \&PVE::Cluster::complete_next_vmid,
+ description => 'VMID for the clone.' }),
name => {
optional => 1,
type => 'string', format => 'dns-name',
}),
storage => get_standard_option('pve-storage-id', {
description => "Target storage for full clone.",
- requires => 'full',
optional => 1,
}),
'format' => {
- description => "Target format for file storage.",
- requires => 'full',
+ description => "Target format for file storage. Only valid for full clone.",
type => 'string',
optional => 1,
enum => [ 'raw', 'qcow2', 'vmdk'],
full => {
optional => 1,
type => 'boolean',
- description => "Create a full copy of all disk. This is always done when " .
+ description => "Create a full copy of all disks. This is always done when " .
"you clone a normal VM. For VM templates, we try to create a linked clone by default.",
- default => 0,
},
target => get_standard_option('pve-node', {
description => "Target node. Only allowed if the original VM is on shared storage.",
die "snapshot '$snapname' does not exist\n"
if $snapname && !defined( $conf->{snapshots}->{$snapname});
+ my $full = extract_param($param, 'full');
+ if (!defined($full)) {
+ $full = !PVE::QemuConfig->is_template($conf);
+ }
+
+ die "parameter 'storage' not allowed for linked clones\n"
+ if defined($storage) && !$full;
+
+ die "parameter 'format' not allowed for linked clones\n"
+ if defined($format) && !$full;
+
my $oldconf = $snapname ? $conf->{snapshots}->{$snapname} : $conf;
my $sharedvm = &$check_storage_access_clone($rpcenv, $authuser, $storecfg, $oldconf, $storage);
} elsif (PVE::QemuServer::is_valid_drivename($opt)) {
my $drive = PVE::QemuServer::parse_drive($opt, $value);
die "unable to parse drive options for '$opt'\n" if !$drive;
- if (PVE::QemuServer::drive_is_cdrom($drive)) {
+ if (PVE::QemuServer::drive_is_cdrom($drive, 1)) {
$newconf->{$opt} = $value; # simply copy configuration
} else {
- if ($param->{full}) {
+ if ($full || PVE::QemuServer::drive_is_cloudinit($drive)) {
die "Full clone feature is not supported for drive '$opt'\n"
if !PVE::Storage::volume_has_feature($storecfg, 'copy', $drive->{file}, $snapname, $running);
$fullclone->{$opt} = 1;
}
# auto generate a new uuid
- my ($uuid, $uuid_str);
- UUID::generate($uuid);
- UUID::unparse($uuid, $uuid_str);
my $smbios1 = PVE::QemuServer::parse_smbios1($newconf->{smbios1} || '');
- $smbios1->{uuid} = $uuid_str;
+ $smbios1->{uuid} = PVE::QemuServer::generate_uuid();
$newconf->{smbios1} = PVE::QemuServer::print_smbios1($smbios1);
+ # auto generate a new vmgenid if the option was set
+ if ($newconf->{vmgenid}) {
+ $newconf->{vmgenid} = PVE::QemuServer::generate_uuid();
+ }
+
delete $newconf->{template};
if ($param->{name}) {
}
delete $newconf->{lock};
+
+ # do not write pending changes
+ if (my @changes = keys %{$newconf->{pending}}) {
+ my $pending = join(',', @changes);
+ warn "found pending changes for '$pending', discarding for clone\n";
+ delete $newconf->{pending};
+ }
+
PVE::QemuConfig->write_config($newid, $newconf);
if ($target) {
my $old_volid = $drive->{file} || die "disk '$disk' has no associated volume\n";
- die "you can't move a cdrom\n" if PVE::QemuServer::drive_is_cdrom($drive);
+ die "you can't move a cdrom\n" if PVE::QemuServer::drive_is_cdrom($drive, 1);
my $oldfmt;
my ($oldstoreid, $oldvolname) = PVE::Storage::parse_volume_id($old_volid);
PVE::QemuConfig->write_config($vmid, $conf);
+ if ($running && PVE::QemuServer::parse_guest_agent($conf)->{fstrim_cloned_disks} && PVE::QemuServer::qga_check_running($vmid)) {
+ eval { PVE::QemuServer::vm_mon_cmd($vmid, "guest-fstrim"); };
+ }
+
eval {
# try to deactivate volumes - avoid lvm LVs to be active on several nodes
PVE::Storage::deactivate_volumes($storecfg, [ $newdrive->{file} ])
return $res;
}});
-my $guest_agent_commands = [
- 'ping',
- 'get-time',
- 'info',
- 'fsfreeze-status',
- 'fsfreeze-freeze',
- 'fsfreeze-thaw',
- 'fstrim',
- 'network-get-interfaces',
- 'get-vcpus',
- 'get-fsinfo',
- 'get-memory-blocks',
- 'get-memory-block-info',
- 'suspend-hybrid',
- 'suspend-ram',
- 'suspend-disk',
- 'shutdown',
- ];
-
-__PACKAGE__->register_method({
- name => 'agent',
- path => '{vmid}/agent',
- method => 'POST',
- protected => 1,
- proxyto => 'node',
- description => "Execute Qemu Guest Agent commands.",
- permissions => {
- check => ['perm', '/vms/{vmid}', [ 'VM.Monitor' ]],
- },
- parameters => {
- additionalProperties => 0,
- properties => {
- node => get_standard_option('pve-node'),
- vmid => get_standard_option('pve-vmid', {
- completion => \&PVE::QemuServer::complete_vmid_running }),
- command => {
- type => 'string',
- description => "The QGA command.",
- enum => $guest_agent_commands,
- },
- },
- },
- returns => {
- type => 'object',
- description => "Returns an object with a single `result` property. The type of that
-property depends on the executed command.",
- },
- code => sub {
- my ($param) = @_;
-
- my $vmid = $param->{vmid};
-
- my $conf = PVE::QemuConfig->load_config ($vmid); # check if VM exists
-
- die "No Qemu Guest Agent\n" if !defined($conf->{agent});
- die "VM $vmid is not running\n" if !PVE::QemuServer::check_running($vmid);
-
- my $cmd = $param->{command};
-
- my $res = PVE::QemuServer::vm_mon_cmd($vmid, "guest-$cmd");
-
- return { result => $res };
- }});
-
__PACKAGE__->register_method({
name => 'resize_vm',
path => '{vmid}/resize',
type => 'array',
items => {
type => "object",
- properties => {},
+ properties => {
+ name => {
+ description => "Snapshot identifier. Value 'current' identifies the current VM.",
+ type => 'string',
+ },
+ vmstate => {
+ description => "Snapshot includes RAM.",
+ type => 'boolean',
+ optional => 1,
+ },
+ description => {
+ description => "Snapshot description.",
+ type => 'string',
+ },
+ snaptime => {
+ description => "Snapshot creation time",
+ type => 'integer',
+ renderer => 'timestamp',
+ optional => 1,
+ },
+ parent => {
+ description => "Parent snapshot identifier.",
+ type => 'string',
+ optional => 1,
+ },
+ },
},
links => [ { rel => 'child', href => "{name}" } ],
},
}
my $running = PVE::QemuServer::check_running($vmid, 1) ? 1 : 0;
- my $current = { name => 'current', digest => $conf->{digest}, running => $running };
+ my $current = {
+ name => 'current',
+ digest => $conf->{digest},
+ running => $running,
+ description => "You are here!",
+ };
$current->{parent} = $conf->{parent} if $conf->{parent};
push @$res, $current;