]> git.proxmox.com Git - pve-container.git/commitdiff
Merge branch 'sdn/dhcp-support'
authorWolfgang Bumiller <w.bumiller@proxmox.com>
Tue, 21 Nov 2023 12:18:37 +0000 (13:18 +0100)
committerWolfgang Bumiller <w.bumiller@proxmox.com>
Tue, 21 Nov 2023 12:18:37 +0000 (13:18 +0100)
1  2 
src/PVE/LXC.pm
src/PVE/LXC/Config.pm
src/lxc-pve-prestart-hook

diff --combined src/PVE/LXC.pm
index 259fcb25b4c30514adfba71e4650164dee2fb526,847b8c8e9c376fe37c06730fbb82022ebcc7635c..b2f1628dc9a70c11732a9501b50241be1d351fd0
@@@ -4,8 -4,8 +4,8 @@@ use strict
  use warnings;
  
  use Cwd qw();
 -use Errno qw(ELOOP ENOTDIR EROFS ECONNREFUSED EEXIST);
 -use Fcntl qw(O_RDONLY O_WRONLY O_NOFOLLOW O_DIRECTORY);
 +use Errno qw(ELOOP ENOENT ENOTDIR EROFS ECONNREFUSED EEXIST);
 +use Fcntl qw(O_RDONLY O_WRONLY O_NOFOLLOW O_DIRECTORY :mode);
  use File::Path;
  use File::Spec;
  use IO::Poll qw(POLLIN POLLHUP);
@@@ -46,6 -46,7 +46,7 @@@ use PVE::LXC::Tools
  my $have_sdn;
  eval {
      require PVE::Network::SDN::Zones;
+     require PVE::Network::SDN::Vnets;
      $have_sdn = 1;
  };
  
@@@ -639,29 -640,6 +640,29 @@@ sub update_lxc_config 
        $raw .= "lxc.mount.auto = sys:mixed\n";
      }
  
 +    PVE::LXC::Config->foreach_passthrough_device($conf, sub {
 +      my ($key, $device) = @_;
 +
 +      die "Path is not defined for passthrough device $key"
 +          unless (defined($device->{path}));
 +
 +      my $absolute_path = $device->{path};
 +      my ($mode, $rdev) = (stat($absolute_path))[2, 6];
 +
 +      die "Device $absolute_path does not exist\n" if $! == ENOENT;
 +
 +      die "Error accessing device $absolute_path\n"
 +          if (!defined($mode) || !defined($rdev));
 +
 +      die "$absolute_path is not a device\n"
 +          if (!S_ISBLK($mode) && !S_ISCHR($mode));
 +
 +      my $major = PVE::Tools::dev_t_major($rdev);
 +      my $minor = PVE::Tools::dev_t_minor($rdev);
 +      my $device_type_char = S_ISBLK($mode) ? 'b' : 'c';
 +      $raw .= "lxc.cgroup2.devices.allow = $device_type_char $major:$minor rw\n";
 +    });
 +
      # 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
@@@ -921,6 -899,8 +922,8 @@@ sub destroy_lxc_container 
        });
      }
  
+     delete_ifaces_ipams_ips($conf, $vmid);
      rmdir "/var/lib/lxc/$vmid/rootfs";
      unlink "/var/lib/lxc/$vmid/config";
      rmdir "/var/lib/lxc/$vmid";
@@@ -984,27 -964,47 +987,47 @@@ sub update_net 
            safe_string_ne($oldnet->{name}, $newnet->{name})) {
  
            PVE::Network::veth_delete($veth);
+           if ($have_sdn && safe_string_ne($oldnet->{hwaddr}, $newnet->{hwaddr})) {
+               eval { PVE::Network::SDN::Vnets::del_ips_from_mac($oldnet->{bridge}, $oldnet->{hwaddr}, $conf->{hostname}) };
+               warn $@ if $@;
+               PVE::Network::SDN::Vnets::add_next_free_cidr($newnet->{bridge}, $conf->{hostname}, $newnet->{hwaddr}, $vmid, undef, 1);
+               PVE::Network::SDN::Vnets::add_dhcp_mapping($newnet->{bridge}, $newnet->{hwaddr});
+           }
            delete $conf->{$opt};
            PVE::LXC::Config->write_config($vmid, $conf);
  
            hotplug_net($vmid, $conf, $opt, $newnet, $netid);
  
        } else {
-           if (safe_string_ne($oldnet->{bridge}, $newnet->{bridge}) ||
+           my $bridge_changed = safe_string_ne($oldnet->{bridge}, $newnet->{bridge});
+           if ($bridge_changed ||
                safe_num_ne($oldnet->{tag}, $newnet->{tag}) ||
                safe_num_ne($oldnet->{firewall}, $newnet->{firewall}) ||
                safe_boolean_ne($oldnet->{link_down}, $newnet->{link_down})
            ) {
                if ($oldnet->{bridge}) {
+                   my $oldbridge = $oldnet->{bridge};
                    PVE::Network::tap_unplug($veth);
                    foreach (qw(bridge tag firewall)) {
                        delete $oldnet->{$_};
                    }
                    $conf->{$opt} = PVE::LXC::Config->print_lxc_network($oldnet);
                    PVE::LXC::Config->write_config($vmid, $conf);
+                   if ($have_sdn && $bridge_changed) {
+                       eval { PVE::Network::SDN::Vnets::del_ips_from_mac($oldbridge, $oldnet->{hwaddr}, $conf->{hostname}) };
+                       warn $@ if $@;
+                   }
                }
  
+               if ($have_sdn && $bridge_changed) {
+                   PVE::Network::SDN::Vnets::add_next_free_cidr($newnet->{bridge}, $conf->{hostname}, $newnet->{hwaddr}, $vmid, undef, 1);
+               }
                PVE::LXC::net_tap_plug($veth, $newnet);
  
                # This includes the rate:
            PVE::LXC::Config->write_config($vmid, $conf);
        }
      } else {
+       PVE::Network::SDN::Vnets::add_next_free_cidr($newnet->{bridge}, $conf->{hostname}, $newnet->{hwaddr}, $vmid, undef, 1);
+       PVE::Network::SDN::Vnets::add_dhcp_mapping($newnet->{bridge}, $newnet->{hwaddr});
        hotplug_net($vmid, $conf, $opt, $newnet, $netid);
      }
  
@@@ -1367,8 -1370,6 +1393,8 @@@ sub check_ct_modify_config_perm 
            $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Network']);
            check_bridge_access($rpcenv, $authuser, $oldconf->{$opt}) if $oldconf->{$opt};
            check_bridge_access($rpcenv, $authuser, $newconf->{$opt}) if $newconf->{$opt};
 +      } elsif ($opt =~ m/^dev\d+$/) {
 +          raise_perm_exc("configuring device passthrough is only allowed for root\@pam");
        } elsif ($opt eq 'nameserver' || $opt eq 'searchdomain' || $opt eq 'hostname') {
            $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Network']);
        } elsif ($opt eq 'features') {
@@@ -2418,34 -2419,6 +2444,34 @@@ sub validate_id_maps 
      }
  }
  
 +sub map_ct_id_to_host {
 +    my ($id, $id_map, $id_type) = @_;
 +
 +    for my $mapping (@$id_map) {
 +      my ($type, $ct, $host, $length) = @$mapping;
 +
 +      next if ($type ne $id_type);
 +
 +      if ($id >= $ct && $id < ($ct + $length)) {
 +          return $host - $ct + $id;
 +      }
 +    }
 +
 +    return $id;
 +}
 +
 +sub map_ct_uid_to_host {
 +    my ($uid, $id_map) = @_;
 +
 +    return map_ct_id_to_host($uid, $id_map, 'u');
 +}
 +
 +sub map_ct_gid_to_host {
 +    my ($gid, $id_map) = @_;
 +
 +    return map_ct_id_to_host($gid, $id_map, 'g');
 +}
 +
  sub userns_command {
      my ($id_map) = @_;
      if (@$id_map) {
@@@ -2791,4 -2764,30 +2817,30 @@@ sub thaw($) 
      }
  }
  
+ sub create_ifaces_ipams_ips {
+     my ($conf, $vmid) = @_;
+     return if !$have_sdn;
+     for my $opt (keys %$conf) {
+       next if $opt !~ m/^net(\d+)$/;
+       my $net = PVE::LXC::Config->parse_lxc_network($conf->{$opt});
+       next if $net->{type} ne 'veth';
+       PVE::Network::SDN::Vnets::add_next_free_cidr($net->{bridge}, $conf->{hostname}, $net->{hwaddr}, $vmid, undef, 1);
+     }
+ }
+ sub delete_ifaces_ipams_ips {
+     my ($conf, $vmid) = @_;
+     return if !$have_sdn;
+     for my $opt (keys %$conf) {
+       next if $opt !~ m/^net(\d+)$/;
+       my $net = PVE::LXC::Config->parse_lxc_network($conf->{$opt});
+       eval { PVE::Network::SDN::Vnets::del_ips_from_mac($net->{bridge}, $net->{hwaddr}, $conf->{hostname}) };
+       warn $@ if $@;
+     }
+ }
  1;
diff --combined src/PVE/LXC/Config.pm
index e5accfb5e259373d2690829f0ec159b6c990d623,53662b7d220d0bbb155131c6209d861863143fdb..0fa9b8a52f20ef00627c385de58ee8807d50468c
@@@ -22,6 -22,12 +22,12 @@@ use constant 
      FITHAW   => 0xc0045878,
  };
  
+ my $have_sdn;
+ eval {
+     require PVE::Network::SDN::Vnets;
+     $have_sdn = 1;
+ };
  my $nodename = PVE::INotify::nodename();
  my $lock_handles =  {};
  my $lockdir = "/run/lock/lxc";
@@@ -29,7 -35,6 +35,7 @@@ mkdir $lockdir
  mkdir "/etc/pve/nodes/$nodename/lxc";
  my $MAX_MOUNT_POINTS = 256;
  my $MAX_UNUSED_DISKS = $MAX_MOUNT_POINTS;
 +my $MAX_DEVICES = 256;
  
  # BEGIN implemented abstract methods from PVE::AbstractConfig
  
@@@ -909,63 -914,6 +915,63 @@@ for (my $i = 0; $i < $MAX_UNUSED_DISKS
      }
  }
  
 +PVE::JSONSchema::register_format('pve-lxc-dev-string', \&verify_lxc_dev_string);
 +sub verify_lxc_dev_string {
 +    my ($dev, $noerr) = @_;
 +
 +    # do not allow /./ or /../ or /.$ or /..$
 +    # enforce /dev/ at the beginning
 +
 +    if (
 +      $dev =~ m@/\.\.?(?:/|$)@ ||
 +      $dev !~ m!^/dev/!
 +    ) {
 +      return undef if $noerr;
 +      die "$dev is not a valid device path\n";
 +    }
 +
 +    return $dev;
 +}
 +
 +my $dev_desc = {
 +    path => {
 +      optional => 1,
 +      type => 'string',
 +      default_key => 1,
 +      format => 'pve-lxc-dev-string',
 +      format_description => 'Path',
 +      description => 'Device to pass through to the container',
 +      verbose_description => 'Path to the device to pass through to the container',
 +    },
 +    mode => {
 +      optional => 1,
 +      type => 'string',
 +      pattern => '0[0-7]{3}',
 +      format_description => 'Octal access mode',
 +      description => 'Access mode to be set on the device node',
 +    },
 +    uid => {
 +      optional => 1,
 +      type => 'integer',
 +      minimum => 0,
 +      description => 'User ID to be assigned to the device node',
 +    },
 +    gid => {
 +      optional => 1,
 +      type => 'integer',
 +      minimum => 0,
 +      description => 'Group ID to be assigned to the device node',
 +    },
 +};
 +
 +for (my $i = 0; $i < $MAX_DEVICES; $i++) {
 +    $confdesc->{"dev$i"} = {
 +      optional => 1,
 +      type => 'string', format => $dev_desc,
 +      description => "Device to pass through to the container",
 +    }
 +}
 +
  sub parse_pct_config {
      my ($filename, $raw, $strict) = @_;
  
@@@ -1313,23 -1261,6 +1319,23 @@@ sub parse_volume 
      return;
  }
  
 +sub parse_device {
 +    my ($class, $device_string, $noerr) = @_;
 +
 +    my $res = eval { PVE::JSONSchema::parse_property_string($dev_desc, $device_string) };
 +    if ($@) {
 +      return undef if $noerr;
 +      die $@;
 +    }
 +
 +    if (!defined($res->{path})) {
 +      return undef if $noerr;
 +      die "Path has to be defined\n";
 +    }
 +
 +    return $res;
 +}
 +
  sub print_volume {
      my ($class, $key, $volume) = @_;
  
@@@ -1458,6 -1389,12 +1464,12 @@@ sub vmconfig_hotplug_pending 
            } elsif ($opt =~ m/^net(\d)$/) {
                my $netid = $1;
                PVE::Network::veth_delete("veth${vmid}i$netid");
+               if ($have_sdn) {
+                   my $net = PVE::LXC::Config->parse_lxc_network($conf->{$opt});
+                   print "delete ips from $opt\n";
+                   eval { PVE::Network::SDN::Vnets::del_ips_from_mac($net->{bridge}, $net->{hwaddr}, $conf->{hostname}) };
+                   warn $@ if $@;
+               }
            } else {
                die "skip\n"; # skip non-hotpluggable opts
            }
@@@ -1534,6 -1471,12 +1546,12 @@@ sub vmconfig_apply_pending 
            } elsif ($opt =~ m/^unused(\d+)$/) {
                PVE::LXC::delete_mountpoint_volume($storecfg, $vmid, $conf->{$opt})
                    if !$class->is_volume_in_use($conf, $conf->{$opt}, 1, 1);
+           } elsif ($opt =~ m/^net(\d+)$/) {
+               if ($have_sdn) {
+                   my $net = $class->parse_lxc_network($conf->{$opt});
+                   eval { PVE::Network::SDN::Vnets::del_ips_from_mac($net->{bridge}, $net->{hwaddr}, $conf->{hostname}) };
+                   warn $@ if $@;
+               }
            }
        };
        if (my $err = $@) {
                my $netid = $1;
                my $net = $class->parse_lxc_network($conf->{pending}->{$opt});
                $conf->{pending}->{$opt} = $class->print_lxc_network($net);
+               if ($have_sdn) {
+                   if ($conf->{$opt}) {
+                       my $old_net = $class->parse_lxc_network($conf->{$opt});
+                       if ($old_net->{bridge} ne $net->{bridge} || $old_net->{hwaddr} ne $net->{hwaddr}) {
+                           PVE::Network::SDN::Vnets::del_ips_from_mac($old_net->{bridge}, $old_net->{hwaddr}, $conf->{name});
+                           PVE::Network::SDN::Vnets::add_next_free_cidr($net->{bridge}, $conf->{hostname}, $net->{hwaddr}, $vmid, undef, 1);
+                       }
+                   } else {
+                       PVE::Network::SDN::Vnets::add_next_free_cidr($net->{bridge}, $conf->{hostname}, $net->{hwaddr}, $vmid, undef, 1);
+                   }
+               }
            }
        };
        if (my $err = $@) {
@@@ -1837,16 -1791,4 +1866,16 @@@ sub get_derived_property 
      }
  }
  
 +sub foreach_passthrough_device {
 +    my ($class, $conf, $func, @param) = @_;
 +
 +    for my $key (keys %$conf) {
 +      next if $key !~ m/^dev(\d+)$/;
 +
 +      my $device = $class->parse_device($conf->{$key});
 +
 +      $func->($key, $device, @param);
 +    }
 +}
 +
  1;
index 438ffad8c6b251bbdc90ed05996a787721589d6c,fc577e4aba24af6279ef6dfd4cdf914e3510894b..ab357743c86e08e60f933af3d2039b7fe551d8d7
@@@ -6,7 -6,6 +6,7 @@@ use strict
  use warnings;
  
  use Fcntl qw(O_DIRECTORY :mode);
 +use File::Basename;
  use File::Path;
  use POSIX;
  
@@@ -22,6 -21,12 +22,12 @@@ use PVE::Storage
  use PVE::Syscall qw(:fsmount);
  use PVE::Tools qw(AT_FDCWD O_PATH);
  
+ my $have_sdn;
+ eval {
+     require PVE::Network::SDN::Vnets;
+     $have_sdn = 1;
+ };
  my $WARNFD;
  sub log_warn {
      my ($vmid, $message) = @_;
@@@ -59,13 -64,11 +65,13 @@@ PVE::LXC::Tools::lxc_hook('pre-start', 
      # Delete any leftover reboot-trigger file
      unlink("/var/lib/lxc/$vmid/reboot");
  
 -    my $devlist_file = "/var/lib/lxc/$vmid/devices";
 -    unlink $devlist_file;
 +    # Delete the old device list file
 +    # in case it was left over from a previous version of pve-container.
 +    unlink("/var/lib/lxc/$vmid/devices");
 +
      my $devices = [];
  
 -    my (undef, $rootuid, $rootgid) = PVE::LXC::parse_id_maps($conf);
 +    my ($id_map, $rootuid, $rootgid) = PVE::LXC::parse_id_maps($conf);
  
      # Unmount first when the user mounted the container with "pct mount".
      eval {
  
      PVE::LXC::Config->foreach_volume($conf, $setup_mountpoint);
  
 +    # Device passthrough
 +    my $passthrough_devices = [];
 +
 +    my $passthrough_dir = "/var/lib/lxc/$vmid/passthrough";
 +    File::Path::make_path($passthrough_dir);
 +    PVE::Tools::mount("none", $passthrough_dir, "tmpfs", 0, "size=8k")
 +        or die ("Could not mount tmpfs for device passthrough at $passthrough_dir: $!");
 +
 +    my $setup_passthrough_device = sub {
 +      my ($key, $device) = @_;
 +
 +      my $absolute_path = $device->{path};
 +      my ($mode, $rdev) = (stat($absolute_path))[2, 6];
 +
 +      die "Could not get mode or device ID of $absolute_path\n"
 +          if (!defined($mode) || !defined($rdev));
 +
 +      my $passthrough_device_path = $passthrough_dir . $absolute_path;
 +      File::Path::make_path(dirname($passthrough_device_path));
 +      PVE::Tools::mknod($passthrough_device_path, $mode, $rdev)
 +          or die("failed to mknod $passthrough_device_path: $!\n");
 +
 +      # Use chmod because umask could mess with the access mode on mknod
 +      my $passthrough_mode = 0660;
 +      $passthrough_mode = oct($device->{mode}) if defined($device->{mode});
 +      chmod $passthrough_mode, $passthrough_device_path
 +          or die "failed to chmod $passthrough_mode $passthrough_device_path: $!\n";
 +
 +      # Set uid and gid of the device node
 +      my $uid = 0;
 +      my $gid = 0;
 +      $uid = $device->{uid} if defined($device->{uid});
 +      $gid = $device->{gid} if defined($device->{gid});
 +      $uid = PVE::LXC::map_ct_uid_to_host($uid, $id_map);
 +      $gid = PVE::LXC::map_ct_gid_to_host($gid, $id_map);
 +      chown $uid, $gid, $passthrough_device_path
 +          or die("failed to chown $uid:$gid $passthrough_device_path: $!\n");
 +
 +      push @$passthrough_devices, [$absolute_path, $mode, $rdev];
 +    };
 +
 +    PVE::LXC::Config->foreach_passthrough_device($conf, $setup_passthrough_device);
 +
      my $lxc_setup = PVE::LXC::Setup->new($conf, $rootdir);
      $lxc_setup->pre_start_hook();
  
            my $minor = PVE::Tools::dev_t_minor($rdev);
            $devlist .= "b:$major:$minor:$dev\n";
        }
 -      PVE::Tools::file_set_contents($devlist_file, $devlist);
 +      PVE::Tools::file_set_contents("/var/lib/lxc/$vmid/passthrough/mounts", $devlist);
 +    }
 +
 +    if (@$passthrough_devices) {
 +      my $devlist = '';
 +      for my $dev (@$passthrough_devices) {
 +          my ($path, $mode, $rdev) = @$dev;
 +          my $major = PVE::Tools::dev_t_major($rdev);
 +          my $minor = PVE::Tools::dev_t_minor($rdev);
 +          my $device_type_char = S_ISBLK($mode) ? 'b' : 'c';
 +          $devlist .= "$device_type_char:$major:$minor:$path\n";
 +      }
 +      PVE::Tools::file_set_contents("/var/lib/lxc/$vmid/passthrough/devices", $devlist);
      }
+     if ($have_sdn) {
+       for my $k (keys %$conf) {
+           next if $k !~ /^net(\d+)/;
+           my $net = PVE::LXC::Config->parse_lxc_network($conf->{$k});
+           next if $net->{type} ne 'veth';
+           PVE::Network::SDN::Vnets::add_dhcp_mapping($net->{bridge}, $net->{hwaddr});
+       }
+     }
  });
  
  # Leftover cgroups prevent lxc from starting without any useful information