]> git.proxmox.com Git - pve-container.git/blob - src/PVE/API2/LXC.pm
9c3548a1608b127183935b9add09e786e217a7f1
[pve-container.git] / src / PVE / API2 / LXC.pm
1 package PVE::API2::LXC;
2
3 use strict;
4 use warnings;
5
6 use PVE::SafeSyslog;
7 use PVE::Tools qw(extract_param run_command);
8 use PVE::Exception qw(raise raise_param_exc);
9 use PVE::INotify;
10 use PVE::Cluster qw(cfs_read_file);
11 use PVE::AccessControl;
12 use PVE::Firewall;
13 use PVE::Storage;
14 use PVE::RESTHandler;
15 use PVE::RPCEnvironment;
16 use PVE::LXC;
17 use PVE::LXC::Create;
18 use PVE::LXC::Migrate;
19 use PVE::API2::LXC::Config;
20 use PVE::API2::LXC::Status;
21 use PVE::API2::LXC::Snapshot;
22 use PVE::HA::Config;
23 use PVE::JSONSchema qw(get_standard_option);
24 use base qw(PVE::RESTHandler);
25
26 use Data::Dumper; # fixme: remove
27
28 __PACKAGE__->register_method ({
29 subclass => "PVE::API2::LXC::Config",
30 path => '{vmid}/config',
31 });
32
33 __PACKAGE__->register_method ({
34 subclass => "PVE::API2::LXC::Status",
35 path => '{vmid}/status',
36 });
37
38 __PACKAGE__->register_method ({
39 subclass => "PVE::API2::LXC::Snapshot",
40 path => '{vmid}/snapshot',
41 });
42
43 __PACKAGE__->register_method ({
44 subclass => "PVE::API2::Firewall::CT",
45 path => '{vmid}/firewall',
46 });
47
48 __PACKAGE__->register_method({
49 name => 'vmlist',
50 path => '',
51 method => 'GET',
52 description => "LXC container index (per node).",
53 permissions => {
54 description => "Only list CTs where you have VM.Audit permissons on /vms/<vmid>.",
55 user => 'all',
56 },
57 proxyto => 'node',
58 protected => 1, # /proc files are only readable by root
59 parameters => {
60 additionalProperties => 0,
61 properties => {
62 node => get_standard_option('pve-node'),
63 },
64 },
65 returns => {
66 type => 'array',
67 items => {
68 type => "object",
69 properties => {},
70 },
71 links => [ { rel => 'child', href => "{vmid}" } ],
72 },
73 code => sub {
74 my ($param) = @_;
75
76 my $rpcenv = PVE::RPCEnvironment::get();
77 my $authuser = $rpcenv->get_user();
78
79 my $vmstatus = PVE::LXC::vmstatus();
80
81 my $res = [];
82 foreach my $vmid (keys %$vmstatus) {
83 next if !$rpcenv->check($authuser, "/vms/$vmid", [ 'VM.Audit' ], 1);
84
85 my $data = $vmstatus->{$vmid};
86 $data->{vmid} = $vmid;
87 push @$res, $data;
88 }
89
90 return $res;
91
92 }});
93
94 __PACKAGE__->register_method({
95 name => 'create_vm',
96 path => '',
97 method => 'POST',
98 description => "Create or restore a container.",
99 permissions => {
100 user => 'all', # check inside
101 description => "You need 'VM.Allocate' permissions on /vms/{vmid} or on the VM pool /pool/{pool}. " .
102 "For restore, it is enough if the user has 'VM.Backup' permission and the VM already exists. " .
103 "You also need 'Datastore.AllocateSpace' permissions on the storage.",
104 },
105 protected => 1,
106 proxyto => 'node',
107 parameters => {
108 additionalProperties => 0,
109 properties => PVE::LXC::json_config_properties({
110 node => get_standard_option('pve-node'),
111 vmid => get_standard_option('pve-vmid', { completion => \&PVE::Cluster::complete_next_vmid }),
112 ostemplate => {
113 description => "The OS template or backup file.",
114 type => 'string',
115 maxLength => 255,
116 completion => \&PVE::LXC::complete_os_templates,
117 },
118 password => {
119 optional => 1,
120 type => 'string',
121 description => "Sets root password inside container.",
122 minLength => 5,
123 },
124 storage => get_standard_option('pve-storage-id', {
125 description => "Default Storage.",
126 default => 'local',
127 optional => 1,
128 completion => \&PVE::Storage::complete_storage_enabled,
129 }),
130 force => {
131 optional => 1,
132 type => 'boolean',
133 description => "Allow to overwrite existing container.",
134 },
135 restore => {
136 optional => 1,
137 type => 'boolean',
138 description => "Mark this as restore task.",
139 },
140 pool => {
141 optional => 1,
142 type => 'string', format => 'pve-poolid',
143 description => "Add the VM to the specified pool.",
144 },
145 'ignore-unpack-errors' => {
146 optional => 1,
147 type => 'boolean',
148 description => "Ignore errors when extracting the template.",
149 },
150 }),
151 },
152 returns => {
153 type => 'string',
154 },
155 code => sub {
156 my ($param) = @_;
157
158 my $rpcenv = PVE::RPCEnvironment::get();
159
160 my $authuser = $rpcenv->get_user();
161
162 my $node = extract_param($param, 'node');
163
164 my $vmid = extract_param($param, 'vmid');
165
166 my $ignore_unpack_errors = extract_param($param, 'ignore-unpack-errors');
167
168 my $basecfg_fn = PVE::LXC::config_file($vmid);
169
170 my $same_container_exists = -f $basecfg_fn;
171
172 # 'unprivileged' is read-only, so we can't pass it to update_pct_config
173 my $unprivileged = extract_param($param, 'unprivileged');
174
175 my $restore = extract_param($param, 'restore');
176
177 if ($restore) {
178 # fixme: limit allowed parameters
179
180 }
181
182 my $force = extract_param($param, 'force');
183
184 if (!($same_container_exists && $restore && $force)) {
185 PVE::Cluster::check_vmid_unused($vmid);
186 } else {
187 my $conf = PVE::LXC::load_config($vmid);
188 PVE::LXC::check_protection($conf, "unable to restore CT $vmid");
189 }
190
191 my $password = extract_param($param, 'password');
192
193 my $pool = extract_param($param, 'pool');
194
195 if (defined($pool)) {
196 $rpcenv->check_pool_exist($pool);
197 $rpcenv->check_perm_modify($authuser, "/pool/$pool");
198 }
199
200 if ($rpcenv->check($authuser, "/vms/$vmid", ['VM.Allocate'], 1)) {
201 # OK
202 } elsif ($pool && $rpcenv->check($authuser, "/pool/$pool", ['VM.Allocate'], 1)) {
203 # OK
204 } elsif ($restore && $force && $same_container_exists &&
205 $rpcenv->check($authuser, "/vms/$vmid", ['VM.Backup'], 1)) {
206 # OK: user has VM.Backup permissions, and want to restore an existing VM
207 } else {
208 raise_perm_exc();
209 }
210
211 PVE::LXC::check_ct_modify_config_perm($rpcenv, $authuser, $vmid, $pool, [ keys %$param]);
212
213 my $storage = extract_param($param, 'storage') // 'local';
214
215 my $storage_cfg = cfs_read_file("storage.cfg");
216
217 my $ostemplate = extract_param($param, 'ostemplate');
218
219 my $archive;
220
221 if ($ostemplate eq '-') {
222 die "pipe requires cli environment\n"
223 if $rpcenv->{type} ne 'cli';
224 die "pipe can only be used with restore tasks\n"
225 if !$restore;
226 $archive = '-';
227 die "restore from pipe requires rootfs parameter\n" if !defined($param->{rootfs});
228 } else {
229 $rpcenv->check_volume_access($authuser, $storage_cfg, $vmid, $ostemplate);
230 $archive = PVE::Storage::abs_filesystem_path($storage_cfg, $ostemplate);
231 }
232
233 my $check_and_activate_storage = sub {
234 my ($sid) = @_;
235
236 my $scfg = PVE::Storage::storage_check_node($storage_cfg, $sid, $node);
237
238 raise_param_exc({ storage => "storage '$sid' does not support container directories"})
239 if !$scfg->{content}->{rootdir};
240
241 $rpcenv->check($authuser, "/storage/$sid", ['Datastore.AllocateSpace']);
242
243 PVE::Storage::activate_storage($storage_cfg, $sid);
244 };
245
246 my $conf = {};
247
248 my $no_disk_param = {};
249 foreach my $opt (keys %$param) {
250 my $value = $param->{$opt};
251 if ($opt eq 'rootfs' || $opt =~ m/^mp\d+$/) {
252 # allow to use simple numbers (add default storage in that case)
253 $param->{$opt} = "$storage:$value" if $value =~ m/^\d+(\.\d+)?$/;
254 } else {
255 $no_disk_param->{$opt} = $value;
256 }
257 }
258
259 # check storage access, activate storage
260 PVE::LXC::foreach_mountpoint($param, sub {
261 my ($ms, $mountpoint) = @_;
262
263 my $volid = $mountpoint->{volume};
264 my $mp = $mountpoint->{mp};
265
266 if ($mountpoint->{type} ne 'volume') { # bind or device
267 die "Only root can pass arbitrary filesystem paths.\n"
268 if $authuser ne 'root@pam';
269 } else {
270 my ($sid, $volname) = PVE::Storage::parse_volume_id($volid);
271 &$check_and_activate_storage($sid);
272 }
273 });
274
275 # check/activate default storage
276 &$check_and_activate_storage($storage) if !defined($param->{rootfs});
277
278 PVE::LXC::update_pct_config($vmid, $conf, 0, $no_disk_param);
279
280 $conf->{unprivileged} = 1 if $unprivileged;
281
282 my $check_vmid_usage = sub {
283 if ($force) {
284 die "can't overwrite running container\n"
285 if PVE::LXC::check_running($vmid);
286 } else {
287 PVE::Cluster::check_vmid_unused($vmid);
288 }
289 };
290
291 my $code = sub {
292 &$check_vmid_usage(); # final check after locking
293
294 PVE::Cluster::check_cfs_quorum();
295 my $vollist = [];
296
297 eval {
298 if (!defined($param->{rootfs})) {
299 if ($restore) {
300 my (undef, $disksize) = PVE::LXC::Create::recover_config($archive);
301 die "unable to detect disk size - please specify rootfs (size)\n"
302 if !$disksize;
303 $disksize /= 1024 * 1024 * 1024; # create_disks expects GB as unit size
304 $param->{rootfs} = "$storage:$disksize";
305 } else {
306 $param->{rootfs} = "$storage:4"; # defaults to 4GB
307 }
308 }
309
310 $vollist = PVE::LXC::create_disks($storage_cfg, $vmid, $param, $conf);
311
312 PVE::LXC::Create::create_rootfs($storage_cfg, $vmid, $conf, $archive, $password, $restore, $ignore_unpack_errors);
313 # set some defaults
314 $conf->{hostname} ||= "CT$vmid";
315 $conf->{memory} ||= 512;
316 $conf->{swap} //= 512;
317 PVE::LXC::create_config($vmid, $conf);
318 };
319 if (my $err = $@) {
320 PVE::LXC::destroy_disks($storage_cfg, $vollist);
321 PVE::LXC::destroy_config($vmid);
322 die $err;
323 }
324 PVE::AccessControl::add_vm_to_pool($vmid, $pool) if $pool;
325 };
326
327 my $realcmd = sub { PVE::LXC::lock_container($vmid, 1, $code); };
328
329 &$check_vmid_usage(); # first check before locking
330
331 return $rpcenv->fork_worker($restore ? 'vzrestore' : 'vzcreate',
332 $vmid, $authuser, $realcmd);
333
334 }});
335
336 __PACKAGE__->register_method({
337 name => 'vmdiridx',
338 path => '{vmid}',
339 method => 'GET',
340 proxyto => 'node',
341 description => "Directory index",
342 permissions => {
343 user => 'all',
344 },
345 parameters => {
346 additionalProperties => 0,
347 properties => {
348 node => get_standard_option('pve-node'),
349 vmid => get_standard_option('pve-vmid'),
350 },
351 },
352 returns => {
353 type => 'array',
354 items => {
355 type => "object",
356 properties => {
357 subdir => { type => 'string' },
358 },
359 },
360 links => [ { rel => 'child', href => "{subdir}" } ],
361 },
362 code => sub {
363 my ($param) = @_;
364
365 # test if VM exists
366 my $conf = PVE::LXC::load_config($param->{vmid});
367
368 my $res = [
369 { subdir => 'config' },
370 { subdir => 'status' },
371 { subdir => 'vncproxy' },
372 { subdir => 'vncwebsocket' },
373 { subdir => 'spiceproxy' },
374 { subdir => 'migrate' },
375 { subdir => 'clone' },
376 # { subdir => 'initlog' },
377 { subdir => 'rrd' },
378 { subdir => 'rrddata' },
379 { subdir => 'firewall' },
380 { subdir => 'snapshot' },
381 { subdir => 'resize' },
382 ];
383
384 return $res;
385 }});
386
387
388 __PACKAGE__->register_method({
389 name => 'rrd',
390 path => '{vmid}/rrd',
391 method => 'GET',
392 protected => 1, # fixme: can we avoid that?
393 permissions => {
394 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
395 },
396 description => "Read VM RRD statistics (returns PNG)",
397 parameters => {
398 additionalProperties => 0,
399 properties => {
400 node => get_standard_option('pve-node'),
401 vmid => get_standard_option('pve-vmid'),
402 timeframe => {
403 description => "Specify the time frame you are interested in.",
404 type => 'string',
405 enum => [ 'hour', 'day', 'week', 'month', 'year' ],
406 },
407 ds => {
408 description => "The list of datasources you want to display.",
409 type => 'string', format => 'pve-configid-list',
410 },
411 cf => {
412 description => "The RRD consolidation function",
413 type => 'string',
414 enum => [ 'AVERAGE', 'MAX' ],
415 optional => 1,
416 },
417 },
418 },
419 returns => {
420 type => "object",
421 properties => {
422 filename => { type => 'string' },
423 },
424 },
425 code => sub {
426 my ($param) = @_;
427
428 return PVE::Cluster::create_rrd_graph(
429 "pve2-vm/$param->{vmid}", $param->{timeframe},
430 $param->{ds}, $param->{cf});
431
432 }});
433
434 __PACKAGE__->register_method({
435 name => 'rrddata',
436 path => '{vmid}/rrddata',
437 method => 'GET',
438 protected => 1, # fixme: can we avoid that?
439 permissions => {
440 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
441 },
442 description => "Read VM RRD statistics",
443 parameters => {
444 additionalProperties => 0,
445 properties => {
446 node => get_standard_option('pve-node'),
447 vmid => get_standard_option('pve-vmid'),
448 timeframe => {
449 description => "Specify the time frame you are interested in.",
450 type => 'string',
451 enum => [ 'hour', 'day', 'week', 'month', 'year' ],
452 },
453 cf => {
454 description => "The RRD consolidation function",
455 type => 'string',
456 enum => [ 'AVERAGE', 'MAX' ],
457 optional => 1,
458 },
459 },
460 },
461 returns => {
462 type => "array",
463 items => {
464 type => "object",
465 properties => {},
466 },
467 },
468 code => sub {
469 my ($param) = @_;
470
471 return PVE::Cluster::create_rrd_data(
472 "pve2-vm/$param->{vmid}", $param->{timeframe}, $param->{cf});
473 }});
474
475 __PACKAGE__->register_method({
476 name => 'destroy_vm',
477 path => '{vmid}',
478 method => 'DELETE',
479 protected => 1,
480 proxyto => 'node',
481 description => "Destroy the container (also delete all uses files).",
482 permissions => {
483 check => [ 'perm', '/vms/{vmid}', ['VM.Allocate']],
484 },
485 parameters => {
486 additionalProperties => 0,
487 properties => {
488 node => get_standard_option('pve-node'),
489 vmid => get_standard_option('pve-vmid', { completion => \&PVE::LXC::complete_ctid_stopped }),
490 },
491 },
492 returns => {
493 type => 'string',
494 },
495 code => sub {
496 my ($param) = @_;
497
498 my $rpcenv = PVE::RPCEnvironment::get();
499
500 my $authuser = $rpcenv->get_user();
501
502 my $vmid = $param->{vmid};
503
504 # test if container exists
505 my $conf = PVE::LXC::load_config($vmid);
506
507 my $storage_cfg = cfs_read_file("storage.cfg");
508
509 PVE::LXC::check_protection($conf, "can't remove CT $vmid");
510
511 die "unable to remove CT $vmid - used in HA resources\n"
512 if PVE::HA::Config::vm_is_ha_managed($vmid);
513
514 my $running_error_msg = "unable to destroy CT $vmid - container is running\n";
515
516 die $running_error_msg if PVE::LXC::check_running($vmid); # check early
517
518 my $code = sub {
519 # reload config after lock
520 $conf = PVE::LXC::load_config($vmid);
521 PVE::LXC::check_lock($conf);
522
523 die $running_error_msg if PVE::LXC::check_running($vmid);
524
525 PVE::LXC::destroy_lxc_container($storage_cfg, $vmid, $conf);
526 PVE::AccessControl::remove_vm_access($vmid);
527 PVE::Firewall::remove_vmfw_conf($vmid);
528 };
529
530 my $realcmd = sub { PVE::LXC::lock_container($vmid, 1, $code); };
531
532 return $rpcenv->fork_worker('vzdestroy', $vmid, $authuser, $realcmd);
533 }});
534
535 my $sslcert;
536
537 __PACKAGE__->register_method ({
538 name => 'vncproxy',
539 path => '{vmid}/vncproxy',
540 method => 'POST',
541 protected => 1,
542 permissions => {
543 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
544 },
545 description => "Creates a TCP VNC proxy connections.",
546 parameters => {
547 additionalProperties => 0,
548 properties => {
549 node => get_standard_option('pve-node'),
550 vmid => get_standard_option('pve-vmid'),
551 websocket => {
552 optional => 1,
553 type => 'boolean',
554 description => "use websocket instead of standard VNC.",
555 },
556 },
557 },
558 returns => {
559 additionalProperties => 0,
560 properties => {
561 user => { type => 'string' },
562 ticket => { type => 'string' },
563 cert => { type => 'string' },
564 port => { type => 'integer' },
565 upid => { type => 'string' },
566 },
567 },
568 code => sub {
569 my ($param) = @_;
570
571 my $rpcenv = PVE::RPCEnvironment::get();
572
573 my $authuser = $rpcenv->get_user();
574
575 my $vmid = $param->{vmid};
576 my $node = $param->{node};
577
578 my $authpath = "/vms/$vmid";
579
580 my $ticket = PVE::AccessControl::assemble_vnc_ticket($authuser, $authpath);
581
582 $sslcert = PVE::Tools::file_get_contents("/etc/pve/pve-root-ca.pem", 8192)
583 if !$sslcert;
584
585 my ($remip, $family);
586
587 if ($node ne PVE::INotify::nodename()) {
588 ($remip, $family) = PVE::Cluster::remote_node_ip($node);
589 } else {
590 $family = PVE::Tools::get_host_address_family($node);
591 }
592
593 my $port = PVE::Tools::next_vnc_port($family);
594
595 # NOTE: vncterm VNC traffic is already TLS encrypted,
596 # so we select the fastest chipher here (or 'none'?)
597 my $remcmd = $remip ?
598 ['/usr/bin/ssh', '-t', $remip] : [];
599
600 my $conf = PVE::LXC::load_config($vmid, $node);
601 my $concmd = PVE::LXC::get_console_command($vmid, $conf);
602
603 my $shcmd = [ '/usr/bin/dtach', '-A',
604 "/var/run/dtach/vzctlconsole$vmid",
605 '-r', 'winch', '-z', @$concmd];
606
607 my $realcmd = sub {
608 my $upid = shift;
609
610 syslog ('info', "starting lxc vnc proxy $upid\n");
611
612 my $timeout = 10;
613
614 my $cmd = ['/usr/bin/vncterm', '-rfbport', $port,
615 '-timeout', $timeout, '-authpath', $authpath,
616 '-perm', 'VM.Console'];
617
618 if ($param->{websocket}) {
619 $ENV{PVE_VNC_TICKET} = $ticket; # pass ticket to vncterm
620 push @$cmd, '-notls', '-listen', 'localhost';
621 }
622
623 push @$cmd, '-c', @$remcmd, @$shcmd;
624
625 run_command($cmd);
626
627 return;
628 };
629
630 my $upid = $rpcenv->fork_worker('vncproxy', $vmid, $authuser, $realcmd);
631
632 PVE::Tools::wait_for_vnc_port($port);
633
634 return {
635 user => $authuser,
636 ticket => $ticket,
637 port => $port,
638 upid => $upid,
639 cert => $sslcert,
640 };
641 }});
642
643 __PACKAGE__->register_method({
644 name => 'vncwebsocket',
645 path => '{vmid}/vncwebsocket',
646 method => 'GET',
647 permissions => {
648 description => "You also need to pass a valid ticket (vncticket).",
649 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
650 },
651 description => "Opens a weksocket for VNC traffic.",
652 parameters => {
653 additionalProperties => 0,
654 properties => {
655 node => get_standard_option('pve-node'),
656 vmid => get_standard_option('pve-vmid'),
657 vncticket => {
658 description => "Ticket from previous call to vncproxy.",
659 type => 'string',
660 maxLength => 512,
661 },
662 port => {
663 description => "Port number returned by previous vncproxy call.",
664 type => 'integer',
665 minimum => 5900,
666 maximum => 5999,
667 },
668 },
669 },
670 returns => {
671 type => "object",
672 properties => {
673 port => { type => 'string' },
674 },
675 },
676 code => sub {
677 my ($param) = @_;
678
679 my $rpcenv = PVE::RPCEnvironment::get();
680
681 my $authuser = $rpcenv->get_user();
682
683 my $authpath = "/vms/$param->{vmid}";
684
685 PVE::AccessControl::verify_vnc_ticket($param->{vncticket}, $authuser, $authpath);
686
687 my $port = $param->{port};
688
689 return { port => $port };
690 }});
691
692 __PACKAGE__->register_method ({
693 name => 'spiceproxy',
694 path => '{vmid}/spiceproxy',
695 method => 'POST',
696 protected => 1,
697 proxyto => 'node',
698 permissions => {
699 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
700 },
701 description => "Returns a SPICE configuration to connect to the CT.",
702 parameters => {
703 additionalProperties => 0,
704 properties => {
705 node => get_standard_option('pve-node'),
706 vmid => get_standard_option('pve-vmid'),
707 proxy => get_standard_option('spice-proxy', { optional => 1 }),
708 },
709 },
710 returns => get_standard_option('remote-viewer-config'),
711 code => sub {
712 my ($param) = @_;
713
714 my $vmid = $param->{vmid};
715 my $node = $param->{node};
716 my $proxy = $param->{proxy};
717
718 my $authpath = "/vms/$vmid";
719 my $permissions = 'VM.Console';
720
721 my $conf = PVE::LXC::load_config($vmid);
722
723 die "CT $vmid not running\n" if !PVE::LXC::check_running($vmid);
724
725 my $concmd = PVE::LXC::get_console_command($vmid, $conf);
726
727 my $shcmd = ['/usr/bin/dtach', '-A',
728 "/var/run/dtach/vzctlconsole$vmid",
729 '-r', 'winch', '-z', @$concmd];
730
731 my $title = "CT $vmid";
732
733 return PVE::API2Tools::run_spiceterm($authpath, $permissions, $vmid, $node, $proxy, $title, $shcmd);
734 }});
735
736
737 __PACKAGE__->register_method({
738 name => 'migrate_vm',
739 path => '{vmid}/migrate',
740 method => 'POST',
741 protected => 1,
742 proxyto => 'node',
743 description => "Migrate the container to another node. Creates a new migration task.",
744 permissions => {
745 check => ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
746 },
747 parameters => {
748 additionalProperties => 0,
749 properties => {
750 node => get_standard_option('pve-node'),
751 vmid => get_standard_option('pve-vmid', { completion => \&PVE::LXC::complete_ctid }),
752 target => get_standard_option('pve-node', {
753 description => "Target node.",
754 completion => \&PVE::Cluster::complete_migration_target,
755 }),
756 online => {
757 type => 'boolean',
758 description => "Use online/live migration.",
759 optional => 1,
760 },
761 },
762 },
763 returns => {
764 type => 'string',
765 description => "the task ID.",
766 },
767 code => sub {
768 my ($param) = @_;
769
770 my $rpcenv = PVE::RPCEnvironment::get();
771
772 my $authuser = $rpcenv->get_user();
773
774 my $target = extract_param($param, 'target');
775
776 my $localnode = PVE::INotify::nodename();
777 raise_param_exc({ target => "target is local node."}) if $target eq $localnode;
778
779 PVE::Cluster::check_cfs_quorum();
780
781 PVE::Cluster::check_node_exists($target);
782
783 my $targetip = PVE::Cluster::remote_node_ip($target);
784
785 my $vmid = extract_param($param, 'vmid');
786
787 # test if VM exists
788 PVE::LXC::load_config($vmid);
789
790 # try to detect errors early
791 if (PVE::LXC::check_running($vmid)) {
792 die "can't migrate running container without --online\n"
793 if !$param->{online};
794 }
795
796 if (PVE::HA::Config::vm_is_ha_managed($vmid) && $rpcenv->{type} ne 'ha') {
797
798 my $hacmd = sub {
799 my $upid = shift;
800
801 my $service = "ct:$vmid";
802
803 my $cmd = ['ha-manager', 'migrate', $service, $target];
804
805 print "Executing HA migrate for CT $vmid to node $target\n";
806
807 PVE::Tools::run_command($cmd);
808
809 return;
810 };
811
812 return $rpcenv->fork_worker('hamigrate', $vmid, $authuser, $hacmd);
813
814 } else {
815
816 my $realcmd = sub {
817 my $upid = shift;
818
819 PVE::LXC::Migrate->migrate($target, $targetip, $vmid, $param);
820
821 return;
822 };
823
824 return $rpcenv->fork_worker('vzmigrate', $vmid, $authuser, $realcmd);
825 }
826 }});
827
828 __PACKAGE__->register_method({
829 name => 'vm_feature',
830 path => '{vmid}/feature',
831 method => 'GET',
832 proxyto => 'node',
833 protected => 1,
834 description => "Check if feature for virtual machine is available.",
835 permissions => {
836 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
837 },
838 parameters => {
839 additionalProperties => 0,
840 properties => {
841 node => get_standard_option('pve-node'),
842 vmid => get_standard_option('pve-vmid'),
843 feature => {
844 description => "Feature to check.",
845 type => 'string',
846 enum => [ 'snapshot' ],
847 },
848 snapname => get_standard_option('pve-lxc-snapshot-name', {
849 optional => 1,
850 }),
851 },
852 },
853 returns => {
854 type => "object",
855 properties => {
856 hasFeature => { type => 'boolean' },
857 #nodes => {
858 #type => 'array',
859 #items => { type => 'string' },
860 #}
861 },
862 },
863 code => sub {
864 my ($param) = @_;
865
866 my $node = extract_param($param, 'node');
867
868 my $vmid = extract_param($param, 'vmid');
869
870 my $snapname = extract_param($param, 'snapname');
871
872 my $feature = extract_param($param, 'feature');
873
874 my $conf = PVE::LXC::load_config($vmid);
875
876 if($snapname){
877 my $snap = $conf->{snapshots}->{$snapname};
878 die "snapshot '$snapname' does not exist\n" if !defined($snap);
879 $conf = $snap;
880 }
881 my $storage_cfg = PVE::Storage::config();
882 #Maybe include later
883 #my $nodelist = PVE::LXC::shared_nodes($conf, $storage_cfg);
884 my $hasFeature = PVE::LXC::has_feature($feature, $conf, $storage_cfg, $snapname);
885
886 return {
887 hasFeature => $hasFeature,
888 #nodes => [ keys %$nodelist ],
889 };
890 }});
891
892 __PACKAGE__->register_method({
893 name => 'template',
894 path => '{vmid}/template',
895 method => 'POST',
896 protected => 1,
897 proxyto => 'node',
898 description => "Create a Template.",
899 permissions => {
900 description => "You need 'VM.Allocate' permissions on /vms/{vmid}",
901 check => [ 'perm', '/vms/{vmid}', ['VM.Allocate']],
902 },
903 parameters => {
904 additionalProperties => 0,
905 properties => {
906 node => get_standard_option('pve-node'),
907 vmid => get_standard_option('pve-vmid', { completion => \&PVE::LXC::complete_ctid_stopped }),
908 },
909 },
910 returns => { type => 'null'},
911 code => sub {
912 my ($param) = @_;
913
914 my $rpcenv = PVE::RPCEnvironment::get();
915
916 my $authuser = $rpcenv->get_user();
917
918 my $node = extract_param($param, 'node');
919
920 my $vmid = extract_param($param, 'vmid');
921
922 my $updatefn = sub {
923
924 my $conf = PVE::LXC::load_config($vmid);
925 PVE::LXC::check_lock($conf);
926
927 die "unable to create template, because CT contains snapshots\n"
928 if $conf->{snapshots} && scalar(keys %{$conf->{snapshots}});
929
930 die "you can't convert a template to a template\n"
931 if PVE::LXC::is_template($conf);
932
933 die "you can't convert a CT to template if the CT is running\n"
934 if PVE::LXC::check_running($vmid);
935
936 my $realcmd = sub {
937 PVE::LXC::template_create($vmid, $conf);
938 };
939
940 $conf->{template} = 1;
941
942 PVE::LXC::write_config($vmid, $conf);
943 # and remove lxc config
944 PVE::LXC::update_lxc_config(undef, $vmid, $conf);
945
946 return $rpcenv->fork_worker('vztemplate', $vmid, $authuser, $realcmd);
947 };
948
949 PVE::LXC::lock_container($vmid, undef, $updatefn);
950
951 return undef;
952 }});
953
954 __PACKAGE__->register_method({
955 name => 'clone_vm',
956 path => '{vmid}/clone',
957 method => 'POST',
958 protected => 1,
959 proxyto => 'node',
960 description => "Create a container clone/copy",
961 permissions => {
962 description => "You need 'VM.Clone' permissions on /vms/{vmid}, " .
963 "and 'VM.Allocate' permissions " .
964 "on /vms/{newid} (or on the VM pool /pool/{pool}). You also need " .
965 "'Datastore.AllocateSpace' on any used storage.",
966 check =>
967 [ 'and',
968 ['perm', '/vms/{vmid}', [ 'VM.Clone' ]],
969 [ 'or',
970 [ 'perm', '/vms/{newid}', ['VM.Allocate']],
971 [ 'perm', '/pool/{pool}', ['VM.Allocate'], require_param => 'pool'],
972 ],
973 ]
974 },
975 parameters => {
976 additionalProperties => 0,
977 properties => {
978 node => get_standard_option('pve-node'),
979 vmid => get_standard_option('pve-vmid', { completion => \&PVE::LXC::complete_ctid }),
980 newid => get_standard_option('pve-vmid', {
981 completion => \&PVE::Cluster::complete_next_vmid,
982 description => 'VMID for the clone.' }),
983 hostname => {
984 optional => 1,
985 type => 'string', format => 'dns-name',
986 description => "Set a hostname for the new CT.",
987 },
988 description => {
989 optional => 1,
990 type => 'string',
991 description => "Description for the new CT.",
992 },
993 pool => {
994 optional => 1,
995 type => 'string', format => 'pve-poolid',
996 description => "Add the new CT to the specified pool.",
997 },
998 snapname => get_standard_option('pve-lxc-snapshot-name', {
999 optional => 1,
1000 }),
1001 storage => get_standard_option('pve-storage-id', {
1002 description => "Target storage for full clone.",
1003 requires => 'full',
1004 optional => 1,
1005 }),
1006 full => {
1007 optional => 1,
1008 type => 'boolean',
1009 description => "Create a full copy of all disk. This is always done when " .
1010 "you clone a normal CT. For CT templates, we try to create a linked clone by default.",
1011 default => 0,
1012 },
1013 # target => get_standard_option('pve-node', {
1014 # description => "Target node. Only allowed if the original VM is on shared storage.",
1015 # optional => 1,
1016 # }),
1017 },
1018 },
1019 returns => {
1020 type => 'string',
1021 },
1022 code => sub {
1023 my ($param) = @_;
1024
1025 my $rpcenv = PVE::RPCEnvironment::get();
1026
1027 my $authuser = $rpcenv->get_user();
1028
1029 my $node = extract_param($param, 'node');
1030
1031 my $vmid = extract_param($param, 'vmid');
1032
1033 my $newid = extract_param($param, 'newid');
1034
1035 my $pool = extract_param($param, 'pool');
1036
1037 if (defined($pool)) {
1038 $rpcenv->check_pool_exist($pool);
1039 }
1040
1041 my $snapname = extract_param($param, 'snapname');
1042
1043 my $storage = extract_param($param, 'storage');
1044
1045 my $localnode = PVE::INotify::nodename();
1046
1047 my $storecfg = PVE::Storage::config();
1048
1049 if ($storage) {
1050 # check if storage is enabled on local node
1051 PVE::Storage::storage_check_enabled($storecfg, $storage);
1052 }
1053
1054 PVE::Cluster::check_cfs_quorum();
1055
1056 my $running = PVE::LXC::check_running($vmid) || 0;
1057
1058 my $clonefn = sub {
1059
1060 # do all tests after lock
1061 # we also try to do all tests before we fork the worker
1062 my $conf = PVE::LXC::load_config($vmid);
1063
1064 PVE::LXC::check_lock($conf);
1065
1066 my $verify_running = PVE::LXC::check_running($vmid) || 0;
1067
1068 die "unexpected state change\n" if $verify_running != $running;
1069
1070 die "snapshot '$snapname' does not exist\n"
1071 if $snapname && !defined( $conf->{snapshots}->{$snapname});
1072
1073 my $oldconf = $snapname ? $conf->{snapshots}->{$snapname} : $conf;
1074
1075 my $conffile = PVE::LXC::config_file($newid);
1076 die "unable to create CT $newid: config file already exists\n"
1077 if -f $conffile;
1078
1079 my $newconf = { lock => 'clone' };
1080 my $mountpoints = {};
1081 my $fullclone = {};
1082 my $vollist = [];
1083
1084 foreach my $opt (keys %$oldconf) {
1085 my $value = $oldconf->{$opt};
1086
1087 # no need to copy unused images, because VMID(owner) changes anyways
1088 next if $opt =~ m/^unused\d+$/;
1089
1090 if (($opt eq 'rootfs') || ($opt =~ m/^mp\d+$/)) {
1091 my $mp = $opt eq 'rootfs' ?
1092 PVE::LXC::parse_ct_rootfs($value) :
1093 PVE::LXC::parse_ct_mountpoint($value);
1094
1095 if ($mp->{type} eq 'volume') {
1096 my $volid = $mp->{volume};
1097 if ($param->{full}) {
1098 die "fixme: full clone not implemented";
1099
1100 die "Full clone feature for '$volid' is not available\n"
1101 if !PVE::Storage::volume_has_feature($storecfg, 'copy', $volid, $snapname, $running);
1102 $fullclone->{$opt} = 1;
1103 } else {
1104 # not full means clone instead of copy
1105 die "Linked clone feature for '$volid' is not available\n"
1106 if !PVE::Storage::volume_has_feature($storecfg, 'clone', $volid, $snapname, $running);
1107 }
1108
1109 $mountpoints->{$opt} = $mp;
1110 push @$vollist, $volid;
1111
1112 } else {
1113 # TODO: allow bind mounts?
1114 die "unable to clone mountpint '$opt' (type $mp->{type})\n";
1115 }
1116
1117 } else {
1118 # copy everything else
1119 $newconf->{$opt} = $value;
1120 }
1121 }
1122
1123 delete $newconf->{template};
1124 if ($param->{hostname}) {
1125 $newconf->{hostname} = $param->{hostname};
1126 }
1127
1128 if ($param->{description}) {
1129 $newconf->{description} = $param->{description};
1130 }
1131
1132 # create empty/temp config - this fails if CT already exists on other node
1133 PVE::Tools::file_set_contents($conffile, "# ctclone temporary file\nlock: clone\n");
1134
1135 my $realcmd = sub {
1136 my $upid = shift;
1137
1138 my $newvollist = [];
1139
1140 eval {
1141 local $SIG{INT} = $SIG{TERM} = $SIG{QUIT} = $SIG{HUP} = sub { die "interrupted by signal\n"; };
1142
1143 PVE::Storage::activate_volumes($storecfg, $vollist, $snapname);
1144
1145 foreach my $opt (keys %$mountpoints) {
1146 my $mp = $mountpoints->{$opt};
1147 my $volid = $mp->{volume};
1148
1149 if ($fullclone->{$opt}) {
1150 die "fixme: full clone not implemented\n";
1151 } else {
1152 print "create linked clone of mountpoint $opt ($volid)\n";
1153 my $newvolid = PVE::Storage::vdisk_clone($storecfg, $volid, $newid, $snapname);
1154 push @$newvollist, $newvolid;
1155 $mp->{volume} = $newvolid;
1156
1157 $newconf->{$opt} = PVE::LXC::print_ct_mountpoint($mp, $opt eq 'rootfs');
1158 PVE::LXC::write_config($newid, $newconf);
1159 }
1160 }
1161
1162 delete $newconf->{lock};
1163 PVE::LXC::write_config($newid, $newconf);
1164
1165 PVE::AccessControl::add_vm_to_pool($newid, $pool) if $pool;
1166 };
1167 if (my $err = $@) {
1168 unlink $conffile;
1169
1170 sleep 1; # some storage like rbd need to wait before release volume - really?
1171
1172 foreach my $volid (@$newvollist) {
1173 eval { PVE::Storage::vdisk_free($storecfg, $volid); };
1174 warn $@ if $@;
1175 }
1176 die "clone failed: $err";
1177 }
1178
1179 return;
1180 };
1181
1182 PVE::Firewall::clone_vmfw_conf($vmid, $newid);
1183
1184 return $rpcenv->fork_worker('vzclone', $vmid, $authuser, $realcmd);
1185
1186 };
1187
1188 return PVE::LXC::lock_container($vmid, undef, $clonefn);
1189 }});
1190
1191
1192 __PACKAGE__->register_method({
1193 name => 'resize_vm',
1194 path => '{vmid}/resize',
1195 method => 'PUT',
1196 protected => 1,
1197 proxyto => 'node',
1198 description => "Resize a container mountpoint.",
1199 permissions => {
1200 check => ['perm', '/vms/{vmid}', ['VM.Config.Disk'], any => 1],
1201 },
1202 parameters => {
1203 additionalProperties => 0,
1204 properties => {
1205 node => get_standard_option('pve-node'),
1206 vmid => get_standard_option('pve-vmid', { completion => \&PVE::LXC::complete_ctid }),
1207 disk => {
1208 type => 'string',
1209 description => "The disk you want to resize.",
1210 enum => [PVE::LXC::mountpoint_names()],
1211 },
1212 size => {
1213 type => 'string',
1214 pattern => '\+?\d+(\.\d+)?[KMGT]?',
1215 description => "The new size. With the '+' sign the value is added to the actual size of the volume and without it, the value is taken as an absolute one. Shrinking disk size is not supported.",
1216 },
1217 digest => {
1218 type => 'string',
1219 description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
1220 maxLength => 40,
1221 optional => 1,
1222 }
1223 },
1224 },
1225 returns => {
1226 type => 'string',
1227 description => "the task ID.",
1228 },
1229 code => sub {
1230 my ($param) = @_;
1231
1232 my $rpcenv = PVE::RPCEnvironment::get();
1233
1234 my $authuser = $rpcenv->get_user();
1235
1236 my $node = extract_param($param, 'node');
1237
1238 my $vmid = extract_param($param, 'vmid');
1239
1240 my $digest = extract_param($param, 'digest');
1241
1242 my $sizestr = extract_param($param, 'size');
1243 my $ext = ($sizestr =~ s/^\+//);
1244 my $newsize = PVE::JSONSchema::parse_size($sizestr);
1245 die "invalid size string" if !defined($newsize);
1246
1247 die "no options specified\n" if !scalar(keys %$param);
1248
1249 PVE::LXC::check_ct_modify_config_perm($rpcenv, $authuser, $vmid, undef, [keys %$param]);
1250
1251 my $storage_cfg = cfs_read_file("storage.cfg");
1252
1253 my $code = sub {
1254
1255 my $conf = PVE::LXC::load_config($vmid);
1256 PVE::LXC::check_lock($conf);
1257
1258 PVE::Tools::assert_if_modified($digest, $conf->{digest});
1259
1260 my $running = PVE::LXC::check_running($vmid);
1261
1262 my $disk = $param->{disk};
1263 my $mp = $disk eq 'rootfs' ? PVE::LXC::parse_ct_rootfs($conf->{$disk}) :
1264 PVE::LXC::parse_ct_mountpoint($conf->{$disk});
1265
1266 my $volid = $mp->{volume};
1267
1268 my (undef, undef, $owner, undef, undef, undef, $format) =
1269 PVE::Storage::parse_volname($storage_cfg, $volid);
1270
1271 die "can't resize mountpoint owned by another container ($owner)"
1272 if $vmid != $owner;
1273
1274 die "can't resize volume: $disk if snapshot exists\n"
1275 if %{$conf->{snapshots}} && $format eq 'qcow2';
1276
1277 my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid);
1278
1279 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
1280
1281 my $size = PVE::Storage::volume_size_info($storage_cfg, $volid, 5);
1282 $newsize += $size if $ext;
1283 $newsize = int($newsize);
1284
1285 die "unable to shrink disk size\n" if $newsize < $size;
1286
1287 return if $size == $newsize;
1288
1289 PVE::Cluster::log_msg('info', $authuser, "update CT $vmid: resize --disk $disk --size $sizestr");
1290 my $realcmd = sub {
1291 # Note: PVE::Storage::volume_resize doesn't do anything if $running=1, so
1292 # we pass 0 here (parameter only makes sense for qemu)
1293 PVE::Storage::volume_resize($storage_cfg, $volid, $newsize, 0);
1294
1295 $mp->{size} = $newsize;
1296 $conf->{$disk} = PVE::LXC::print_ct_mountpoint($mp, $disk eq 'rootfs');
1297
1298 PVE::LXC::write_config($vmid, $conf);
1299
1300 if ($format eq 'raw') {
1301 my $path = PVE::Storage::path($storage_cfg, $volid, undef);
1302 if ($running) {
1303
1304 $mp->{mp} = '/';
1305 my $use_loopdev = (PVE::LXC::mountpoint_mount_path($mp, $storage_cfg))[1];
1306 $path = PVE::LXC::query_loopdev($path) if $use_loopdev;
1307 die "internal error: CT running but mountpoint not attached to a loop device"
1308 if !$path;
1309 PVE::Tools::run_command(['losetup', '--set-capacity', $path]) if $use_loopdev;
1310
1311 # In order for resize2fs to know that we need online-resizing a mountpoint needs
1312 # to be visible to it in its namespace.
1313 # To not interfere with the rest of the system we unshare the current mount namespace,
1314 # mount over /tmp and then run resize2fs.
1315
1316 # interestingly we don't need to e2fsck on mounted systems...
1317 my $quoted = PVE::Tools::shellquote($path);
1318 my $cmd = "mount --make-rprivate / && mount $quoted /tmp && resize2fs $quoted";
1319 PVE::Tools::run_command(['unshare', '-m', '--', 'sh', '-c', $cmd]);
1320 } else {
1321 PVE::Tools::run_command(['e2fsck', '-f', '-y', $path]);
1322 PVE::Tools::run_command(['resize2fs', $path]);
1323 }
1324 }
1325 };
1326
1327 return $rpcenv->fork_worker('resize', $vmid, $authuser, $realcmd);
1328 };
1329
1330 return PVE::LXC::lock_container($vmid, undef, $code);;
1331 }});
1332
1333 1;