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