use POSIX qw(EINTR);
use File::Path;
+use File::Spec;
+use Cwd qw();
use Fcntl ':flock';
use PVE::Cluster qw(cfs_register_file cfs_read_file);
use PVE::SafeSyslog;
use PVE::INotify;
use PVE::JSONSchema qw(get_standard_option);
-use PVE::Tools qw($IPV6RE $IPV4RE);
+use PVE::Tools qw($IPV6RE $IPV4RE dir_glob_foreach);
use PVE::Network;
+use PVE::AccessControl;
+use PVE::ProcFSTools;
use Data::Dumper;
cfs_register_file('/lxc/', \&parse_pct_config, \&write_pct_config);
-PVE::JSONSchema::register_format('pve-lxc-network', \&verify_lxc_network);
-sub verify_lxc_network {
- my ($value, $noerr) = @_;
-
- return $value if parse_lxc_network($value);
-
- return undef if $noerr;
-
- die "unable to parse network setting\n";
-}
-
-PVE::JSONSchema::register_format('pve-ct-mountpoint', \&verify_ct_mountpoint);
-sub verify_ct_mountpoint {
- my ($value, $noerr) = @_;
-
- return $value if parse_ct_mountpoint($value);
-
- return undef if $noerr;
-
- die "unable to parse CT mountpoint options\n";
-}
+my $rootfs_desc = {
+ volume => {
+ type => 'string',
+ default_key => 1,
+ format_description => 'volume',
+ description => 'Volume, device or directory to mount into the container.',
+ },
+ backup => {
+ type => 'boolean',
+ format_description => '[1|0]',
+ description => 'Whether to include the mountpoint in backups.',
+ optional => 1,
+ },
+ size => {
+ type => 'string',
+ format_description => 'DiskSize',
+ pattern => '\d+[TGMK]?',
+ description => 'Volume size (read only value).',
+ optional => 1,
+ },
+};
PVE::JSONSchema::register_standard_option('pve-ct-rootfs', {
- type => 'string', format => 'pve-ct-mountpoint',
- typetext => '[volume=]volume,] [,backup=yes|no] [,size=\d+]',
+ type => 'string', format => $rootfs_desc,
description => "Use volume as container root.",
optional => 1,
});
+PVE::JSONSchema::register_standard_option('pve-lxc-snapshot-name', {
+ description => "The name of the snapshot.",
+ type => 'string', format => 'pve-configid',
+ maxLength => 40,
+});
+
my $confdesc = {
lock => {
optional => 1,
ostype => {
optional => 1,
type => 'string',
- enum => ['debian', 'ubuntu', 'centos'],
+ enum => ['debian', 'ubuntu', 'centos', 'archlinux'],
description => "OS type. Corresponds to lxc setup scripts in /usr/share/lxc/config/<ostype>.common.conf.",
},
+ console => {
+ optional => 1,
+ type => 'boolean',
+ description => "Attach a console device (/dev/console) to the container.",
+ default => 1,
+ },
tty => {
optional => 1,
type => 'integer',
description => "Specify the number of tty available to the container",
minimum => 0,
maximum => 6,
- default => 4,
+ default => 2,
},
cpulimit => {
optional => 1,
hostname => {
optional => 1,
description => "Set a host name for the container.",
- type => 'string',
+ type => 'string', format => 'dns-name',
maxLength => 255,
},
description => {
},
searchdomain => {
optional => 1,
- type => 'string',
+ type => 'string', format => 'dns-name-list',
description => "Sets DNS search domains for a container. Create will automatically use the setting from the host if you neither set searchdomain or nameserver.",
},
nameserver => {
optional => 1,
- type => 'string',
+ type => 'string', format => 'address-list',
description => "Sets DNS server IP address for a container. Create will automatically use the setting from the host if you neither set searchdomain or nameserver.",
},
rootfs => get_standard_option('pve-ct-rootfs'),
type => 'integer',
minimum => 0,
},
+ cmode => {
+ optional => 1,
+ description => "Console mode. By default, the console command tries to open a connection to one of the available tty devices. By setting cmode to 'console' it tries to attach to /dev/console instead. If you set cmode to 'shell', it simply invokes a shell inside the container (no login).",
+ type => 'string',
+ enum => ['shell', 'console', 'tty'],
+ default => 'tty',
+ },
+ protection => {
+ optional => 1,
+ type => 'boolean',
+ description => "Sets the protection flag of the container. This will prevent the remove operation. This will prevent the CT or CT's disk remove/update operation.",
+ default => 0,
+ },
};
my $valid_lxc_conf_keys = {
'lxc.' => 1,
};
+my $netconf_desc = {
+ type => {
+ type => 'string',
+ optional => 1,
+ description => "Network interface type.",
+ enum => [qw(veth)],
+ },
+ name => {
+ type => 'string',
+ format_description => 'String',
+ description => 'Name of the network device as seen from inside the container. (lxc.network.name)',
+ pattern => '[-_.\w\d]+',
+ },
+ bridge => {
+ type => 'string',
+ format_description => 'vmbr<Number>',
+ description => 'Bridge to attach the network device to.',
+ pattern => '[-_.\w\d]+',
+ },
+ hwaddr => {
+ type => 'string',
+ format_description => 'MAC',
+ description => 'Bridge to attach the network device to. (lxc.network.hwaddr)',
+ pattern => qr/(?:[a-f0-9]{2}:){5}[a-f0-9]{2}/i,
+ optional => 1,
+ },
+ mtu => {
+ type => 'integer',
+ format_description => 'Number',
+ description => 'Maximum transfer unit of the interface. (lxc.network.mtu)',
+ optional => 1,
+ },
+ ip => {
+ type => 'string',
+ format => 'pve-ipv4-config',
+ format_description => 'IPv4Format/CIDR',
+ description => 'IPv4 address in CIDR format.',
+ optional => 1,
+ },
+ gw => {
+ type => 'string',
+ format => 'ipv4',
+ format_description => 'GatewayIPv4',
+ description => 'Default gateway for IPv4 traffic.',
+ optional => 1,
+ },
+ ip6 => {
+ type => 'string',
+ format => 'pve-ipv6-config',
+ format_description => 'IPv6Format/CIDR',
+ description => 'IPv6 address in CIDR format.',
+ optional => 1,
+ },
+ gw6 => {
+ type => 'string',
+ format => 'ipv6',
+ format_description => 'GatewayIPv6',
+ description => 'Default gateway for IPv6 traffic.',
+ optional => 1,
+ },
+ firewall => {
+ type => 'boolean',
+ format_description => '[1|0]',
+ description => "Controls whether this interface's firewall rules should be used.",
+ optional => 1,
+ },
+ tag => {
+ type => 'integer',
+ format_description => 'VlanNo',
+ minimum => '2',
+ maximum => '4094',
+ description => "VLAN tag foro this interface.",
+ optional => 1,
+ },
+};
+PVE::JSONSchema::register_format('pve-lxc-network', $netconf_desc);
+
my $MAX_LXC_NETWORKS = 10;
for (my $i = 0; $i < $MAX_LXC_NETWORKS; $i++) {
$confdesc->{"net$i"} = {
optional => 1,
- type => 'string', format => 'pve-lxc-network',
- description => "Specifies network interfaces for the container.\n\n".
- "The string should have the follow format:\n\n".
- "-net<[0-9]> bridge=<vmbr<Nummber>>[,hwaddr=<MAC>]\n".
- "[,mtu=<Number>][,name=<String>][,ip=<IPv4Format/CIDR>]\n".
- ",ip6=<IPv6Format/CIDR>][,gw=<GatwayIPv4>]\n".
- ",gw6=<GatwayIPv6>][,firewall=<[1|0]>][,tag=<VlanNo>]",
+ type => 'string', format => $netconf_desc,
+ description => "Specifies network interfaces for the container.",
+ };
+}
+
+my $mp_desc = {
+ %$rootfs_desc,
+ mp => {
+ type => 'string',
+ format_description => 'Path',
+ description => 'Path to the mountpoint as seen from inside the container.',
+ optional => 1,
+ },
+};
+PVE::JSONSchema::register_format('pve-ct-mountpoint', $mp_desc);
+
+my $MAX_MOUNT_POINTS = 10;
+for (my $i = 0; $i < $MAX_MOUNT_POINTS; $i++) {
+ $confdesc->{"mp$i"} = {
+ optional => 1,
+ type => 'string', format => $mp_desc,
+ description => "Use volume as container mount point (experimental feature).",
+ optional => 1,
};
}
foreach my $key (sort keys %$conf) {
next if $key eq 'digest' || $key eq 'description' || $key eq 'pending' ||
$key eq 'snapshots' || $key eq 'snapname' || $key eq 'lxc';
- $raw .= "$key: $conf->{$key}\n";
+ my $value = $conf->{$key};
+ die "detected invalid newline inside property '$key'\n" if $value =~ m/\n/;
+ $raw .= "$key: $value\n";
}
if (my $lxcconf = $conf->{lxc}) {
next;
}
- if ($line =~ m/^(lxc\.[a-z0-9\.]+)(:|\s*=)\s*(.*?)\s*$/) {
+ if ($line =~ m/^(lxc\.[a-z0-9_\-\.]+)(:|\s*=)\s*(.*?)\s*$/) {
my $key = $1;
my $value = $3;
if ($valid_lxc_conf_keys->{$key} || $key =~ m/^lxc\.cgroup\./) {
$descr .= PVE::Tools::decode_text($2);
} elsif ($line =~ m/snapstate:\s*(prepare|delete)\s*$/) {
$conf->{snapstate} = $1;
- } elsif ($line =~ m/^([a-z][a-z_]*\d*):\s*(\S+)\s*$/) {
+ } elsif ($line =~ m/^([a-z][a-z_]*\d*):\s*(\S.*)\s*$/) {
my $key = $1;
my $value = $2;
eval { $value = check_type($key, $value); };
}
sub load_config {
- my ($vmid) = @_;
+ my ($vmid, $node) = @_;
- my $cfspath = cfs_config_path($vmid);
+ $node = $nodename if !$node;
+ my $cfspath = cfs_config_path($vmid, $node);
my $conf = PVE::Cluster::cfs_read_file($cfspath);
die "container $vmid does not exists\n" if !defined($conf);
sub lock_filename {
my ($vmid) = @_;
- return "$lockdir/pve-config-{$vmid}.lock";
+ return "$lockdir/pve-config-${vmid}.lock";
}
sub lock_aquire {
die "can't aquire lock - $!\n";
}
- $lock_handles->{$$}->{$filename}->{refcount}++;
-
print STDERR " OK\n";
}
+
+ $lock_handles->{$$}->{$filename}->{refcount}++;
};
eval { PVE::Tools::run_with_timeout($timeout, $lock_func); };
my $d = $list->{$vmid};
next if $d->{status} ne 'running';
- $d->{uptime} = 100; # fixme:
+ my $pid = find_lxc_pid($vmid);
+ my $ctime = (stat("/proc/$pid"))[10]; # 10 = ctime
+ $d->{uptime} = time - $ctime; # the method lxcfs uses
$d->{mem} = read_cgroup_value('memory', $vmid, 'memory.usage_in_bytes');
$d->{swap} = read_cgroup_value('memory', $vmid, 'memory.memsw.usage_in_bytes') - $d->{mem};
return int($size);
};
+my $format_size = sub {
+ my ($size) = @_;
+
+ $size = int($size);
+
+ my $kb = int($size/1024);
+ return $size if $kb*1024 != $size;
+
+ my $mb = int($kb/1024);
+ return "${kb}K" if $mb*1024 != $kb;
+
+ my $gb = int($mb/1024);
+ return "${mb}M" if $gb*1024 != $mb;
+
+ return "${gb}G";
+};
+
sub parse_ct_mountpoint {
my ($data) = @_;
$data //= '';
- my $res = {};
-
- foreach my $p (split (/,/, $data)) {
- next if $p =~ m/^\s*$/;
-
- if ($p =~ m/^(volume|backup|size)=(.+)$/) {
- my ($k, $v) = ($1, $2);
- return undef if defined($res->{$k});
- $res->{$k} = $v;
- } else {
- if (!$res->{volume} && $p !~ m/=/) {
- $res->{volume} = $p;
- } else {
- return undef;
- }
- }
+ my $res;
+ eval { $res = PVE::JSONSchema::parse_property_string($mp_desc, $data) };
+ if ($@) {
+ warn $@;
+ return undef;
}
- return undef if !$res->{volume};
-
- return undef if $res->{backup} && $res->{backup} !~ m/^(yes|no)$/;
+ return undef if !defined($res->{volume});
if ($res->{size}) {
return undef if !defined($res->{size} = &$parse_size($res->{size}));
}
sub print_ct_mountpoint {
- my ($info) = @_;
+ my ($info, $nomp) = @_;
my $opts = '';
die "missing volume\n" if !$info->{volume};
- foreach my $o ('size', 'backup') {
- $opts .= ",$o=info->{$o}" if defined($info->{$o});
+ foreach my $o (qw(backup)) {
+ $opts .= ",$o=$info->{$o}" if defined($info->{$o});
}
+ if ($info->{size}) {
+ $opts .= ",size=" . &$format_size($info->{size});
+ }
+
+ $opts .= ",mp=$info->{mp}" if !$nomp;
+
return "$info->{volume}$opts";
}
return $res if !$data;
- foreach my $pv (split (/,/, $data)) {
- if ($pv =~ m/^(bridge|hwaddr|mtu|name|ip|ip6|gw|gw6|firewall|tag)=(\S+)$/) {
- $res->{$1} = $2;
- } else {
- return undef;
- }
+ eval { $res = PVE::JSONSchema::parse_property_string($netconf_desc, $data) };
+ if ($@) {
+ warn $@;
+ return undef;
}
$res->{type} = 'veth';
die "VM is locked ($conf->{'lock'})\n" if $conf->{'lock'};
}
+sub check_protection {
+ my ($vm_conf, $err_msg) = @_;
+
+ if ($vm_conf->{protection}) {
+ die "$err_msg - protection mode enabled\n";
+ }
+}
+
sub update_lxc_config {
my ($storage_cfg, $vmid, $conf) = @_;
$raw .= "lxc.arch = $conf->{arch}\n";
my $ostype = $conf->{ostype} || die "missing 'ostype' - internal error";
- if ($ostype eq 'debian' || $ostype eq 'ubuntu' || $ostype eq 'centos') {
+ if ($ostype =~ /^(?:debian | ubuntu | centos | archlinux)$/x) {
$raw .= "lxc.include = /usr/share/lxc/config/$ostype.common.conf\n";
} else {
die "implement me";
}
- my $ttycount = $conf->{tty} // 4;
+ if (!has_dev_console($conf)) {
+ $raw .= "lxc.console = none\n";
+ $raw .= "lxc.cgroup.devices.deny = c 5:1 rwm\n";
+ }
+
+ my $ttycount = get_tty_count($conf);
$raw .= "lxc.tty = $ttycount\n";
+ # some init scripts expects a linux terminal (turnkey).
+ $raw .= "lxc.environment = TERM=linux\n";
+
my $utsname = $conf->{hostname} || "CT$vmid";
$raw .= "lxc.utsname = $utsname\n";
my $shares = $conf->{cpuunits} || 1024;
$raw .= "lxc.cgroup.cpu.shares = $shares\n";
- my $rootinfo = PVE::LXC::parse_ct_mountpoint($conf->{rootfs});
- my $volid = $rootinfo->{volume};
- my ($storage, $volname) = PVE::Storage::parse_volume_id($volid);
-
- my $scfg = PVE::Storage::storage_config($storage_cfg, $storage);
- if ($scfg->{type} eq 'dir' || $scfg->{type} eq 'nfs') {
- my $rootfs = PVE::Storage::path($storage_cfg, $volid);
- $raw .= "lxc.rootfs = loop:$rootfs\n";
- } elsif ($scfg->{type} eq 'zfspool') {
- my $rootfs = PVE::Storage::path($storage_cfg, $volid);
- $raw .= "lxc.rootfs = $rootfs\n";
- } elsif ($scfg->{type} eq 'drbd' || $scfg->{type} eq 'rbd') {
- my $rootdev = PVE::Storage::path($storage_cfg, $volid);
- $raw .= "lxc.rootfs = $rootdev\n";
- } else {
- die "unsupported storage type '$scfg->{type}'\n";
- }
+ my $mountpoint = parse_ct_mountpoint($conf->{rootfs});
+ $mountpoint->{mp} = '/';
+
+ my ($path, $use_loopdev) = mountpoint_mount_path($mountpoint, $storage_cfg);
+ $path = "loop:$path" if $use_loopdev;
+
+ $raw .= "lxc.rootfs = $path\n";
my $netcount = 0;
foreach my $k (keys %$conf) {
my @nohotplug;
+ my $new_disks = 0;
+
my $rootdir;
if ($running) {
my $pid = find_lxc_pid($vmid);
write_cgroup_value("memory", $vmid, "memory.memsw.limit_in_bytes", -1);
} elsif ($opt eq 'description' || $opt eq 'onboot' || $opt eq 'startup') {
delete $conf->{$opt};
- } elsif ($opt eq 'nameserver' || $opt eq 'searchdomain') {
+ } elsif ($opt eq 'nameserver' || $opt eq 'searchdomain' ||
+ $opt eq 'tty' || $opt eq 'console' || $opt eq 'cmode') {
delete $conf->{$opt};
push @nohotplug, $opt;
next if $running;
next if !$running;
my $netid = $1;
PVE::Network::veth_delete("veth${vmid}i$netid");
+ } elsif ($opt eq 'protection') {
+ delete $conf->{$opt};
+ } elsif ($opt =~ m/^mp(\d+)$/) {
+ check_protection($conf, "can't remove CT $vmid drive '$opt'");
+ delete $conf->{$opt};
+ push @nohotplug, $opt;
+ next if $running;
} else {
die "implement me"
}
- PVE::LXC::write_config($vmid, $conf) if $running;
+ write_config($vmid, $conf) if $running;
}
}
$conf->{memory} = $wanted_memory;
$conf->{swap} = $wanted_swap;
- PVE::LXC::write_config($vmid, $conf) if $running;
+ write_config($vmid, $conf) if $running;
}
foreach my $opt (keys %$param) {
$conf->{$opt} = $value ? 1 : 0;
} elsif ($opt eq 'startup') {
$conf->{$opt} = $value;
- } elsif ($opt eq 'tty') {
+ } elsif ($opt eq 'tty' || $opt eq 'console' || $opt eq 'cmode') {
$conf->{$opt} = $value;
push @nohotplug, $opt;
next if $running;
} else {
update_net($vmid, $conf, $opt, $net, $netid, $rootdir);
}
+ } elsif ($opt eq 'protection') {
+ $conf->{$opt} = $value ? 1 : 0;
+ } elsif ($opt =~ m/^mp(\d+)$/) {
+ check_protection($conf, "can't update CT $vmid drive '$opt'");
+ $conf->{$opt} = $value;
+ $new_disks = 1;
+ push @nohotplug, $opt;
+ next;
+ } elsif ($opt eq 'rootfs') {
+ check_protection($conf, "can't update CT $vmid drive '$opt'");
+ die "implement me: $opt";
} else {
die "implement me: $opt";
}
- PVE::LXC::write_config($vmid, $conf) if $running;
+ write_config($vmid, $conf) if $running;
}
if ($running && scalar(@nohotplug)) {
die "unable to modify " . join(',', @nohotplug) . " while container is running\n";
}
+
+ if ($new_disks) {
+ my $storage_cfg = PVE::Storage::config();
+ create_disks($storage_cfg, $vmid, $conf, $conf);
+ }
+}
+
+sub has_dev_console {
+ my ($conf) = @_;
+
+ return !(defined($conf->{console}) && !$conf->{console});
+}
+
+sub get_tty_count {
+ my ($conf) = @_;
+
+ return $conf->{tty} // $confdesc->{tty}->{default};
+}
+
+sub get_cmode {
+ my ($conf) = @_;
+
+ return $conf->{cmode} // $confdesc->{cmode}->{default};
+}
+
+sub get_console_command {
+ my ($vmid, $conf) = @_;
+
+ my $cmode = get_cmode($conf);
+
+ if ($cmode eq 'console') {
+ return ['lxc-console', '-n', $vmid, '-t', 0];
+ } elsif ($cmode eq 'tty') {
+ return ['lxc-console', '-n', $vmid];
+ } elsif ($cmode eq 'shell') {
+ return ['lxc-attach', '--clear-env', '-n', $vmid];
+ } else {
+ die "internal error";
+ }
}
sub get_primary_ips {
sub destroy_lxc_container {
my ($storage_cfg, $vmid, $conf) = @_;
- my $rootinfo = PVE::LXC::parse_ct_mountpoint($conf->{rootfs});
- if (defined($rootinfo->{volume})) {
- my ($vtype, $name, $owner) = PVE::Storage::parse_volname($storage_cfg, $rootinfo->{volume});
- PVE::Storage::vdisk_free($storage_cfg, $rootinfo->{volume}) if $vmid == $owner;;
- }
+ foreach_mountpoint($conf, sub {
+ my ($ms, $mountpoint) = @_;
+
+ # skip bind mounts and block devices
+ if ($mountpoint->{volume} =~ m|^/|) {
+ return;
+ }
+
+ my ($vtype, $name, $owner) = PVE::Storage::parse_volname($storage_cfg, $mountpoint->{volume});
+ PVE::Storage::vdisk_free($storage_cfg, $mountpoint->{volume}) if $vmid == $owner;
+ });
+
rmdir "/var/lib/lxc/$vmid/rootfs";
unlink "/var/lib/lxc/$vmid/config";
rmdir "/var/lib/lxc/$vmid";
}
sub vm_stop_cleanup {
- my ($storeage_cfg, $vmid, $conf, $keepActive) = @_;
+ my ($storage_cfg, $vmid, $conf, $keepActive) = @_;
eval {
if (!$keepActive) {
- my $rootinfo = PVE::LXC::parse_ct_mountpoint($conf->{rootfs});
- PVE::Storage::deactivate_volumes($storeage_cfg, [$rootinfo->{volume}]);
+
+ my $vollist = get_vm_volumes($conf);
+ PVE::Storage::deactivate_volumes($storage_cfg, $vollist);
}
};
warn $@ if $@; # avoid errors - just warn
PVE::Network::veth_delete($veth);
delete $conf->{$opt};
- PVE::LXC::write_config($vmid, $conf);
+ write_config($vmid, $conf);
hotplug_net($vmid, $conf, $opt, $newnet, $netid);
delete $oldnet->{$_};
}
$conf->{$opt} = print_lxc_network($oldnet);
- PVE::LXC::write_config($vmid, $conf);
+ write_config($vmid, $conf);
}
PVE::Network::tap_plug($veth, $newnet->{bridge}, $newnet->{tag}, $newnet->{firewall});
$oldnet->{$_} = $newnet->{$_} if $newnet->{$_};
}
$conf->{$opt} = print_lxc_network($oldnet);
- PVE::LXC::write_config($vmid, $conf);
+ write_config($vmid, $conf);
}
} else {
hotplug_net($vmid, $conf, $opt, $newnet, $netid);
}
$conf->{$opt} = print_lxc_network($done);
- PVE::LXC::write_config($vmid, $conf);
+ write_config($vmid, $conf);
}
sub update_ipconfig {
my ($vmid, $conf, $opt, $eth, $newnet, $rootdir) = @_;
- my $lxc_setup = PVE::LXCSetup->new($conf, $rootdir);
+ my $lxc_setup = PVE::LXC::Setup->new($conf, $rootdir);
my $optdata = parse_lxc_network($conf->{$opt});
my $deleted = [];
}
}
$conf->{$opt} = print_lxc_network($optdata);
- PVE::LXC::write_config($vmid, $conf);
+ write_config($vmid, $conf);
$lxc_setup->setup_network($conf);
};
$snap->{'description'} = $comment if $comment;
$conf->{snapshots}->{$snapname} = $snap;
- PVE::LXC::write_config($vmid, $conf);
+ write_config($vmid, $conf);
};
lock_container($vmid, 10, $updatefn);
delete $conf->{lock};
$conf->{parent} = $snapname;
- PVE::LXC::write_config($vmid, $conf);
+ write_config($vmid, $conf);
};
lock_container($vmid, 10 ,$updatefn);
sub has_feature {
my ($feature, $conf, $storecfg, $snapname) = @_;
- #Fixme add other drives if necessary.
my $err;
- my $rootinfo = PVE::LXC::parse_ct_mountpoint($conf->{rootfs});
- $err = 1 if !PVE::Storage::volume_has_feature($storecfg, $feature, $rootinfo->{volume}, $snapname);
+ foreach_mountpoint($conf, sub {
+ my ($ms, $mountpoint) = @_;
+
+ return if $err; # skip further test
+
+ $err = 1 if !PVE::Storage::volume_has_feature($storecfg, $feature, $mountpoint->{volume}, $snapname);
+
+ # TODO: implement support for mountpoints
+ die "unable to handle mountpoint '$ms' - feature not implemented\n"
+ if $ms ne 'rootfs';
+ });
return $err ? 0 : 1;
}
my $conf = load_config($vmid);
- my $cmd = "/usr/bin/lxc-freeze -n $vmid";
my $running = check_running($vmid);
eval {
if ($running) {
- PVE::Tools::run_command($cmd);
+ PVE::Tools::run_command(['/usr/bin/lxc-freeze', '-n', $vmid]);
+ PVE::Tools::run_command(['/bin/sync']);
};
my $storecfg = PVE::Storage::config();
- my $rootinfo = PVE::LXC::parse_ct_mountpoint($conf->{rootfs});
+ my $rootinfo = parse_ct_mountpoint($conf->{rootfs});
my $volid = $rootinfo->{volume};
- $cmd = "/usr/bin/lxc-unfreeze -n $vmid";
if ($running) {
- PVE::Tools::run_command($cmd);
+ PVE::Tools::run_command(['/usr/bin/lxc-unfreeze', '-n', $vmid]);
};
PVE::Storage::volume_snapshot($storecfg, $volid, $snapname);
$snap->{snapstate} = 'delete';
- PVE::LXC::write_config($vmid, $conf);
+ write_config($vmid, $conf);
};
lock_container($vmid, 10, $updatefn);
delete $conf->{snapshots}->{$snapname};
- PVE::LXC::write_config($vmid, $conf);
+ write_config($vmid, $conf);
};
my $rootfs = $conf->{snapshots}->{$snapname}->{rootfs};
- my $rootinfo = PVE::LXC::parse_ct_mountpoint($rootfs);
+ my $rootinfo = parse_ct_mountpoint($rootfs);
my $volid = $rootinfo->{volume};
eval {
die "snapshot '$snapname' does not exist\n" if !defined($snap);
my $rootfs = $snap->{rootfs};
- my $rootinfo = PVE::LXC::parse_ct_mountpoint($rootfs);
+ my $rootinfo = parse_ct_mountpoint($rootfs);
my $volid = $rootinfo->{volume};
PVE::Storage::volume_rollback_is_possible($storecfg, $volid, $snapname);
delete $conf->{snapname};
$conf->{parent} = $snapname;
- PVE::LXC::write_config($vmid, $conf);
+ write_config($vmid, $conf);
};
my $unlockfn = sub {
delete $conf->{lock};
- PVE::LXC::write_config($vmid, $conf);
+ write_config($vmid, $conf);
};
lock_container($vmid, 10, $updatefn);
my $storecfg = PVE::Storage::config();
- my $rootinfo = PVE::LXC::parse_ct_mountpoint($conf->{rootfs});
+ my $rootinfo = parse_ct_mountpoint($conf->{rootfs});
my $volid = $rootinfo->{volume};
die "Template feature is not available for '$volid'\n"
my $template_volid = PVE::Storage::vdisk_create_base($storecfg, $volid);
$rootinfo->{volume} = $template_volid;
- $conf->{rootfs} = print_ct_mountpoint($rootinfo);
+ $conf->{rootfs} = print_ct_mountpoint($rootinfo, 1);
write_config($vmid, $conf);
}
return 1 if defined $conf->{template} && $conf->{template} == 1;
}
+sub mountpoint_names {
+ my ($reverse) = @_;
+
+ my @names = ('rootfs');
+
+ for (my $i = 0; $i < $MAX_MOUNT_POINTS; $i++) {
+ push @names, "mp$i";
+ }
+
+ return $reverse ? reverse @names : @names;
+}
+
+# The container might have *different* symlinks than the host. realpath/abs_path
+# use the actual filesystem to resolve links.
+sub sanitize_mountpoint {
+ my ($mp) = @_;
+ $mp = '/' . $mp; # we always start with a slash
+ $mp =~ s@/{2,}@/@g; # collapse sequences of slashes
+ $mp =~ s@/\./@@g; # collapse /./
+ $mp =~ s@/\.(/)?$@$1@; # collapse a trailing /. or /./
+ $mp =~ s@(.*)/[^/]+/\.\./@$1/@g; # collapse /../ without regard for symlinks
+ $mp =~ s@/\.\.(/)?$@$1@; # collapse trailing /.. or /../ disregarding symlinks
+ return $mp;
+}
+
+sub foreach_mountpoint_full {
+ my ($conf, $reverse, $func) = @_;
+
+ foreach my $key (mountpoint_names($reverse)) {
+ my $value = $conf->{$key};
+ next if !defined($value);
+ my $mountpoint = parse_ct_mountpoint($value);
+
+ # just to be sure: rootfs is /
+ my $path = $key eq 'rootfs' ? '/' : $mountpoint->{mp};
+ $mountpoint->{mp} = sanitize_mountpoint($path);
+
+ $path = $mountpoint->{volume};
+ $mountpoint->{volume} = sanitize_mountpoint($path) if $path =~ m|^/|;
+
+ &$func($key, $mountpoint);
+ }
+}
+
+sub foreach_mountpoint {
+ my ($conf, $func) = @_;
+
+ foreach_mountpoint_full($conf, 0, $func);
+}
+
+sub foreach_mountpoint_reverse {
+ my ($conf, $func) = @_;
+
+ foreach_mountpoint_full($conf, 1, $func);
+}
+
+sub check_ct_modify_config_perm {
+ my ($rpcenv, $authuser, $vmid, $pool, $key_list) = @_;
+
+ return 1 if $authuser ne 'root@pam';
+
+ foreach my $opt (@$key_list) {
+
+ if ($opt eq 'cpus' || $opt eq 'cpuunits' || $opt eq 'cpulimit') {
+ $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.CPU']);
+ } elsif ($opt eq 'rootfs' || $opt =~ /^mp\d+$/) {
+ $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Disk']);
+ } elsif ($opt eq 'memory' || $opt eq 'swap') {
+ $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Memory']);
+ } elsif ($opt =~ m/^net\d+$/ || $opt eq 'nameserver' ||
+ $opt eq 'searchdomain' || $opt eq 'hostname') {
+ $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Network']);
+ } else {
+ $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Options']);
+ }
+ }
+
+ return 1;
+}
+
+sub umount_all {
+ my ($vmid, $storage_cfg, $conf, $noerr) = @_;
+
+ my $rootdir = "/var/lib/lxc/$vmid/rootfs";
+ my $volid_list = get_vm_volumes($conf);
+
+ foreach_mountpoint_reverse($conf, sub {
+ my ($ms, $mountpoint) = @_;
+
+ my $volid = $mountpoint->{volume};
+ my $mount = $mountpoint->{mp};
+
+ return if !$volid || !$mount;
+
+ my $mount_path = "$rootdir/$mount";
+ $mount_path =~ s!/+!/!g;
+
+ return if !PVE::ProcFSTools::is_mounted($mount_path);
+
+ eval {
+ PVE::Tools::run_command(['umount', '-d', $mount_path]);
+ };
+ if (my $err = $@) {
+ if ($noerr) {
+ warn $err;
+ } else {
+ die $err;
+ }
+ }
+ });
+}
+
+sub mount_all {
+ my ($vmid, $storage_cfg, $conf) = @_;
+
+ my $rootdir = "/var/lib/lxc/$vmid/rootfs";
+ File::Path::make_path($rootdir);
+
+ my $volid_list = get_vm_volumes($conf);
+ PVE::Storage::activate_volumes($storage_cfg, $volid_list);
+
+ eval {
+ foreach_mountpoint($conf, sub {
+ my ($ms, $mountpoint) = @_;
+
+ my $volid = $mountpoint->{volume};
+ my $mount = $mountpoint->{mp};
+
+ return if !$volid || !$mount;
+
+ my $image_path = PVE::Storage::path($storage_cfg, $volid);
+ my ($vtype, undef, undef, undef, undef, $isBase, $format) =
+ PVE::Storage::parse_volname($storage_cfg, $volid);
+
+ die "unable to mount base volume - internal error" if $isBase;
+
+ mountpoint_mount($mountpoint, $rootdir, $storage_cfg);
+ });
+ };
+ if (my $err = $@) {
+ warn "mounting container failed - $err";
+ umount_all($vmid, $storage_cfg, $conf, 1);
+ }
+
+ return $rootdir;
+}
+
+
+sub mountpoint_mount_path {
+ my ($mountpoint, $storage_cfg, $snapname) = @_;
+
+ return mountpoint_mount($mountpoint, undef, $storage_cfg, $snapname);
+}
+
+my $check_mount_path = sub {
+ my ($path) = @_;
+ $path = File::Spec->canonpath($path);
+ my $real = Cwd::realpath($path);
+ if ($real ne $path) {
+ die "mount path modified by symlink: $path != $real";
+ }
+};
+
+# use $rootdir = undef to just return the corresponding mount path
+sub mountpoint_mount {
+ my ($mountpoint, $rootdir, $storage_cfg, $snapname) = @_;
+
+ my $volid = $mountpoint->{volume};
+ my $mount = $mountpoint->{mp};
+
+ return if !$volid || !$mount;
+
+ my $mount_path;
+
+ if (defined($rootdir)) {
+ $rootdir =~ s!/+$!!;
+ $mount_path = "$rootdir/$mount";
+ $mount_path =~ s!/+!/!g;
+ &$check_mount_path($mount_path);
+ File::Path::mkpath($mount_path);
+ }
+
+ my ($storage, $volname) = PVE::Storage::parse_volume_id($volid, 1);
+
+ die "unknown snapshot path for '$volid'" if !$storage && defined($snapname);
+
+ if ($storage) {
+
+ my $scfg = PVE::Storage::storage_config($storage_cfg, $storage);
+ my $path = PVE::Storage::path($storage_cfg, $volid, $snapname);
+
+ my ($vtype, undef, undef, undef, undef, $isBase, $format) =
+ PVE::Storage::parse_volname($storage_cfg, $volid);
+
+ if ($format eq 'subvol') {
+ if ($mount_path) {
+ if ($snapname) {
+ if ($scfg->{type} eq 'zfspool') {
+ my $path_arg = $path;
+ $path_arg =~ s!^/+!!;
+ PVE::Tools::run_command(['mount', '-o', 'ro,noload', '-t', 'zfs', $path_arg, $mount_path]);
+ } else {
+ die "cannot mount subvol snapshots for storage type '$scfg->{type}'\n";
+ }
+ } else {
+ PVE::Tools::run_command(['mount', '-o', 'bind', $path, $mount_path]);
+ }
+ }
+ return wantarray ? ($path, 0) : $path;
+ } elsif ($format eq 'raw') {
+ my $use_loopdev = 0;
+ my @extra_opts;
+ if ($scfg->{path}) {
+ push @extra_opts, '-o', 'loop';
+ $use_loopdev = 1;
+ } elsif ($scfg->{type} eq 'drbd' || $scfg->{type} eq 'lvm' || $scfg->{type} eq 'rbd') {
+ # do nothing
+ } else {
+ die "unsupported storage type '$scfg->{type}'\n";
+ }
+ if ($mount_path) {
+ if ($isBase || defined($snapname)) {
+ PVE::Tools::run_command(['mount', '-o', 'ro,noload', @extra_opts, $path, $mount_path]);
+ } else {
+ PVE::Tools::run_command(['mount', @extra_opts, $path, $mount_path]);
+ }
+ }
+ return wantarray ? ($path, $use_loopdev) : $path;
+ } else {
+ die "unsupported image format '$format'\n";
+ }
+ } elsif ($volid =~ m|^/dev/.+|) {
+ PVE::Tools::run_command(['mount', $volid, $mount_path]) if $mount_path;
+ return wantarray ? ($volid, 0) : $volid;
+ } elsif ($volid !~ m|^/dev/.+| && $volid =~ m|^/.+| && -d $volid) {
+ &$check_mount_path($volid);
+ PVE::Tools::run_command(['mount', '-o', 'bind', $volid, $mount_path]) if $mount_path;
+ return wantarray ? ($volid, 0) : $volid;
+ }
+
+ die "unsupported storage";
+}
+
+sub get_vm_volumes {
+ my ($conf, $excludes) = @_;
+
+ my $vollist = [];
+
+ foreach_mountpoint($conf, sub {
+ my ($ms, $mountpoint) = @_;
+
+ return if $excludes && $ms eq $excludes;
+
+ my $volid = $mountpoint->{volume};
+
+ return if !$volid || $volid =~ m|^/|;
+
+ my ($sid, $volname) = PVE::Storage::parse_volume_id($volid, 1);
+ return if !$sid;
+
+ push @$vollist, $volid;
+ });
+
+ return $vollist;
+}
+
+sub mkfs {
+ my ($dev) = @_;
+
+ PVE::Tools::run_command(['mkfs.ext4', '-O', 'mmp', $dev]);
+}
+
+sub format_disk {
+ my ($storage_cfg, $volid) = @_;
+
+ if ($volid =~ m!^/dev/.+!) {
+ mkfs($volid);
+ return;
+ }
+
+ my ($storage, $volname) = PVE::Storage::parse_volume_id($volid, 1);
+
+ die "cannot format volume '$volid' with no storage\n" if !$storage;
+
+ PVE::Storage::activate_volumes($storage_cfg, [$volid]);
+
+ my $path = PVE::Storage::path($storage_cfg, $volid);
+
+ my ($vtype, undef, undef, undef, undef, $isBase, $format) =
+ PVE::Storage::parse_volname($storage_cfg, $volid);
+
+ die "cannot format volume '$volid' (format == $format)\n"
+ if $format ne 'raw';
+
+ mkfs($path);
+}
+
+sub destroy_disks {
+ my ($storecfg, $vollist) = @_;
+
+ foreach my $volid (@$vollist) {
+ eval { PVE::Storage::vdisk_free($storecfg, $volid); };
+ warn $@ if $@;
+ }
+}
+
+sub create_disks {
+ my ($storecfg, $vmid, $settings, $conf) = @_;
+
+ my $vollist = [];
+
+ eval {
+ foreach_mountpoint($settings, sub {
+ my ($ms, $mountpoint) = @_;
+
+ my $volid = $mountpoint->{volume};
+ my $mp = $mountpoint->{mp};
+
+ my ($storage, $volname) = PVE::Storage::parse_volume_id($volid, 1);
+
+ return if !$storage;
+
+ if ($volid =~ m/^([^:\s]+):(\d+(\.\d+)?)$/) {
+ my ($storeid, $size_gb) = ($1, $2);
+
+ my $size_kb = int(${size_gb}*1024) * 1024;
+
+ my $scfg = PVE::Storage::storage_config($storecfg, $storage);
+ # fixme: use better naming ct-$vmid-disk-X.raw?
+
+ if ($scfg->{type} eq 'dir' || $scfg->{type} eq 'nfs') {
+ if ($size_kb > 0) {
+ $volid = PVE::Storage::vdisk_alloc($storecfg, $storage, $vmid, 'raw',
+ undef, $size_kb);
+ format_disk($storecfg, $volid);
+ } else {
+ $volid = PVE::Storage::vdisk_alloc($storecfg, $storage, $vmid, 'subvol',
+ undef, 0);
+ }
+ } elsif ($scfg->{type} eq 'zfspool') {
+
+ $volid = PVE::Storage::vdisk_alloc($storecfg, $storage, $vmid, 'subvol',
+ undef, $size_kb);
+ } elsif ($scfg->{type} eq 'drbd' || $scfg->{type} eq 'lvm') {
+
+ $volid = PVE::Storage::vdisk_alloc($storecfg, $storage, $vmid, 'raw', undef, $size_kb);
+ format_disk($storecfg, $volid);
+
+ } elsif ($scfg->{type} eq 'rbd') {
+
+ die "krbd option must be enabled on storage type '$scfg->{type}'\n" if !$scfg->{krbd};
+ $volid = PVE::Storage::vdisk_alloc($storecfg, $storage, $vmid, 'raw', undef, $size_kb);
+ format_disk($storecfg, $volid);
+ } else {
+ die "unable to create containers on storage type '$scfg->{type}'\n";
+ }
+ push @$vollist, $volid;
+ my $new_mountpoint = { volume => $volid, size => $size_kb*1024, mp => $mp };
+ $conf->{$ms} = print_ct_mountpoint($new_mountpoint, $ms eq 'rootfs');
+ } else {
+ # use specified/existing volid
+ }
+ });
+ };
+ # free allocated images on error
+ if (my $err = $@) {
+ destroy_disks($storecfg, $vollist);
+ die $err;
+ }
+ return $vollist;
+}
+
+# bash completion helper
+
+sub complete_os_templates {
+ my ($cmdname, $pname, $cvalue) = @_;
+
+ my $cfg = PVE::Storage::config();
+
+ my $storeid;
+
+ if ($cvalue =~ m/^([^:]+):/) {
+ $storeid = $1;
+ }
+
+ my $vtype = $cmdname eq 'restore' ? 'backup' : 'vztmpl';
+ my $data = PVE::Storage::template_list($cfg, $storeid, $vtype);
+
+ my $res = [];
+ foreach my $id (keys %$data) {
+ foreach my $item (@{$data->{$id}}) {
+ push @$res, $item->{volid} if defined($item->{volid});
+ }
+ }
+
+ return $res;
+}
+
+my $complete_ctid_full = sub {
+ my ($running) = @_;
+
+ my $idlist = vmstatus();
+
+ my $active_hash = list_active_containers();
+
+ my $res = [];
+
+ foreach my $id (keys %$idlist) {
+ my $d = $idlist->{$id};
+ if (defined($running)) {
+ next if $d->{template};
+ next if $running && !$active_hash->{$id};
+ next if !$running && $active_hash->{$id};
+ }
+ push @$res, $id;
+
+ }
+ return $res;
+};
+
+sub complete_ctid {
+ return &$complete_ctid_full();
+}
+
+sub complete_ctid_stopped {
+ return &$complete_ctid_full(0);
+}
+
+sub complete_ctid_running {
+ return &$complete_ctid_full(1);
+}
+
1;