polish(ipv6): keep the plain NIC shape, fold the /64 into vps quota
- a container without a whole /64 now gets exactly the NIC it has always had (ipv6.address + the /112 in ipv6.routes, the /112 route via that address). Only a /64 owner uses the routes-only form, so an upgraded host is unchanged for everything that does not touch the new key. - drop the `vps extra64` command: `vps quota <name> --extra64` assigns a block instead, next to the quotas it belongs with (the panel's quota dialog is the other entry point). Nothing else about quota changed. - docs updated for the merged flag. Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
This commit is contained in:
1 parent
8c728096cc
commit
b5a9ba940f
6 files changed
+46
-45
No files matched your search
+3
-2
@@ -345,8 +345,9 @@ the block is free for the next container.
|
||||
(N left)`, disabled once the pool is empty; the batch dialog has the same one.
|
||||
`vps add --extra64` is the CLI equivalent.
|
||||
- **Admin → quota**: `Assign a whole /64` for a container that has none; a
|
||||
container that already owns one shows it read-only. `vps extra64 <name>` is
|
||||
the CLI equivalent.
|
||||
container that already owns one shows it read-only. `vps quota <name>
|
||||
--extra64` is the CLI equivalent (the same dialog's other fields keep
|
||||
working).
|
||||
- **User panel**: the block is listed next to the container's IPv6 address
|
||||
(`IPv6 prefix: <block>/64`), presented plainly — nothing advertises it as
|
||||
something to ask for.
|
||||
@@ -250,6 +250,19 @@ func blockRoutes(block112, extra string) string {
|
||||
}
|
||||
}
|
||||
|
||||
// deviceIPv6Addr is the ipv6.address a container's NIC should declare: the
|
||||
// account's primary address — unless it owns a whole /64, in which case none.
|
||||
// A declared address turns every declared route into a via-address one, and the
|
||||
// kernel refuses that form for a block the account's own /112 route covers (see
|
||||
// applyExtraRoutes). A container without a block therefore keeps exactly the NIC
|
||||
// shape it has always had, on a host that never configured the feature or not.
|
||||
func deviceIPv6Addr(primary, extra string) string {
|
||||
if extra != "" {
|
||||
return ""
|
||||
}
|
||||
return primary
|
||||
}
|
||||
|
||||
// applyExtraRoutes rewrites a container's eth0 IPv6 wiring so its whole /64 is
|
||||
// routed to it: ipv6.routes carries the /112 AND the block, and ipv6.address is
|
||||
// REMOVED. Both halves matter, and both were found the hard way:
|
||||
|
||||
@@ -530,7 +530,7 @@ func (m *Manager) Add(name string, opt AddOptions) (*Result, error) {
|
||||
cloned := cloneOwner != ""
|
||||
if cloned {
|
||||
if err := m.lx.CloneFromSnapshot(cloneOwner, cloneSnap, name,
|
||||
m.cfg.Incus.Pool, m.cfg.Incus.Bridge, ip, "", blockStr, poolAddr,
|
||||
m.cfg.Incus.Pool, m.cfg.Incus.Bridge, ip, deviceIPv6Addr(ipv6, extraBlock), blockStr, poolAddr,
|
||||
m.cfg.Net.ExtIF, opt.CPU, opt.MemMB, opt.DiskGB); err != nil {
|
||||
return nil, fmt.Errorf("clone shared checkpoint: %w", err)
|
||||
}
|
||||
@@ -544,7 +544,7 @@ func (m *Manager) Add(name string, opt AddOptions) (*Result, error) {
|
||||
if err := m.lx.EnsureImage(image); err != nil {
|
||||
return nil, fmt.Errorf("ensure image %s: %w", image, err)
|
||||
}
|
||||
if err := m.lx.Launch(m.cfg.Incus.Pool, m.cfg.Incus.Bridge, name, image, ip, "", blockStr, poolAddr, m.cfg.Net.ExtIF, opt.CPU, opt.MemMB, opt.DiskGB); err != nil {
|
||||
if err := m.lx.Launch(m.cfg.Incus.Pool, m.cfg.Incus.Bridge, name, image, ip, deviceIPv6Addr(ipv6, extraBlock), blockStr, poolAddr, m.cfg.Net.ExtIF, opt.CPU, opt.MemMB, opt.DiskGB); err != nil {
|
||||
return nil, fmt.Errorf("launch container: %w", err)
|
||||
}
|
||||
}
|
||||
@@ -1526,7 +1526,7 @@ func (m *Manager) Reinstall(name, image string) (string, error) {
|
||||
// account owns one (it keeps it across a reinstall).
|
||||
blockStr = blockRoutes(blockStr, u.IPv6ExtraBlock)
|
||||
}
|
||||
if err := m.lx.Launch(m.cfg.Incus.Pool, m.cfg.Incus.Bridge, u.Name, image, u.IP, "", blockStr, u.IPv6Address, m.cfg.Net.ExtIF, u.CPU, u.MemMB, u.DiskGB); err != nil {
|
||||
if err := m.lx.Launch(m.cfg.Incus.Pool, m.cfg.Incus.Bridge, u.Name, image, u.IP, deviceIPv6Addr(ipv6, u.IPv6ExtraBlock), blockStr, u.IPv6Address, m.cfg.Net.ExtIF, u.CPU, u.MemMB, u.DiskGB); err != nil {
|
||||
rollback()
|
||||
return "", fmt.Errorf("recreate container: %w", err)
|
||||
}
|
||||
|
||||
@@ -252,17 +252,19 @@ func (m *Manager) ReinstallFromShare(name, code string, runInit bool) (string, e
|
||||
m.db.UpdateUserStatus(u.ID, db.StatusFailed)
|
||||
return "", err
|
||||
}
|
||||
blockStr := ""
|
||||
blockStr, addr := "", ""
|
||||
if m.cfg.IPv6ModeEffective() != cfg.IPv6ModePool {
|
||||
primary, _ := m.IPv6Addr(u.Name)
|
||||
if block, _ := m.IPv6Block(u.Name); block != nil {
|
||||
blockStr = block.String()
|
||||
}
|
||||
// The account keeps its whole /64 across the reinstall, so the clone
|
||||
// gets the same declared routes.
|
||||
// gets the same declared routes (and the same NIC shape).
|
||||
blockStr = blockRoutes(blockStr, u.IPv6ExtraBlock)
|
||||
addr = deviceIPv6Addr(primary, u.IPv6ExtraBlock)
|
||||
}
|
||||
if err := m.lx.CloneFromSnapshot(owner.Name, snap, tmp,
|
||||
m.cfg.Incus.Pool, m.cfg.Incus.Bridge, u.IP, "", blockStr, u.IPv6Address,
|
||||
m.cfg.Incus.Pool, m.cfg.Incus.Bridge, u.IP, addr, blockStr, u.IPv6Address,
|
||||
m.cfg.Net.ExtIF, u.CPU, u.MemMB, u.DiskGB); err != nil {
|
||||
m.db.UpdateUserStatus(u.ID, db.StatusFailed)
|
||||
return "", fmt.Errorf("clone shared checkpoint: %w", err)
|
||||
|
||||
@@ -835,21 +835,24 @@ func freshNICAddrs() (map[string]string, error) {
|
||||
// following the disk here.
|
||||
func (m *Manager) instanceConfig(u *db.User) (map[string]string, map[string]lx.Device) {
|
||||
poolMode := m.cfg.IPv6ModeEffective() == cfg.IPv6ModePool
|
||||
block, poolAddr := "", ""
|
||||
block, poolAddr, addr := "", "", ""
|
||||
if poolMode {
|
||||
// Only pool mode keeps the address on the routed NIC, and only when
|
||||
// the user actually has one.
|
||||
poolAddr = u.IPv6Address
|
||||
} else {
|
||||
primary, _ := m.IPv6Addr(u.Name)
|
||||
if b, _ := m.IPv6Block(u.Name); b != nil {
|
||||
block = b.String()
|
||||
}
|
||||
// The imported account never carries a whole /64 (the block is
|
||||
// host-local, like the /112 index), so only the /112 is declared.
|
||||
// host-local, like the /112 index), so this normally keeps the plain
|
||||
// /112-only shape.
|
||||
block = blockRoutes(block, u.IPv6ExtraBlock)
|
||||
addr = deviceIPv6Addr(primary, u.IPv6ExtraBlock)
|
||||
}
|
||||
return m.lx.InstanceSpec(m.cfg.Incus.Pool, m.cfg.Incus.Bridge, u.IP,
|
||||
"", block, poolAddr, m.cfg.Net.ExtIF, u.CPU, u.MemMB, u.DiskGB)
|
||||
addr, block, poolAddr, m.cfg.Net.ExtIF, u.CPU, u.MemMB, u.DiskGB)
|
||||
}
|
||||
|
||||
// requireStopped refuses to export a container that is not stopped. The check
|
||||
|
||||
+16
-34
@@ -151,12 +151,6 @@ func main() {
|
||||
}
|
||||
case "quota":
|
||||
err = userQuota(os.Args[2:])
|
||||
case "extra64":
|
||||
if len(os.Args) != 3 {
|
||||
err = fmt.Errorf("usage: vps extra64 <name>")
|
||||
break
|
||||
}
|
||||
err = userExtra64(os.Args[2])
|
||||
case "power":
|
||||
if len(os.Args) != 4 {
|
||||
err = fmt.Errorf("usage: vps power <name> start|stop|restart")
|
||||
@@ -208,8 +202,7 @@ func usage() {
|
||||
usage:
|
||||
vps list [name] all users, or one user's detail
|
||||
vps add <name> [--cpu 1] [--mem 1G] [--disk 10G] [--bandwidth 100] [--days 30] [--extra64]
|
||||
vps quota <name> [--cpu 2] [--mem 2G] [--disk 20G] [--bandwidth 200] [--days 30] [--clear-expiry]
|
||||
vps extra64 <name> assign a whole /64 block from net.ipv6_extra_prefix
|
||||
vps quota <name> [--cpu 2] [--mem 2G] [--disk 20G] [--bandwidth 200] [--days 30] [--clear-expiry] [--extra64]
|
||||
vps power <name> start|stop|restart
|
||||
vps passwd <name> reissue user panel password (shown once)
|
||||
vps admin-passwd reset admin panel password (shown once)
|
||||
@@ -1403,32 +1396,9 @@ func userDel(name string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// userExtra64 hands an existing container a whole /64 out of
|
||||
// net.ipv6_extra_prefix (the same operation as the admin panel's quota dialog).
|
||||
// Like there, it can only assign: a block stays with the account until the
|
||||
// account is deleted.
|
||||
func userExtra64(name string) error {
|
||||
c, err := cfg.Load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d, err := db.Open(c.Panel.DB)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer d.Close()
|
||||
m := mgr.New(c, d)
|
||||
block, err := m.AssignExtraBlock(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("user %s: whole /64 %s\n", name, block)
|
||||
return nil
|
||||
}
|
||||
|
||||
func userQuota(args []string) error {
|
||||
if len(args) < 1 {
|
||||
return fmt.Errorf("usage: vps quota <name> [--cpu 2] [--mem 2G] [--disk 20G] [--bandwidth 100] [--days 30] [--clear-expiry]")
|
||||
return fmt.Errorf("usage: vps quota <name> [--cpu 2] [--mem 2G] [--disk 20G] [--bandwidth 100] [--days 30] [--clear-expiry] [--extra64]")
|
||||
}
|
||||
name := args[0]
|
||||
fs := flag.NewFlagSet("update", flag.ContinueOnError)
|
||||
@@ -1437,12 +1407,14 @@ func userQuota(args []string) error {
|
||||
var memS, diskS, bandwidthS string
|
||||
var days int
|
||||
var clearExpiry bool
|
||||
var extra64 bool
|
||||
fs.StringVar(&cpuS, "cpu", "", "")
|
||||
fs.StringVar(&memS, "mem", "", "")
|
||||
fs.StringVar(&diskS, "disk", "", "")
|
||||
fs.StringVar(&bandwidthS, "bandwidth", "", "") // GiB/month, 0 = unlimited
|
||||
fs.IntVar(&days, "days", 0, "") // extend validity by N days
|
||||
fs.BoolVar(&clearExpiry, "clear-expiry", false, "")
|
||||
fs.BoolVar(&extra64, "extra64", false, "") // assign a whole /64 from net.ipv6_extra_prefix
|
||||
if err := fs.Parse(args[1:]); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1528,12 +1500,12 @@ func userQuota(args []string) error {
|
||||
bandwidthChanged := setBandwidth || bandwidthGB != u.BandwidthQuotaGB
|
||||
setDays, clearExpiry := provided["days"], provided["clear-expiry"]
|
||||
expiryChanged := setDays || clearExpiry
|
||||
if cpu == u.CPU && mem == u.MemMB && disk == u.DiskGB && !bandwidthChanged && !expiryChanged {
|
||||
if cpu == u.CPU && mem == u.MemMB && disk == u.DiskGB && !bandwidthChanged && !expiryChanged && !extra64 {
|
||||
if inter.IsTTY() {
|
||||
fmt.Println("no changes, exiting")
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("nothing to update: pass at least one of --cpu/--mem/--disk/--bandwidth/--days/--clear-expiry")
|
||||
return fmt.Errorf("nothing to update: pass at least one of --cpu/--mem/--disk/--bandwidth/--days/--clear-expiry/--extra64")
|
||||
}
|
||||
if disk < u.DiskGB {
|
||||
return fmt.Errorf("disk can only grow: current %d GiB, cannot shrink to %d GiB", u.DiskGB, disk)
|
||||
@@ -1561,6 +1533,16 @@ func userQuota(args []string) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// --extra64 hands the container a whole /64 out of net.ipv6_extra_prefix
|
||||
// (the same operation as the admin panel's quota dialog). Like there it can
|
||||
// only assign: a block stays with the account until the account is deleted.
|
||||
// Nothing to do when the host hands no blocks out, so a failed assignment
|
||||
// (no prefix configured, pool exhausted) is reported rather than ignored.
|
||||
if extra64 {
|
||||
if _, err := m.AssignExtraBlock(name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
fresh, err := d.GetUserByName(name)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
Reference in new issue
Block a user