]> git.proxmox.com Git - pve-container.git/blobdiff - src/PVE/LXC.pm
add feature flags using apparmor profile generation
[pve-container.git] / src / PVE / LXC.pm
index 1ef8a08469f211d2fa042ff7f852d05cfb14a253..89f289e9113de6a4540d03e142807ed65cbfc3ab 100644 (file)
@@ -2,6 +2,7 @@ package PVE::LXC;
 
 use strict;
 use warnings;
+
 use POSIX qw(EINTR);
 
 use Socket;
@@ -9,572 +10,31 @@ use Socket;
 use File::Path;
 use File::Spec;
 use Cwd qw();
-use Fcntl qw(O_RDONLY :flock);
+use Fcntl qw(O_RDONLY O_NOFOLLOW O_DIRECTORY);
+use Errno qw(ELOOP ENOTDIR EROFS ECONNREFUSED);
+use IO::Socket::UNIX;
 
-use PVE::Cluster qw(cfs_register_file cfs_read_file);
+use PVE::Exception qw(raise_perm_exc);
 use PVE::Storage;
 use PVE::SafeSyslog;
 use PVE::INotify;
 use PVE::JSONSchema qw(get_standard_option);
-use PVE::Tools qw($IPV6RE $IPV4RE dir_glob_foreach lock_file lock_file_full);
+use PVE::Tools qw($IPV6RE $IPV4RE dir_glob_foreach lock_file lock_file_full O_PATH);
+use PVE::CpuSet;
 use PVE::Network;
 use PVE::AccessControl;
 use PVE::ProcFSTools;
+use PVE::Syscall;
+use PVE::LXC::Config;
+
 use Time::HiRes qw (gettimeofday);
 
-use Data::Dumper;
+my $LXC_CONFIG_PATH = '/usr/share/lxc/config';
 
 my $nodename = PVE::INotify::nodename();
 
 my $cpuinfo= PVE::ProcFSTools::read_cpuinfo();
 
-our $COMMON_TAR_FLAGS = [ '--sparse', '--numeric-owner', '--acls',
-                          '--xattrs',
-                          '--xattrs-include=user.*',
-                          '--xattrs-include=security.capability',
-                          '--warning=no-xattr-write' ];
-
-cfs_register_file('/lxc/', \&parse_pct_config, \&write_pct_config);
-
-my $rootfs_desc = {
-    volume => {
-       type => 'string',
-       default_key => 1,
-       format => 'pve-lxc-mp-string',
-       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 => 'disk-size',
-       format_description => 'DiskSize',
-       description => 'Volume size (read only value).',
-       optional => 1,
-    },
-    acl => {
-       type => 'boolean',
-       format_description => 'acl',
-       description => 'Explicitly enable or disable ACL support.',
-       optional => 1,
-    },
-    ro => {
-       type => 'boolean',
-       format_description => 'ro',
-       description => 'Read-only mountpoint (not supported with bind mounts)',
-       optional => 1,
-    },
-    quota => {
-       type => 'boolean',
-       format_description => '[0|1]',
-       description => 'Enable user quotas inside the container (not supported with zfs subvolumes)',
-       optional => 1,
-    },
-};
-
-PVE::JSONSchema::register_standard_option('pve-ct-rootfs', {
-    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,
-       type => 'string',
-       description => "Lock/unlock the VM.",
-       enum => [qw(migrate backup snapshot rollback)],
-    },
-    onboot => {
-       optional => 1,
-       type => 'boolean',
-       description => "Specifies whether a VM will be started during system bootup.",
-       default => 0,
-    },
-    startup => get_standard_option('pve-startup-order'),
-    template => {
-       optional => 1,
-       type => 'boolean',
-       description => "Enable/disable Template.",
-       default => 0,
-    },
-    arch => {
-       optional => 1,
-       type => 'string',
-       enum => ['amd64', 'i386'],
-       description => "OS architecture type.",
-       default => 'amd64',
-    },
-    ostype => {
-       optional => 1,
-       type => 'string',
-       enum => ['debian', 'ubuntu', 'centos', 'fedora', 'opensuse', '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 => 2,
-    },
-    cpulimit => {
-       optional => 1,
-       type => 'number',
-       description => "Limit of CPU usage. Note if the computer has 2 CPUs, it has a total of '2' CPU time. Value '0' indicates no CPU limit.",
-       minimum => 0,
-       maximum => 128,
-       default => 0,
-    },
-    cpuunits => {
-       optional => 1,
-       type => 'integer',
-       description => "CPU weight for a VM. Argument is used in the kernel fair scheduler. The larger the number is, the more CPU time this VM gets. Number is relative to the weights of all the other running VMs.\n\nNOTE: You can disable fair-scheduler configuration by setting this to 0.",
-       minimum => 0,
-       maximum => 500000,
-       default => 1024,
-    },
-    memory => {
-       optional => 1,
-       type => 'integer',
-       description => "Amount of RAM for the VM in MB.",
-       minimum => 16,
-       default => 512,
-    },
-    swap => {
-       optional => 1,
-       type => 'integer',
-       description => "Amount of SWAP for the VM in MB.",
-       minimum => 0,
-       default => 512,
-    },
-    hostname => {
-       optional => 1,
-       description => "Set a host name for the container.",
-       type => 'string', format => 'dns-name',
-       maxLength => 255,
-    },
-    description => {
-       optional => 1,
-       type => 'string',
-       description => "Container description. Only used on the configuration web interface.",
-    },
-    searchdomain => {
-       optional => 1,
-       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 nor nameserver.",
-    },
-    nameserver => {
-       optional => 1,
-       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 nor nameserver.",
-    },
-    rootfs => get_standard_option('pve-ct-rootfs'),
-    parent => {
-       optional => 1,
-       type => 'string', format => 'pve-configid',
-       maxLength => 40,
-       description => "Parent snapshot name. This is used internally, and should not be modified.",
-    },
-    snaptime => {
-       optional => 1,
-       description => "Timestamp for snapshots.",
-       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 CT or CT's disk remove/update operation.",
-       default => 0,
-    },
-    unprivileged => {
-       optional => 1,
-       type => 'boolean',
-       description => "Makes the container run as unprivileged user. (Should not be modified manually.)",
-       default => 0,
-    },
-};
-
-my $valid_lxc_conf_keys = {
-    'lxc.include' => 1,
-    'lxc.arch' => 1,
-    'lxc.utsname' => 1,
-    'lxc.haltsignal' => 1,
-    'lxc.rebootsignal' => 1,
-    'lxc.stopsignal' => 1,
-    'lxc.init_cmd' => 1,
-    'lxc.network.type' => 1,
-    'lxc.network.flags' => 1,
-    'lxc.network.link' => 1,
-    'lxc.network.mtu' => 1,
-    'lxc.network.name' => 1,
-    'lxc.network.hwaddr' => 1,
-    'lxc.network.ipv4' => 1,
-    'lxc.network.ipv4.gateway' => 1,
-    'lxc.network.ipv6' => 1,
-    'lxc.network.ipv6.gateway' => 1,
-    'lxc.network.script.up' => 1,
-    'lxc.network.script.down' => 1,
-    'lxc.pts' => 1,
-    'lxc.console.logfile' => 1,
-    'lxc.console' => 1,
-    'lxc.tty' => 1,
-    'lxc.devttydir' => 1,
-    'lxc.hook.autodev' => 1,
-    'lxc.autodev' => 1,
-    'lxc.kmsg' => 1,
-    'lxc.mount' => 1,
-    'lxc.mount.entry' => 1,
-    'lxc.mount.auto' => 1,
-    'lxc.rootfs' => 'lxc.rootfs is auto generated from rootfs',
-    'lxc.rootfs.mount' => 1,
-    'lxc.rootfs.options' => 'lxc.rootfs.options is not supported' .
-                            ', please use mountpoint options in the "rootfs" key',
-    # lxc.cgroup.*
-    'lxc.cap.drop' => 1,
-    'lxc.cap.keep' => 1,
-    'lxc.aa_profile' => 1,
-    'lxc.aa_allow_incomplete' => 1,
-    'lxc.se_context' => 1,
-    'lxc.seccomp' => 1,
-    'lxc.id_map' => 1,
-    'lxc.hook.pre-start' => 1,
-    'lxc.hook.pre-mount' => 1,
-    'lxc.hook.mount' => 1,
-    'lxc.hook.start' => 1,
-    'lxc.hook.stop' => 1,
-    'lxc.hook.post-stop' => 1,
-    'lxc.hook.clone' => 1,
-    'lxc.hook.destroy' => 1,
-    'lxc.loglevel' => 1,
-    'lxc.logfile' => 1,
-    'lxc.start.auto' => 1,
-    'lxc.start.delay' => 1,
-    'lxc.start.order' => 1,
-    'lxc.group' => 1,
-    'lxc.environment' => 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]+',
-       optional => 1,
-    },
-    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)',
-       minimum => 64, # minimum ethernet frame is 64 bytes
-       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 for this interface.",
-       optional => 1,
-    },
-    trunks => {
-       type => 'string',
-       pattern => qr/\d+(?:;\d+)*/,
-       format_description => 'vlanid[;vlanid...]',
-       description => "VLAN ids to pass through the 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 => $netconf_desc,
-       description => "Specifies network interfaces for the container.",
-    };
-}
-
-PVE::JSONSchema::register_format('pve-lxc-mp-string', \&verify_lxc_mp_string);
-sub verify_lxc_mp_string{
-    my ($mp, $noerr) = @_;
-
-    # do not allow:
-    # /./ or /../ 
-    # /. or /.. at the end
-    # ../ at the beginning
-    
-    if($mp =~ m@/\.\.?/@ ||
-       $mp =~ m@/\.\.?$@ ||
-       $mp =~ m@^\.\./@){
-       return undef if $noerr;
-       die "$mp contains illegal character sequences\n";
-    }
-    return $mp;
-}
-
-my $mp_desc = {
-    %$rootfs_desc,
-    mp => {
-       type => 'string',
-       format => 'pve-lxc-mp-string',
-       format_description => 'Path',
-       description => 'Path to the mountpoint as seen from inside the container.',
-    },
-};
-PVE::JSONSchema::register_format('pve-ct-mountpoint', $mp_desc);
-
-my $unuseddesc = {
-    optional => 1,
-    type => 'string', format => 'pve-volume-id',
-    description => "Reference to unused volumes.",
-};
-
-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,
-    };
-}
-
-my $MAX_UNUSED_DISKS = $MAX_MOUNT_POINTS;
-for (my $i = 0; $i < $MAX_MOUNT_POINTS; $i++) {
-    $confdesc->{"unused$i"} = $unuseddesc;
-}
-
-sub write_pct_config {
-    my ($filename, $conf) = @_;
-
-    delete $conf->{snapstate}; # just to be sure
-
-    my $generate_raw_config = sub {
-       my ($conf) = @_;
-
-       my $raw = '';
-
-       # add description as comment to top of file
-       my $descr = $conf->{description} || '';
-       foreach my $cl (split(/\n/, $descr)) {
-           $raw .= '#' .  PVE::Tools::encode_text($cl) . "\n";
-       }
-
-       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';
-           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}) {
-           foreach my $entry (@$lxcconf) {
-               my ($k, $v) = @$entry;
-               $raw .= "$k: $v\n";
-           }
-       }
-       
-       return $raw;
-    };
-
-    my $raw = &$generate_raw_config($conf);
-
-    foreach my $snapname (sort keys %{$conf->{snapshots}}) {
-       $raw .= "\n[$snapname]\n";
-       $raw .= &$generate_raw_config($conf->{snapshots}->{$snapname});
-    }
-
-    return $raw;
-}
-
-sub check_type {
-    my ($key, $value) = @_;
-
-    die "unknown setting '$key'\n" if !$confdesc->{$key};
-
-    my $type = $confdesc->{$key}->{type};
-
-    if (!defined($value)) {
-       die "got undefined value\n";
-    }
-
-    if ($value =~ m/[\n\r]/) {
-       die "property contains a line feed\n";
-    }
-
-    if ($type eq 'boolean') {
-       return 1 if ($value eq '1') || ($value =~ m/^(on|yes|true)$/i);
-       return 0 if ($value eq '0') || ($value =~ m/^(off|no|false)$/i);
-       die "type check ('boolean') failed - got '$value'\n";
-    } elsif ($type eq 'integer') {
-       return int($1) if $value =~ m/^(\d+)$/;
-       die "type check ('integer') failed - got '$value'\n";
-    } elsif ($type eq 'number') {
-        return $value if $value =~ m/^(\d+)(\.\d+)?$/;
-        die "type check ('number') failed - got '$value'\n";
-    } elsif ($type eq 'string') {
-       if (my $fmt = $confdesc->{$key}->{format}) {
-           PVE::JSONSchema::check_format($fmt, $value);
-           return $value;
-       }
-       return $value;
-    } else {
-       die "internal error"
-    }
-}
-
-sub parse_pct_config {
-    my ($filename, $raw) = @_;
-
-    return undef if !defined($raw);
-
-    my $res = {
-       digest => Digest::SHA::sha1_hex($raw),
-       snapshots => {},
-    };
-
-    $filename =~ m|/lxc/(\d+).conf$|
-       || die "got strange filename '$filename'";
-
-    my $vmid = $1;
-
-    my $conf = $res;
-    my $descr = '';
-    my $section = '';
-
-    my @lines = split(/\n/, $raw);
-    foreach my $line (@lines) {
-       next if $line =~ m/^\s*$/;
-
-       if ($line =~ m/^\[([a-z][a-z0-9_\-]+)\]\s*$/i) {
-           $section = $1;
-           $conf->{description} = $descr if $descr;
-           $descr = '';
-           $conf = $res->{snapshots}->{$section} = {};
-           next;
-       }
-
-       if ($line =~ m/^\#(.*)\s*$/) {
-           $descr .= PVE::Tools::decode_text($1) . "\n";
-           next;
-       }
-
-       if ($line =~ m/^(lxc\.[a-z0-9_\-\.]+)(:|\s*=)\s*(.*?)\s*$/) {
-           my $key = $1;
-           my $value = $3;
-           my $validity = $valid_lxc_conf_keys->{$key} || 0;
-           if ($validity eq 1 || $key =~ m/^lxc\.cgroup\./) {
-               push @{$conf->{lxc}}, [$key, $value];
-           } elsif (my $errmsg = $validity) {
-               warn "vm $vmid - $key: $errmsg\n";
-           } else {
-               warn "vm $vmid - unable to parse config: $line\n";
-           }
-       } elsif ($line =~ m/^(description):\s*(.*\S)\s*$/) {
-           $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*$/) {
-           my $key = $1;
-           my $value = $2;
-           eval { $value = check_type($key, $value); };
-           warn "vm $vmid - unable to parse value of '$key' - $@" if $@;
-           $conf->{$key} = $value;
-       } else {
-           warn "vm $vmid - unable to parse config: $line\n";
-       }
-    }
-
-    $conf->{description} = $descr if $descr;
-
-    delete $res->{snapstate}; # just to be sure
-
-    return $res;
-}
-
 sub config_list {
     my $vmlist = PVE::Cluster::get_vmlist();
     my $res = {};
@@ -586,105 +46,15 @@ sub config_list {
        my $d = $ids->{$vmid};
        next if !$d->{node} || $d->{node} ne $nodename;
        next if !$d->{type} || $d->{type} ne 'lxc';
-       $res->{$vmid}->{type} = 'lxc';
+       $res->{$vmid} = { type => 'lxc', vmid => $vmid };
     }
     return $res;
 }
 
-sub cfs_config_path {
-    my ($vmid, $node) = @_;
-
-    $node = $nodename if !$node;
-    return "nodes/$node/lxc/$vmid.conf";
-}
-
-sub config_file {
-    my ($vmid, $node) = @_;
-
-    my $cfspath = cfs_config_path($vmid, $node);
-    return "/etc/pve/$cfspath";
-}
-
-sub load_config {
-    my ($vmid, $node) = @_;
-
-    $node = $nodename if !$node;
-    my $cfspath = cfs_config_path($vmid, $node);
-
-    my $conf = PVE::Cluster::cfs_read_file($cfspath);
-    die "container $vmid does not exist\n" if !defined($conf);
-
-    return $conf;
-}
-
-sub create_config {
-    my ($vmid, $conf) = @_;
-
-    my $dir = "/etc/pve/nodes/$nodename/lxc";
-    mkdir $dir;
-
-    write_config($vmid, $conf);
-}
-
 sub destroy_config {
     my ($vmid) = @_;
 
-    unlink config_file($vmid, $nodename);
-}
-
-sub write_config {
-    my ($vmid, $conf) = @_;
-
-    my $cfspath = cfs_config_path($vmid);
-
-    PVE::Cluster::cfs_write_file($cfspath, $conf);
-}
-
-# flock: we use one file handle per process, so lock file
-# can be called multiple times and will succeed for the same process.
-
-my $lock_handles =  {};
-my $lockdir = "/run/lock/lxc";
-
-sub lock_filename {
-    my ($vmid) = @_;
-
-    return "$lockdir/pve-config-${vmid}.lock";
-}
-
-sub lock_container {
-    my ($vmid, $timeout, $code, @param) = @_;
-
-    $timeout = 10 if !$timeout;
-
-    my $filename = lock_filename($vmid);
-
-    mkdir $lockdir if !-d $lockdir;
-
-    my $res = PVE::Tools::lock_file_full($filename, $timeout, 0, $code, @param);
-
-    die $@ if $@;
-
-    return $res;
-}
-
-sub option_exists {
-    my ($name) = @_;
-
-    return defined($confdesc->{$name});
-}
-
-# add JSON properties for create and set function
-sub json_config_properties {
-    my $prop = shift;
-
-    foreach my $opt (keys %$confdesc) {
-       next if $opt eq 'parent' || $opt eq 'snaptime';
-       next if $prop->{$opt};
-       $prop->{$opt} = $confdesc->{$opt};
-    }
-
-    return $prop;
+    unlink PVE::LXC::Config->config_file($vmid, $nodename);
 }
 
 # container status helpers
@@ -700,7 +70,7 @@ sub list_active_containers {
     return $res if !$fh;
 
     while (defined(my $line = <$fh>)) {
-       if ($line =~ m/^[a-f0-9]+:\s\S+\s\S+\s\S+\s\S+\s\S+\s\d+\s(\S+)$/) {
+       if ($line =~ m/^[a-f0-9]+:\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+\d+\s+(\S+)$/) {
            my $path = $1;
            if ($path =~ m!^@/var/lib/lxc/(\d+)/command$!) {
                $res->{$1} = 1;
@@ -733,9 +103,9 @@ sub get_container_disk_usage {
 my $last_proc_vmid_stat;
 
 my $parse_cpuacct_stat = sub {
-    my ($vmid) = @_;
+    my ($vmid, $unprivileged) = @_;
 
-    my $raw = read_cgroup_value('cpuacct', $vmid, 'cpuacct.stat', 1);
+    my $raw = read_cgroup_value('cpuacct', $vmid, $unprivileged, 'cpuacct.stat', 1);
 
     my $stat = {};
 
@@ -749,10 +119,53 @@ my $parse_cpuacct_stat = sub {
     return $stat;
 };
 
+our $vmstatus_return_properties = {
+    vmid => get_standard_option('pve-vmid'),
+    status => {
+       description => "LXC Container status.",
+       type => 'string',
+       enum => ['stopped', 'running'],
+    },
+    maxmem => {
+       description => "Maximum memory in bytes.",
+       type => 'integer',
+       optional => 1,
+       renderer => 'bytes',
+    },
+    maxswap => {
+       description => "Maximum SWAP memory in bytes.",
+       type => 'integer',
+       optional => 1,
+       renderer => 'bytes',
+    },
+    maxdisk => {
+       description => "Root disk size in bytes.",
+       type => 'integer',
+       optional => 1,
+       renderer => 'bytes',
+    },
+    name => {
+       description => "Container name.",
+       type => 'string',
+       optional => 1,
+    },
+    uptime => {
+       description => "Uptime.",
+       type => 'integer',
+       optional => 1,
+       renderer => 'duration',
+    },
+    cpus => {
+       description => "Maximum usable CPUs.",
+       type => 'number',
+       optional => 1,
+    },
+};
+
 sub vmstatus {
     my ($opt_vmid) = @_;
 
-    my $list = $opt_vmid ? { $opt_vmid => { type => 'lxc' }} : config_list();
+    my $list = $opt_vmid ? { $opt_vmid => { type => 'lxc', vmid => $opt_vmid }} : config_list();
 
     my $active_hash = list_active_containers();
 
@@ -761,6 +174,9 @@ sub vmstatus {
     my $cdtime = gettimeofday;
 
     my $uptime = (PVE::ProcFSTools::read_proc_uptime(1))[0];
+    my $clock_ticks = POSIX::sysconf(&POSIX::_SC_CLK_TCK);
+
+    my $unprivileged = {};
 
     foreach my $vmid (keys %$list) {
        my $d = $list->{$vmid};
@@ -770,13 +186,18 @@ sub vmstatus {
 
        $d->{status} = $d->{pid} ? 'running' : 'stopped';
 
-       my $cfspath = cfs_config_path($vmid);
+       my $cfspath = PVE::LXC::Config->cfs_config_path($vmid);
        my $conf = PVE::Cluster::cfs_read_file($cfspath) || {};
 
+       $unprivileged->{$vmid} = $conf->{unprivileged};
+
        $d->{name} = $conf->{'hostname'} || "CT$vmid";
        $d->{name} =~ s/[\s]//g;
 
-       $d->{cpus} = $conf->{cpulimit} || $cpucount;
+       $d->{cpus} = $conf->{cores} || $conf->{cpulimit};
+       $d->{cpus} = $cpucount if !$d->{cpus};
+
+       $d->{lock} = $conf->{lock} || '';
 
        if ($d->{pid}) {
            my $res = get_container_disk_usage($vmid, $d->{pid});
@@ -786,8 +207,8 @@ sub vmstatus {
            $d->{disk} = 0;
            # use 4GB by default ??
            if (my $rootfs = $conf->{rootfs}) {
-               my $rootinfo = parse_ct_rootfs($rootfs);
-               $d->{maxdisk} = int(($rootinfo->{size} || 4)*1024*1024)*1024;
+               my $rootinfo = PVE::LXC::Config->parse_ct_rootfs($rootfs);
+               $d->{maxdisk} = $rootinfo->{size} || (4*1024*1024*1024);
            } else {
                $d->{maxdisk} = 4*1024*1024*1024;
            }
@@ -807,7 +228,7 @@ sub vmstatus {
        $d->{diskread} = 0;
        $d->{diskwrite} = 0;
 
-       $d->{template} = is_template($conf);
+       $d->{template} = PVE::LXC::Config->is_template($conf);
     }
 
     foreach my $vmid (keys %$list) {
@@ -816,48 +237,67 @@ sub vmstatus {
 
        next if !$pid; # skip stopped CTs
 
-       my $ctime = (stat("/proc/$pid"))[10]; # 10 = ctime
-       $d->{uptime} = time - $ctime; # the method lxcfs uses
+       my $proc_pid_stat = PVE::ProcFSTools::read_proc_pid_stat($pid);
+       $d->{uptime} = int(($uptime - $proc_pid_stat->{starttime}) / $clock_ticks); # the method lxcfs uses
+
+       my $unpriv = $unprivileged->{$vmid};
+
+       if (-d '/sys/fs/cgroup/memory') {
+           my $memory_stat = read_cgroup_list('memory', $vmid, $unpriv, 'memory.stat');
+           my $mem_usage_in_bytes = read_cgroup_value('memory', $vmid, $unpriv, 'memory.usage_in_bytes');
 
-       $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};
+           $d->{mem} = $mem_usage_in_bytes - $memory_stat->{total_cache};
+           $d->{swap} = read_cgroup_value('memory', $vmid, $unpriv, 'memory.memsw.usage_in_bytes') - $mem_usage_in_bytes;
+       } else {
+           $d->{mem} = 0;
+           $d->{swap} = 0;
+       }
 
-       my $blkio_bytes = read_cgroup_value('blkio', $vmid, 'blkio.throttle.io_service_bytes', 1);
-       my @bytes = split(/\n/, $blkio_bytes);
-       foreach my $byte (@bytes) {
-           if (my ($key, $value) = $byte =~ /(Read|Write)\s+(\d+)/) {
-               $d->{diskread} = $2 if $key eq 'Read';
-               $d->{diskwrite} = $2 if $key eq 'Write';
+       if (-d '/sys/fs/cgroup/blkio') {
+           my $blkio_bytes = read_cgroup_value('blkio', $vmid, $unpriv, 'blkio.throttle.io_service_bytes', 1);
+           my @bytes = split(/\n/, $blkio_bytes);
+           foreach my $byte (@bytes) {
+               if (my ($key, $value) = $byte =~ /(Read|Write)\s+(\d+)/) {
+                   $d->{diskread} += $2 if $key eq 'Read';
+                   $d->{diskwrite} += $2 if $key eq 'Write';
+               }
            }
+       } else {
+           $d->{diskread} = 0;
+           $d->{diskwrite} = 0;
        }
 
-       my $pstat = &$parse_cpuacct_stat($vmid);
+       if (-d '/sys/fs/cgroup/cpuacct') {
+           my $pstat = $parse_cpuacct_stat->($vmid, $unpriv);
 
-       my $used = $pstat->{utime} + $pstat->{stime};
+           my $used = $pstat->{utime} + $pstat->{stime};
 
-       my $old = $last_proc_vmid_stat->{$vmid};
-       if (!$old) {
-           $last_proc_vmid_stat->{$vmid} = {
-               time => $cdtime,
-               used => $used,
-               cpu => 0,
-           };
-           next;
-       }
+           my $old = $last_proc_vmid_stat->{$vmid};
+           if (!$old) {
+               $last_proc_vmid_stat->{$vmid} = {
+                   time => $cdtime,
+                   used => $used,
+                   cpu => 0,
+               };
+               next;
+           }
 
-       my $dtime = ($cdtime -  $old->{time}) * $cpucount * $cpuinfo->{user_hz};
+           my $dtime = ($cdtime -  $old->{time}) * $cpucount * $cpuinfo->{user_hz};
 
-       if ($dtime > 1000) {
-           my $dutime = $used -  $old->{used};
+           if ($dtime > 1000) {
+               my $dutime = $used -  $old->{used};
 
-           $d->{cpu} = (($dutime/$dtime)* $cpucount) / $d->{cpus};
-           $last_proc_vmid_stat->{$vmid} = {
-               time => $cdtime,
-               used => $used,
-               cpu => $d->{cpu},
-           };
+               $d->{cpu} = (($dutime/$dtime)* $cpucount) / $d->{cpus};
+               $last_proc_vmid_stat->{$vmid} = {
+                   time => $cdtime,
+                   used => $used,
+                   cpu => $d->{cpu},
+               };
+           } else {
+               $d->{cpu} = $old->{cpu};
+           }
        } else {
-           $d->{cpu} = $old->{cpu};
+           $d->{cpu} = 0;
        }
     }
 
@@ -878,88 +318,19 @@ sub vmstatus {
     return $list;
 }
 
-sub classify_mountpoint {
-    my ($vol) = @_;
-    if ($vol =~ m!^/!) {
-       return 'device' if $vol =~ m!^/dev/!;
-       return 'bind';
-    }
-    return 'volume';
-}
-
-my $parse_ct_mountpoint_full = sub {
-    my ($desc, $data, $noerr) = @_;
-
-    $data //= '';
-
-    my $res;
-    eval { $res = PVE::JSONSchema::parse_property_string($desc, $data) };
-    if ($@) {
-       return undef if $noerr;
-       die $@;
-    }
-
-    if (defined(my $size = $res->{size})) {
-       $size = PVE::JSONSchema::parse_size($size);
-       if (!defined($size)) {
-           return undef if $noerr;
-           die "invalid size: $size\n";
-       }
-       $res->{size} = $size;
-    }
-
-    $res->{type} = classify_mountpoint($res->{volume});
-
-    return $res;
-};
-
-sub parse_ct_rootfs {
-    my ($data, $noerr) = @_;
-
-    my $res =  &$parse_ct_mountpoint_full($rootfs_desc, $data, $noerr);
-
-    $res->{mp} = '/' if defined($res);
-
-    return $res;
-}
-
-sub parse_ct_mountpoint {
-    my ($data, $noerr) = @_;
+sub read_cgroup_list($$$$) {
+    my ($group, $vmid, $unprivileged, $name) = @_;
 
-    return &$parse_ct_mountpoint_full($mp_desc, $data, $noerr);
-}
+    my $content = read_cgroup_value($group, $vmid, $unprivileged, $name, 1);
 
-sub print_ct_mountpoint {
-    my ($info, $nomp) = @_;
-    my $skip = [ 'type' ];
-    push @$skip, 'mp' if $nomp;
-    return PVE::JSONSchema::print_property_string($info, $mp_desc, $skip);
+    return { split(/\s+/, $content) };
 }
 
-sub print_lxc_network {
-    my $net = shift;
-    return PVE::JSONSchema::print_property_string($net, $netconf_desc);
-}
+sub read_cgroup_value($$$$$) {
+    my ($group, $vmid, $unprivileged, $name, $full) = @_;
 
-sub parse_lxc_network {
-    my ($data) = @_;
-
-    my $res = {};
-
-    return $res if !$data;
-
-    $res = PVE::JSONSchema::parse_property_string($netconf_desc, $data);
-
-    $res->{type} = 'veth';
-    $res->{hwaddr} = PVE::Tools::random_ether_addr() if !$res->{hwaddr};
-
-    return $res;
-}
-
-sub read_cgroup_value {
-    my ($group, $vmid, $name, $full) = @_;
-
-    my $path = "/sys/fs/cgroup/$group/lxc/$vmid/$name";
+    my $nsdir = $unprivileged ? '' : 'ns/';
+    my $path = "/sys/fs/cgroup/$group/lxc/$vmid/${nsdir}$name";
 
     return PVE::Tools::file_get_contents($path) if $full;
 
@@ -1029,22 +400,111 @@ sub parse_ipv4_cidr {
     die "unable to parse ipv4 address/mask\n";
 }
 
-sub check_lock {
-    my ($conf) = @_;
-
-    die "VM is locked ($conf->{'lock'})\n" if $conf->{'lock'};
-}
+sub get_cgroup_subsystems {
+       my $v1 = {};
+       my $v2 = 0;
+       my $data = PVE::Tools::file_get_contents('/proc/self/cgroup');
+       while ($data =~ /^\d+:([^:\n]*):.*$/gm) {
+               my $type = $1;
+               if (length($type)) {
+                       $v1->{$_} = 1 foreach split(/,/, $type);
+               } else {
+                       $v2 = 1;
+               }
+       }
+       return wantarray ? ($v1, $v2) : $v1;
+}
+
+# Currently we do not need to create seccomp profile 'files' as the only
+# choice our configuration actually allows is "with or without keyctl()",
+# so we distinguish between using lxc's "default" seccomp profile and our
+# added pve-userns.seccomp file.
+#
+# This returns a configuration line added to the raw lxc config.
+sub make_seccomp_config {
+    my ($conf, $unprivileged, $features) = @_;
+    # User-configured profile has precedence, note that the user's entry would
+    # be written 'after' this line anyway...
+    if (PVE::LXC::Config->has_lxc_entry($conf, 'lxc.seccomp.profile')) {
+       # Warn the user if this conflicts with a feature:
+       if ($features->{keyctl}) {
+           warn "explicitly configured lxc.seccomp.profile overrides the following settings: features:keyctl\n";
+       }
+       return '';
+    }
+
+    # Privileged containers keep using the default (which is already part of
+    # the files included via lxc.include, so we don't need to write it out,
+    # that way it stays admin-configurable via /usr/share/lxc/config/... as
+    # well)
+    return '' if !$unprivileged;
+
+    # Unprivileged containers will get keyctl() disabled by default as a
+    # workaround for systemd-networkd behavior. But we have an option to
+    # explicitly enable it:
+    return '' if $features->{keyctl};
+
+    # Finally we're in an unprivileged container without `keyctl` set
+    # explicitly. We have a file prepared for this:
+    return "lxc.seccomp.profile = $LXC_CONFIG_PATH/pve-userns.seccomp\n";
+}
+
+# Since lxc-3.0.2 we can have lxc generate a profile for the container
+# automatically. The default should be equivalent to the old
+# `lxc-container-default-cgns` profile.
+#
+# Additionally this also added `lxc.apparmor.raw` which can be used to inject
+# additional lines into the profile. We can use that to allow mounting specific
+# file systems.
+sub make_apparmor_config {
+    my ($conf, $unprivileged, $features) = @_;
+
+    # user-configured profile has precedence, but first we go through our own
+    # code to figure out whether we should warn the user:
+
+    my $raw = "lxc.apparmor.profile = generated\n";
+    my @profile_uses;
+
+    # There's lxc.apparmor.allow_nesting now, which will add the necessary
+    # apparmor lines, create an apparmor namespace for the container, but also
+    # adds proc and sysfs mounts to /dev/.lxc/{proc,sys}. These do not have
+    # lxcfs mounted over them, because that would prevent the container from
+    # mounting new instances of them for nested containers.
+    if ($features->{nesting}) {
+       push @profile_uses, 'features:nesting';
+       $raw .= "lxc.apparmor.allow_nesting = 1\n"
+    } else {
+       # In the default profile in /etc/apparmor.d we patch this in because
+       # otherwise a container can for example run `chown` on /sys, breaking
+       # access to it for non-CAP_DAC_OVERRIDE tools on the host:
+       $raw .= "lxc.apparmor.raw = deny mount -> /proc/,\n";
+       $raw .= "lxc.apparmor.raw = deny mount -> /sys/,\n";
+       # Preferably we could use the 'remount' flag but this does not sit well
+       # with apparmor_parser currently:
+       #  mount options=(rw, nosuid, nodev, noexec, remount) -> /sys/,
+    }
+
+    if (my $mount = $features->{mount}) {
+       push @profile_uses, 'features:mount';
+       foreach my $fs (PVE::Tools::split_list($mount)) {
+           $raw .= "lxc.apparmor.raw = mount fstype=$fs,\n";
+       }
+    }
 
-sub check_protection {
-    my ($vm_conf, $err_msg) = @_;
+    # More to come?
 
-    if ($vm_conf->{protection}) {
-       die "$err_msg - protection mode enabled\n";
+    if (PVE::LXC::Config->has_lxc_entry($conf, 'lxc.apparmor.profile')) {
+       if (length(my $used = join(', ', @profile_uses))) {
+           warn "explicitly configured lxc.apparmor.profile overrides the following settings: $used\n";
+       }
+       return '';
     }
+
+    return $raw;
 }
 
 sub update_lxc_config {
-    my ($storage_cfg, $vmid, $conf) = @_;
+    my ($vmid, $conf) = @_;
 
     my $dir = "/var/lib/lxc/$vmid";
 
@@ -1060,19 +520,26 @@ sub update_lxc_config {
     die "missing 'arch' - internal error" if !$conf->{arch};
     $raw .= "lxc.arch = $conf->{arch}\n";
 
-    my $unprivileged = $conf->{unprivileged};
-    my $custom_idmap = grep { $_->[0] eq 'lxc.id_map' } @{$conf->{lxc}};
+    my $custom_idmap = PVE::LXC::Config->has_lxc_entry($conf, 'lxc.idmap');
+    my $unprivileged = $conf->{unprivileged} || $custom_idmap;
 
     my $ostype = $conf->{ostype} || die "missing 'ostype' - internal error";
-    if ($ostype =~ /^(?:debian | ubuntu | centos | fedora | opensuse | archlinux)$/x) {
-       $raw .= "lxc.include = /usr/share/lxc/config/$ostype.common.conf\n";
-       if ($unprivileged || $custom_idmap) {
-           $raw .= "lxc.include = /usr/share/lxc/config/$ostype.userns.conf\n"
-       }
-    } else {
-       die "implement me (ostype $ostype)";
+
+    my $cfgpath = '/usr/share/lxc/config';
+    my $inc = "$cfgpath/$ostype.common.conf";
+    $inc ="$cfgpath/common.conf" if !-f $inc;
+    $raw .= "lxc.include = $inc\n";
+    if ($unprivileged) {
+       $inc = "$cfgpath/$ostype.userns.conf";
+       $inc = "$cfgpath/userns.conf" if !-f $inc;
+       $raw .= "lxc.include = $inc\n";
     }
 
+    my $features = PVE::LXC::Config->parse_features($conf->{features});
+
+    $raw .= make_seccomp_config($conf, $unprivileged, $features);
+    $raw .= make_apparmor_config($conf, $unprivileged, $features);
+
     # WARNING: DO NOT REMOVE this without making sure that loop device nodes
     # cannot be exposed to the container with r/w access (cgroup perms).
     # When this is enabled mounts will still remain in the monitor's namespace
@@ -1080,71 +547,92 @@ sub update_lxc_config {
     # files while the container is running!
     $raw .= "lxc.monitor.unshare = 1\n";
 
+    my $cgv1 = get_cgroup_subsystems();
+
     # Should we read them from /etc/subuid?
     if ($unprivileged && !$custom_idmap) {
-       $raw .= "lxc.id_map = u 0 100000 65536\n";
-       $raw .= "lxc.id_map = g 0 100000 65536\n";
+       $raw .= "lxc.idmap = u 0 100000 65536\n";
+       $raw .= "lxc.idmap = g 0 100000 65536\n";
     }
 
-    if (!has_dev_console($conf)) {
-       $raw .= "lxc.console = none\n";
-       $raw .= "lxc.cgroup.devices.deny = c 5:1 rwm\n";
+    if (!PVE::LXC::Config->has_dev_console($conf)) {
+       $raw .= "lxc.console.path = none\n";
+       $raw .= "lxc.cgroup.devices.deny = c 5:1 rwm\n" if $cgv1->{devices};
     }
 
-    my $ttycount = get_tty_count($conf);
-    $raw .= "lxc.tty = $ttycount\n";
+    my $ttycount = PVE::LXC::Config->get_tty_count($conf);
+    $raw .= "lxc.tty.max = $ttycount\n";
 
     # some init scripts expect a linux terminal (turnkey).
     $raw .= "lxc.environment = TERM=linux\n";
     
     my $utsname = $conf->{hostname} || "CT$vmid";
-    $raw .= "lxc.utsname = $utsname\n";
+    $raw .= "lxc.uts.name = $utsname\n";
+
+    if ($cgv1->{memory}) {
+       my $memory = $conf->{memory} || 512;
+       my $swap = $conf->{swap} // 0;
 
-    my $memory = $conf->{memory} || 512;
-    my $swap = $conf->{swap} // 0;
+       my $lxcmem = int($memory*1024*1024);
+       $raw .= "lxc.cgroup.memory.limit_in_bytes = $lxcmem\n";
 
-    my $lxcmem = int($memory*1024*1024);
-    $raw .= "lxc.cgroup.memory.limit_in_bytes = $lxcmem\n";
+       my $lxcswap = int(($memory + $swap)*1024*1024);
+       $raw .= "lxc.cgroup.memory.memsw.limit_in_bytes = $lxcswap\n";
+    }
 
-    my $lxcswap = int(($memory + $swap)*1024*1024);
-    $raw .= "lxc.cgroup.memory.memsw.limit_in_bytes = $lxcswap\n";
+    if ($cgv1->{cpu}) {
+       if (my $cpulimit = $conf->{cpulimit}) {
+           $raw .= "lxc.cgroup.cpu.cfs_period_us = 100000\n";
+           my $value = int(100000*$cpulimit);
+           $raw .= "lxc.cgroup.cpu.cfs_quota_us = $value\n";
+       }
 
-    if (my $cpulimit = $conf->{cpulimit}) {
-       $raw .= "lxc.cgroup.cpu.cfs_period_us = 100000\n";
-       my $value = int(100000*$cpulimit);
-       $raw .= "lxc.cgroup.cpu.cfs_quota_us = $value\n";
+       my $shares = $conf->{cpuunits} || 1024;
+       $raw .= "lxc.cgroup.cpu.shares = $shares\n";
     }
 
-    my $shares = $conf->{cpuunits} || 1024;
-    $raw .= "lxc.cgroup.cpu.shares = $shares\n";
+    die "missing 'rootfs' configuration\n"
+       if !defined($conf->{rootfs});
 
-    my $mountpoint = parse_ct_rootfs($conf->{rootfs});
+    my $mountpoint = PVE::LXC::Config->parse_ct_rootfs($conf->{rootfs});
 
-    $raw .= "lxc.rootfs = $dir/rootfs\n";
+    $raw .= "lxc.rootfs.path = $dir/rootfs\n";
 
-    my $netcount = 0;
-    foreach my $k (keys %$conf) {
+    foreach my $k (sort keys %$conf) {
        next if $k !~ m/^net(\d+)$/;
        my $ind = $1;
-       my $d = parse_lxc_network($conf->{$k});
-       $netcount++;
-       $raw .= "lxc.network.type = veth\n";
-       $raw .= "lxc.network.veth.pair = veth${vmid}i${ind}\n";
-       $raw .= "lxc.network.hwaddr = $d->{hwaddr}\n" if defined($d->{hwaddr});
-       $raw .= "lxc.network.name = $d->{name}\n" if defined($d->{name});
-       $raw .= "lxc.network.mtu = $d->{mtu}\n" if defined($d->{mtu});
+       my $d = PVE::LXC::Config->parse_lxc_network($conf->{$k});
+       $raw .= "lxc.net.$ind.type = veth\n";
+       $raw .= "lxc.net.$ind.veth.pair = veth${vmid}i${ind}\n";
+       $raw .= "lxc.net.$ind.hwaddr = $d->{hwaddr}\n" if defined($d->{hwaddr});
+       $raw .= "lxc.net.$ind.name = $d->{name}\n" if defined($d->{name});
+       $raw .= "lxc.net.$ind.mtu = $d->{mtu}\n" if defined($d->{mtu});
     }
 
-    if (my $lxcconf = $conf->{lxc}) {
-       foreach my $entry (@$lxcconf) {
-           my ($k, $v) = @$entry;
-           $netcount++ if $k eq 'lxc.network.type';
-           $raw .= "$k = $v\n";
+    if ($cgv1->{cpuset}) {
+       my $had_cpuset = 0;
+       if (my $lxcconf = $conf->{lxc}) {
+           foreach my $entry (@$lxcconf) {
+               my ($k, $v) = @$entry;
+               $had_cpuset = 1 if $k eq 'lxc.cgroup.cpuset.cpus';
+               $raw .= "$k = $v\n";
+           }
+       }
+
+       my $cores = $conf->{cores};
+       if (!$had_cpuset && $cores) {
+           my $cpuset = eval { PVE::CpuSet->new_from_cgroup('lxc', 'effective_cpus') };
+           $cpuset = PVE::CpuSet->new_from_cgroup('', 'effective_cpus') if !$cpuset;
+           my @members = $cpuset->members();
+           while (scalar(@members) > $cores) {
+               my $randidx = int(rand(scalar(@members)));
+               $cpuset->delete($members[$randidx]);
+               splice(@members, $randidx, 1); # keep track of the changes
+           }
+           $raw .= "lxc.cgroup.cpuset.cpus = ".$cpuset->short_string()."\n";
        }
     }
 
-    $raw .= "lxc.network.type = empty\n" if !$netcount;
-    
     File::Path::mkpath("$dir/rootfs");
 
     PVE::Tools::file_set_contents("$dir/config", $raw);
@@ -1175,229 +663,25 @@ sub verify_searchdomain_list {
     return join(' ', @list);
 }
 
-sub add_unused_volume {
-    my ($config, $volid) = @_;
-
-    my $key;
-    for (my $ind = $MAX_UNUSED_DISKS - 1; $ind >= 0; $ind--) {
-       my $test = "unused$ind";
-       if (my $vid = $config->{$test}) {
-           return if $vid eq $volid; # do not add duplicates
-       } else {
-           $key = $test;
-       }
-    }
-
-    die "Too many unused volumes - please delete them first.\n" if !$key;
-
-    $config->{$key} = $volid;
-
-    return $key;
-}
-
-sub update_pct_config {
-    my ($vmid, $conf, $running, $param, $delete) = @_;
-
-    my @nohotplug;
-
-    my $new_disks = 0;
-    my @deleted_volumes;
-
-    my $rootdir;
-    if ($running) {
-       my $pid = find_lxc_pid($vmid);
-       $rootdir = "/proc/$pid/root";
-    }
-
-    my $hotplug_error = sub {
-       if ($running) {
-           push @nohotplug, @_;
-           return 1;
-       } else {
-           return 0;
-       }
-    };
-
-    if (defined($delete)) {
-       foreach my $opt (@$delete) {
-           if (!exists($conf->{$opt})) {
-               warn "no such option: $opt\n";
-               next;
-           }
-
-           if ($opt eq 'hostname' || $opt eq 'memory' || $opt eq 'rootfs') {
-               die "unable to delete required option '$opt'\n";
-           } elsif ($opt eq 'swap') {
-               delete $conf->{$opt};
-               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' ||
-                    $opt eq 'tty' || $opt eq 'console' || $opt eq 'cmode') {
-               next if $hotplug_error->($opt);
-               delete $conf->{$opt};
-           } elsif ($opt =~ m/^net(\d)$/) {
-               delete $conf->{$opt};
-               next if !$running;
-               my $netid = $1;
-               PVE::Network::veth_delete("veth${vmid}i$netid");
-           } elsif ($opt eq 'protection') {
-               delete $conf->{$opt};
-           } elsif ($opt =~ m/^unused(\d+)$/) {
-               next if $hotplug_error->($opt);
-               check_protection($conf, "can't remove CT $vmid drive '$opt'");
-               push @deleted_volumes, $conf->{$opt};
-               delete $conf->{$opt};
-           } elsif ($opt =~ m/^mp(\d+)$/) {
-               next if $hotplug_error->($opt);
-               check_protection($conf, "can't remove CT $vmid drive '$opt'");
-               my $mountpoint = parse_ct_mountpoint($conf->{$opt});
-               if ($mountpoint->{type} eq 'volume') {
-                   add_unused_volume($conf, $mountpoint->{volume})
-               }
-               delete $conf->{$opt};
-           } elsif ($opt eq 'unprivileged') {
-               die "unable to delete read-only option: '$opt'\n";
-           } else {
-               die "implement me (delete: $opt)"
-           }
-           write_config($vmid, $conf) if $running;
-       }
-    }
-
-    # There's no separate swap size to configure, there's memory and "total"
-    # memory (iow. memory+swap). This means we have to change them together.
-    my $wanted_memory = PVE::Tools::extract_param($param, 'memory');
-    my $wanted_swap =  PVE::Tools::extract_param($param, 'swap');
-    if (defined($wanted_memory) || defined($wanted_swap)) {
-
-       my $old_memory = ($conf->{memory} || 512);
-       my $old_swap = ($conf->{swap} || 0);
-
-       $wanted_memory //= $old_memory;
-       $wanted_swap //= $old_swap;
-
-        my $total = $wanted_memory + $wanted_swap;
-       if ($running) {
-           my $old_total = $old_memory + $old_swap;
-           if ($total > $old_total) {
-               write_cgroup_value("memory", $vmid, "memory.memsw.limit_in_bytes", int($total*1024*1024));
-               write_cgroup_value("memory", $vmid, "memory.limit_in_bytes", int($wanted_memory*1024*1024));
-           } else {
-               write_cgroup_value("memory", $vmid, "memory.limit_in_bytes", int($wanted_memory*1024*1024));
-               write_cgroup_value("memory", $vmid, "memory.memsw.limit_in_bytes", int($total*1024*1024));
-           }
-       }
-       $conf->{memory} = $wanted_memory;
-       $conf->{swap} = $wanted_swap;
-
-       write_config($vmid, $conf) if $running;
-    }
-
-    foreach my $opt (keys %$param) {
-       my $value = $param->{$opt};
-       if ($opt eq 'hostname') {
-           $conf->{$opt} = $value;
-       } elsif ($opt eq 'onboot') {
-           $conf->{$opt} = $value ? 1 : 0;
-       } elsif ($opt eq 'startup') {
-           $conf->{$opt} = $value;
-       } elsif ($opt eq 'tty' || $opt eq 'console' || $opt eq 'cmode') {
-           next if $hotplug_error->($opt);
-           $conf->{$opt} = $value;
-       } elsif ($opt eq 'nameserver') {
-           next if $hotplug_error->($opt);
-           my $list = verify_nameserver_list($value);
-           $conf->{$opt} = $list;
-       } elsif ($opt eq 'searchdomain') {
-           next if $hotplug_error->($opt);
-           my $list = verify_searchdomain_list($value);
-           $conf->{$opt} = $list;
-       } elsif ($opt eq 'cpulimit') {
-           next if $hotplug_error->($opt); # FIXME: hotplug
-           $conf->{$opt} = $value;
-       } elsif ($opt eq 'cpuunits') {
-           $conf->{$opt} = $value;
-           write_cgroup_value("cpu", $vmid, "cpu.shares", $value);
-       } elsif ($opt eq 'description') {
-           $conf->{$opt} = PVE::Tools::encode_text($value);
-       } elsif ($opt =~ m/^net(\d+)$/) {
-           my $netid = $1;
-           my $net = parse_lxc_network($value);
-           if (!$running) {
-               $conf->{$opt} = print_lxc_network($net);
-           } else {
-               update_net($vmid, $conf, $opt, $net, $netid, $rootdir);
-           }
-       } elsif ($opt eq 'protection') {
-           $conf->{$opt} = $value ? 1 : 0;
-        } elsif ($opt =~ m/^mp(\d+)$/) {
-           next if $hotplug_error->($opt);
-           check_protection($conf, "can't update CT $vmid drive '$opt'");
-           $conf->{$opt} = $value;
-           $new_disks = 1;
-        } elsif ($opt eq 'rootfs') {
-           next if $hotplug_error->($opt);
-           check_protection($conf, "can't update CT $vmid drive '$opt'");
-           $conf->{$opt} = $value;
-       } elsif ($opt eq 'unprivileged') {
-           die "unable to modify read-only option: '$opt'\n";
-       } else {
-           die "implement me: $opt";
-       }
-       write_config($vmid, $conf) if $running;
-    }
-
-    if (@deleted_volumes) {
-       my $storage_cfg = PVE::Storage::config();
-       foreach my $volume (@deleted_volumes) {
-           delete_mountpoint_volume($storage_cfg, $vmid, $volume);
-       }
-    }
-
-    if ($new_disks) {
-       my $storage_cfg = PVE::Storage::config();
-       create_disks($storage_cfg, $vmid, $conf, $conf);
-    }
-
-    # This should be the last thing we do here
-    if ($running && scalar(@nohotplug)) {
-       die "unable to modify " . join(',', @nohotplug) . " while container is running\n";
-    }
-}
-
-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 ($vmid, $conf, $noescapechar) = @_;
 
-    my $cmode = get_cmode($conf);
+    my $cmode = PVE::LXC::Config->get_cmode($conf);
 
+    my $cmd = [];
     if ($cmode eq 'console') {
-       return ['lxc-console', '-n',  $vmid, '-t', 0];
+       push @$cmd, 'lxc-console', '-n',  $vmid, '-t', 0;
+       push @$cmd, '-e', -1 if $noescapechar;
     } elsif ($cmode eq 'tty') {
-       return ['lxc-console', '-n',  $vmid];
+       push @$cmd, 'lxc-console', '-n',  $vmid;
+       push @$cmd, '-e', -1 if $noescapechar;
     } elsif ($cmode eq 'shell') {
-       return ['lxc-attach', '--clear-env', '-n', $vmid];
+       push @$cmd, 'lxc-attach', '--clear-env', '-n', $vmid;
     } else {
        die "internal error";
     }
+
+    return $cmd;
 }
 
 sub get_primary_ips {
@@ -1406,7 +690,7 @@ sub get_primary_ips {
     # return data from net0
 
     return undef if !defined($conf->{net0});
-    my $net = parse_lxc_network($conf->{net0});
+    my $net = PVE::LXC::Config->parse_lxc_network($conf->{net0});
 
     my $ipv4 = $net->{ip};
     if ($ipv4) {
@@ -1431,16 +715,16 @@ sub get_primary_ips {
 sub delete_mountpoint_volume {
     my ($storage_cfg, $vmid, $volume) = @_;
 
-    return if classify_mountpoint($volume) ne 'volume';
+    return if PVE::LXC::Config->classify_mountpoint($volume) ne 'volume';
 
     my ($vtype, $name, $owner) = PVE::Storage::parse_volname($storage_cfg, $volume);
     PVE::Storage::vdisk_free($storage_cfg, $volume) if $vmid == $owner;
 }
 
 sub destroy_lxc_container {
-    my ($storage_cfg, $vmid, $conf) = @_;
+    my ($storage_cfg, $vmid, $conf, $replacement_conf) = @_;
 
-    foreach_mountpoint($conf, sub {
+    PVE::LXC::Config->foreach_mountpoint($conf, sub {
        my ($ms, $mountpoint) = @_;
        delete_mountpoint_volume($storage_cfg, $vmid, $mountpoint->{volume});
     });
@@ -1448,7 +732,11 @@ sub destroy_lxc_container {
     rmdir "/var/lib/lxc/$vmid/rootfs";
     unlink "/var/lib/lxc/$vmid/config";
     rmdir "/var/lib/lxc/$vmid";
-    destroy_config($vmid);
+    if (defined $replacement_conf) {
+       PVE::LXC::Config->write_config($vmid, $replacement_conf);
+    } else {
+       destroy_config($vmid);
+    }
 
     #my $cmd = ['lxc-destroy', '-n', $vmid ];
     #PVE::Tools::run_command($cmd);
@@ -1460,7 +748,7 @@ sub vm_stop_cleanup {
     eval {
        if (!$keepActive) {
 
-            my $vollist = get_vm_volumes($conf);
+            my $vollist = PVE::LXC::Config->get_vm_volumes($conf);
            PVE::Storage::deactivate_volumes($storage_cfg, $vollist);
        }
     };
@@ -1499,36 +787,44 @@ sub update_net {
     my $eth = $newnet->{name};
 
     if (my $oldnetcfg = $conf->{$opt}) {
-       my $oldnet = parse_lxc_network($oldnetcfg);
+       my $oldnet = PVE::LXC::Config->parse_lxc_network($oldnetcfg);
 
        if (&$safe_string_ne($oldnet->{hwaddr}, $newnet->{hwaddr}) ||
            &$safe_string_ne($oldnet->{name}, $newnet->{name})) {
 
            PVE::Network::veth_delete($veth);
            delete $conf->{$opt};
-           write_config($vmid, $conf);
+           PVE::LXC::Config->write_config($vmid, $conf);
 
            hotplug_net($vmid, $conf, $opt, $newnet, $netid);
 
-       } elsif (&$safe_string_ne($oldnet->{bridge}, $newnet->{bridge}) ||
-                &$safe_num_ne($oldnet->{tag}, $newnet->{tag}) ||
-                &$safe_num_ne($oldnet->{firewall}, $newnet->{firewall})) {
+       } else {
+           if (&$safe_string_ne($oldnet->{bridge}, $newnet->{bridge}) ||
+               &$safe_num_ne($oldnet->{tag}, $newnet->{tag}) ||
+               &$safe_num_ne($oldnet->{firewall}, $newnet->{firewall})) {
 
                if ($oldnet->{bridge}) {
                    PVE::Network::tap_unplug($veth);
                    foreach (qw(bridge tag firewall)) {
                        delete $oldnet->{$_};
                    }
-                   $conf->{$opt} = print_lxc_network($oldnet);
-                   write_config($vmid, $conf);
+                   $conf->{$opt} = PVE::LXC::Config->print_lxc_network($oldnet);
+                   PVE::LXC::Config->write_config($vmid, $conf);
                }
 
-               PVE::Network::tap_plug($veth, $newnet->{bridge}, $newnet->{tag}, $newnet->{firewall}, $newnet->{trunks});
-               foreach (qw(bridge tag firewall)) {
+               PVE::Network::tap_plug($veth, $newnet->{bridge}, $newnet->{tag}, $newnet->{firewall}, $newnet->{trunks}, $newnet->{rate});
+               # This includes the rate:
+               foreach (qw(bridge tag firewall rate)) {
                    $oldnet->{$_} = $newnet->{$_} if $newnet->{$_};
                }
-               $conf->{$opt} = print_lxc_network($oldnet);
-               write_config($vmid, $conf);
+           } elsif (&$safe_string_ne($oldnet->{rate}, $newnet->{rate})) {
+               # Rate can be applied on its own but any change above needs to
+               # include the rate in tap_plug since OVS resets everything.
+               PVE::Network::tap_rate_limit($veth, $newnet->{rate});
+               $oldnet->{rate} = $newnet->{rate}
+           }
+           $conf->{$opt} = PVE::LXC::Config->print_lxc_network($oldnet);
+           PVE::LXC::Config->write_config($vmid, $conf);
        }
     } else {
        hotplug_net($vmid, $conf, $opt, $newnet, $netid);
@@ -1545,7 +841,7 @@ sub hotplug_net {
     my $eth = $newnet->{name};
 
     PVE::Network::veth_create($veth, $vethpeer, $newnet->{bridge}, $newnet->{hwaddr});
-    PVE::Network::tap_plug($veth, $newnet->{bridge}, $newnet->{tag}, $newnet->{firewall}, $newnet->{trunks});
+    PVE::Network::tap_plug($veth, $newnet->{bridge}, $newnet->{tag}, $newnet->{firewall}, $newnet->{trunks}, $newnet->{rate});
 
     # attach peer in container
     my $cmd = ['lxc-device', '-n', $vmid, 'add', $vethpeer, "$eth" ];
@@ -1559,9 +855,9 @@ sub hotplug_net {
     foreach (qw(bridge tag firewall hwaddr name)) {
        $done->{$_} = $newnet->{$_} if $newnet->{$_};
     }
-    $conf->{$opt} = print_lxc_network($done);
+    $conf->{$opt} = PVE::LXC::Config->print_lxc_network($done);
 
-    write_config($vmid, $conf);
+    PVE::LXC::Config->write_config($vmid, $conf);
 }
 
 sub update_ipconfig {
@@ -1569,7 +865,7 @@ sub update_ipconfig {
 
     my $lxc_setup = PVE::LXC::Setup->new($conf, $rootdir);
 
-    my $optdata = parse_lxc_network($conf->{$opt});
+    my $optdata = PVE::LXC::Config->parse_lxc_network($conf->{$opt});
     my $deleted = [];
     my $added = [];
     my $nscmd = sub {
@@ -1589,9 +885,10 @@ sub update_ipconfig {
        my $newip = $newnet->{$ip};
        my $newgw = $newnet->{$gw};
        my $oldip = $optdata->{$ip};
+       my $oldgw = $optdata->{$gw};
 
        my $change_ip = &$safe_string_ne($oldip, $newip);
-       my $change_gw = &$safe_string_ne($optdata->{$gw}, $newgw);
+       my $change_gw = &$safe_string_ne($oldgw, $newgw);
 
        return if !$change_ip && !$change_gw;
 
@@ -1634,6 +931,11 @@ sub update_ipconfig {
                # warn and continue
                warn $@ if $@;
            }
+           if ($oldgw && $oldip && !PVE::Network::is_ip_in_cidr($oldgw, $oldip)) {
+               eval { &$ipcmd($family_opt, 'route', 'del', $oldgw, 'dev', $eth); };
+               # warn if the route was deleted manually
+               warn $@ if $@;
+           }
        }
 
        # from this point on we save the configuration
@@ -1654,392 +956,93 @@ sub update_ipconfig {
 
            if ($ipversion == 4) {
                &$nscmd({}, 'sysctl', "net.ipv4.conf.$eth.promote_secondaries=$promote");
-           }
-       }
-
-       foreach my $property ($ip, $gw) {
-           if ($newnet->{$property}) {
-               $optdata->{$property} = $newnet->{$property};
-           } else {
-               delete $optdata->{$property};
-           }
-       }
-       $conf->{$opt} = print_lxc_network($optdata);
-       write_config($vmid, $conf);
-       $lxc_setup->setup_network($conf);
-    };
-
-    &$change_ip_config(4);
-    &$change_ip_config(6);
-
-}
-
-# Internal snapshots
-
-# NOTE: Snapshot create/delete involves several non-atomic
-# actions, and can take a long time.
-# So we try to avoid locking the file and use the 'lock' variable
-# inside the config file instead.
-
-my $snapshot_copy_config = sub {
-    my ($source, $dest) = @_;
-
-    foreach my $k (keys %$source) {
-       next if $k eq 'snapshots';
-       next if $k eq 'snapstate';
-       next if $k eq 'snaptime';
-       next if $k eq 'vmstate';
-       next if $k eq 'lock';
-       next if $k eq 'digest';
-       next if $k eq 'description';
-
-       $dest->{$k} = $source->{$k};
-    }
-};
-
-my $snapshot_prepare = sub {
-    my ($vmid, $snapname, $comment) = @_;
-
-    my $snap;
-
-    my $updatefn =  sub {
-
-       my $conf = load_config($vmid);
-
-       die "you can't take a snapshot if it's a template\n"
-           if is_template($conf);
-
-       check_lock($conf);
-
-       $conf->{lock} = 'snapshot';
-
-       die "snapshot name '$snapname' already used\n"
-           if defined($conf->{snapshots}->{$snapname});
-
-       my $storecfg = PVE::Storage::config();
-       my $feature = $snapname eq 'vzdump' ? 'vzdump' : 'snapshot';
-       die "snapshot feature is not available\n" if !has_feature($feature, $conf, $storecfg);
-
-       $snap = $conf->{snapshots}->{$snapname} = {};
-
-       &$snapshot_copy_config($conf, $snap);
-
-       $snap->{'snapstate'} = "prepare";
-       $snap->{'snaptime'} = time();
-       $snap->{'description'} = $comment if $comment;
-       $conf->{snapshots}->{$snapname} = $snap;
-
-       write_config($vmid, $conf);
-    };
-
-    lock_container($vmid, 10, $updatefn);
-
-    return $snap;
-};
-
-my $snapshot_commit = sub {
-    my ($vmid, $snapname) = @_;
-
-    my $updatefn = sub {
-
-       my $conf = load_config($vmid);
-
-       die "missing snapshot lock\n"
-           if !($conf->{lock} && $conf->{lock} eq 'snapshot');
-
-       die "snapshot '$snapname' does not exist\n"
-           if !defined($conf->{snapshots}->{$snapname});
-
-       die "wrong snapshot state\n"
-           if !($conf->{snapshots}->{$snapname}->{'snapstate'} && 
-                $conf->{snapshots}->{$snapname}->{'snapstate'} eq "prepare");
-
-       delete $conf->{snapshots}->{$snapname}->{'snapstate'};
-       delete $conf->{lock};
-       $conf->{parent} = $snapname;
-
-       write_config($vmid, $conf);
-    };
-
-    lock_container($vmid, 10 ,$updatefn);
-};
-
-sub has_feature {
-    my ($feature, $conf, $storecfg, $snapname) = @_;
-    
-    my $err;
-    my $vzdump = $feature eq 'vzdump';
-    $feature = 'snapshot' if $vzdump;
-
-    foreach_mountpoint($conf, sub {
-       my ($ms, $mountpoint) = @_;
-
-       return if $err; # skip further test
-       return if $vzdump && $ms ne 'rootfs' && !$mountpoint->{backup};
-       
-       $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 $enter_namespace = sub {
-    my ($vmid, $pid, $which, $type) = @_;
-    sysopen my $fd, "/proc/$pid/ns/$which", O_RDONLY
-       or die "failed to open $which namespace of container $vmid: $!\n";
-    PVE::Tools::setns(fileno($fd), $type)
-       or die "failed to enter $which namespace of container $vmid: $!\n";
-    close $fd;
-};
-
-my $do_syncfs = sub {
-    my ($vmid, $pid, $socket) = @_;
-
-    &$enter_namespace($vmid, $pid, 'mnt', PVE::Tools::CLONE_NEWNS);
-
-    # Tell the parent process to start reading our /proc/mounts
-    print {$socket} "go\n";
-    $socket->flush();
-
-    # Receive /proc/self/mounts
-    my $mountdata = do { local $/ = undef; <$socket> };
-    close $socket;
-
-    # Now sync all mountpoints...
-    my $mounts = PVE::ProcFSTools::parse_mounts($mountdata);
-    foreach my $mp (@$mounts) {
-       my ($what, $dir, $fs) = @$mp;
-       next if $fs eq 'fuse.lxcfs';
-       eval { PVE::Tools::sync_mountpoint($dir); };
-       warn $@ if $@;
-    }
-};
-
-sub sync_container_namespace {
-    my ($vmid) = @_;
-    my $pid = find_lxc_pid($vmid);
-
-    # SOCK_DGRAM is nicer for barriers but cannot be slurped
-    socketpair my $pfd, my $cfd, AF_UNIX, SOCK_STREAM, PF_UNSPEC
-       or die "failed to create socketpair: $!\n";
-
-    my $child = fork();
-    die "fork failed: $!\n" if !defined($child);
-
-    if (!$child) {
-       eval {
-           close $pfd;
-           &$do_syncfs($vmid, $pid, $cfd);
-       };
-       if (my $err = $@) {
-           warn $err;
-           POSIX::_exit(1);
-       }
-       POSIX::_exit(0);
-    }
-    close $cfd;
-    my $go = <$pfd>;
-    die "failed to enter container namespace\n" if $go ne "go\n";
-
-    open my $mounts, '<', "/proc/$child/mounts"
-       or die "failed to open container's /proc/mounts: $!\n";
-    my $mountdata = do { local $/ = undef; <$mounts> };
-    close $mounts;
-    print {$pfd} $mountdata;
-    close $pfd;
-
-    while (waitpid($child, 0) != $child) {}
-    die "failed to sync container namespace\n" if $? != 0;
-}
-
-sub snapshot_create {
-    my ($vmid, $snapname, $comment) = @_;
-
-    my $snap = &$snapshot_prepare($vmid, $snapname, $comment);
-
-    my $conf = load_config($vmid);
-
-    my $running = check_running($vmid);
-    
-    my $unfreeze = 0;
-
-    my $drivehash = {};
-
-    eval {
-       if ($running) {
-           $unfreeze = 1;
-           PVE::Tools::run_command(['/usr/bin/lxc-freeze', '-n', $vmid]);
-           sync_container_namespace($vmid);
-       };
-
-       my $storecfg = PVE::Storage::config();
-       my $rootinfo = parse_ct_rootfs($conf->{rootfs});
-       my $volid = $rootinfo->{volume};
-
-       PVE::Storage::volume_snapshot($storecfg, $volid, $snapname);
-       $drivehash->{rootfs} = 1;
-    };
-    my $err = $@;
-    
-    if ($unfreeze) {
-       eval { PVE::Tools::run_command(['/usr/bin/lxc-unfreeze', '-n', $vmid]); };
-       warn $@ if $@;
-    }
-    
-    if ($err) {
-       eval { snapshot_delete($vmid, $snapname, 1, $drivehash); };
-       warn "$@\n" if $@;
-       die "$err\n";
-    }
-
-    &$snapshot_commit($vmid, $snapname);
-}
-
-# Note: $drivehash is only set when called from snapshot_create.
-sub snapshot_delete {
-    my ($vmid, $snapname, $force, $drivehash) = @_;
-
-    my $snap;
-
-    my $conf;
-
-    my $updatefn =  sub {
-
-       $conf = load_config($vmid);
-
-       die "you can't delete a snapshot if vm is a template\n"
-           if is_template($conf);
-
-       $snap = $conf->{snapshots}->{$snapname};
-
-       if (!$drivehash) {
-           check_lock($conf);
-       }
-
-       die "snapshot '$snapname' does not exist\n" if !defined($snap);
-
-       $snap->{snapstate} = 'delete';
-
-       write_config($vmid, $conf);
-    };
-
-    lock_container($vmid, 10, $updatefn);
-
-    my $storecfg = PVE::Storage::config();
-
-    my $unlink_parent = sub {
-
-       my ($confref, $new_parent) = @_;
-
-       if ($confref->{parent} && $confref->{parent} eq $snapname) {
-           if ($new_parent) {
-               $confref->{parent} = $new_parent;
-           } else {
-               delete $confref->{parent};
-           }
-       }
-    };
-
-    my $del_snap =  sub {
-
-       $conf = load_config($vmid);
-
-       if ($drivehash) {
-           delete $conf->{lock};
-       } else {
-           check_lock($conf);
+           }
        }
 
-       my $parent = $conf->{snapshots}->{$snapname}->{parent};
-       foreach my $snapkey (keys %{$conf->{snapshots}}) {
-           &$unlink_parent($conf->{snapshots}->{$snapkey}, $parent);
+       foreach my $property ($ip, $gw) {
+           if ($newnet->{$property}) {
+               $optdata->{$property} = $newnet->{$property};
+           } else {
+               delete $optdata->{$property};
+           }
        }
-
-       &$unlink_parent($conf, $parent);
-
-       delete $conf->{snapshots}->{$snapname};
-
-       write_config($vmid, $conf);
+       $conf->{$opt} = PVE::LXC::Config->print_lxc_network($optdata);
+       PVE::LXC::Config->write_config($vmid, $conf);
+       $lxc_setup->setup_network($conf);
     };
 
-    my $rootfs = $conf->{snapshots}->{$snapname}->{rootfs};
-    my $rootinfo = parse_ct_rootfs($rootfs);
-    my $volid = $rootinfo->{volume};
-
-    eval {
-       PVE::Storage::volume_snapshot_delete($storecfg, $volid, $snapname);
-    };
-    my $err = $@;
+    &$change_ip_config(4);
+    &$change_ip_config(6);
 
-    if(!$err || ($err && $force)) {
-       lock_container($vmid, 10, $del_snap);
-       if ($err) {
-           die "Can't delete snapshot: $vmid $snapname $err\n";
-       }
-    }
 }
 
-sub snapshot_rollback {
-    my ($vmid, $snapname) = @_;
-
-    my $storecfg = PVE::Storage::config();
-
-    my $conf = load_config($vmid);
-
-    die "you can't rollback if vm is a template\n" if is_template($conf);
-
-    my $snap = $conf->{snapshots}->{$snapname};
-
-    die "snapshot '$snapname' does not exist\n" if !defined($snap);
-
-    my $rootfs = $snap->{rootfs};
-    my $rootinfo = parse_ct_rootfs($rootfs);
-    my $volid = $rootinfo->{volume};
-
-    PVE::Storage::volume_rollback_is_possible($storecfg, $volid, $snapname);
-
-    my $updatefn = sub {
-
-       die "unable to rollback to incomplete snapshot (snapstate = $snap->{snapstate})\n" 
-           if $snap->{snapstate};
-
-       check_lock($conf);
+my $enter_namespace = sub {
+    my ($vmid, $pid, $which, $type) = @_;
+    sysopen my $fd, "/proc/$pid/ns/$which", O_RDONLY
+       or die "failed to open $which namespace of container $vmid: $!\n";
+    PVE::Tools::setns(fileno($fd), $type)
+       or die "failed to enter $which namespace of container $vmid: $!\n";
+    close $fd;
+};
 
-       system("lxc-stop -n $vmid --kill") if check_running($vmid);
+my $do_syncfs = sub {
+    my ($vmid, $pid, $socket) = @_;
 
-       die "unable to rollback vm $vmid: vm is running\n"
-           if check_running($vmid);
+    &$enter_namespace($vmid, $pid, 'mnt', PVE::Tools::CLONE_NEWNS);
 
-       $conf->{lock} = 'rollback';
+    # Tell the parent process to start reading our /proc/mounts
+    print {$socket} "go\n";
+    $socket->flush();
 
-       my $forcemachine;
+    # Receive /proc/self/mounts
+    my $mountdata = do { local $/ = undef; <$socket> };
+    close $socket;
 
-       # copy snapshot config to current config
+    # Now sync all mountpoints...
+    my $mounts = PVE::ProcFSTools::parse_mounts($mountdata);
+    foreach my $mp (@$mounts) {
+       my ($what, $dir, $fs) = @$mp;
+       next if $fs eq 'fuse.lxcfs';
+       eval { PVE::Tools::sync_mountpoint($dir); };
+       warn $@ if $@;
+    }
+};
 
-       my $tmp_conf = $conf;
-       &$snapshot_copy_config($tmp_conf->{snapshots}->{$snapname}, $conf);
-       $conf->{snapshots} = $tmp_conf->{snapshots};
-       delete $conf->{snaptime};
-       delete $conf->{snapname};
-       $conf->{parent} = $snapname;
+sub sync_container_namespace {
+    my ($vmid) = @_;
+    my $pid = find_lxc_pid($vmid);
 
-       write_config($vmid, $conf);
-    };
+    # SOCK_DGRAM is nicer for barriers but cannot be slurped
+    socketpair my $pfd, my $cfd, AF_UNIX, SOCK_STREAM, PF_UNSPEC
+       or die "failed to create socketpair: $!\n";
 
-    my $unlockfn = sub {
-       delete $conf->{lock};
-       write_config($vmid, $conf);
-    };
+    my $child = fork();
+    die "fork failed: $!\n" if !defined($child);
 
-    lock_container($vmid, 10, $updatefn);
+    if (!$child) {
+       eval {
+           close $pfd;
+           &$do_syncfs($vmid, $pid, $cfd);
+       };
+       if (my $err = $@) {
+           warn $err;
+           POSIX::_exit(1);
+       }
+       POSIX::_exit(0);
+    }
+    close $cfd;
+    my $go = <$pfd>;
+    die "failed to enter container namespace\n" if $go ne "go\n";
 
-    PVE::Storage::volume_snapshot_rollback($storecfg, $volid, $snapname);
+    open my $mounts, '<', "/proc/$child/mounts"
+       or die "failed to open container's /proc/mounts: $!\n";
+    my $mountdata = do { local $/ = undef; <$mounts> };
+    close $mounts;
+    print {$pfd} $mountdata;
+    close $pfd;
 
-    lock_container($vmid, 5, $unlockfn);
+    while (waitpid($child, 0) != $child) {}
+    die "failed to sync container namespace\n" if $? != 0;
 }
 
 sub template_create {
@@ -2047,84 +1050,64 @@ sub template_create {
 
     my $storecfg = PVE::Storage::config();
 
-    my $rootinfo = parse_ct_rootfs($conf->{rootfs});
-    my $volid = $rootinfo->{volume};
-
-    die "Template feature is not available for '$volid'\n"
-       if !PVE::Storage::volume_has_feature($storecfg, 'template', $volid);
-
-    PVE::Storage::activate_volumes($storecfg, [$volid]);
-
-    my $template_volid = PVE::Storage::vdisk_create_base($storecfg, $volid);
-    $rootinfo->{volume} = $template_volid;
-    $conf->{rootfs} = print_ct_mountpoint($rootinfo, 1);
-
-    write_config($vmid, $conf);
-}
-
-sub is_template {
-    my ($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;
-}
-
+    PVE::LXC::Config->foreach_mountpoint($conf, sub {
+       my ($ms, $mountpoint) = @_;
 
-sub foreach_mountpoint_full {
-    my ($conf, $reverse, $func) = @_;
+       my $volid = $mountpoint->{volume};
 
-    foreach my $key (mountpoint_names($reverse)) {
-       my $value = $conf->{$key};
-       next if !defined($value);
-       my $mountpoint = $key eq 'rootfs' ? parse_ct_rootfs($value, 1) : parse_ct_mountpoint($value, 1);
-       next if !defined($mountpoint);
+       die "Template feature is not available for '$volid'\n"
+           if !PVE::Storage::volume_has_feature($storecfg, 'template', $volid);
+    });
 
-       &$func($key, $mountpoint);
-    }
-}
+    PVE::LXC::Config->foreach_mountpoint($conf, sub {
+       my ($ms, $mountpoint) = @_;
 
-sub foreach_mountpoint {
-    my ($conf, $func) = @_;
+       my $volid = $mountpoint->{volume};
 
-    foreach_mountpoint_full($conf, 0, $func);
-}
+       PVE::Storage::activate_volumes($storecfg, [$volid]);
 
-sub foreach_mountpoint_reverse {
-    my ($conf, $func) = @_;
+       my $template_volid = PVE::Storage::vdisk_create_base($storecfg, $volid);
+       $mountpoint->{volume} = $template_volid;
+       $conf->{$ms} = PVE::LXC::Config->print_ct_mountpoint($mountpoint, $ms eq "rootfs");
+    });
 
-    foreach_mountpoint_full($conf, 1, $func);
+    PVE::LXC::Config->write_config($vmid, $conf);
 }
 
 sub check_ct_modify_config_perm {
-    my ($rpcenv, $authuser, $vmid, $pool, $key_list) = @_;
-
-    return 1 if $authuser ne 'root@pam';
+    my ($rpcenv, $authuser, $vmid, $pool, $newconf, $delete) = @_;
 
-    foreach my $opt (@$key_list) {
+    return 1 if $authuser eq 'root@pam';
 
-       if ($opt eq 'cpus' || $opt eq 'cpuunits' || $opt eq 'cpulimit') {
+    my $check = sub {
+       my ($opt, $delete) = @_;
+       if ($opt eq 'cores' || $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']);
+           return if $delete;
+           my $data = $opt eq 'rootfs' ? PVE::LXC::Config->parse_ct_rootfs($newconf->{$opt})
+                                       : PVE::LXC::Config->parse_ct_mountpoint($newconf->{$opt});
+           raise_perm_exc("mount point type $data->{type} is only allowed for root\@pam")
+               if $data->{type} ne 'volume';
        } 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']);
+       } elsif ($opt eq 'features') {
+           # For now this is restricted to root@pam
+           raise_perm_exc("changing feature flags is only allowed for root\@pam");
        } else {
            $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Options']);
        }
+    };
+
+    foreach my $opt (keys %$newconf) {
+       &$check($opt, 0);
+    }
+    foreach my $opt (@$delete) {
+       &$check($opt, 1);
     }
 
     return 1;
@@ -2134,9 +1117,9 @@ sub umount_all {
     my ($vmid, $storage_cfg, $conf, $noerr) = @_;
 
     my $rootdir = "/var/lib/lxc/$vmid/rootfs";
-    my $volid_list = get_vm_volumes($conf);
+    my $volid_list = PVE::LXC::Config->get_vm_volumes($conf);
 
-    foreach_mountpoint_reverse($conf, sub {
+    PVE::LXC::Config->foreach_mountpoint_reverse($conf, sub {
        my ($ms, $mountpoint) = @_;
 
        my $volid = $mountpoint->{volume};
@@ -2163,18 +1146,20 @@ sub umount_all {
 }
 
 sub mount_all {
-    my ($vmid, $storage_cfg, $conf) = @_;
+    my ($vmid, $storage_cfg, $conf, $ignore_ro) = @_;
 
     my $rootdir = "/var/lib/lxc/$vmid/rootfs";
     File::Path::make_path($rootdir);
 
-    my $volid_list = get_vm_volumes($conf);
+    my $volid_list = PVE::LXC::Config->get_vm_volumes($conf);
     PVE::Storage::activate_volumes($storage_cfg, $volid_list);
 
     eval {
-       foreach_mountpoint($conf, sub {
+       PVE::LXC::Config->foreach_mountpoint($conf, sub {
            my ($ms, $mountpoint) = @_;
 
+           $mountpoint->{ro} = 0 if $ignore_ro;
+
            mountpoint_mount($mountpoint, $rootdir, $storage_cfg);
         });
     };
@@ -2194,15 +1179,6 @@ sub mountpoint_mount_path {
     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";
-    }
-};
-
 sub query_loopdev {
     my ($path) = @_;
     my $found;
@@ -2222,7 +1198,15 @@ sub query_loopdev {
 # Returns the loop device.
 sub run_with_loopdev {
     my ($func, $file) = @_;
-    my $device;
+    my $device = query_loopdev($file);
+    # Try to reuse an existing device
+    if ($device) {
+       # We assume that whoever setup the loop device is responsible for
+       # detaching it.
+       &$func($device);
+       return $device;
+    }
+
     my $parser = sub {
        my $line = shift;
        if ($line =~ m@^(/dev/loop\d+)$@) {
@@ -2238,6 +1222,143 @@ sub run_with_loopdev {
     return $device;
 }
 
+# In scalar mode: returns a file handle to the deepest directory node.
+# In list context: returns a list of:
+#   * the deepest directory node
+#   * the 2nd deepest directory (parent of the above)
+#   * directory name of the last directory
+# So that the path $2/$3 should lead to $1 afterwards.
+sub walk_tree_nofollow($$$) {
+    my ($start, $subdir, $mkdir) = @_;
+
+    # splitdir() returns '' for empty components including the leading /
+    my @comps = grep { length($_)>0 } File::Spec->splitdir($subdir);
+
+    sysopen(my $fd, $start, O_PATH | O_DIRECTORY)
+       or die "failed to open start directory $start: $!\n";
+
+    my $dir = $start;
+    my $last_component = undef;
+    my $second = $fd;
+    foreach my $component (@comps) {
+       $dir .= "/$component";
+       my $next = PVE::Tools::openat(fileno($fd), $component, O_NOFOLLOW | O_DIRECTORY);
+
+       if (!$next) {
+           # failed, check for symlinks and try to create the path
+           die "symlink encountered at: $dir\n" if $! == ELOOP || $! == ENOTDIR;
+           die "cannot open directory $dir: $!\n" if !$mkdir;
+
+           # We don't check for errors on mkdirat() here and just try to
+           # openat() again, since at least one error (EEXIST) is an
+           # expected possibility if multiple containers start
+           # simultaneously. If someone else injects a symlink now then
+           # the subsequent openat() will fail due to O_NOFOLLOW anyway.
+           PVE::Tools::mkdirat(fileno($fd), $component, 0755);
+
+           $next = PVE::Tools::openat(fileno($fd), $component, O_NOFOLLOW | O_DIRECTORY);
+           die "failed to create path: $dir: $!\n" if !$next;
+       }
+
+       close $second if defined($last_component);
+       $last_component = $component;
+       $second = $fd;
+       $fd = $next;
+    }
+
+    return ($fd, defined($last_component) && $second, $last_component) if wantarray;
+    close $second if defined($last_component);
+    return $fd;
+}
+
+# To guard against symlink attack races against other currently running
+# containers with shared recursive bind mount hierarchies we prepare a
+# directory handle for the directory we're mounting over to verify the
+# mountpoint afterwards.
+sub __bindmount_prepare {
+    my ($hostroot, $dir) = @_;
+    my $srcdh = walk_tree_nofollow($hostroot, $dir, 0);
+    return $srcdh;
+}
+
+# Assuming we mount to rootfs/a/b/c, verify with the directory handle to 'b'
+# ($parentfd) that 'b/c' (openat($parentfd, 'c')) really leads to the directory
+# we intended to bind mount.
+sub __bindmount_verify {
+    my ($srcdh, $parentfd, $last_dir, $ro) = @_;
+    my $destdh;
+    if ($parentfd) {
+       # Open the mount point path coming from the parent directory since the
+       # filehandle we would have gotten as first result of walk_tree_nofollow
+       # earlier is still a handle to the underlying directory instead of the
+       # mounted path.
+       $destdh = PVE::Tools::openat(fileno($parentfd), $last_dir, PVE::Tools::O_PATH | O_NOFOLLOW | O_DIRECTORY);
+       die "failed to open mount point: $!\n" if !$destdh;
+       if ($ro) {
+           my $dot = '.';
+           # no separate function because 99% of the time it's the wrong thing to use.
+           if (syscall(PVE::Syscall::faccessat, fileno($destdh), $dot, &POSIX::W_OK, 0) != -1) {
+               die "failed to mark bind mount read only\n";
+           }
+           die "read-only check failed: $!\n" if $! != EROFS;
+       }
+    } else {
+       # For the rootfs we don't have a parentfd so we open the path directly.
+       # Note that this means bindmounting any prefix of the host's
+       # /var/lib/lxc/$vmid path into another container is considered a grave
+       # security error.
+       sysopen $destdh, $last_dir, O_PATH | O_DIRECTORY;
+       die "failed to open mount point: $!\n" if !$destdh;
+    }
+
+    my ($srcdev, $srcinode) = stat($srcdh);
+    my ($dstdev, $dstinode) = stat($destdh);
+    close $srcdh;
+    close $destdh;
+
+    return ($srcdev == $dstdev && $srcinode == $dstinode);
+}
+
+# Perform the actual bind mounting:
+sub __bindmount_do {
+    my ($dir, $dest, $ro, @extra_opts) = @_;
+    PVE::Tools::run_command(['mount', '-o', 'bind', @extra_opts, $dir, $dest]);
+    if ($ro) {
+       eval { PVE::Tools::run_command(['mount', '-o', 'bind,remount,ro', $dest]); };
+       if (my $err = $@) {
+           warn "bindmount error\n";
+           # don't leave writable bind-mounts behind...
+           PVE::Tools::run_command(['umount', $dest]);
+           die $err;
+       }
+    }
+}
+
+sub bindmount {
+    my ($dir, $parentfd, $last_dir, $dest, $ro, @extra_opts) = @_;
+
+    my $srcdh = __bindmount_prepare('/', $dir);
+
+    __bindmount_do($dir, $dest, $ro, @extra_opts);
+
+    if (!__bindmount_verify($srcdh, $parentfd, $last_dir, $ro)) {
+       PVE::Tools::run_command(['umount', $dest]);
+       die "detected mount path change at: $dir\n";
+    }
+}
+
+# Cleanup $rootdir a bit (double and trailing slashes), build the mount path
+# from $rootdir and $mount and walk the path from $rootdir to the final
+# directory to check for symlinks.
+sub __mount_prepare_rootdir {
+    my ($rootdir, $mount) = @_;
+    $rootdir =~ s!/+!/!g;
+    $rootdir =~ s!/+$!!;
+    my $mount_path = "$rootdir/$mount";
+    my ($mpfd, $parentfd, $last_dir) = walk_tree_nofollow($rootdir, $mount, 1);
+    return ($rootdir, $mount_path, $mpfd, $parentfd, $last_dir);
+}
+
 # use $rootdir = undef to just return the corresponding mount path
 sub mountpoint_mount {
     my ($mountpoint, $rootdir, $storage_cfg, $snapname) = @_;
@@ -2250,14 +1371,14 @@ sub mountpoint_mount {
     
     return if !$volid || !$mount;
 
+    $mount =~ s!/+!/!g;
+
     my $mount_path;
+    my ($mpfd, $parentfd, $last_dir);
     
     if (defined($rootdir)) {
-       $rootdir =~ s!/+$!!;
-       $mount_path = "$rootdir/$mount";
-       $mount_path =~ s!/+!/!g;
-       &$check_mount_path($mount_path);
-       File::Path::mkpath($mount_path);
+       ($rootdir, $mount_path, $mpfd, $parentfd, $last_dir) =
+           __mount_prepare_rootdir($rootdir, $mount);
     }
     
     my ($storage, $volname) = PVE::Storage::parse_volume_id($volid, 1);
@@ -2265,19 +1386,24 @@ sub mountpoint_mount {
     die "unknown snapshot path for '$volid'" if !$storage && defined($snapname);
 
     my $optstring = '';
-    if (defined($mountpoint->{acl})) {
-       $optstring .= ($mountpoint->{acl} ? 'acl' : 'noacl');
-    }
-    if ($mountpoint->{ro}) {
-       $optstring .= ',' if $optstring;
-       $optstring .= 'ro';
+    my $acl = $mountpoint->{acl};
+    if (defined($acl)) {
+       $optstring .= ($acl ? 'acl' : 'noacl');
     }
+    my $readonly = $mountpoint->{ro};
 
-    my @extra_opts = ('-o', $optstring);
+    my @extra_opts;
+    @extra_opts = ('-o', $optstring) if $optstring;
 
     if ($storage) {
 
        my $scfg = PVE::Storage::storage_config($storage_cfg, $storage);
+
+       # early sanity checks:
+       # we otherwise call realpath on the rbd url
+       die "containers on rbd storage without krbd are not supported\n"
+           if $scfg->{type} eq 'rbd' && !$scfg->{krbd};
+
        my $path = PVE::Storage::path($storage_cfg, $volid, $snapname);
 
        my ($vtype, undef, undef, undef, undef, $isBase, $format) =
@@ -2296,15 +1422,25 @@ sub mountpoint_mount {
                        die "cannot mount subvol snapshots for storage type '$scfg->{type}'\n";
                    }
                } else {
-                   if ($mountpoint->{ro}) {
-                       die "read-only bind mounts not supported\n";
+                   if (defined($acl) && $scfg->{type} eq 'zfspool') {
+                       my $acltype = ($acl ? 'acltype=posixacl' : 'acltype=noacl');
+                       my (undef, $name) = PVE::Storage::parse_volname($storage_cfg, $volid);
+                       $name .= "\@$snapname" if defined($snapname);
+                       PVE::Tools::run_command(['zfs', 'set', $acltype, "$scfg->{pool}/$name"]);
                    }
-                   PVE::Tools::run_command(['mount', '-o', 'bind', @extra_opts, $path, $mount_path]);
+                   bindmount($path, $parentfd, $last_dir//$rootdir, $mount_path, $readonly, @extra_opts);
                    warn "cannot enable quota control for bind mounted subvolumes\n" if $quota;
                }
            }
-           return wantarray ? ($path, 0, $mounted_dev) : $path;
+           return wantarray ? ($path, 0, undef) : $path;
        } elsif ($format eq 'raw' || $format eq 'iso') {
+           # NOTE: 'mount' performs canonicalization without the '-c' switch, which for
+           # device-mapper devices is special-cased to use the /dev/mapper symlinks.
+           # Our autodev hook expects the /dev/dm-* device currently
+           # and will create the /dev/mapper symlink accordingly
+           $path = Cwd::realpath($path);
+           die "failed to get device path\n" if !$path;
+           ($path) = ($path =~ /^(.*)$/s); #untaint
            my $domount = sub {
                my ($path) = @_;
                if ($mount_path) {
@@ -2316,6 +1452,7 @@ sub mountpoint_mount {
                        if ($quota) {
                            push @extra_opts, '-o', 'usrjquota=aquota.user,grpjquota=aquota.group,jqfmt=vfsv0';
                        }
+                       push @extra_opts, '-o', 'ro' if $readonly;
                        PVE::Tools::run_command(['mount', @extra_opts, $path, $mount_path]);
                    }
                }
@@ -2336,18 +1473,15 @@ sub mountpoint_mount {
            die "unsupported image format '$format'\n";
        }
     } elsif ($type eq 'device') {
+       push @extra_opts, '-o', 'ro' if $readonly;
+       push @extra_opts, '-o', 'usrjquota=aquota.user,grpjquota=aquota.group,jqfmt=vfsv0' if $quota;
+       # See the NOTE above about devicemapper canonicalization
+       my ($devpath) = (Cwd::realpath($volid) =~ /^(.*)$/s); # realpath() taints
        PVE::Tools::run_command(['mount', @extra_opts, $volid, $mount_path]) if $mount_path;
-       return wantarray ? ($volid, 0, $volid) : $volid;
+       return wantarray ? ($volid, 0, $devpath) : $volid;
     } elsif ($type eq 'bind') {
-       if ($mountpoint->{ro}) {
-           die "read-only bind mounts not supported\n";
-           # Theoretically we'd have to execute both:
-           # mount -o bind $a $b
-           # mount -o bind,remount,ro $a $b
-       }
        die "directory '$volid' does not exist\n" if ! -d $volid;
-       &$check_mount_path($volid);
-       PVE::Tools::run_command(['mount', '-o', 'bind', @extra_opts, $volid, $mount_path]) if $mount_path;
+       bindmount($volid, $parentfd, $last_dir//$rootdir, $mount_path, $readonly, @extra_opts) if $mount_path;
        warn "cannot enable quota control for bind mounts\n" if $quota;
        return wantarray ? ($volid, 0, undef) : $volid;
     }
@@ -2355,29 +1489,6 @@ sub mountpoint_mount {
     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 || $mountpoint->{type} ne 'volume';
-
-        my ($sid, $volname) = PVE::Storage::parse_volume_id($volid, 1);
-        return if !$sid;
-
-        push @$vollist, $volid;
-    });
-
-    return $vollist;
-}
-
 sub mkfs {
     my ($dev, $rootuid, $rootgid) = @_;
 
@@ -2420,6 +1531,60 @@ sub destroy_disks {
     }
 }
 
+sub alloc_disk {
+    my ($storecfg, $vmid, $storage, $size_kb, $rootuid, $rootgid) = @_;
+
+    my $needs_chown = 0;
+    my $volid;
+
+    my $scfg = PVE::Storage::storage_config($storecfg, $storage);
+    # fixme: use better naming ct-$vmid-disk-X.raw?
+
+    eval {
+       my $do_format = 0;
+       if ($scfg->{type} eq 'dir' || $scfg->{type} eq 'nfs' || $scfg->{type} eq 'cifs' ) {
+           if ($size_kb > 0) {
+               $volid = PVE::Storage::vdisk_alloc($storecfg, $storage, $vmid, 'raw',
+                                                  undef, $size_kb);
+               $do_format = 1;
+           } else {
+               $volid = PVE::Storage::vdisk_alloc($storecfg, $storage, $vmid, 'subvol',
+                                                  undef, 0);
+               $needs_chown = 1;
+           }
+       } elsif ($scfg->{type} eq 'zfspool') {
+
+           $volid = PVE::Storage::vdisk_alloc($storecfg, $storage, $vmid, 'subvol',
+                                              undef, $size_kb);
+           $needs_chown = 1;
+       } elsif ($scfg->{type} eq 'drbd' || $scfg->{type} eq 'lvm' || $scfg->{type} eq 'lvmthin') {
+
+           $volid = PVE::Storage::vdisk_alloc($storecfg, $storage, $vmid, 'raw', undef, $size_kb);
+           $do_format = 1;
+
+       } 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);
+           $do_format = 1;
+       } else {
+           die "unable to create containers on storage type '$scfg->{type}'\n";
+       }
+       format_disk($storecfg, $volid, $rootuid, $rootgid) if $do_format;
+    };
+    if (my $err = $@) {
+       # in case formatting got interrupted:
+       if (defined($volid)) {
+           eval { PVE::Storage::vdisk_free($storecfg, $volid); };
+           warn $@ if $@;
+       }
+       die $err;
+    }
+
+    return ($volid, $needs_chown);
+}
+
+our $NEW_DISK_RE = qr/^([^:\s]+):(\d+(\.\d+)?)$/;
 sub create_disks {
     my ($storecfg, $vmid, $settings, $conf) = @_;
 
@@ -2429,7 +1594,7 @@ sub create_disks {
        my (undef, $rootuid, $rootgid) = PVE::LXC::parse_id_maps($conf);
        my $chown_vollist = [];
 
-       foreach_mountpoint($settings, sub {
+       PVE::LXC::Config->foreach_mountpoint($settings, sub {
            my ($ms, $mountpoint) = @_;
 
            my $volid = $mountpoint->{volume};
@@ -2437,49 +1602,21 @@ sub create_disks {
 
            my ($storage, $volname) = PVE::Storage::parse_volume_id($volid, 1);
 
-           if ($storage && ($volid =~ m/^([^:\s]+):(\d+(\.\d+)?)$/)) {
+           if ($storage && ($volid =~ $NEW_DISK_RE)) {
                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, $rootuid, $rootgid);
-                   } else {
-                       $volid = PVE::Storage::vdisk_alloc($storecfg, $storage, $vmid, 'subvol',
-                                                          undef, 0);
-                       push @$chown_vollist, $volid;
-                   }
-               } elsif ($scfg->{type} eq 'zfspool') {
-
-                   $volid = PVE::Storage::vdisk_alloc($storecfg, $storage, $vmid, 'subvol',
-                                                      undef, $size_kb);
-                   push @$chown_vollist, $volid;
-               } elsif ($scfg->{type} eq 'drbd' || $scfg->{type} eq 'lvm' || $scfg->{type} eq 'lvmthin') {
-
-                   $volid = PVE::Storage::vdisk_alloc($storecfg, $storage, $vmid, 'raw', undef, $size_kb);
-                   format_disk($storecfg, $volid, $rootuid, $rootgid);
-
-               } 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, $rootuid, $rootgid);
-               } else {
-                   die "unable to create containers on storage type '$scfg->{type}'\n";
-               }
+               my $needs_chown = 0;
+               ($volid, $needs_chown) = alloc_disk($storecfg, $vmid, $storage, $size_kb, $rootuid, $rootgid);
+               push @$chown_vollist, $volid if $needs_chown;
                push @$vollist, $volid;
                $mountpoint->{volume} = $volid;
                $mountpoint->{size} = $size_kb * 1024;
-               $conf->{$ms} = print_ct_mountpoint($mountpoint, $ms eq 'rootfs');
+               $conf->{$ms} = PVE::LXC::Config->print_ct_mountpoint($mountpoint, $ms eq 'rootfs');
            } else {
                 # use specified/existing volid/dir/device
-                $conf->{$ms} = print_ct_mountpoint($mountpoint, $ms eq 'rootfs');
+                $conf->{$ms} = PVE::LXC::Config->print_ct_mountpoint($mountpoint, $ms eq 'rootfs');
            }
        });
 
@@ -2568,7 +1705,8 @@ sub parse_id_maps {
     my $lxc = $conf->{lxc};
     foreach my $entry (@$lxc) {
        my ($key, $value) = @$entry;
-       next if $key ne 'lxc.id_map';
+       # FIXME: remove the 'id_map' variant when lxc-3.0 arrives
+       next if $key ne 'lxc.idmap' && $key ne 'lxc.id_map';
        if ($value =~ /^([ug])\s+(\d+)\s+(\d+)\s+(\d+)\s*$/) {
            my ($type, $ct, $host, $length) = ($1, $2, $3, $4);
            push @$id_map, [$type, $ct, $host, $length];
@@ -2577,7 +1715,7 @@ sub parse_id_maps {
                $rootgid = $host if $type eq 'g';
            }
        } else {
-           die "failed to parse id_map: $value\n";
+           die "failed to parse idmap: $value\n";
        }
     }
 
@@ -2599,4 +1737,163 @@ sub userns_command {
     return [];
 }
 
+sub vm_start {
+    my ($vmid, $conf, $skiplock) = @_;
+
+    update_lxc_config($vmid, $conf);
+
+    my $skiplock_flag_fn = "/run/lxc/skiplock-$vmid";
+
+    if ($skiplock) {
+       open(my $fh, '>', $skiplock_flag_fn) || die "failed to open $skiplock_flag_fn for writing: $!\n";
+       close($fh);
+    }
+
+    my $cmd = ['systemctl', 'start', "pve-container\@$vmid"];
+
+    eval { PVE::Tools::run_command($cmd); };
+    if (my $err = $@) {
+       unlink $skiplock_flag_fn;
+       die $err;
+    }
+
+    return;
+}
+
+# Helper to stop a container completely and make sure it has stopped completely.
+# This is necessary because we want the post-stop hook to have completed its
+# unmount-all step, but post-stop happens after lxc puts the container into the
+# STOPPED state.
+sub vm_stop {
+    my ($vmid, $kill, $shutdown_timeout, $exit_timeout) = @_;
+
+    # Open the container's command socket.
+    my $path = "\0/var/lib/lxc/$vmid/command";
+    my $sock = IO::Socket::UNIX->new(
+       Type => SOCK_STREAM(),
+       Peer => $path,
+    );
+    if (!$sock) {
+       return if $! == ECONNREFUSED; # The container is not running
+       die "failed to open container ${vmid}'s command socket: $!\n";
+    }
+
+    # Stop the container:
+
+    my $cmd = ['lxc-stop', '-n', $vmid];
+
+    if ($kill) {
+       push @$cmd, '--kill'; # doesn't allow timeouts
+    } elsif (defined($shutdown_timeout)) {
+       push @$cmd, '--timeout', $shutdown_timeout;
+       # Give run_command 5 extra seconds
+       $shutdown_timeout += 5;
+    }
+
+    eval { PVE::Tools::run_command($cmd, timeout => $shutdown_timeout) };
+    if (my $err = $@) {
+       warn $@ if $@;
+    }
+
+    my $result = 1;
+    my $wait = sub { $result = <$sock>; };
+    if (defined($exit_timeout)) {
+       PVE::Tools::run_with_timeout($exit_timeout, $wait);
+    } else {
+       $wait->();
+    }
+
+    return if !defined $result; # monitor is gone and the ct has stopped.
+    die "container did not stop\n";
+}
+
+sub run_unshared {
+    my ($code) = @_;
+
+    return PVE::Tools::run_fork(sub {
+       # Unshare the mount namespace
+       die "failed to unshare mount namespace: $!\n"
+           if !PVE::Tools::unshare(PVE::Tools::CLONE_NEWNS);
+       PVE::Tools::run_command(['mount', '--make-rslave', '/']);
+       return $code->();
+    });
+}
+
+my $copy_volume = sub {
+    my ($src_volid, $src, $dst_volid, $dest, $storage_cfg, $snapname) = @_;
+
+    my $src_mp = { volume => $src_volid, mp => '/' };
+    $src_mp->{type} = PVE::LXC::Config->classify_mountpoint($src_volid);
+
+    my $dst_mp = { volume => $dst_volid, mp => '/' };
+    $dst_mp->{type} = PVE::LXC::Config->classify_mountpoint($dst_volid);
+
+    my @mounted;
+    eval {
+       # mount and copy
+       mkdir $src;
+       mountpoint_mount($src_mp, $src, $storage_cfg, $snapname);
+       push @mounted, $src;
+       mkdir $dest;
+       mountpoint_mount($dst_mp, $dest, $storage_cfg);
+       push @mounted, $dest;
+
+       PVE::Tools::run_command(['/usr/bin/rsync', '--stats', '-X', '-A', '--numeric-ids',
+                                '-aH', '--whole-file', '--sparse', '--one-file-system',
+                                "$src/", $dest]);
+    };
+    my $err = $@;
+    foreach my $mount (reverse @mounted) {
+       eval { PVE::Tools::run_command(['/bin/umount', '--lazy', $mount], errfunc => sub{})};
+       warn "Can't umount $mount\n" if $@;
+    }
+
+    # If this fails they're used as mount points in a concurrent operation
+    # (which should not happen but there's also no real need to get rid of them).
+    rmdir $dest;
+    rmdir $src;
+
+    die $err if $err;
+};
+
+# Should not be called after unsharing the mount namespace!
+sub copy_volume {
+    my ($mp, $vmid, $storage, $storage_cfg, $conf, $snapname) = @_;
+
+    die "cannot copy volumes of type $mp->{type}\n" if $mp->{type} ne 'volume';
+    File::Path::make_path("/var/lib/lxc/$vmid");
+    my $dest = "/var/lib/lxc/$vmid/.copy-volume-1";
+    my $src  = "/var/lib/lxc/$vmid/.copy-volume-2";
+
+    # get id's for unprivileged container
+    my (undef, $rootuid, $rootgid) = parse_id_maps($conf);
+
+    # Allocate the disk before unsharing in order to make sure zfs subvolumes
+    # are visible in this namespace, otherwise the host only sees the empty
+    # (not-mounted) directory.
+    my $new_volid;
+    eval {
+       # Make sure $mp contains a correct size.
+       $mp->{size} = PVE::Storage::volume_size_info($storage_cfg, $mp->{volume});
+       my $needs_chown;
+       ($new_volid, $needs_chown) = alloc_disk($storage_cfg, $vmid, $storage, $mp->{size}/1024, $rootuid, $rootgid);
+       if ($needs_chown) {
+           PVE::Storage::activate_volumes($storage_cfg, [$new_volid], undef);
+           my $path = PVE::Storage::path($storage_cfg, $new_volid, undef);
+           chown($rootuid, $rootgid, $path);
+       }
+
+       run_unshared(sub {
+           $copy_volume->($mp->{volume}, $src, $new_volid, $dest, $storage_cfg, $snapname);
+       });
+    };
+    if (my $err = $@) {
+       PVE::Storage::vdisk_free($storage_cfg, $new_volid)
+           if defined($new_volid);
+       die $err;
+    }
+
+    return $new_volid;
+}
+
 1;