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']);
PVE::Storage::check_volume_access($rpcenv, $authuser, $storecfg, $vmid, $volid);
}
});
+
+ $rpcenv->check($authuser, "/storage/$settings->{vmstatestorage}", ['Datastore.AllocateSpace'])
+ if defined($settings->{vmstatestorage});
};
my $check_storage_access_clone = sub {
}
});
+ $rpcenv->check($authuser, "/storage/$conf->{vmstatestorage}", ['Datastore.AllocateSpace'])
+ if defined($conf->{vmstatestorage});
+
return $sharedvm;
};
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;
my $defformat = PVE::Storage::storage_default_format($storecfg, $storeid);
my $fmt = $disk->{format} || $defformat;
+ $size = PVE::Tools::convert_size($size, 'gb' => 'kb'); # vdisk_alloc uses kb
+
my $volid;
if ($ds eq 'efidisk0') {
- # handle efidisk
- my $ovmfvars = '/usr/share/kvm/OVMF_VARS-pure-efi.fd';
- die "uefi vars image not found\n" if ! -f $ovmfvars;
- $volid = PVE::Storage::vdisk_alloc($storecfg, $storeid, $vmid,
- $fmt, undef, 128);
- $disk->{file} = $volid;
- $disk->{size} = 128*1024;
- my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid);
- my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
- my $qemufmt = PVE::QemuServer::qemu_img_format($scfg, $volname);
- my $path = PVE::Storage::path($storecfg, $volid);
- my $efidiskcmd = ['/usr/bin/qemu-img', 'convert', '-n', '-f', 'raw', '-O', $qemufmt];
- push @$efidiskcmd, $ovmfvars;
- push @$efidiskcmd, $path;
-
- PVE::Storage::activate_volumes($storecfg, [$volid]);
-
- eval { PVE::Tools::run_command($efidiskcmd); };
- my $err = $@;
- die "Copying of EFI Vars image failed: $err" if $err;
+ ($volid, $size) = PVE::QemuServer::create_efidisk($storecfg, $storeid, $vmid, $fmt);
} else {
- $volid = PVE::Storage::vdisk_alloc($storecfg, $storeid, $vmid,
- $fmt, undef, $size*1024*1024);
- $disk->{file} = $volid;
- $disk->{size} = $size*1024*1024*1024;
+ $volid = PVE::Storage::vdisk_alloc($storecfg, $storeid, $vmid, $fmt, undef, $size);
}
push @$vollist, $volid;
+ $disk->{file} = $volid;
+ $disk->{size} = PVE::Tools::convert_size($size, 'kb' => 'b');
delete $disk->{format}; # no longer needed
$res->{$ds} = PVE::QemuServer::print_drive($vmid, $disk);
} else {
my $diskoptions = {
'boot' => 1,
'bootdisk' => 1,
+ 'vmstatestorage' => 1,
};
my $check_vm_modify_config_perm = sub {
$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 ($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 => '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',
+ }
}),
},
returns => {
my $pool = extract_param($param, 'pool');
+ my $bwlimit = extract_param($param, 'bwlimit');
+
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)) {
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;
};
$vollist = &$create_disks($rpcenv, $authuser, $conf, $storecfg, $vmid, $pool, $param, $storage);
- # try to be smart about bootdisk
- my @disks = PVE::QemuServer::valid_drive_names();
- my $firstdisk;
- foreach my $ds (reverse @disks) {
- next if !$conf->{$ds};
- my $disk = PVE::QemuServer::parse_drive($ds, $conf->{$ds});
- next if PVE::QemuServer::drive_is_cdrom($disk);
- $firstdisk = $ds;
- }
-
- if (!$conf->{bootdisk} && $firstdisk) {
- $conf->{bootdisk} = $firstdisk;
+ if (!$conf->{bootdisk}) {
+ my $firstdisk = PVE::QemuServer::resolve_first_disk($conf);
+ $conf->{bootdisk} = $firstdisk if $firstdisk;
}
# auto generate uuid if user did not specify smbios1 option
if (!$conf->{smbios1}) {
- my ($uuid, $uuid_str);
- UUID::generate($uuid);
- UUID::unparse($uuid, $uuid_str);
- $conf->{smbios1} = "uuid=$uuid_str";
+ $conf->{smbios1} = PVE::QemuServer::generate_smbios1_uuid();
}
PVE::QemuConfig->write_config($vmid, $conf);
{ subdir => 'status' },
{ subdir => 'unlink' },
{ subdir => 'vncproxy' },
+ { subdir => 'termproxy' },
{ subdir => 'migrate' },
{ subdir => 'resize' },
{ subdir => 'move' },
path => '{vmid}/firewall',
});
+__PACKAGE__->register_method ({
+ subclass => "PVE::API2::Qemu::Agent",
+ path => '{vmid}/agent',
+});
+
__PACKAGE__->register_method({
name => 'rrd',
path => '{vmid}/rrd',
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();
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 {
if ($node ne 'localhost' && $node ne PVE::INotify::nodename()) {
($remip, $family) = PVE::Cluster::remote_node_ip($node);
# NOTE: kvm VNC traffic is already TLS encrypted or is known unsecure
- $remcmd = ['/usr/bin/ssh', '-T', '-o', 'BatchMode=yes', $remip];
+ $remcmd = ['/usr/bin/ssh', '-e', 'none', '-T', '-o', 'BatchMode=yes', $remip];
} else {
$family = PVE::Tools::get_host_address_family($node);
}
if ($conf->{vga} && ($conf->{vga} =~ m/^serial\d+$/)) {
- die "Websocket mode is not supported in vga serial mode!" if $websocket;
- my $termcmd = [ '/usr/sbin/qm', 'terminal', $vmid, '-iface', $conf->{vga} ];
- #my $termcmd = "/usr/bin/qm terminal -iface $conf->{vga}";
+ my $termcmd = [ '/usr/sbin/qm', 'terminal', $vmid, '-iface', $conf->{vga}, '-escape', '0' ];
+
$cmd = ['/usr/bin/vncterm', '-rfbport', $port,
'-timeout', $timeout, '-authpath', $authpath,
- '-perm', 'Sys.Console', '-c', @$remcmd, @$termcmd];
+ '-perm', 'Sys.Console'];
+
+ if ($param->{websocket}) {
+ $ENV{PVE_VNC_TICKET} = $ticket; # pass ticket to vncterm
+ push @$cmd, '-notls', '-listen', 'localhost';
+ }
+
+ push @$cmd, '-c', @$remcmd, @$termcmd;
+
PVE::Tools::run_command($cmd);
+
} else {
$ENV{LC_PVE_TICKET} = $ticket if $websocket; # set ticket with "qm vncproxy"
};
}});
+__PACKAGE__->register_method({
+ name => 'termproxy',
+ path => '{vmid}/termproxy',
+ method => 'POST',
+ protected => 1,
+ permissions => {
+ check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
+ },
+ description => "Creates a TCP proxy connections.",
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ node => get_standard_option('pve-node'),
+ vmid => get_standard_option('pve-vmid'),
+ serial=> {
+ optional => 1,
+ type => 'string',
+ enum => [qw(serial0 serial1 serial2 serial3)],
+ description => "opens a serial terminal (defaults to display)",
+ },
+ },
+ },
+ returns => {
+ additionalProperties => 0,
+ properties => {
+ user => { type => 'string' },
+ ticket => { type => 'string' },
+ port => { type => 'integer' },
+ upid => { type => 'string' },
+ },
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $rpcenv = PVE::RPCEnvironment::get();
+
+ my $authuser = $rpcenv->get_user();
+
+ my $vmid = $param->{vmid};
+ my $node = $param->{node};
+ my $serial = $param->{serial};
+
+ my $conf = PVE::QemuConfig->load_config($vmid, $node); # check if VM exists
+
+ if (!defined($serial)) {
+ if ($conf->{vga} && $conf->{vga} =~ m/^serial\d+$/) {
+ $serial = $conf->{vga};
+ }
+ }
+
+ my $authpath = "/vms/$vmid";
+
+ my $ticket = PVE::AccessControl::assemble_vnc_ticket($authuser, $authpath);
+
+ my ($remip, $family);
+
+ if ($node ne 'localhost' && $node ne PVE::INotify::nodename()) {
+ ($remip, $family) = PVE::Cluster::remote_node_ip($node);
+ } else {
+ $family = PVE::Tools::get_host_address_family($node);
+ }
+
+ my $port = PVE::Tools::next_vnc_port($family);
+
+ my $remcmd = $remip ?
+ ['/usr/bin/ssh', '-e', 'none', '-t', $remip, '--'] : [];
+
+ my $termcmd = [ '/usr/sbin/qm', 'terminal', $vmid, '-escape', '0'];
+ push @$termcmd, '-iface', $serial if $serial;
+
+ my $realcmd = sub {
+ my $upid = shift;
+
+ syslog('info', "starting qemu termproxy $upid\n");
+
+ my $cmd = ['/usr/bin/termproxy', $port, '--path', $authpath,
+ '--perm', 'VM.Console', '--'];
+ push @$cmd, @$remcmd, @$termcmd;
+
+ PVE::Tools::run_command($cmd);
+ };
+
+ my $upid = $rpcenv->fork_worker('vncproxy', $vmid, $authuser, $realcmd, 1);
+
+ PVE::Tools::wait_for_vnc_port($port);
+
+ return {
+ user => $authuser,
+ ticket => $ticket,
+ port => $port,
+ upid => $upid,
+ };
+ }});
+
__PACKAGE__->register_method({
name => 'vncwebsocket',
path => '{vmid}/vncwebsocket',
$status->{spice} = 1 if PVE::QemuServer::vga_conf_has_spice($conf->{vga});
+ $status->{agent} = 1 if $conf->{agent};
+
return $status;
}});
# read spice ticket from STDIN
my $spice_ticket;
if ($stateuri && ($stateuri eq 'tcp') && $migratedfrom && ($rpcenv->{type} eq 'cli')) {
- if (defined(my $line = <>)) {
+ if (defined(my $line = <STDIN>)) {
chomp $line;
$spice_ticket = $line;
}
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;
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);
my $newvollist = [];
eval {
- local $SIG{INT} = $SIG{TERM} = $SIG{QUIT} = $SIG{HUP} = sub { die "interrupted by signal\n"; };
+ local $SIG{INT} =
+ local $SIG{TERM} =
+ local $SIG{QUIT} =
+ local $SIG{HUP} = sub { die "interrupted by signal\n"; };
warn "moving disk with snapshots, snapshots will not be moved!\n"
if $snapshotted;
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',
proxyto => 'node',
description => "Get snapshot configuration",
permissions => {
- check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
+ check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot', 'VM.Snapshot.Rollback' ], any => 1],
},
parameters => {
additionalProperties => 0,
proxyto => 'node',
description => "Rollback VM state to specified snapshot.",
permissions => {
- check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]],
+ check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot', 'VM.Snapshot.Rollback' ], any => 1],
},
parameters => {
additionalProperties => 0,