use strict;
use warnings;
-use POSIX;
+use POSIX qw(LONG_MAX);
use Filesys::Df;
use Time::Local qw(timegm_nocheck);
+use HTTP::Status qw(:constants);
use PVE::pvecfg;
use PVE::Tools;
+use PVE::API2Tools;
use PVE::ProcFSTools;
use PVE::SafeSyslog;
-use PVE::Cluster;
+use PVE::Cluster qw(cfs_read_file);
use PVE::INotify;
+use PVE::Exception qw(raise raise_perm_exc raise_param_exc);
use PVE::RESTHandler;
use PVE::RPCEnvironment;
use PVE::JSONSchema qw(get_standard_option);
use PVE::AccessControl;
-use PVE::OpenVZ;
+use PVE::Storage;
+use PVE::Firewall;
+use PVE::LXC;
+use PVE::APLInfo;
+use PVE::Report;
+use PVE::HA::Env::PVE2;
+use PVE::HA::Config;
+use PVE::QemuConfig;
+use PVE::QemuServer;
+use PVE::API2::Subscription;
use PVE::API2::Services;
use PVE::API2::Network;
use PVE::API2::Tasks;
use PVE::API2::Storage::Scan;
use PVE::API2::Storage::Status;
use PVE::API2::Qemu;
-use PVE::API2::OpenVZ;
+use PVE::API2::LXC;
+use PVE::API2::LXC::Status;
use PVE::API2::VZDump;
+use PVE::API2::APT;
+use PVE::API2::Ceph;
+use PVE::API2::Firewall::Host;
+use Digest::MD5;
+use Digest::SHA;
+use PVE::API2::Disks;
use JSON;
use base qw(PVE::RESTHandler);
});
__PACKAGE__->register_method ({
- subclass => "PVE::API2::OpenVZ",
- path => 'openvz',
+ subclass => "PVE::API2::LXC",
+ path => 'lxc',
+});
+
+__PACKAGE__->register_method ({
+ subclass => "PVE::API2::Ceph",
+ path => 'ceph',
});
__PACKAGE__->register_method ({
path => 'services',
});
+__PACKAGE__->register_method ({
+ subclass => "PVE::API2::Subscription",
+ path => 'subscription',
+});
+
__PACKAGE__->register_method ({
subclass => "PVE::API2::Network",
path => 'network',
path => 'storage',
});
+__PACKAGE__->register_method ({
+ subclass => "PVE::API2::Disks",
+ path => 'disks',
+});
+
+__PACKAGE__->register_method ({
+ subclass => "PVE::API2::APT",
+ path => 'apt',
+});
+
+__PACKAGE__->register_method ({
+ subclass => "PVE::API2::Firewall::Host",
+ path => 'firewall',
+});
+
__PACKAGE__->register_method ({
name => 'index',
path => '',
my ($param) = @_;
my $result = [
+ { name => 'ceph' },
+ { name => 'disks' },
+ { name => 'apt' },
{ name => 'version' },
{ name => 'syslog' },
{ name => 'status' },
+ { name => 'subscription' },
+ { name => 'report' },
{ name => 'tasks' },
{ name => 'rrd' }, # fixme: remove?
{ name => 'rrddata' },# fixme: remove?
{ name => 'vncshell' },
+ { name => 'spiceshell' },
{ name => 'time' },
{ name => 'dns' },
{ name => 'services' },
{ name => 'scan' },
{ name => 'storage' },
- { name => 'upload' },
{ name => 'qemu' },
- { name => 'openvz' },
+ { name => 'lxc' },
{ name => 'vzdump' },
- { name => 'ubcfailcnt' },
{ name => 'network' },
- { name => 'network_changes' },
+ { name => 'aplinfo' },
+ { name => 'startall' },
+ { name => 'stopall' },
+ { name => 'netstat' },
+ { name => 'firewall' },
];
return $result;
return PVE::pvecfg::version_info();
}});
-__PACKAGE__->register_method({
- name => 'beancounters_failcnt',
- path => 'ubcfailcnt',
- permissions => {
- path => '/nodes/{node}',
- privs => [ 'Sys.Audit' ],
- },
- method => 'GET',
- proxyto => 'node',
- protected => 1, # openvz /proc entries are only readable by root
- description => "Get user_beancounters failcnt for all active containers.",
- parameters => {
- additionalProperties => 0,
- properties => {
- node => get_standard_option('pve-node'),
- },
- },
- returns => {
- type => 'array',
- items => {
- type => "object",
- properties => {
- id => { type => 'string' },
- failcnt => { type => 'number' },
- },
- },
- },
- code => sub {
- my ($param) = @_;
-
- my $ubchash = PVE::OpenVZ::read_user_beancounters();
-
- my $res = [];
- foreach my $vmid (keys %$ubchash) {
- next if !$vmid;
- push @$res, { id => $vmid, failcnt => $ubchash->{$vmid}->{failcntsum} };
-
- }
- return $res;
- }});
-
-__PACKAGE__->register_method({
- name => 'network_changes',
- path => 'network_changes',
- method => 'GET',
- permissions => {
- path => '/nodes/{node}',
- privs => [ 'Sys.Audit' ],
- },
- description => "Get network configuration changes (diff) since last boot.",
- proxyto => 'node',
- parameters => {
- additionalProperties => 0,
- properties => {
- node => get_standard_option('pve-node'),
- },
- },
- returns => { type => "string" },
- code => sub {
- my ($param) = @_;
-
- my $res = PVE::INotify::read_file('interfaces', 1);
-
- return $res->{changes} || '';
- }});
-
-__PACKAGE__->register_method({
- name => 'revert_network_changes',
- path => 'network_changes',
- method => 'DELETE',
- permissions => {
- path => '/nodes/{node}',
- privs => [ 'Sys.Modify' ],
- },
- protected => 1,
- description => "Revert network configuration changes.",
- proxyto => 'node',
- parameters => {
- additionalProperties => 0,
- properties => {
- node => get_standard_option('pve-node'),
- },
- },
- returns => { type => "null" },
- code => sub {
- my ($param) = @_;
-
- unlink "/etc/network/interfaces.new";
-
- return undef;
- }});
-
__PACKAGE__->register_method({
name => 'status',
path => 'status',
method => 'GET',
permissions => {
- path => '/nodes/{node}',
- privs => [ 'Sys.Audit' ],
+ check => ['perm', '/nodes/{node}', [ 'Sys.Audit' ]],
},
description => "Read node status",
proxyto => 'node',
total => $meminfo->{memtotal},
used => $meminfo->{memused},
};
+
+ $res->{ksm} = {
+ shared => $meminfo->{memshared},
+ };
+
$res->{swap} = {
free => $meminfo->{swapfree},
total => $meminfo->{swaptotal},
return $res;
}});
+__PACKAGE__->register_method({
+ name => 'netstat',
+ path => 'netstat',
+ method => 'GET',
+ permissions => {
+ check => ['perm', '/nodes/{node}', [ 'Sys.Audit' ]],
+ },
+ description => "Read tap/vm network device interface counters",
+ proxyto => 'node',
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ node => get_standard_option('pve-node'),
+ },
+ },
+ returns => {
+ type => "array",
+ items => {
+ type => "object",
+ properties => {},
+ },
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $res = [ ];
+
+ my $netdev = PVE::ProcFSTools::read_proc_net_dev();
+ foreach my $dev (keys %$netdev) {
+ next if $dev !~ m/^(?:tap|veth)([1-9]\d*)i(\d+)$/;
+ my $vmid = $1;
+ my $netid = $2;
+
+ push(
+ @$res,
+ {
+ vmid => $vmid,
+ dev => "net$netid",
+ in => $netdev->{$dev}->{transmit},
+ out => $netdev->{$dev}->{receive},
+ }
+ );
+ }
+
+ return $res;
+ }});
+
+__PACKAGE__->register_method({
+ name => 'execute',
+ path => 'execute',
+ method => 'POST',
+ permissions => {
+ check => ['perm', '/nodes/{node}', [ 'Sys.Audit' ]],
+ },
+ description => "Execute multiple commands in order.",
+ proxyto => 'node',
+ protected => 1, # avoid problems with proxy code
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ node => get_standard_option('pve-node'),
+ commands => {
+ description => "JSON encoded array of commands.",
+ type => "string",
+ }
+ },
+ },
+ returns => {
+ type => 'array',
+ properties => {
+
+ },
+ },
+ code => sub {
+ my ($param) = @_;
+ my $res = [];
+
+ my $rpcenv = PVE::RPCEnvironment::get();
+ my $user = $rpcenv->get_user();
+
+ my $commands = eval { decode_json($param->{commands}); };
+
+ die "commands param did not contain valid JSON: $@" if $@;
+ die "commands is not an array" if ref($commands) ne "ARRAY";
+
+ foreach my $cmd (@$commands) {
+ eval {
+ die "$cmd is not a valid command" if (ref($cmd) ne "HASH" || !$cmd->{path} || !$cmd->{method});
+
+ $cmd->{args} //= {};
+
+ my $path = "nodes/$param->{node}/$cmd->{path}";
+
+ my $uri_param = {};
+ my ($handler, $info) = PVE::API2->find_handler($cmd->{method}, $path, $uri_param);
+ if (!$handler || !$info) {
+ die "no handler for '$path'\n";
+ }
+
+ foreach my $p (keys %{$cmd->{args}}) {
+ raise_param_exc({ $p => "duplicate parameter" }) if defined($uri_param->{$p});
+ $uri_param->{$p} = $cmd->{args}->{$p};
+ }
+
+ # check access permissions
+ $rpcenv->check_api2_permissions($info->{permissions}, $user, $uri_param);
+
+ push @$res, {
+ status => HTTP_OK,
+ data => $handler->handle($info, $uri_param),
+ };
+ };
+ if (my $err = $@) {
+ my $resp = { status => HTTP_INTERNAL_SERVER_ERROR };
+ if (ref($err) eq "PVE::Exception") {
+ $resp->{status} = $err->{code} if $err->{code};
+ $resp->{errors} = $err->{errors} if $err->{errors};
+ $resp->{message} = $err->{msg};
+ } else {
+ $resp->{message} = $err;
+ }
+ push @$res, $resp;
+ }
+ }
+
+ return $res;
+ }});
+
+
__PACKAGE__->register_method({
name => 'node_cmd',
path => 'status',
method => 'POST',
permissions => {
- path => '/nodes/{node}',
- privs => [ 'Sys.PowerMgmt' ],
+ check => ['perm', '/nodes/{node}', [ 'Sys.PowerMgmt' ]],
},
protected => 1,
description => "Reboot or shutdown a node.",
method => 'GET',
protected => 1, # fixme: can we avoid that?
permissions => {
- path => '/nodes/{node}',
- privs => [ 'Sys.Audit' ],
+ check => ['perm', '/nodes/{node}', [ 'Sys.Audit' ]],
},
description => "Read node RRD statistics (returns PNG)",
parameters => {
method => 'GET',
protected => 1, # fixme: can we avoid that?
permissions => {
- path => '/nodes/{node}',
- privs => [ 'Sys.Audit' ],
+ check => ['perm', '/nodes/{node}', [ 'Sys.Audit' ]],
},
description => "Read node RRD statistics",
parameters => {
description => "Read system log",
proxyto => 'node',
permissions => {
- path => '/nodes/{node}',
- privs => [ 'Sys.Syslog' ],
+ check => ['perm', '/nodes/{node}', [ 'Sys.Syslog' ]],
},
protected => 1,
parameters => {
minimum => 0,
optional => 1,
},
+ since => {
+ type=> 'string',
+ pattern => '^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}(:\d{2})?)?$',
+ description => "Display all log since this date-time string.",
+ optional => 1,
+ },
+ until => {
+ type=> 'string',
+ pattern => '^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}(:\d{2})?)?$',
+ description => "Display all log until this date-time string.",
+ optional => 1,
+ },
},
},
returns => {
my $user = $rpcenv->get_user();
my $node = $param->{node};
- my ($count, $lines) = PVE::Tools::dump_logfile("/var/log/syslog", $param->{start}, $param->{limit});
+ my ($count, $lines) = PVE::Tools::dump_journal($param->{start}, $param->{limit},
+ $param->{since}, $param->{until});
$rpcenv->set_result_attrib('total', $count);
-
- return $lines;
+
+ return $lines;
}});
my $sslcert;
method => 'POST',
protected => 1,
permissions => {
- path => '/nodes/{node}',
- privs => [ 'Sys.Console' ],
+ description => "Restricted to users on realm 'pam'",
+ check => ['perm', '/nodes/{node}', [ 'Sys.Console' ]],
},
description => "Creates a VNC Shell proxy.",
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
+ upgrade => {
+ type => 'boolean',
+ description => "Run 'apt-get dist-upgrade' instead of normal shell.",
+ optional => 1,
+ default => 0,
+ },
+ websocket => {
+ optional => 1,
+ type => 'boolean',
+ description => "use websocket instead of standard vnc.",
+ },
},
},
returns => {
my $rpcenv = PVE::RPCEnvironment::get();
- my $user = $rpcenv->get_user();
+ my ($user, undef, $realm) = PVE::AccessControl::verify_username($rpcenv->get_user());
+
+ raise_perm_exc("realm != pam") if $realm ne 'pam';
- my $ticket = PVE::AccessControl::assemble_ticket($user);
+ raise_perm_exc('user != root@pam') if $param->{upgrade} && $user ne 'root@pam';
my $node = $param->{node};
+ my $authpath = "/nodes/$node";
+
+ my $ticket = PVE::AccessControl::assemble_vnc_ticket($user, $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, $family);
- my $remip;
-
if ($node ne PVE::INotify::nodename()) {
- $remip = PVE::Cluster::remote_node_ip($node);
+ ($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);
+
# NOTE: vncterm VNC traffic is already TLS encrypted,
# so we select the fastest chipher here (or 'none'?)
my $remcmd = $remip ?
- ['/usr/bin/ssh', '-c', 'blowfish-cbc', '-t', $remip] : [];
+ ['/usr/bin/ssh', '-t', $remip] : [];
- my $shcmd = $user eq 'root@pam' ? [ "/bin/bash", "-l" ] : [ "/bin/login" ];
+ my $shcmd;
+
+ if ($user eq 'root@pam') {
+ if ($param->{upgrade}) {
+ my $upgradecmd = "pveupgrade --shell";
+ $upgradecmd = PVE::Tools::shellquote($upgradecmd) if $remip;
+ $shcmd = [ '/bin/bash', '-c', $upgradecmd ];
+ } else {
+ $shcmd = [ '/bin/login', '-f', 'root' ];
+ }
+ } else {
+ $shcmd = [ '/bin/login' ];
+ }
my $timeout = 10;
- # fixme: do we want to require special auth permissions?
- # example "-perm Shell"
- my @cmd = ('/usr/bin/vncterm', '-rfbport', $port,
- '-timeout', $timeout, '-authpath', "/nodes/$node",
- '-perm', 'Sys.Console', '-c', @$remcmd, @$shcmd);
+ my $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
+ push @$cmd, '-notls', '-listen', 'localhost';
+ }
+
+ push @$cmd, '-c', @$remcmd, @$shcmd;
my $realcmd = sub {
my $upid = shift;
syslog ('info', "starting vnc proxy $upid\n");
- my $cmdstr = join (' ', @cmd);
+ my $cmdstr = join (' ', @$cmd);
syslog ('info', "launch command: $cmdstr");
- if (system(@cmd) != 0) {
- my $msg = "vncterm failed - $?";
- syslog ('err', $msg);
- return;
+ eval {
+ foreach my $k (keys %ENV) {
+ next if $k eq 'PVE_VNC_TICKET';
+ next if $k eq 'PATH' || $k eq 'TERM' || $k eq 'USER' || $k eq 'HOME' || $k eq 'LANG' || $k eq 'LANGUAGE';
+ delete $ENV{$k};
+ }
+ $ENV{PWD} = '/';
+
+ PVE::Tools::run_command($cmd, errmsg => "vncterm failed", keeplocale => 1);
+ };
+ if (my $err = $@) {
+ syslog ('err', $err);
}
return;
};
my $upid = $rpcenv->fork_worker('vncshell', "", $user, $realcmd);
+
+ PVE::Tools::wait_for_vnc_port($port);
return {
user => $user,
};
}});
+__PACKAGE__->register_method({
+ name => 'vncwebsocket',
+ path => 'vncwebsocket',
+ method => 'GET',
+ permissions => {
+ description => "Restricted to users on realm 'pam'. You also need to pass a valid ticket (vncticket).",
+ check => ['perm', '/nodes/{node}', [ 'Sys.Console' ]],
+ },
+ description => "Opens a weksocket for VNC traffic.",
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ node => get_standard_option('pve-node'),
+ vncticket => {
+ description => "Ticket from previous call to vncproxy.",
+ type => 'string',
+ maxLength => 512,
+ },
+ port => {
+ description => "Port number returned by previous vncproxy call.",
+ type => 'integer',
+ minimum => 5900,
+ maximum => 5999,
+ },
+ },
+ },
+ returns => {
+ type => "object",
+ properties => {
+ port => { type => 'string' },
+ },
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $rpcenv = PVE::RPCEnvironment::get();
+
+ my ($user, undef, $realm) = PVE::AccessControl::verify_username($rpcenv->get_user());
+
+ raise_perm_exc("realm != pam") if $realm ne 'pam';
+
+ my $authpath = "/nodes/$param->{node}";
+
+ PVE::AccessControl::verify_vnc_ticket($param->{vncticket}, $user, $authpath);
+
+ my $port = $param->{port};
+
+ return { port => $port };
+ }});
+
+__PACKAGE__->register_method ({
+ name => 'spiceshell',
+ path => 'spiceshell',
+ method => 'POST',
+ protected => 1,
+ proxyto => 'node',
+ permissions => {
+ description => "Restricted to users on realm 'pam'",
+ check => ['perm', '/nodes/{node}', [ 'Sys.Console' ]],
+ },
+ description => "Creates a SPICE shell.",
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ node => get_standard_option('pve-node'),
+ proxy => get_standard_option('spice-proxy', { optional => 1 }),
+ upgrade => {
+ type => 'boolean',
+ description => "Run 'apt-get dist-upgrade' instead of normal shell.",
+ optional => 1,
+ default => 0,
+ },
+ },
+ },
+ returns => get_standard_option('remote-viewer-config'),
+ code => sub {
+ my ($param) = @_;
+
+ my $rpcenv = PVE::RPCEnvironment::get();
+ my $authuser = $rpcenv->get_user();
+
+ my ($user, undef, $realm) = PVE::AccessControl::verify_username($authuser);
+
+ raise_perm_exc("realm != pam") if $realm ne 'pam';
+
+ raise_perm_exc('user != root@pam') if $param->{upgrade} && $user ne 'root@pam';
+
+ my $node = $param->{node};
+ my $proxy = $param->{proxy};
+
+ my $authpath = "/nodes/$node";
+ my $permissions = 'Sys.Console';
+
+ my $shcmd;
+
+ if ($user eq 'root@pam') {
+ if ($param->{upgrade}) {
+ my $upgradecmd = "pveupgrade --shell";
+ $shcmd = [ '/bin/bash', '-c', $upgradecmd ];
+ } else {
+ $shcmd = [ '/bin/login', '-f', 'root' ];
+ }
+ } else {
+ $shcmd = [ '/bin/login' ];
+ }
+
+ my $title = "Shell on '$node'";
+
+ return PVE::API2Tools::run_spiceterm($authpath, $permissions, 0, $node, $proxy, $title, $shcmd);
+ }});
+
__PACKAGE__->register_method({
name => 'dns',
path => 'dns',
method => 'GET',
permissions => {
- path => '/nodes/{node}',
- privs => [ 'Sys.Audit' ],
+ check => ['perm', '/nodes/{node}', [ 'Sys.Audit' ]],
},
description => "Read DNS settings.",
proxyto => 'node',
path => 'dns',
method => 'PUT',
description => "Write DNS settings.",
+ permissions => {
+ check => ['perm', '/nodes/{node}', [ 'Sys.Modify' ]],
+ },
proxyto => 'node',
protected => 1,
parameters => {
},
dns1 => {
description => 'First name server IP address.',
- type => 'string', format => 'ipv4',
+ type => 'string', format => 'ip',
optional => 1,
},
dns2 => {
description => 'Second name server IP address.',
- type => 'string', format => 'ipv4',
+ type => 'string', format => 'ip',
optional => 1,
},
dns3 => {
description => 'Third name server IP address.',
- type => 'string', format => 'ipv4',
+ type => 'string', format => 'ip',
optional => 1,
},
},
path => 'time',
method => 'GET',
permissions => {
- path => '/nodes/{node}',
- privs => [ 'Sys.Audit' ],
- },
+ check => ['perm', '/nodes/{node}', [ 'Sys.Audit' ]],
+ },
description => "Read server time and time zone settings.",
proxyto => 'node',
parameters => {
path => 'time',
method => 'PUT',
description => "Set time zone.",
+ permissions => {
+ check => ['perm', '/nodes/{node}', [ 'Sys.Modify' ]],
+ },
proxyto => 'node',
protected => 1,
parameters => {
return undef;
}});
-__PACKAGE__->register_method ({
- name => 'upload',
- path => 'upload',
- method => 'POST',
+__PACKAGE__->register_method({
+ name => 'aplinfo',
+ path => 'aplinfo',
+ method => 'GET',
permissions => {
- path => '/storage/{storage}',
- privs => [ 'Datastore.AllocateSpace' ],
+ user => 'all',
},
- description => "Upload content.",
+ description => "Get list of appliances.",
+ proxyto => 'node',
parameters => {
additionalProperties => 0,
- properties => {
+ properties => {
node => get_standard_option('pve-node'),
- storage => get_standard_option('pve-storage-id'),
- filename => {
- description => "The name of the file to create/upload.",
- type => 'string',
- },
- vmid => get_standard_option
- ('pve-vmid', {
- description => "Specify owner VM",
- optional => 1,
- }),
},
},
returns => {
- description => "Volume identifier",
- type => 'string',
+ type => 'array',
+ items => {
+ type => "object",
+ properties => {},
+ },
},
code => sub {
my ($param) = @_;
- # todo: can we proxy file uploads to remote nodes?
- if ($param->{node} ne PVE::INotify::nodename()) {
- raise_param_exc({ node => "can't upload content to remote node" });
+ my $res = [];
+
+ my $list = PVE::APLInfo::load_data();
+
+ foreach my $template (keys %{$list->{all}}) {
+ my $pd = $list->{all}->{$template};
+ next if $pd->{'package'} eq 'pve-web-news';
+ push @$res, $pd;
}
+ return $res;
+ }});
+
+__PACKAGE__->register_method({
+ name => 'apl_download',
+ path => 'aplinfo',
+ method => 'POST',
+ permissions => {
+ check => ['perm', '/storage/{storage}', ['Datastore.AllocateTemplate']],
+ },
+ description => "Download appliance templates.",
+ proxyto => 'node',
+ protected => 1,
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ node => get_standard_option('pve-node'),
+ storage => get_standard_option('pve-storage-id', {
+ description => "The storage where the template will be stored",
+ completion => \&PVE::Storage::complete_storage_enabled,
+ }),
+ template => { type => 'string',
+ description => "The template wich will downloaded",
+ maxLength => 255,
+ completion => \&complete_templet_repo,
+ },
+ },
+ },
+ returns => { type => "string" },
+ code => sub {
+ my ($param) = @_;
+
+ my $rpcenv = PVE::RPCEnvironment::get();
+
+ my $user = $rpcenv->get_user();
+
my $node = $param->{node};
- my $storeid = $param->{storage};
- my $name = $param->{filename};
- my $fh = CGI::upload('filename') || die "unable to get file handle\n";
+ my $list = PVE::APLInfo::load_data();
- syslog ('info', "UPLOAD $name to $node $storeid");
-
- # fixme:
- die "upload not implemented\n";
+ my $template = $param->{template};
+ my $pd = $list->{all}->{$template};
+
+ raise_param_exc({ template => "no such template"}) if !$pd;
+
+ my $cfg = PVE::Storage::config();
+ my $scfg = PVE::Storage::storage_check_enabled($cfg, $param->{storage}, $node);
+
+ die "unknown template type '$pd->{type}'\n"
+ if !($pd->{type} eq 'openvz' || $pd->{type} eq 'lxc');
- my $buffer = "";
- my $tmpname = "/tmp/proxmox_upload-$$.bin";
+ die "storage '$param->{storage}' does not support templates\n"
+ if !$scfg->{content}->{vztmpl};
+
+ my $src = $pd->{location};
+ my $tmpldir = PVE::Storage::get_vztmpl_dir($cfg, $param->{storage});
+ my $dest = "$tmpldir/$template";
+ my $tmpdest = "$tmpldir/${template}.tmp.$$";
+
+ my $worker = sub {
+ my $upid = shift;
+
+ print "starting template download from: $src\n";
+ print "target file: $dest\n";
+
+ my $check_hash = sub {
+ my ($template_info, $filename, $noerr) = @_;
+
+ my $digest;
+ my $expected;
+
+ eval {
+ open(my $fh, '<', $filename) or die "Can't open '$filename': $!";
+ binmode($fh);
+ if (defined($template_info->{sha512sum})) {
+ $expected = $template_info->{sha512sum};
+ $digest = Digest::SHA->new(512)->addfile($fh)->hexdigest;
+ } elsif (defined($template_info->{md5sum})) {
+ #fallback to MD5
+ $expected = $template_info->{md5sum};
+ $digest = Digest::MD5->new->addfile($fh)->hexdigest;
+ } else {
+ die "no expected checksum defined";
+ }
+ close($fh);
+ };
+
+ die "checking hash failed - $@\n" if $@ && !$noerr;
+
+ return ($digest, $digest ? lc($digest) eq lc($expected) : 0);
+ };
+
+ eval {
+ if (-f $dest) {
+ my ($hash, $correct) = &$check_hash($pd, $dest, 1);
+
+ if ($hash && $correct) {
+ print "file already exists $hash - no need to download\n";
+ return;
+ }
+ }
+
+ local %ENV;
+ my $dccfg = PVE::Cluster::cfs_read_file('datacenter.cfg');
+ if ($dccfg->{http_proxy}) {
+ $ENV{http_proxy} = $dccfg->{http_proxy};
+ }
+
+ my @cmd = ('/usr/bin/wget', '--progress=dot:mega', '-O', $tmpdest, $src);
+ if (system (@cmd) != 0) {
+ die "download failed - $!\n";
+ }
+
+ my ($hash, $correct) = &$check_hash($pd, $tmpdest);
+
+ die "could not calculate checksum\n" if !$hash;
+
+ if (!$correct) {
+ my $expected = $pd->{sha512sum} // $pd->{md5sum};
+ die "wrong checksum: $hash != $expected\n";
+ }
+
+ if (!rename($tmpdest, $dest)) {
+ die "unable to save file - $!\n";
+ }
+ };
+ my $err = $@;
+
+ unlink $tmpdest;
+
+ if ($err) {
+ print "\n";
+ die $err if $err;
+ }
+
+ print "download finished\n";
+ };
+
+ return $rpcenv->fork_worker('download', undef, $user, $worker);
+ }});
+
+__PACKAGE__->register_method({
+ name => 'report',
+ path => 'report',
+ method => 'GET',
+ permissions => {
+ check => ['perm', '/nodes/{node}', [ 'Sys.Audit' ]],
+ },
+ protected => 1,
+ description => "Gather various systems information about a node",
+ proxyto => 'node',
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ node => get_standard_option('pve-node'),
+ },
+ },
+ returns => {
+ type => 'string',
+ },
+ code => sub {
+ return PVE::Report::generate();
+ }});
+
+# returns a list of VMIDs, those can be filtered by
+# * current parent node
+# * vmid whitelist
+# * guest is a template (default: skip)
+# * guest is HA manged (default: skip)
+my $get_filtered_vmlist = sub {
+ my ($nodename, $vmfilter, $templates, $ha_managed) = @_;
+
+ my $vmlist = PVE::Cluster::get_vmlist();
+
+ my $vms_allowed = {};
+ if (defined($vmfilter)) {
+ foreach my $vmid (PVE::Tools::split_list($vmfilter)) {
+ $vms_allowed->{$vmid} = 1;
+ }
+ }
+
+ my $res = {};
+ foreach my $vmid (keys %{$vmlist->{ids}}) {
+ next if %$vms_allowed && !$vms_allowed->{$vmid};
+
+ my $d = $vmlist->{ids}->{$vmid};
+ next if $nodename && $d->{node} ne $nodename;
eval {
- open FILE, ">$tmpname" || die "can't open temporary file '$tmpname' - $!\n";
- while (read($fh, $buffer, 32768)) {
- die "write failed - $!" unless print FILE $buffer;
+ my $class;
+ if ($d->{type} eq 'lxc') {
+ $class = 'PVE::LXC::Config';
+ } elsif ($d->{type} eq 'qemu') {
+ $class = 'PVE::QemuConfig';
+ } else {
+ die "unknown VM type '$d->{type}'\n";
}
- close FILE || die " can't close temporary file '$tmpname' - $!\n";
+
+ my $conf = $class->load_config($vmid);
+ return if !$templates && $class->is_template($conf);
+ return if !$ha_managed && PVE::HA::Config::vm_is_ha_managed($vmid);
+
+ $res->{$vmid} = $conf;
+ $res->{$vmid}->{type} = $d->{type};
};
- my $err = $@;
+ warn $@ if $@;
+ }
+
+ return $res;
+};
+
+# return all VMs which should get started/stopped on power up/down
+my $get_start_stop_list = sub {
+ my ($nodename, $autostart, $vmfilter) = @_;
+
+ my $vmlist = &$get_filtered_vmlist($nodename, $vmfilter);
- if ($err) {
- unlink $tmpname;
- die $err;
+ my $resList = {};
+ foreach my $vmid (keys %$vmlist) {
+ my $conf = $vmlist->{$vmid};
+
+ next if $autostart && !$conf->{onboot};
+
+ my $startup = {};
+ if ($conf->{startup}) {
+ $startup = PVE::JSONSchema::pve_parse_startup_order($conf->{startup});
}
- unlink $tmpname; # fixme: proxy to local host import
+ $startup->{order} = LONG_MAX if !defined($startup->{order});
- # fixme: return volid
+ $resList->{$startup->{order}}->{$vmid} = $startup;
+ $resList->{$startup->{order}}->{$vmid}->{type} = $conf->{type};
+ }
- return undef;
+ return $resList;
+};
+
+__PACKAGE__->register_method ({
+ name => 'startall',
+ path => 'startall',
+ method => 'POST',
+ protected => 1,
+ permissions => {
+ check => ['perm', '/', [ 'VM.PowerMgmt' ]],
+ },
+ proxyto => 'node',
+ description => "Start all VMs and containers (when onboot=1).",
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ node => get_standard_option('pve-node'),
+ force => {
+ optional => 1,
+ type => 'boolean',
+ description => "force if onboot=0.",
+ },
+ vms => {
+ description => "Only consider Guests with these IDs.",
+ type => 'string', format => 'pve-vmid-list',
+ optional => 1,
+ },
+ },
+ },
+ returns => {
+ type => 'string',
+ },
+ code => sub {
+ my ($param) = @_;
+ my $rpcenv = PVE::RPCEnvironment::get();
+ my $authuser = $rpcenv->get_user();
+
+ my $nodename = $param->{node};
+ $nodename = PVE::INotify::nodename() if $nodename eq 'localhost';
+
+ my $force = $param->{force};
+
+ my $code = sub {
+
+ $rpcenv->{type} = 'priv'; # to start tasks in background
+
+ if (!PVE::Cluster::check_cfs_quorum(1)) {
+ print "waiting for quorum ...\n";
+ do {
+ sleep(1);
+ } while (!PVE::Cluster::check_cfs_quorum(1));
+ print "got quorum\n";
+ }
+ my $autostart = $force ? undef : 1;
+ my $startList = &$get_start_stop_list($nodename, $autostart, $param->{vms});
+
+ # Note: use numeric sorting with <=>
+ foreach my $order (sort {$a <=> $b} keys %$startList) {
+ my $vmlist = $startList->{$order};
+
+ foreach my $vmid (sort {$a <=> $b} keys %$vmlist) {
+ my $d = $vmlist->{$vmid};
+
+ PVE::Cluster::check_cfs_quorum(); # abort when we loose quorum
+
+ eval {
+ my $default_delay = 0;
+ my $upid;
+
+ if ($d->{type} eq 'lxc') {
+ return if PVE::LXC::check_running($vmid);
+ print STDERR "Starting CT $vmid\n";
+ $upid = PVE::API2::LXC::Status->vm_start({node => $nodename, vmid => $vmid });
+ } elsif ($d->{type} eq 'qemu') {
+ $default_delay = 3; # to reduce load
+ return if PVE::QemuServer::check_running($vmid, 1);
+ print STDERR "Starting VM $vmid\n";
+ $upid = PVE::API2::Qemu->vm_start({node => $nodename, vmid => $vmid });
+ } else {
+ die "unknown VM type '$d->{type}'\n";
+ }
+
+ my $res = PVE::Tools::upid_decode($upid);
+ while (PVE::ProcFSTools::check_process_running($res->{pid})) {
+ sleep(1);
+ }
+
+ my $status = PVE::Tools::upid_read_status($upid);
+ if ($status eq 'OK') {
+ # use default delay to reduce load
+ my $delay = defined($d->{up}) ? int($d->{up}) : $default_delay;
+ if ($delay > 0) {
+ print STDERR "Waiting for $delay seconds (startup delay)\n" if $d->{up};
+ for (my $i = 0; $i < $delay; $i++) {
+ sleep(1);
+ }
+ }
+ } else {
+ if ($d->{type} eq 'lxc') {
+ print STDERR "Starting CT $vmid failed: $status\n";
+ } elsif ($d->{type} eq 'qemu') {
+ print STDERR "Starting VM $vmid failed: status\n";
+ }
+ }
+ };
+ warn $@ if $@;
+ }
+ }
+ return;
+ };
+
+ return $rpcenv->fork_worker('startall', undef, $authuser, $code);
}});
+my $create_stop_worker = sub {
+ my ($nodename, $type, $vmid, $down_timeout) = @_;
+
+ my $upid;
+ if ($type eq 'lxc') {
+ return if !PVE::LXC::check_running($vmid);
+ my $timeout = defined($down_timeout) ? int($down_timeout) : 60;
+ print STDERR "Stopping CT $vmid (timeout = $timeout seconds)\n";
+ $upid = PVE::API2::LXC::Status->vm_shutdown({node => $nodename, vmid => $vmid,
+ timeout => $timeout, forceStop => 1 });
+ } elsif ($type eq 'qemu') {
+ return if !PVE::QemuServer::check_running($vmid, 1);
+ my $timeout = defined($down_timeout) ? int($down_timeout) : 60*3;
+ print STDERR "Stopping VM $vmid (timeout = $timeout seconds)\n";
+ $upid = PVE::API2::Qemu->vm_shutdown({node => $nodename, vmid => $vmid,
+ timeout => $timeout, forceStop => 1 });
+ } else {
+ die "unknown VM type '$type'\n";
+ }
+
+ return $upid;
+};
+
+__PACKAGE__->register_method ({
+ name => 'stopall',
+ path => 'stopall',
+ method => 'POST',
+ protected => 1,
+ permissions => {
+ check => ['perm', '/', [ 'VM.PowerMgmt' ]],
+ },
+ proxyto => 'node',
+ description => "Stop all VMs and Containers.",
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ node => get_standard_option('pve-node'),
+ vms => {
+ description => "Only consider Guests with these IDs.",
+ type => 'string', format => 'pve-vmid-list',
+ optional => 1,
+ },
+ },
+ },
+ returns => {
+ type => 'string',
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $rpcenv = PVE::RPCEnvironment::get();
+ my $authuser = $rpcenv->get_user();
+
+ my $nodename = $param->{node};
+ $nodename = PVE::INotify::nodename() if $nodename eq 'localhost';
+
+ my $code = sub {
+
+ $rpcenv->{type} = 'priv'; # to start tasks in background
+
+ my $stopList = &$get_start_stop_list($nodename, undef, $param->{vms});
+
+ my $cpuinfo = PVE::ProcFSTools::read_cpuinfo();
+ my $datacenterconfig = cfs_read_file('datacenter.cfg');
+ # if not set by user spawn max cpu count number of workers
+ my $maxWorkers = $datacenterconfig->{max_workers} || $cpuinfo->{cpus};
+
+ foreach my $order (sort {$b <=> $a} keys %$stopList) {
+ my $vmlist = $stopList->{$order};
+ my $workers = {};
+
+ my $finish_worker = sub {
+ my $pid = shift;
+ my $d = $workers->{$pid};
+ return if !$d;
+ delete $workers->{$pid};
+
+ syslog('info', "end task $d->{upid}");
+ };
+
+ foreach my $vmid (sort {$b <=> $a} keys %$vmlist) {
+ my $d = $vmlist->{$vmid};
+ my $upid;
+ eval { $upid = &$create_stop_worker($nodename, $d->{type}, $vmid, $d->{down}); };
+ warn $@ if $@;
+ next if !$upid;
+
+ my $res = PVE::Tools::upid_decode($upid, 1);
+ next if !$res;
+
+ my $pid = $res->{pid};
+
+ $workers->{$pid} = { type => $d->{type}, upid => $upid, vmid => $vmid };
+ while (scalar(keys %$workers) >= $maxWorkers) {
+ foreach my $p (keys %$workers) {
+ if (!PVE::ProcFSTools::check_process_running($p)) {
+ &$finish_worker($p);
+ }
+ }
+ sleep(1);
+ }
+ }
+ while (scalar(keys %$workers)) {
+ foreach my $p (keys %$workers) {
+ if (!PVE::ProcFSTools::check_process_running($p)) {
+ &$finish_worker($p);
+ }
+ }
+ sleep(1);
+ }
+ }
+
+ syslog('info', "all VMs and CTs stopped");
+
+ return;
+ };
+
+ return $rpcenv->fork_worker('stopall', undef, $authuser, $code);
+ }});
+
+my $create_migrate_worker = sub {
+ my ($nodename, $type, $vmid, $target) = @_;
+
+ my $upid;
+ if ($type eq 'lxc') {
+ my $online = PVE::LXC::check_running($vmid) ? 1 : 0;
+ print STDERR "Migrating CT $vmid\n";
+ $upid = PVE::API2::LXC->migrate_vm({node => $nodename, vmid => $vmid, target => $target,
+ online => $online });
+ } elsif ($type eq 'qemu') {
+ my $online = PVE::QemuServer::check_running($vmid, 1) ? 1 : 0;
+ print STDERR "Migrating VM $vmid\n";
+ $upid = PVE::API2::Qemu->migrate_vm({node => $nodename, vmid => $vmid, target => $target,
+ online => $online });
+ } else {
+ die "unknown VM type '$type'\n";
+ }
+
+ my $res = PVE::Tools::upid_decode($upid);
+
+ return $res->{pid};
+};
+
+__PACKAGE__->register_method ({
+ name => 'migrateall',
+ path => 'migrateall',
+ method => 'POST',
+ proxyto => 'node',
+ protected => 1,
+ permissions => {
+ check => ['perm', '/', [ 'VM.Migrate' ]],
+ },
+ description => "Migrate all VMs and Containers.",
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ node => get_standard_option('pve-node'),
+ target => get_standard_option('pve-node', { description => "Target node." }),
+ maxworkers => {
+ description => "Maximal number of parallel migration job." .
+ " If not set use 'max_workers' from datacenter.cfg," .
+ " one of both must be set!",
+ optional => 1,
+ type => 'integer',
+ minimum => 1
+ },
+ vms => {
+ description => "Only consider Guests with these IDs.",
+ type => 'string', format => 'pve-vmid-list',
+ optional => 1,
+ },
+ },
+ },
+ returns => {
+ type => 'string',
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $rpcenv = PVE::RPCEnvironment::get();
+ my $authuser = $rpcenv->get_user();
+
+ my $nodename = $param->{node};
+ $nodename = PVE::INotify::nodename() if $nodename eq 'localhost';
+
+ my $target = $param->{target};
+
+ my $datacenterconfig = cfs_read_file('datacenter.cfg');
+ # prefer parameter over datacenter cfg settings
+ my $maxWorkers = $param->{maxworkers} || $datacenterconfig->{max_workers} ||
+ die "either 'maxworkers' parameter or max_workers in datacenter.cfg must be set!\n";
+
+ my $code = sub {
+ $rpcenv->{type} = 'priv'; # to start tasks in background
+
+ my $vmlist = &$get_filtered_vmlist($nodename, $param->{vms}, 1, 1);
+
+ my $workers = {};
+ foreach my $vmid (sort keys %$vmlist) {
+ my $d = $vmlist->{$vmid};
+ my $pid;
+ eval { $pid = &$create_migrate_worker($nodename, $d->{type}, $vmid, $target); };
+ warn $@ if $@;
+ next if !$pid;
+
+ $workers->{$pid} = 1;
+ while (scalar(keys %$workers) >= $maxWorkers) {
+ foreach my $p (keys %$workers) {
+ if (!PVE::ProcFSTools::check_process_running($p)) {
+ delete $workers->{$p};
+ }
+ }
+ sleep(1);
+ }
+ }
+ while (scalar(keys %$workers)) {
+ foreach my $p (keys %$workers) {
+ if (!PVE::ProcFSTools::check_process_running($p)) {
+ delete $workers->{$p};
+ }
+ }
+ sleep(1);
+ }
+ return;
+ };
+
+ return $rpcenv->fork_worker('migrateall', undef, $authuser, $code);
+
+ }});
+
+# bash completion helper
+
+sub complete_templet_repo {
+ my ($cmdname, $pname, $cvalue) = @_;
+
+ my $repo = PVE::APLInfo::load_data();
+ my $res = [];
+ foreach my $templ (keys %{$repo->{all}}) {
+ next if $templ !~ m/^$cvalue/;
+ push @$res, $templ;
+ }
+
+ return $res;
+}
+
package PVE::API2::Nodes;
use strict;
use PVE::Cluster;
use PVE::RESTHandler;
use PVE::RPCEnvironment;
+use PVE::API2Tools;
use base qw(PVE::RESTHandler);
type => "object",
properties => {},
},
- links => [ { rel => 'child', href => "{name}" } ],
+ links => [ { rel => 'child', href => "{node}" } ],
},
code => sub {
my ($param) = @_;
my $clinfo = PVE::Cluster::get_clinfo();
my $res = [];
- my $nodename = PVE::INotify::nodename();
- my $nodelist = $clinfo->{nodelist};
-
+ my $nodelist = PVE::Cluster::get_nodelist();
+ my $members = PVE::Cluster::get_members();
my $rrd = PVE::Cluster::rrd_dump();
- my @nodes = $nodelist ? (keys %$nodelist) : $nodename;
-
- foreach my $node (@nodes) {
- my $entry = { name => $node };
- if (my $d = $rrd->{"pve2-node/$node"}) {
-
- $entry->{uptime} = $d->[0];
- $entry->{maxcpu} = $d->[3];
- $entry->{cpu} = $d->[4];
- $entry->{maxmem} = $d->[6];
- $entry->{mem} = $d->[7];
- $entry->{maxdisk} = $d->[10];
- $entry->{disk} = $d->[11];
- }
-
+ foreach my $node (@$nodelist) {
+ my $entry = PVE::API2Tools::extract_node_stats($node, $members, $rrd);
push @$res, $entry;
}