mirror of
https://git.proxmox.com/git/pve-manager
synced 2025-05-29 19:30:08 +00:00

since the jobs are configured clusterwide in pmxcfs, a user can use any
node to update the config of them. for some configs (schedule/enabled)
we need to update the last runtime in the state file, but this
is sadly only node-local.
to also update the state file on the other nodes, we introduce
a new 'detect_changed_runtime_props' function that checks and saves relevant
properties from the config to the statefile each round of the scheduler if they
changed.
this way, we can detect changes in those and update the last runtime too.
the only situation where we don't detect a config change is when the
user changes back to the previous configuration in between iterations.
This can be ignored though, since it would not be scheduled then
anyway.
in 'synchronize_job_states_with_config' we switch from reading the
jobstate unconditionally to check the existance of the statefile
(which is the only condition that can return undef anyway)
so that we don't read the file multiple times each round.
Fixes: 0c8d7468
("fix #4053: don't run vzdump jobs when they change from
disabled->enabled")
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
Reviewed-by: Fiona Ebner <f.ebner@proxmox.com>
689 lines
17 KiB
Perl
689 lines
17 KiB
Perl
package PVE::API2::Backup;
|
|
|
|
use strict;
|
|
use warnings;
|
|
use Digest::SHA;
|
|
use UUID qw(uuid);
|
|
|
|
use PVE::SafeSyslog;
|
|
use PVE::Tools qw(extract_param);
|
|
use PVE::Cluster qw(cfs_lock_file cfs_read_file cfs_write_file);
|
|
use PVE::RESTHandler;
|
|
use PVE::RPCEnvironment;
|
|
use PVE::JSONSchema;
|
|
use PVE::Storage;
|
|
use PVE::Exception qw(raise_param_exc);
|
|
use PVE::VZDump;
|
|
use PVE::VZDump::Common;
|
|
use PVE::Jobs; # for VZDump Jobs
|
|
use Proxmox::RS::CalendarEvent;
|
|
|
|
use base qw(PVE::RESTHandler);
|
|
|
|
use constant ALL_DAYS => 'mon,tue,wed,thu,fri,sat,sun';
|
|
|
|
PVE::JSONSchema::register_format('pve-day-of-week', \&verify_day_of_week);
|
|
sub verify_day_of_week {
|
|
my ($value, $noerr) = @_;
|
|
|
|
return $value if $value =~ m/^(mon|tue|wed|thu|fri|sat|sun)$/;
|
|
|
|
return undef if $noerr;
|
|
|
|
die "invalid day '$value'\n";
|
|
}
|
|
|
|
my $vzdump_job_id_prop = {
|
|
type => 'string',
|
|
description => "The job ID.",
|
|
maxLength => 50
|
|
};
|
|
|
|
my $assert_param_permission = sub {
|
|
my ($param, $user) = @_;
|
|
return if $user eq 'root@pam'; # always OK
|
|
|
|
for my $key (qw(tmpdir dumpdir script)) {
|
|
raise_param_exc({ $key => "Only root may set this option."}) if exists $param->{$key};
|
|
}
|
|
};
|
|
|
|
my $convert_to_schedule = sub {
|
|
my ($job) = @_;
|
|
|
|
my $starttime = $job->{starttime};
|
|
|
|
return "$starttime" if !$job->{dow}; # dow is restrictive, so none means all days
|
|
|
|
# normalize as it could be a null-separated list previously
|
|
my $dow = join(',', PVE::Tools::split_list($job->{dow}));
|
|
|
|
return $dow eq ALL_DAYS ? "$starttime" : "$dow $starttime";
|
|
};
|
|
|
|
my $schedule_param_check = sub {
|
|
my ($param, $required) = @_;
|
|
if (defined($param->{schedule})) {
|
|
if (defined($param->{starttime})) {
|
|
raise_param_exc({ starttime => "'starttime' and 'schedule' cannot both be set" });
|
|
}
|
|
} elsif (!defined($param->{starttime})) {
|
|
raise_param_exc({ schedule => "neither 'starttime' nor 'schedule' were set" })
|
|
if $required;
|
|
} else {
|
|
$param->{schedule} = $convert_to_schedule->($param);
|
|
}
|
|
|
|
delete $param->{starttime};
|
|
delete $param->{dow};
|
|
};
|
|
|
|
__PACKAGE__->register_method({
|
|
name => 'index',
|
|
path => '',
|
|
method => 'GET',
|
|
description => "List vzdump backup schedule.",
|
|
permissions => {
|
|
check => ['perm', '/', ['Sys.Audit']],
|
|
},
|
|
parameters => {
|
|
additionalProperties => 0,
|
|
properties => {},
|
|
},
|
|
returns => {
|
|
type => 'array',
|
|
items => {
|
|
type => "object",
|
|
properties => {
|
|
id => $vzdump_job_id_prop
|
|
},
|
|
},
|
|
links => [ { rel => 'child', href => "{id}" } ],
|
|
},
|
|
code => sub {
|
|
my ($param) = @_;
|
|
|
|
my $rpcenv = PVE::RPCEnvironment::get();
|
|
my $user = $rpcenv->get_user();
|
|
|
|
my $data = cfs_read_file('vzdump.cron');
|
|
my $jobs_data = cfs_read_file('jobs.cfg');
|
|
my $order = $jobs_data->{order};
|
|
my $jobs = $jobs_data->{ids};
|
|
|
|
my $res = $data->{jobs} || [];
|
|
foreach my $job (@$res) {
|
|
$job->{schedule} = $convert_to_schedule->($job);
|
|
}
|
|
|
|
foreach my $jobid (sort { $order->{$a} <=> $order->{$b} } keys %$jobs) {
|
|
my $job = $jobs->{$jobid};
|
|
next if $job->{type} ne 'vzdump';
|
|
|
|
if (my $schedule = $job->{schedule}) {
|
|
# vzdump jobs are cluster wide, there maybe was no local run
|
|
# so simply calculate from now
|
|
my $last_run = time();
|
|
my $calspec = Proxmox::RS::CalendarEvent->new($schedule);
|
|
my $next_run = $calspec->compute_next_event($last_run);
|
|
$job->{'next-run'} = $next_run if defined($next_run);
|
|
}
|
|
|
|
push @$res, $job;
|
|
}
|
|
|
|
return $res;
|
|
}});
|
|
|
|
__PACKAGE__->register_method({
|
|
name => 'create_job',
|
|
path => '',
|
|
method => 'POST',
|
|
protected => 1,
|
|
description => "Create new vzdump backup job.",
|
|
permissions => {
|
|
check => ['perm', '/', ['Sys.Modify']],
|
|
description => "The 'tmpdir', 'dumpdir' and 'script' parameters are additionally restricted to the 'root\@pam' user.",
|
|
},
|
|
parameters => {
|
|
additionalProperties => 0,
|
|
properties => PVE::VZDump::Common::json_config_properties({
|
|
id => {
|
|
type => 'string',
|
|
description => "Job ID (will be autogenerated).",
|
|
format => 'pve-configid',
|
|
optional => 1, # FIXME: make required on 8.0
|
|
},
|
|
schedule => {
|
|
description => "Backup schedule. The format is a subset of `systemd` calendar events.",
|
|
type => 'string', format => 'pve-calendar-event',
|
|
maxLength => 128,
|
|
optional => 1,
|
|
},
|
|
starttime => {
|
|
type => 'string',
|
|
description => "Job Start time.",
|
|
pattern => '\d{1,2}:\d{1,2}',
|
|
typetext => 'HH:MM',
|
|
optional => 1,
|
|
},
|
|
dow => {
|
|
type => 'string', format => 'pve-day-of-week-list',
|
|
optional => 1,
|
|
description => "Day of week selection.",
|
|
requires => 'starttime',
|
|
default => ALL_DAYS,
|
|
},
|
|
enabled => {
|
|
type => 'boolean',
|
|
optional => 1,
|
|
description => "Enable or disable the job.",
|
|
default => '1',
|
|
},
|
|
'repeat-missed' => {
|
|
optional => 1,
|
|
type => 'boolean',
|
|
description => "If true, the job will be run as soon as possible if it was missed".
|
|
" while the scheduler was not running.",
|
|
default => 0,
|
|
},
|
|
comment => {
|
|
optional => 1,
|
|
type => 'string',
|
|
description => "Description for the Job.",
|
|
maxLength => 512,
|
|
},
|
|
}),
|
|
},
|
|
returns => { type => 'null' },
|
|
code => sub {
|
|
my ($param) = @_;
|
|
|
|
my $rpcenv = PVE::RPCEnvironment::get();
|
|
my $user = $rpcenv->get_user();
|
|
|
|
$assert_param_permission->($param, $user);
|
|
|
|
if (my $pool = $param->{pool}) {
|
|
$rpcenv->check_pool_exist($pool);
|
|
$rpcenv->check($user, "/pool/$pool", ['VM.Backup']);
|
|
}
|
|
|
|
$schedule_param_check->($param, 1);
|
|
|
|
$param->{enabled} = 1 if !defined($param->{enabled});
|
|
|
|
# autogenerate id for api compatibility FIXME remove with 8.0
|
|
my $id = extract_param($param, 'id') // UUID::uuid();
|
|
|
|
cfs_lock_file('jobs.cfg', undef, sub {
|
|
my $data = cfs_read_file('jobs.cfg');
|
|
|
|
die "Job '$id' already exists\n"
|
|
if $data->{ids}->{$id};
|
|
|
|
PVE::VZDump::verify_vzdump_parameters($param, 1);
|
|
my $plugin = PVE::Jobs::Plugin->lookup('vzdump');
|
|
my $opts = $plugin->check_config($id, $param, 1, 1);
|
|
|
|
$data->{ids}->{$id} = $opts;
|
|
|
|
PVE::Jobs::create_job($id, 'vzdump', $opts);
|
|
|
|
cfs_write_file('jobs.cfg', $data);
|
|
});
|
|
die "$@" if ($@);
|
|
|
|
return undef;
|
|
}});
|
|
|
|
__PACKAGE__->register_method({
|
|
name => 'read_job',
|
|
path => '{id}',
|
|
method => 'GET',
|
|
description => "Read vzdump backup job definition.",
|
|
permissions => {
|
|
check => ['perm', '/', ['Sys.Audit']],
|
|
},
|
|
parameters => {
|
|
additionalProperties => 0,
|
|
properties => {
|
|
id => $vzdump_job_id_prop
|
|
},
|
|
},
|
|
returns => {
|
|
type => 'object',
|
|
},
|
|
code => sub {
|
|
my ($param) = @_;
|
|
|
|
my $rpcenv = PVE::RPCEnvironment::get();
|
|
my $user = $rpcenv->get_user();
|
|
|
|
my $data = cfs_read_file('vzdump.cron');
|
|
|
|
my $jobs = $data->{jobs} || [];
|
|
|
|
foreach my $job (@$jobs) {
|
|
if ($job->{id} eq $param->{id}) {
|
|
$job->{schedule} = $convert_to_schedule->($job);
|
|
return $job;
|
|
}
|
|
}
|
|
|
|
my $jobs_data = cfs_read_file('jobs.cfg');
|
|
my $job = $jobs_data->{ids}->{$param->{id}};
|
|
return $job if $job && $job->{type} eq 'vzdump';
|
|
|
|
raise_param_exc({ id => "No such job '$param->{id}'" });
|
|
|
|
}});
|
|
|
|
__PACKAGE__->register_method({
|
|
name => 'delete_job',
|
|
path => '{id}',
|
|
method => 'DELETE',
|
|
description => "Delete vzdump backup job definition.",
|
|
permissions => {
|
|
check => ['perm', '/', ['Sys.Modify']],
|
|
},
|
|
protected => 1,
|
|
parameters => {
|
|
additionalProperties => 0,
|
|
properties => {
|
|
id => $vzdump_job_id_prop
|
|
},
|
|
},
|
|
returns => { type => 'null' },
|
|
code => sub {
|
|
my ($param) = @_;
|
|
|
|
my $rpcenv = PVE::RPCEnvironment::get();
|
|
my $user = $rpcenv->get_user();
|
|
|
|
my $id = $param->{id};
|
|
|
|
my $delete_job = sub {
|
|
my $data = cfs_read_file('vzdump.cron');
|
|
|
|
my $jobs = $data->{jobs} || [];
|
|
my $newjobs = [];
|
|
|
|
my $found;
|
|
foreach my $job (@$jobs) {
|
|
if ($job->{id} eq $id) {
|
|
$found = 1;
|
|
} else {
|
|
push @$newjobs, $job;
|
|
}
|
|
}
|
|
|
|
if (!$found) {
|
|
cfs_lock_file('jobs.cfg', undef, sub {
|
|
my $jobs_data = cfs_read_file('jobs.cfg');
|
|
|
|
if (!defined($jobs_data->{ids}->{$id})) {
|
|
raise_param_exc({ id => "No such job '$id'" });
|
|
}
|
|
delete $jobs_data->{ids}->{$id};
|
|
|
|
PVE::Jobs::remove_job($id, 'vzdump');
|
|
|
|
cfs_write_file('jobs.cfg', $jobs_data);
|
|
});
|
|
die "$@" if $@;
|
|
} else {
|
|
$data->{jobs} = $newjobs;
|
|
|
|
cfs_write_file('vzdump.cron', $data);
|
|
}
|
|
};
|
|
cfs_lock_file('vzdump.cron', undef, $delete_job);
|
|
die "$@" if ($@);
|
|
|
|
return undef;
|
|
}});
|
|
|
|
__PACKAGE__->register_method({
|
|
name => 'update_job',
|
|
path => '{id}',
|
|
method => 'PUT',
|
|
protected => 1,
|
|
description => "Update vzdump backup job definition.",
|
|
permissions => {
|
|
check => ['perm', '/', ['Sys.Modify']],
|
|
description => "The 'tmpdir', 'dumpdir' and 'script' parameters are additionally restricted to the 'root\@pam' user.",
|
|
},
|
|
parameters => {
|
|
additionalProperties => 0,
|
|
properties => PVE::VZDump::Common::json_config_properties({
|
|
id => $vzdump_job_id_prop,
|
|
schedule => {
|
|
description => "Backup schedule. The format is a subset of `systemd` calendar events.",
|
|
type => 'string', format => 'pve-calendar-event',
|
|
maxLength => 128,
|
|
optional => 1,
|
|
},
|
|
starttime => {
|
|
type => 'string',
|
|
description => "Job Start time.",
|
|
pattern => '\d{1,2}:\d{1,2}',
|
|
typetext => 'HH:MM',
|
|
optional => 1,
|
|
},
|
|
dow => {
|
|
type => 'string', format => 'pve-day-of-week-list',
|
|
optional => 1,
|
|
requires => 'starttime',
|
|
description => "Day of week selection.",
|
|
},
|
|
delete => {
|
|
type => 'string', format => 'pve-configid-list',
|
|
description => "A list of settings you want to delete.",
|
|
optional => 1,
|
|
},
|
|
enabled => {
|
|
type => 'boolean',
|
|
optional => 1,
|
|
description => "Enable or disable the job.",
|
|
default => '1',
|
|
},
|
|
'repeat-missed' => {
|
|
optional => 1,
|
|
type => 'boolean',
|
|
description => "If true, the job will be run as soon as possible if it was missed".
|
|
" while the scheduler was not running.",
|
|
default => 0,
|
|
},
|
|
comment => {
|
|
optional => 1,
|
|
type => 'string',
|
|
description => "Description for the Job.",
|
|
maxLength => 512,
|
|
},
|
|
}),
|
|
},
|
|
returns => { type => 'null' },
|
|
code => sub {
|
|
my ($param) = @_;
|
|
|
|
my $rpcenv = PVE::RPCEnvironment::get();
|
|
my $user = $rpcenv->get_user();
|
|
|
|
$assert_param_permission->($param, $user);
|
|
|
|
if (my $pool = $param->{pool}) {
|
|
$rpcenv->check_pool_exist($pool);
|
|
$rpcenv->check($user, "/pool/$pool", ['VM.Backup']);
|
|
}
|
|
|
|
$schedule_param_check->($param);
|
|
|
|
my $id = extract_param($param, 'id');
|
|
my $delete = extract_param($param, 'delete');
|
|
if ($delete) {
|
|
$delete = [PVE::Tools::split_list($delete)];
|
|
}
|
|
|
|
my $update_job = sub {
|
|
my $data = cfs_read_file('vzdump.cron');
|
|
my $jobs_data = cfs_read_file('jobs.cfg');
|
|
|
|
my $jobs = $data->{jobs} || [];
|
|
|
|
die "no options specified\n" if !scalar(keys %$param);
|
|
|
|
PVE::VZDump::verify_vzdump_parameters($param);
|
|
my $plugin = PVE::Jobs::Plugin->lookup('vzdump');
|
|
my $opts = $plugin->check_config($id, $param, 0, 1);
|
|
|
|
# try to find it in old vzdump.cron and convert it to a job
|
|
my ($idx) = grep { $jobs->[$_]->{id} eq $id } (0 .. scalar(@$jobs) - 1);
|
|
|
|
my $job;
|
|
if (defined($idx)) {
|
|
$job = splice @$jobs, $idx, 1;
|
|
$job->{schedule} = $convert_to_schedule->($job);
|
|
delete $job->{starttime};
|
|
delete $job->{dow};
|
|
delete $job->{id};
|
|
$job->{type} = 'vzdump';
|
|
$jobs_data->{ids}->{$id} = $job;
|
|
} else {
|
|
$job = $jobs_data->{ids}->{$id};
|
|
die "no such vzdump job\n" if !$job || $job->{type} ne 'vzdump';
|
|
}
|
|
|
|
my $deletable = {
|
|
comment => 1,
|
|
'repeat-missed' => 1,
|
|
};
|
|
|
|
foreach my $k (@$delete) {
|
|
if (!PVE::VZDump::option_exists($k) && !$deletable->{$k}) {
|
|
raise_param_exc({ delete => "unknown option '$k'" });
|
|
}
|
|
|
|
delete $job->{$k};
|
|
}
|
|
|
|
foreach my $k (keys %$param) {
|
|
$job->{$k} = $param->{$k};
|
|
}
|
|
|
|
$job->{all} = 1 if (defined($job->{exclude}) && !defined($job->{pool}));
|
|
|
|
if (defined($param->{vmid})) {
|
|
delete $job->{all};
|
|
delete $job->{exclude};
|
|
delete $job->{pool};
|
|
} elsif ($param->{all}) {
|
|
delete $job->{vmid};
|
|
delete $job->{pool};
|
|
} elsif ($job->{pool}) {
|
|
delete $job->{vmid};
|
|
delete $job->{all};
|
|
delete $job->{exclude};
|
|
}
|
|
|
|
PVE::VZDump::verify_vzdump_parameters($job, 1);
|
|
|
|
if (defined($idx)) {
|
|
cfs_write_file('vzdump.cron', $data);
|
|
}
|
|
cfs_write_file('jobs.cfg', $jobs_data);
|
|
|
|
PVE::Jobs::detect_changed_runtime_props($id, 'vzdump', $job);
|
|
|
|
return;
|
|
};
|
|
cfs_lock_file('vzdump.cron', undef, sub {
|
|
cfs_lock_file('jobs.cfg', undef, $update_job);
|
|
die "$@" if ($@);
|
|
});
|
|
die "$@" if ($@);
|
|
}});
|
|
|
|
__PACKAGE__->register_method({
|
|
name => 'get_volume_backup_included',
|
|
path => '{id}/included_volumes',
|
|
method => 'GET',
|
|
protected => 1,
|
|
description => "Returns included guests and the backup status of their disks. Optimized to be used in ExtJS tree views.",
|
|
permissions => {
|
|
check => ['perm', '/', ['Sys.Audit']],
|
|
},
|
|
parameters => {
|
|
additionalProperties => 0,
|
|
properties => {
|
|
id => $vzdump_job_id_prop
|
|
},
|
|
},
|
|
returns => {
|
|
type => 'object',
|
|
description => 'Root node of the tree object. Children represent guests, grandchildren represent volumes of that guest.',
|
|
properties => {
|
|
children => {
|
|
type => 'array',
|
|
items => {
|
|
type => 'object',
|
|
properties => {
|
|
id => {
|
|
type => 'integer',
|
|
description => 'VMID of the guest.',
|
|
},
|
|
name => {
|
|
type => 'string',
|
|
description => 'Name of the guest',
|
|
optional => 1,
|
|
},
|
|
type => {
|
|
type => 'string',
|
|
description => 'Type of the guest, VM, CT or unknown for removed but not purged guests.',
|
|
enum => ['qemu', 'lxc', 'unknown'],
|
|
},
|
|
children => {
|
|
type => 'array',
|
|
optional => 1,
|
|
description => 'The volumes of the guest with the information if they will be included in backups.',
|
|
items => {
|
|
type => 'object',
|
|
properties => {
|
|
id => {
|
|
type => 'string',
|
|
description => 'Configuration key of the volume.',
|
|
},
|
|
name => {
|
|
type => 'string',
|
|
description => 'Name of the volume.',
|
|
},
|
|
included => {
|
|
type => 'boolean',
|
|
description => 'Whether the volume is included in the backup or not.',
|
|
},
|
|
reason => {
|
|
type => 'string',
|
|
description => 'The reason why the volume is included (or excluded).',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
code => sub {
|
|
my ($param) = @_;
|
|
|
|
my $rpcenv = PVE::RPCEnvironment::get();
|
|
|
|
my $user = $rpcenv->get_user();
|
|
|
|
my $vzconf = cfs_read_file('vzdump.cron');
|
|
my $all_jobs = $vzconf->{jobs} || [];
|
|
my $job;
|
|
my $rrd = PVE::Cluster::rrd_dump();
|
|
|
|
for my $j (@$all_jobs) {
|
|
if ($j->{id} eq $param->{id}) {
|
|
$job = $j;
|
|
last;
|
|
}
|
|
}
|
|
if (!$job) {
|
|
my $jobs_data = cfs_read_file('jobs.cfg');
|
|
my $j = $jobs_data->{ids}->{$param->{id}};
|
|
if ($j && $j->{type} eq 'vzdump') {
|
|
$job = $j;
|
|
}
|
|
}
|
|
raise_param_exc({ id => "No such job '$param->{id}'" }) if !$job;
|
|
|
|
my $vmlist = PVE::Cluster::get_vmlist();
|
|
|
|
my @job_vmids;
|
|
|
|
my $included_guests = PVE::VZDump::get_included_guests($job);
|
|
|
|
for my $node (keys %{$included_guests}) {
|
|
my $node_vmids = $included_guests->{$node};
|
|
push(@job_vmids, @{$node_vmids});
|
|
}
|
|
|
|
# remove VMIDs to which the user has no permission to not leak infos
|
|
# like the guest name
|
|
my @allowed_vmids = grep {
|
|
$rpcenv->check($user, "/vms/$_", [ 'VM.Audit' ], 1);
|
|
} @job_vmids;
|
|
|
|
my $result = {
|
|
children => [],
|
|
};
|
|
|
|
for my $vmid (@allowed_vmids) {
|
|
|
|
my $children = [];
|
|
|
|
# It's possible that a job has VMIDs configured that are not in
|
|
# vmlist. This could be because a guest was removed but not purged.
|
|
# Since there is no more data available we can only deliver the VMID
|
|
# and no volumes.
|
|
if (!defined $vmlist->{ids}->{$vmid}) {
|
|
push(@{$result->{children}}, {
|
|
id => int($vmid),
|
|
type => 'unknown',
|
|
leaf => 1,
|
|
});
|
|
next;
|
|
}
|
|
|
|
my $type = $vmlist->{ids}->{$vmid}->{type};
|
|
my $node = $vmlist->{ids}->{$vmid}->{node};
|
|
|
|
my $conf;
|
|
my $volumes;
|
|
my $name = "";
|
|
|
|
if ($type eq 'qemu') {
|
|
$conf = PVE::QemuConfig->load_config($vmid, $node);
|
|
$volumes = PVE::QemuConfig->get_backup_volumes($conf);
|
|
$name = $conf->{name};
|
|
} elsif ($type eq 'lxc') {
|
|
$conf = PVE::LXC::Config->load_config($vmid, $node);
|
|
$volumes = PVE::LXC::Config->get_backup_volumes($conf);
|
|
$name = $conf->{hostname};
|
|
} else {
|
|
die "VMID $vmid is neither Qemu nor LXC guest\n";
|
|
}
|
|
|
|
foreach my $volume (@$volumes) {
|
|
my $disk = {
|
|
# id field must be unique for ExtJS tree view
|
|
id => "$vmid:$volume->{key}",
|
|
name => $volume->{volume_config}->{file} // $volume->{volume_config}->{volume},
|
|
included=> $volume->{included},
|
|
reason => $volume->{reason},
|
|
leaf => 1,
|
|
};
|
|
push(@{$children}, $disk);
|
|
}
|
|
|
|
my $leaf = 0;
|
|
# it's possible for a guest to have no volumes configured
|
|
$leaf = 1 if !@{$children};
|
|
|
|
push(@{$result->{children}}, {
|
|
id => int($vmid),
|
|
type => $type,
|
|
name => $name,
|
|
children => $children,
|
|
leaf => $leaf,
|
|
});
|
|
}
|
|
|
|
return $result;
|
|
}});
|
|
|
|
1;
|