]> git.proxmox.com Git - qemu-server.git/blobdiff - PVE/API2/Qemu.pm
Fix #2728: die/warn if target is not a replication target when live-migrating
[qemu-server.git] / PVE / API2 / Qemu.pm
index 867e245ab9a649922d195141a5009acb9034b26b..8da616a503a792264c3858bc52e2cce843407bf3 100644 (file)
@@ -7,6 +7,7 @@ use Net::SSLeay;
 use POSIX;
 use IO::Socket::IP;
 use URI::Escape;
+use Crypt::OpenSSL::Random;
 
 use PVE::Cluster qw (cfs_read_file cfs_write_file);;
 use PVE::RRD;
@@ -21,6 +22,7 @@ use PVE::GuestHelpers;
 use PVE::QemuConfig;
 use PVE::QemuServer;
 use PVE::QemuServer::Drive;
+use PVE::QemuServer::CPUConfig;
 use PVE::QemuServer::Monitor qw(mon_cmd);
 use PVE::QemuMigrate;
 use PVE::RPCEnvironment;
@@ -236,6 +238,26 @@ my $create_disks = sub {
     return $vollist;
 };
 
+my $check_cpu_model_access = sub {
+    my ($rpcenv, $authuser, $new, $existing) = @_;
+
+    return if !defined($new->{cpu});
+
+    my $cpu = PVE::JSONSchema::check_format('pve-vm-cpu-conf', $new->{cpu});
+    return if !$cpu || !$cpu->{cputype}; # always allow default
+    my $cputype = $cpu->{cputype};
+
+    if ($existing && $existing->{cpu}) {
+       # changing only other settings doesn't require permissions for CPU model
+       my $existingCpu = PVE::JSONSchema::check_format('pve-vm-cpu-conf', $existing->{cpu});
+       return if $existingCpu->{cputype} eq $cputype;
+    }
+
+    if (PVE::QemuServer::CPUConfig::is_custom_model($cputype)) {
+       $rpcenv->check($authuser, "/nodes", ['Sys.Audit']);
+    }
+};
+
 my $cpuoptions = {
     'cores' => 1,
     'cpu' => 1,
@@ -336,8 +358,10 @@ 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 ($cloudinitoptions->{$opt} || ($opt =~ m/^(?:net|ipconfig)\d+$/)) {
+       } elsif ($opt =~ m/^(?:net|ipconfig)\d+$/) {
            $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Network']);
+       } elsif ($cloudinitoptions->{$opt}) {
+           $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Cloudinit', 'VM.Config.Network'], 1);
        } elsif ($opt eq 'vmstate') {
            # the user needs Disk and PowerMgmt privileges to change the vmstate
            # also needs privileges on the storage, that will be checked later
@@ -543,6 +567,8 @@ __PACKAGE__->register_method({
 
            &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, $pool, [ keys %$param]);
 
+           &$check_cpu_model_access($rpcenv, $authuser, $param);
+
            foreach my $opt (keys %$param) {
                if (PVE::QemuServer::is_valid_drivename($opt)) {
                    my $drive = PVE::QemuServer::parse_drive($opt, $param->{$opt});
@@ -1072,7 +1098,10 @@ my $update_vm_api  = sub {
        return if PVE::QemuServer::drive_is_cdrom($drive);
 
        my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid, 1);
-       return if $volname eq 'cloudinit';
+       die "cannot add non-managed/pass-through volume to a replicated VM\n"
+           if !defined($storeid);
+
+       return if defined($volname) && $volname eq 'cloudinit';
 
        my $format;
        if ($volid =~ $NEW_DISK_RE) {
@@ -1122,6 +1151,8 @@ my $update_vm_api  = sub {
        die "checksum missmatch (file change by other user?)\n"
            if $digest && $digest ne $conf->{digest};
 
+       &$check_cpu_model_access($rpcenv, $authuser, $param, $conf);
+
        # FIXME: 'suspended' lock should probabyl be a state or "weak" lock?!
        if (scalar(@delete) && grep { $_ eq 'vmstate'} @delete) {
            if (defined($conf->{lock}) && $conf->{lock} eq 'suspended') {
@@ -1325,6 +1356,7 @@ my $vm_config_perm_list = [
            'VM.Config.Network',
            'VM.Config.HWType',
            'VM.Config.Options',
+           'VM.Config.Cloudinit',
     ];
 
 __PACKAGE__->register_method({
@@ -1561,6 +1593,23 @@ __PACKAGE__->register_method({
        return undef;
     }});
 
+# uses good entropy, each char is limited to 6 bit to get printable chars simply
+my $gen_rand_chars = sub {
+    my ($length) = @_;
+
+    die "invalid length $length" if $length < 1;
+
+    my $min = ord('!'); # first printable ascii
+
+    my $rand_bytes = Crypt::OpenSSL::Random::random_bytes($length);
+    die "failed to generate random bytes!\n"
+      if !$rand_bytes;
+
+    my $str = join('', map { chr((ord($_) & 0x3F) + $min) } split('', $rand_bytes));
+
+    return $str;
+};
+
 my $sslcert;
 
 __PACKAGE__->register_method({
@@ -1582,6 +1631,12 @@ __PACKAGE__->register_method({
                type => 'boolean',
                description => "starts websockify instead of vncproxy",
            },
+           'generate-password' => {
+               optional => 1,
+               type => 'boolean',
+               default => 0,
+               description => "Generates a random password to be used as ticket instead of the API ticket.",
+           },
        },
     },
     returns => {
@@ -1589,6 +1644,12 @@ __PACKAGE__->register_method({
        properties => {
            user => { type => 'string' },
            ticket => { type => 'string' },
+           password => {
+               optional => 1,
+               description => "Returned if requested with 'generate-password' param."
+                   ." Consists of printable ASCII characters ('!' .. '~').",
+               type => 'string',
+           },
            cert => { type => 'string' },
            port => { type => 'integer' },
            upid => { type => 'string' },
@@ -1606,11 +1667,20 @@ __PACKAGE__->register_method({
        my $websocket = $param->{websocket};
 
        my $conf = PVE::QemuConfig->load_config($vmid, $node); # check if VM exists
-       my $use_serial = ($conf->{vga} && ($conf->{vga} =~ m/^serial\d+$/));
+
+       my $serial;
+       if ($conf->{vga}) {
+           my $vga = PVE::QemuServer::parse_vga($conf->{vga});
+           $serial = $vga->{type} if $vga->{type} =~ m/^serial\d+$/;
+       }
 
        my $authpath = "/vms/$vmid";
 
        my $ticket = PVE::AccessControl::assemble_vnc_ticket($authuser, $authpath);
+       my $password = $ticket;
+       if ($param->{'generate-password'}) {
+           $password = $gen_rand_chars->(8);
+       }
 
        $sslcert = PVE::Tools::file_get_contents("/etc/pve/pve-root-ca.pem", 8192)
            if !$sslcert;
@@ -1622,7 +1692,7 @@ __PACKAGE__->register_method({
            (undef, $family) = PVE::Cluster::remote_node_ip($node);
            my $sshinfo = PVE::SSHInfo::get_ssh_info($node);
            # NOTE: kvm VNC traffic is already TLS encrypted or is known unsecure
-           $remcmd = PVE::SSHInfo::ssh_info_to_command($sshinfo, $use_serial ? '-t' : '-T');
+           $remcmd = PVE::SSHInfo::ssh_info_to_command($sshinfo, defined($serial) ? '-t' : '-T');
        } else {
            $family = PVE::Tools::get_host_address_family($node);
        }
@@ -1638,16 +1708,16 @@ __PACKAGE__->register_method({
 
            my $cmd;
 
-           if ($use_serial) {
+           if (defined($serial)) {
 
-               my $termcmd = [ '/usr/sbin/qm', 'terminal', $vmid, '-iface', $conf->{vga}, '-escape', '0' ];
+               my $termcmd = [ '/usr/sbin/qm', 'terminal', $vmid, '-iface', $serial, '-escape', '0' ];
 
                $cmd = ['/usr/bin/vncterm', '-rfbport', $port,
                        '-timeout', $timeout, '-authpath', $authpath,
                        '-perm', 'Sys.Console'];
 
                if ($param->{websocket}) {
-                   $ENV{PVE_VNC_TICKET} = $ticket; # pass ticket to vncterm
+                   $ENV{PVE_VNC_TICKET} = $password; # pass ticket to vncterm
                    push @$cmd, '-notls', '-listen', 'localhost';
                }
 
@@ -1657,7 +1727,7 @@ __PACKAGE__->register_method({
 
            } else {
 
-               $ENV{LC_PVE_TICKET} = $ticket if $websocket; # set ticket with "qm vncproxy"
+               $ENV{LC_PVE_TICKET} = $password if $websocket; # set ticket with "qm vncproxy"
 
                $cmd = [@$remcmd, "/usr/sbin/qm", 'vncproxy', $vmid];
 
@@ -1667,7 +1737,7 @@ __PACKAGE__->register_method({
                    LocalPort => $port,
                    Proto => 'tcp',
                    GetAddrInfoFlags => 0,
-                   ) or die "failed to create socket: $!\n";
+               ) or die "failed to create socket: $!\n";
                # Inside the worker we shouldn't have any previous alarms
                # running anyway...:
                alarm(0);
@@ -1692,13 +1762,16 @@ __PACKAGE__->register_method({
 
        PVE::Tools::wait_for_vnc_port($port);
 
-       return {
+       my $res = {
            user => $authuser,
            ticket => $ticket,
            port => $port,
            upid => $upid,
            cert => $sslcert,
        };
+       $res->{password} = $password if $param->{'generate-password'};
+
+       return $res;
     }});
 
 __PACKAGE__->register_method({
@@ -1746,8 +1819,9 @@ __PACKAGE__->register_method({
        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};
+           if ($conf->{vga}) {
+               my $vga = PVE::QemuServer::parse_vga($conf->{vga});
+               $serial = $vga->{type} if $vga->{type} =~ m/^serial\d+$/;
            }
        }
 
@@ -2858,9 +2932,6 @@ __PACKAGE__->register_method({
 
        my $running = PVE::QemuServer::check_running($vmid) || 0;
 
-       # exclusive lock if VM is running - else shared lock is enough;
-       my $shared_lock = $running ? 0 : 1;
-
        my $clonefn = sub {
            # do all tests after lock but before forking worker - if possible
 
@@ -3048,11 +3119,17 @@ __PACKAGE__->register_method({
            return $rpcenv->fork_worker('qmclone', $vmid, $authuser, $realcmd);
        };
 
-       return PVE::QemuConfig->lock_config_mode($vmid, 1, $shared_lock, sub {
-           # Aquire exclusive lock lock for $newid
+       # Aquire exclusive lock lock for $newid
+       my $lock_target_vm = sub {
            return PVE::QemuConfig->lock_config_full($newid, 1, $clonefn);
-       });
+       };
 
+       # exclusive lock if VM is running - else shared lock is enough;
+       if ($running) {
+           return PVE::QemuConfig->lock_config_full($vmid, 1, $lock_target_vm);
+       } else {
+           return PVE::QemuConfig->lock_config_shared($vmid, 1, $lock_target_vm);
+       }
     }});
 
 __PACKAGE__->register_method({
@@ -3458,6 +3535,19 @@ __PACKAGE__->register_method({
 
        if (PVE::QemuServer::check_running($vmid)) {
            die "can't migrate running VM without --online\n" if !$param->{online};
+
+           my $repl_conf = PVE::ReplicationConfig->new();
+           my $is_replicated = $repl_conf->check_for_existing_jobs($vmid, 1);
+           my $is_replicated_to_target = defined($repl_conf->find_local_replication_job($vmid, $target));
+           if ($is_replicated && !$is_replicated_to_target) {
+               if ($param->{force}) {
+                   warn "WARNING: Node '$target' is not a replication target. Existing replication " .
+                        "jobs will fail after migration!\n";
+               } else {
+                   die "Cannot live-migrate replicated VM to node '$target' - not a replication target." .
+                       " Use 'force' to override.\n";
+               }
+           }
        } else {
            warn "VM isn't running. Doing offline migration instead.\n" if $param->{online};
            $param->{online} = 0;
@@ -3692,8 +3782,7 @@ __PACKAGE__->register_method({
 
            PVE::QemuServer::qemu_block_resize($vmid, "drive-$disk", $storecfg, $volid, $newsize);
 
-           my $effective_size = eval { PVE::Storage::volume_size_info($storecfg, $volid, 3); };
-           $drive->{size} = $effective_size // $newsize;
+           $drive->{size} = $newsize;
            $conf->{$disk} = PVE::QemuServer::print_drive($drive);
 
            PVE::QemuConfig->write_config($vmid, $conf);