pve-manager/PVE/API2/OpenVZ.pm
2013-12-10 08:36:58 +01:00

1560 lines
38 KiB
Perl

package PVE::API2::OpenVZ;
use strict;
use warnings;
use File::Basename;
use File::Path;
use POSIX qw (LONG_MAX);
use PVE::SafeSyslog;
use PVE::Tools qw(extract_param run_command);
use PVE::Exception qw(raise raise_param_exc);
use PVE::INotify;
use PVE::Cluster qw(cfs_lock_file cfs_read_file cfs_write_file);
use PVE::AccessControl;
use PVE::Storage;
use PVE::RESTHandler;
use PVE::RPCEnvironment;
use PVE::OpenVZ;
use PVE::OpenVZMigrate;
use PVE::JSONSchema qw(get_standard_option);
use base qw(PVE::RESTHandler);
use Data::Dumper; # fixme: remove
my $pve_base_ovz_config = <<__EOD;
ONBOOT="no"
PHYSPAGES="0:256M"
SWAPPAGES="0:256M"
KMEMSIZE="116M:128M"
DCACHESIZE="58M:64M"
LOCKEDPAGES="128M"
PRIVVMPAGES="unlimited"
SHMPAGES="unlimited"
NUMPROC="unlimited"
VMGUARPAGES="0:unlimited"
OOMGUARPAGES="0:unlimited"
NUMTCPSOCK="unlimited"
NUMFLOCK="unlimited"
NUMPTY="unlimited"
NUMSIGINFO="unlimited"
TCPSNDBUF="unlimited"
TCPRCVBUF="unlimited"
OTHERSOCKBUF="unlimited"
DGRAMRCVBUF="unlimited"
NUMOTHERSOCK="unlimited"
NUMFILE="unlimited"
NUMIPTENT="unlimited"
# Disk quota parameters (in form of softlimit:hardlimit)
DISKSPACE="unlimited:unlimited"
DISKINODES="unlimited:unlimited"
QUOTATIME="0"
QUOTAUGIDLIMIT="0"
# CPU fair scheduler parameter
CPUUNITS="1000"
CPUS="1"
__EOD
my $get_container_storage = sub {
my ($stcfg, $vmid, $veconf) = @_;
my $path = PVE::OpenVZ::get_privatedir($veconf, $vmid);
my ($vtype, $volid) = PVE::Storage::path_to_volume_id($stcfg, $path);
my ($sid, $volname) = PVE::Storage::parse_volume_id($volid, 1) if $volid;
return wantarray ? ($sid, $volname, $path) : $sid;
};
my $check_ct_modify_config_perm = sub {
my ($rpcenv, $authuser, $vmid, $pool, $key_list) = @_;
return 1 if $authuser ne 'root@pam';
foreach my $opt (@$key_list) {
if ($opt eq 'cpus' || $opt eq 'cpuunits' || $opt eq 'cpulimit') {
$rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.CPU']);
} elsif ($opt eq 'disk' || $opt eq 'quotatime' || $opt eq 'quotaugidlimit') {
$rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Disk']);
} elsif ($opt eq 'memory' || $opt eq 'swap') {
$rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Memory']);
} elsif ($opt eq 'netif' || $opt eq 'ip_address' || $opt eq 'nameserver' ||
$opt eq 'searchdomain' || $opt eq 'hostname') {
$rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Network']);
} else {
$rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Options']);
}
}
return 1;
};
__PACKAGE__->register_method({
name => 'vmlist',
path => '',
method => 'GET',
description => "OpenVZ container index (per node).",
permissions => {
description => "Only list VMs where you have VM.Audit permissons on /vms/<vmid>.",
user => 'all',
},
proxyto => 'node',
protected => 1, # openvz proc files are only readable by root
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {},
},
links => [ { rel => 'child', href => "{vmid}" } ],
},
code => sub {
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $vmstatus = PVE::OpenVZ::vmstatus();
my $res = [];
foreach my $vmid (keys %$vmstatus) {
next if !$rpcenv->check($authuser, "/vms/$vmid", [ 'VM.Audit' ], 1);
my $data = $vmstatus->{$vmid};
$data->{vmid} = $vmid;
push @$res, $data;
}
return $res;
}});
my $restore_openvz = sub {
my ($private, $archive, $vmid, $force) = @_;
my $vzconf = PVE::OpenVZ::read_global_vz_config();
my $conffile = PVE::OpenVZ::config_file($vmid);
my $cfgdir = dirname($conffile);
my $root = $vzconf->{rootdir};
$root =~ s/\$VEID/$vmid/;
print "you choose to force overwriting VPS config file, private and root directories.\n" if $force;
die "unable to create CT $vmid - container already exists\n"
if !$force && -f $conffile;
die "unable to create CT $vmid - directory '$private' already exists\n"
if !$force && -d $private;
die "unable to create CT $vmid - directory '$root' already exists\n"
if !$force && -d $root;
my $conf;
eval {
if ($force && -f $conffile) {
my $conf = PVE::OpenVZ::load_config($vmid);
my $oldprivate = PVE::OpenVZ::get_privatedir($conf, $vmid);
rmtree $oldprivate if -d $oldprivate;
my $oldroot = $conf->{ve_root} ? $conf->{ve_root}->{value} : $root;
rmtree $oldroot if -d $oldroot;
};
mkpath $private || die "unable to create private dir '$private'";
mkpath $root || die "unable to create root dir '$root'";
my $cmd = ['tar', 'xpf', $archive, '--totals', '--sparse', '-C', $private];
if ($archive eq '-') {
print "extracting archive from STDIN\n";
run_command($cmd, input => "<&STDIN");
} else {
print "extracting archive '$archive'\n";
run_command($cmd);
}
my $backup_cfg = "$private/etc/vzdump/vps.conf";
if (-f $backup_cfg) {
print "restore configuration to '$conffile'\n";
my $conf = PVE::Tools::file_get_contents($backup_cfg);
$conf =~ s/VE_ROOT=.*/VE_ROOT=\"$root\"/;
$conf =~ s/VE_PRIVATE=.*/VE_PRIVATE=\"$private\"/;
$conf =~ s/host_ifname=veth[0-9]+\./host_ifname=veth${vmid}\./g;
PVE::Tools::file_set_contents($conffile, $conf);
foreach my $s (PVE::OpenVZ::SCRIPT_EXT) {
my $tfn = "$cfgdir/${vmid}.$s";
my $sfn = "$private/etc/vzdump/vps.$s";
if (-f $sfn) {
my $sc = PVE::Tools::file_get_contents($sfn);
PVE::Tools::file_set_contents($tfn, $sc);
}
}
}
rmtree "$private/etc/vzdump";
};
my $err = $@;
if ($err) {
rmtree $private;
rmtree $root;
unlink $conffile;
foreach my $s (PVE::OpenVZ::SCRIPT_EXT) {
unlink "$cfgdir/${vmid}.$s";
}
die $err;
}
return $conf;
};
# create_vm is also used by vzrestore
__PACKAGE__->register_method({
name => 'create_vm',
path => '',
method => 'POST',
description => "Create or restore a container.",
permissions => {
user => 'all', # check inside
description => "You need 'VM.Allocate' permissions on /vms/{vmid} or on the VM pool /pool/{pool}. " .
"For restore, it is enough if the user has 'VM.Backup' permission and the VM already exists. " .
"You also need 'Datastore.AllocateSpace' permissions on the storage.",
},
protected => 1,
proxyto => 'node',
parameters => {
additionalProperties => 0,
properties => PVE::OpenVZ::json_config_properties({
node => get_standard_option('pve-node'),
vmid => get_standard_option('pve-vmid'),
ostemplate => {
description => "The OS template or backup file.",
type => 'string',
maxLength => 255,
},
password => {
optional => 1,
type => 'string',
description => "Sets root password inside container.",
},
storage => get_standard_option('pve-storage-id', {
description => "Target storage.",
default => 'local',
optional => 1,
}),
force => {
optional => 1,
type => 'boolean',
description => "Allow to overwrite existing container.",
},
restore => {
optional => 1,
type => 'boolean',
description => "Mark this as restore task.",
},
pool => {
optional => 1,
type => 'string', format => 'pve-poolid',
description => "Add the VM to the specified pool.",
},
}),
},
returns => {
type => 'string',
},
code => sub {
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $node = extract_param($param, 'node');
my $vmid = extract_param($param, 'vmid');
my $password = extract_param($param, 'password');
my $storage = extract_param($param, 'storage') || 'local';
my $pool = extract_param($param, 'pool');
my $storage_cfg = cfs_read_file("storage.cfg");
my $scfg = PVE::Storage::storage_check_node($storage_cfg, $storage, $node);
raise_param_exc({ storage => "storage '$storage' does not support openvz root directories"})
if !$scfg->{content}->{rootdir};
my $private = PVE::Storage::get_private_dir($storage_cfg, $storage, $vmid);
my $basecfg_fn = PVE::OpenVZ::config_file($vmid);
if (defined($pool)) {
$rpcenv->check_pool_exist($pool);
$rpcenv->check_perm_modify($authuser, "/pool/$pool");
}
$rpcenv->check($authuser, "/storage/$storage", ['Datastore.AllocateSpace']);
if ($rpcenv->check($authuser, "/vms/$vmid", ['VM.Allocate'], 1)) {
# OK
} elsif ($pool && $rpcenv->check($authuser, "/pool/$pool", ['VM.Allocate'], 1)) {
# OK
} elsif ($param->{restore} && $param->{force} && (-f $basecfg_fn) &&
$rpcenv->check($authuser, "/vms/$vmid", ['VM.Backup'], 1)) {
# OK: user has VM.Backup permissions, and want to restore an existing VM
} else {
raise_perm_exc();
}
&$check_ct_modify_config_perm($rpcenv, $authuser, $vmid, $pool, [ keys %$param]);
PVE::Storage::activate_storage($storage_cfg, $storage);
my $conf = PVE::OpenVZ::parse_ovz_config("/tmp/openvz/$vmid.conf", $pve_base_ovz_config);
my $ostemplate = extract_param($param, 'ostemplate');
my $archive;
if ($ostemplate eq '-') {
die "pipe requires cli environment\n"
if $rpcenv->{type} ne 'cli';
die "pipe can only be used with restore tasks\n"
if !$param->{restore};
$archive = '-';
} else {
$rpcenv->check_volume_access($authuser, $storage_cfg, $vmid, $ostemplate);
$archive = PVE::Storage::abs_filesystem_path($storage_cfg, $ostemplate);
}
if (!defined($param->{searchdomain}) &&
!defined($param->{nameserver})) {
my $resolv = PVE::INotify::read_file('resolvconf');
$param->{searchdomain} = $resolv->{search} if $resolv->{search};
my @ns = ();
push @ns, $resolv->{dns1} if $resolv->{dns1};
push @ns, $resolv->{dns2} if $resolv->{dns2};
push @ns, $resolv->{dns3} if $resolv->{dns3};
$param->{nameserver} = join(' ', @ns) if scalar(@ns);
}
# try to append domain to hostmane
if ($param->{hostname} && $param->{hostname} !~ m/\./ &&
$param->{searchdomain}) {
$param->{hostname} .= ".$param->{searchdomain}";
}
my $check_vmid_usage = sub {
if ($param->{force}) {
die "cant overwrite mounted container\n"
if PVE::OpenVZ::check_mounted($conf, $vmid);
} else {
die "CT $vmid already exists\n" if -f $basecfg_fn;
}
};
my $code = sub {
&$check_vmid_usage(); # final check after locking
PVE::OpenVZ::update_ovz_config($vmid, $conf, $param);
my $rawconf = PVE::OpenVZ::generate_raw_config($pve_base_ovz_config, $conf);
PVE::Cluster::check_cfs_quorum();
if ($param->{restore}) {
&$restore_openvz($private, $archive, $vmid, $param->{force});
# is this really needed?
my $cmd = ['vzctl', '--skiplock', '--quiet', 'set', $vmid,
'--applyconfig_map', 'name', '--save'];
run_command($cmd);
# reload config
$conf = PVE::OpenVZ::load_config($vmid);
# and initialize quota
my $disk_quota = $conf->{disk_quota}->{value};
if (!defined($disk_quota) || ($disk_quota != 0)) {
$cmd = ['vzctl', '--skiplock', 'quotainit', $vmid];
run_command($cmd);
}
} else {
PVE::Tools::file_set_contents($basecfg_fn, $rawconf);
my $cmd = ['vzctl', '--skiplock', 'create', $vmid,
'--ostemplate', $archive, '--private', $private];
run_command($cmd);
# hack: vzctl '--userpasswd' starts the CT, but we want
# to avoid that for create
PVE::OpenVZ::set_rootpasswd($private, $password)
if defined($password);
}
PVE::AccessControl::add_vm_to_pool($vmid, $pool) if $pool;
};
my $realcmd = sub { PVE::OpenVZ::lock_container($vmid, 1, $code); };
&$check_vmid_usage(); # first check before locking
return $rpcenv->fork_worker($param->{restore} ? 'vzrestore' : 'vzcreate',
$vmid, $authuser, $realcmd);
}});
my $vm_config_perm_list = [
'VM.Config.Disk',
'VM.Config.CPU',
'VM.Config.Memory',
'VM.Config.Network',
'VM.Config.Options',
];
__PACKAGE__->register_method({
name => 'update_vm',
path => '{vmid}/config',
method => 'PUT',
protected => 1,
proxyto => 'node',
description => "Set virtual machine options.",
permissions => {
check => ['perm', '/vms/{vmid}', $vm_config_perm_list, any => 1],
},
parameters => {
additionalProperties => 0,
properties => PVE::OpenVZ::json_config_properties(
{
node => get_standard_option('pve-node'),
vmid => get_standard_option('pve-vmid'),
digest => {
type => 'string',
description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
maxLength => 40,
optional => 1,
}
}),
},
returns => { type => 'null'},
code => sub {
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $node = extract_param($param, 'node');
my $vmid = extract_param($param, 'vmid');
my $digest = extract_param($param, 'digest');
die "no options specified\n" if !scalar(keys %$param);
&$check_ct_modify_config_perm($rpcenv, $authuser, $vmid, undef, [keys %$param]);
my $code = sub {
my $conf = PVE::OpenVZ::load_config($vmid);
die "checksum missmatch (file change by other user?)\n"
if $digest && $digest ne $conf->{digest};
my $changes = PVE::OpenVZ::update_ovz_config($vmid, $conf, $param);
return if scalar (@$changes) <= 0;
my $cmd = ['vzctl', '--skiplock', 'set', $vmid, @$changes, '--save'];
PVE::Cluster::log_msg('info', $authuser, "update CT $vmid: " . join(' ', @$changes));
run_command($cmd);
};
PVE::OpenVZ::lock_container($vmid, undef, $code);
return undef;
}});
__PACKAGE__->register_method({
name => 'vmdiridx',
path => '{vmid}',
method => 'GET',
proxyto => 'node',
description => "Directory index",
permissions => {
user => 'all',
},
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
vmid => get_standard_option('pve-vmid'),
},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {
subdir => { type => 'string' },
},
},
links => [ { rel => 'child', href => "{subdir}" } ],
},
code => sub {
my ($param) = @_;
# test if VM exists
my $conf = PVE::OpenVZ::load_config($param->{vmid});
my $res = [
{ subdir => 'config' },
{ subdir => 'status' },
{ subdir => 'vncproxy' },
{ subdir => 'spiceproxy' },
{ subdir => 'migrate' },
{ subdir => 'initlog' },
{ subdir => 'rrd' },
{ subdir => 'rrddata' },
];
return $res;
}});
__PACKAGE__->register_method({
name => 'rrd',
path => '{vmid}/rrd',
method => 'GET',
protected => 1, # fixme: can we avoid that?
permissions => {
check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
},
description => "Read VM RRD statistics (returns PNG)",
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
vmid => get_standard_option('pve-vmid'),
timeframe => {
description => "Specify the time frame you are interested in.",
type => 'string',
enum => [ 'hour', 'day', 'week', 'month', 'year' ],
},
ds => {
description => "The list of datasources you want to display.",
type => 'string', format => 'pve-configid-list',
},
cf => {
description => "The RRD consolidation function",
type => 'string',
enum => [ 'AVERAGE', 'MAX' ],
optional => 1,
},
},
},
returns => {
type => "object",
properties => {
filename => { type => 'string' },
},
},
code => sub {
my ($param) = @_;
return PVE::Cluster::create_rrd_graph(
"pve2-vm/$param->{vmid}", $param->{timeframe},
$param->{ds}, $param->{cf});
}});
__PACKAGE__->register_method({
name => 'rrddata',
path => '{vmid}/rrddata',
method => 'GET',
protected => 1, # fixme: can we avoid that?
permissions => {
check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
},
description => "Read VM RRD statistics",
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
vmid => get_standard_option('pve-vmid'),
timeframe => {
description => "Specify the time frame you are interested in.",
type => 'string',
enum => [ 'hour', 'day', 'week', 'month', 'year' ],
},
cf => {
description => "The RRD consolidation function",
type => 'string',
enum => [ 'AVERAGE', 'MAX' ],
optional => 1,
},
},
},
returns => {
type => "array",
items => {
type => "object",
properties => {},
},
},
code => sub {
my ($param) = @_;
return PVE::Cluster::create_rrd_data(
"pve2-vm/$param->{vmid}", $param->{timeframe}, $param->{cf});
}});
__PACKAGE__->register_method({
name => 'initlog',
path => '{vmid}/initlog',
method => 'GET',
protected => 1,
proxyto => 'node',
permissions => {
check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
},
description => "Read init log.",
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
vmid => get_standard_option('pve-vmid'),
start => {
type => 'integer',
minimum => 0,
optional => 1,
},
limit => {
type => 'integer',
minimum => 0,
optional => 1,
},
},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {
n => {
description=> "Line number",
type=> 'integer',
},
t => {
description=> "Line text",
type => 'string',
}
}
}
},
code => sub {
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $vmid = $param->{vmid};
my $conf = PVE::OpenVZ::load_config($vmid);
my $privatedir = PVE::OpenVZ::get_privatedir($conf, $vmid);
my $logfn = "$privatedir/var/log/init.log";
my ($count, $lines) = PVE::Tools::dump_logfile($logfn, $param->{start}, $param->{limit});
$rpcenv->set_result_attrib('total', $count);
return $lines;
}});
__PACKAGE__->register_method({
name => 'vm_config',
path => '{vmid}/config',
method => 'GET',
proxyto => 'node',
description => "Get container configuration.",
permissions => {
check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
},
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
vmid => get_standard_option('pve-vmid'),
},
},
returns => {
type => "object",
properties => {
digest => {
type => 'string',
description => 'SHA1 digest of configuration file. This can be used to prevent concurrent modifications.',
}
},
},
code => sub {
my ($param) = @_;
my $veconf = PVE::OpenVZ::load_config($param->{vmid});
# we only return selected/converted values
my $conf = { digest => $veconf->{digest} };
if ($veconf->{ostemplate} && $veconf->{ostemplate}->{value}) {
$conf->{ostemplate} = $veconf->{ostemplate}->{value};
}
my $stcfg = cfs_read_file("storage.cfg");
my ($sid, undef, $path) = &$get_container_storage($stcfg, $param->{vmid}, $veconf);
$conf->{storage} = $sid || $path;
my $properties = PVE::OpenVZ::json_config_properties();
foreach my $k (keys %$properties) {
next if $k eq 'memory';
next if $k eq 'swap';
next if $k eq 'disk';
next if !$veconf->{$k};
next if !defined($veconf->{$k}->{value});
if ($k eq 'description') {
$conf->{$k} = PVE::Tools::decode_text($veconf->{$k}->{value});
} else {
$conf->{$k} = $veconf->{$k}->{value};
}
}
($conf->{memory}, $conf->{swap}) = PVE::OpenVZ::ovz_config_extract_mem_swap($veconf, 1024*1024);
my $diskspace = $veconf->{diskspace}->{bar} || LONG_MAX;
if ($diskspace == LONG_MAX) {
$conf->{disk} = 0;
} else {
$conf->{disk} = $diskspace/(1024*1024);
}
return $conf;
}});
__PACKAGE__->register_method({
name => 'destroy_vm',
path => '{vmid}',
method => 'DELETE',
protected => 1,
proxyto => 'node',
description => "Destroy the container (also delete all uses files).",
permissions => {
check => [ 'perm', '/vms/{vmid}', ['VM.Allocate']],
},
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
vmid => get_standard_option('pve-vmid'),
},
},
returns => {
type => 'string',
},
code => sub {
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $vmid = $param->{vmid};
# test if VM exists
my $conf = PVE::OpenVZ::load_config($param->{vmid});
my $realcmd = sub {
my $cmd = ['vzctl', 'destroy', $vmid ];
run_command($cmd);
PVE::AccessControl::remove_vm_from_pool($vmid);
};
return $rpcenv->fork_worker('vzdestroy', $vmid, $authuser, $realcmd);
}});
my $sslcert;
__PACKAGE__->register_method ({
name => 'vncproxy',
path => '{vmid}/vncproxy',
method => 'POST',
protected => 1,
permissions => {
check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
},
description => "Creates a TCP VNC proxy connections.",
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
vmid => get_standard_option('pve-vmid'),
},
},
returns => {
additionalProperties => 0,
properties => {
user => { type => 'string' },
ticket => { type => 'string' },
cert => { 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 $authpath = "/vms/$vmid";
my $ticket = PVE::AccessControl::assemble_vnc_ticket($authuser, $authpath);
$sslcert = PVE::Tools::file_get_contents("/etc/pve/pve-root-ca.pem", 8192)
if !$sslcert;
my $port = PVE::Tools::next_vnc_port();
my $remip;
if ($node ne PVE::INotify::nodename()) {
$remip = PVE::Cluster::remote_node_ip($node);
}
# NOTE: vncterm VNC traffic is already TLS encrypted,
# so we select the fastest chipher here (or 'none'?)
my $remcmd = $remip ?
['/usr/bin/ssh', '-t', $remip] : [];
my $shcmd = [ '/usr/bin/dtach', '-A',
"/var/run/dtach/vzctlconsole$vmid",
'-r', 'winch', '-z',
'/usr/sbin/vzctl', 'console', $vmid ];
my $realcmd = sub {
my $upid = shift;
syslog ('info', "starting openvz vnc proxy $upid\n");
my $timeout = 10;
my $cmd = ['/usr/bin/vncterm', '-rfbport', $port,
'-timeout', $timeout, '-authpath', $authpath,
'-perm', 'VM.Console', '-c', @$remcmd, @$shcmd];
run_command($cmd);
return;
};
my $upid = $rpcenv->fork_worker('vncproxy', $vmid, $authuser, $realcmd);
PVE::Tools::wait_for_vnc_port($port);
return {
user => $authuser,
ticket => $ticket,
port => $port,
upid => $upid,
cert => $sslcert,
};
}});
__PACKAGE__->register_method ({
name => 'spiceproxy',
path => '{vmid}/spiceproxy',
method => 'POST',
protected => 1,
proxyto => 'node',
permissions => {
check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
},
description => "Returns a SPICE configuration to connect to the CT.",
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
vmid => get_standard_option('pve-vmid'),
proxy => {
description => "This can be used by the client to specify the proxy server. All nodes in a cluster runs 'spiceproxy', so it is up to the client to choose one. By default, we return the node where the VM is currently running. As resonable setting is to use same node you use to connect to the API (This is window.location.hostname for the JS GUI).",
type => 'string', format => 'dns-name',
optional => 1,
},
},
},
returns => {
description => "Returned values can be directly passed to the 'remote-viewer' application.",
additionalProperties => 1,
properties => {
type => { type => 'string' },
password => { type => 'string' },
proxy => { type => 'string' },
host => { type => 'string' },
'tls-port' => { type => 'integer' },
},
},
code => sub {
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $vmid = $param->{vmid};
my $node = $param->{node};
my $proxy = $param->{proxy};
if (!$proxy) {
my $host = `hostname -f` || PVE::INotify::nodename();
chomp $host;
$proxy = $host;
}
my $authpath = "/vms/$vmid";
my ($ticket, $proxyticket) = PVE::AccessControl::assemble_spice_ticket($authuser, $vmid, $node);
my $filename = "/etc/pve/local/pve-ssl.pem";
my $subject = PVE::QemuServer::read_x509_subject_spice($filename);
my $cacert = PVE::Tools::file_get_contents("/etc/pve/pve-root-ca.pem", 8192);
$cacert =~ s/\n/\\n/g;
my $port = PVE::Tools::next_spice_port();
my $remip;
my $timeout = 10;
my $cmd = ['/usr/bin/spiceterm', '--port', $port, '--addr', '127.0.0.1',
'--timeout', $timeout, '--authpath', $authpath,
'--permissions', 'VM.Console'];
my $dcconf = PVE::Cluster::cfs_read_file('datacenter.cfg');
push @$cmd, '--keymap', $dcconf->{keyboard} if $dcconf->{keyboard};
push @$cmd, '--',
'/usr/bin/dtach', '-A',
"/var/run/dtach/vzctlconsole$vmid",
'-r', 'winch', '-z',
'/usr/sbin/vzctl', 'console', $vmid;
my $realcmd = sub {
my $upid = shift;
syslog('info', "starting openvz vnc proxy $upid\n");
eval {
foreach my $k (keys %ENV) {
next if $k eq 'PATH' || $k eq 'TERM' || $k eq 'USER' || $k eq 'HOME';
delete $ENV{$k};
}
$ENV{PWD} = '/';
$ENV{SPICE_TICKET} = $ticket;
run_command($cmd, errmsg => "spiceterm failed");
};
if (my $err = $@) {
syslog ('err', $err);
}
return;
};
my $upid = $rpcenv->fork_worker('spiceproxy', $vmid, $authuser, $realcmd);
PVE::Tools::wait_for_vnc_port($port);
return {
type => 'spice',
title => "Shell on '$node'",
host => $proxyticket, # this break tls hostname verification, so we need to use 'host-subject'
proxy => "http://$proxy:3128",
'tls-port' => $port,
'host-subject' => $subject,
ca => $cacert,
password => $ticket,
'delete-this-file' => 1,
};
}});
__PACKAGE__->register_method({
name => 'vmcmdidx',
path => '{vmid}/status',
method => 'GET',
proxyto => 'node',
description => "Directory index",
permissions => {
user => 'all',
},
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
vmid => get_standard_option('pve-vmid'),
},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {
subdir => { type => 'string' },
},
},
links => [ { rel => 'child', href => "{subdir}" } ],
},
code => sub {
my ($param) = @_;
# test if VM exists
my $conf = PVE::OpenVZ::load_config($param->{vmid});
my $res = [
{ subdir => 'current' },
{ subdir => 'ubc' },
{ subdir => 'start' },
{ subdir => 'stop' },
];
return $res;
}});
my $vm_is_ha_managed = sub {
my ($vmid) = @_;
my $cc = PVE::Cluster::cfs_read_file('cluster.conf');
if (PVE::Cluster::cluster_conf_lookup_pvevm($cc, 0, $vmid, 1)) {
return 1;
}
return 0;
};
__PACKAGE__->register_method({
name => 'vm_status',
path => '{vmid}/status/current',
method => 'GET',
proxyto => 'node',
protected => 1, # openvz /proc entries are only readable by root
description => "Get virtual machine status.",
permissions => {
check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
},
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
vmid => get_standard_option('pve-vmid'),
},
},
returns => { type => 'object' },
code => sub {
my ($param) = @_;
# test if VM exists
my $conf = PVE::OpenVZ::load_config($param->{vmid});
my $vmstatus = PVE::OpenVZ::vmstatus($param->{vmid});
my $status = $vmstatus->{$param->{vmid}};
$status->{ha} = &$vm_is_ha_managed($param->{vmid});
return $status;
}});
__PACKAGE__->register_method({
name => 'vm_user_beancounters',
path => '{vmid}/status/ubc',
method => 'GET',
proxyto => 'node',
protected => 1, # openvz /proc entries are only readable by root
description => "Get container user_beancounters.",
permissions => {
check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
},
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
vmid => get_standard_option('pve-vmid'),
},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {
id => { type => 'string' },
held => { type => 'number' },
maxheld => { type => 'number' },
bar => { type => 'number' },
lim => { type => 'number' },
failcnt => { type => 'number' },
},
},
},
code => sub {
my ($param) = @_;
# test if VM exists
my $conf = PVE::OpenVZ::load_config($param->{vmid});
my $ubchash = PVE::OpenVZ::read_user_beancounters();
my $ubc = $ubchash->{$param->{vmid}} || {};
delete $ubc->{failcntsum};
return PVE::RESTHandler::hash_to_array($ubc, 'id');
}});
__PACKAGE__->register_method({
name => 'vm_start',
path => '{vmid}/status/start',
method => 'POST',
protected => 1,
proxyto => 'node',
description => "Start the container.",
permissions => {
check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
},
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
vmid => get_standard_option('pve-vmid'),
},
},
returns => {
type => 'string',
},
code => sub {
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $node = extract_param($param, 'node');
my $vmid = extract_param($param, 'vmid');
die "CT $vmid already running\n" if PVE::OpenVZ::check_running($vmid);
if (&$vm_is_ha_managed($vmid) && $rpcenv->{type} ne 'ha') {
my $hacmd = sub {
my $upid = shift;
my $service = "pvevm:$vmid";
my $cmd = ['clusvcadm', '-e', $service, '-m', $node];
print "Executing HA start for CT $vmid\n";
PVE::Tools::run_command($cmd);
return;
};
return $rpcenv->fork_worker('hastart', $vmid, $authuser, $hacmd);
} else {
my $realcmd = sub {
my $upid = shift;
syslog('info', "starting CT $vmid: $upid\n");
my $veconf = PVE::OpenVZ::load_config($vmid);
my $stcfg = cfs_read_file("storage.cfg");
if (my $sid = &$get_container_storage($stcfg, $vmid, $veconf)) {
PVE::Storage::activate_storage($stcfg, $sid);
}
my $vzconf = PVE::OpenVZ::read_global_vz_config();
# make sure mount point is there (see bug #276)
my $root = PVE::OpenVZ::get_rootdir($veconf, $vmid);
mkpath $root || die "unable to create root dir '$root'";
my $cmd = ['vzctl', 'start', $vmid];
run_command($cmd);
return;
};
return $rpcenv->fork_worker('vzstart', $vmid, $authuser, $realcmd);
}
}});
__PACKAGE__->register_method({
name => 'vm_stop',
path => '{vmid}/status/stop',
method => 'POST',
protected => 1,
proxyto => 'node',
description => "Stop the container.",
permissions => {
check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
},
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
vmid => get_standard_option('pve-vmid'),
},
},
returns => {
type => 'string',
},
code => sub {
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $node = extract_param($param, 'node');
my $vmid = extract_param($param, 'vmid');
die "CT $vmid not running\n" if !PVE::OpenVZ::check_running($vmid);
if (&$vm_is_ha_managed($vmid) && $rpcenv->{type} ne 'ha') {
my $hacmd = sub {
my $upid = shift;
my $service = "pvevm:$vmid";
my $cmd = ['clusvcadm', '-d', $service];
print "Executing HA stop for CT $vmid\n";
PVE::Tools::run_command($cmd);
return;
};
return $rpcenv->fork_worker('hastop', $vmid, $authuser, $hacmd);
} else {
my $realcmd = sub {
my $upid = shift;
syslog('info', "stoping CT $vmid: $upid\n");
my $cmd = ['vzctl', 'stop', $vmid, '--fast'];
run_command($cmd);
return;
};
return $rpcenv->fork_worker('vzstop', $vmid, $authuser, $realcmd);
}
}});
__PACKAGE__->register_method({
name => 'vm_mount',
path => '{vmid}/status/mount',
method => 'POST',
protected => 1,
proxyto => 'node',
description => "Mounts container private area.",
permissions => {
check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
},
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
vmid => get_standard_option('pve-vmid'),
},
},
returns => {
type => 'string',
},
code => sub {
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $node = extract_param($param, 'node');
my $vmid = extract_param($param, 'vmid');
die "CT $vmid is running\n" if PVE::OpenVZ::check_running($vmid);
my $realcmd = sub {
my $upid = shift;
syslog('info', "mount CT $vmid: $upid\n");
my $cmd = ['vzctl', 'mount', $vmid];
run_command($cmd);
return;
};
return $rpcenv->fork_worker('vzmount', $vmid, $authuser, $realcmd);
}});
__PACKAGE__->register_method({
name => 'vm_umount',
path => '{vmid}/status/umount',
method => 'POST',
protected => 1,
proxyto => 'node',
description => "Unmounts container private area.",
permissions => {
check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
},
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
vmid => get_standard_option('pve-vmid'),
},
},
returns => {
type => 'string',
},
code => sub {
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $node = extract_param($param, 'node');
my $vmid = extract_param($param, 'vmid');
die "CT $vmid is running\n" if PVE::OpenVZ::check_running($vmid);
my $realcmd = sub {
my $upid = shift;
syslog('info', "umount CT $vmid: $upid\n");
my $cmd = ['vzctl', 'umount', $vmid];
run_command($cmd);
return;
};
return $rpcenv->fork_worker('vzumount', $vmid, $authuser, $realcmd);
}});
__PACKAGE__->register_method({
name => 'vm_shutdown',
path => '{vmid}/status/shutdown',
method => 'POST',
protected => 1,
proxyto => 'node',
description => "Shutdown the container.",
permissions => {
check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]],
},
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
vmid => get_standard_option('pve-vmid'),
timeout => {
description => "Wait maximal timeout seconds.",
type => 'integer',
minimum => 0,
optional => 1,
default => 60,
},
forceStop => {
description => "Make sure the Container stops.",
type => 'boolean',
optional => 1,
default => 0,
}
},
},
returns => {
type => 'string',
},
code => sub {
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $node = extract_param($param, 'node');
my $vmid = extract_param($param, 'vmid');
my $timeout = extract_param($param, 'timeout');
die "CT $vmid not running\n" if !PVE::OpenVZ::check_running($vmid);
my $realcmd = sub {
my $upid = shift;
syslog('info', "shutdown CT $vmid: $upid\n");
my $cmd = ['vzctl', 'stop', $vmid];
$timeout = 60 if !defined($timeout);
eval { run_command($cmd, timeout => $timeout); };
my $err = $@;
return if !$err;
die $err if !$param->{forceStop};
warn "shutdown failed - forcing stop now\n";
push @$cmd, '--fast';
run_command($cmd);
return;
};
my $upid = $rpcenv->fork_worker('vzshutdown', $vmid, $authuser, $realcmd);
return $upid;
}});
__PACKAGE__->register_method({
name => 'migrate_vm',
path => '{vmid}/migrate',
method => 'POST',
protected => 1,
proxyto => 'node',
description => "Migrate the container to another node. Creates a new migration task.",
permissions => {
check => ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
},
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
vmid => get_standard_option('pve-vmid'),
target => get_standard_option('pve-node', { description => "Target node." }),
online => {
type => 'boolean',
description => "Use online/live migration.",
optional => 1,
},
},
},
returns => {
type => 'string',
description => "the task ID.",
},
code => sub {
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $target = extract_param($param, 'target');
my $localnode = PVE::INotify::nodename();
raise_param_exc({ target => "target is local node."}) if $target eq $localnode;
PVE::Cluster::check_cfs_quorum();
PVE::Cluster::check_node_exists($target);
my $targetip = PVE::Cluster::remote_node_ip($target);
my $vmid = extract_param($param, 'vmid');
# test if VM exists
PVE::OpenVZ::load_config($vmid);
# try to detect errors early
if (PVE::OpenVZ::check_running($vmid)) {
die "cant migrate running container without --online\n"
if !$param->{online};
}
if (&$vm_is_ha_managed($vmid) && $rpcenv->{type} ne 'ha') {
my $hacmd = sub {
my $upid = shift;
my $service = "pvevm:$vmid";
my $cmd = ['clusvcadm', '-M', $service, '-m', $target];
print "Executing HA migrate for CT $vmid to node $target\n";
PVE::Tools::run_command($cmd);
return;
};
return $rpcenv->fork_worker('hamigrate', $vmid, $authuser, $hacmd);
} else {
my $realcmd = sub {
my $upid = shift;
PVE::OpenVZMigrate->migrate($target, $targetip, $vmid, $param);
return;
};
return $rpcenv->fork_worker('vzmigrate', $vmid, $authuser, $realcmd);
}
}});
1;