diff --git a/README.md b/README.md index 0b0c8f13..f08c2b1d 100644 --- a/README.md +++ b/README.md @@ -219,7 +219,7 @@ Current supported settings: | Setting | Description | Type | Default | | --- | --- | -- | -- | | `manager` | VM manager (Options: `auto` (depends on OS), `lima`)| string | "auto" | -| `ubuntu` | Ubuntu OS version (Options: `22.04`, `24.04`)| string | +| `ubuntu` | Ubuntu OS version (Options: `22.04`, `24.04`, `26.04`)| string | | `hosts_resolver` | VM hosts resolver (Options: `hosts_file`)| string | | `instance_name` | Custom name for the VM instance | string | First site name alphabetically | | `images` | Custom OS image | object | Set based on `ubuntu` version | diff --git a/cli_config/cli_config.go b/cli_config/cli_config.go index 61d30795..1ccad922 100644 --- a/cli_config/cli_config.go +++ b/cli_config/cli_config.go @@ -67,8 +67,8 @@ func (c *Config) LoadFile(path string) error { return fmt.Errorf("%w: unsupported value for `vm.manager`. Must be one of: auto, lima", InvalidConfigErr) } - if c.Vm.Ubuntu != "" && c.Vm.Ubuntu != "22.04" && c.Vm.Ubuntu != "24.04" { - return fmt.Errorf("%w: unsupported value for `vm.ubuntu`. Must be one of: 22.04, 24.04", InvalidConfigErr) + if c.Vm.Ubuntu != "" && c.Vm.Ubuntu != "22.04" && c.Vm.Ubuntu != "24.04" && c.Vm.Ubuntu != "26.04" { + return fmt.Errorf("%w: unsupported value for `vm.ubuntu`. Must be one of: 22.04, 24.04, 26.04", InvalidConfigErr) } if c.Vm.HostsResolver != "" && c.Vm.HostsResolver != "hosts_file" { diff --git a/cmd/server_create.go b/cmd/server_create.go index afcb07b1..38db7c88 100644 --- a/cmd/server_create.go +++ b/cmd/server_create.go @@ -41,7 +41,7 @@ func (c *ServerCreateCommand) init() { c.flags.StringVar(&c.providerFlag, "provider", "", "Cloud provider (digitalocean, hetzner)") c.flags.StringVar(&c.sshKey, "ssh-key", "", "Path to SSH public key to automatically add to new server") c.flags.StringVar(&c.region, "region", "", "Region to create the server in") - c.flags.StringVar(&c.image, "image", "", "Server image (default: Ubuntu 24.04)") + c.flags.StringVar(&c.image, "image", "", "Server image (default: Ubuntu 26.04)") c.flags.StringVar(&c.size, "size", "", "Server size/type to create") c.flags.BoolVar(&c.skipProvision, "skip-provision", false, "Create the server but skip provisioning") } @@ -276,7 +276,7 @@ Arguments: Options: --provider Cloud provider (digitalocean, hetzner) --region Region to create the server in - --image Server image (default: Ubuntu 24.04) + --image Server image (default: Ubuntu 26.04) --size Server size/type --skip-provision Skip provision after server is created --ssh-key Path to SSH public key to be added on the server diff --git a/pkg/lima/instance.go b/pkg/lima/instance.go index 1c719f2c..653fe049 100644 --- a/pkg/lima/instance.go +++ b/pkg/lima/instance.go @@ -8,6 +8,7 @@ import ( "os" "path/filepath" "regexp" + "strings" "text/template" "github.com/roots/trellis-cli/command" @@ -41,21 +42,28 @@ type Config struct { PortForwards []PortForward `yaml:"portForwards"` } +type Network struct { + VZNAT bool `json:"vzNAT,omitempty"` + MACAddress string `json:"macAddress,omitempty"` + Interface string `json:"interface,omitempty"` +} + type Instance struct { InventoryFile string Sites map[string]*trellis.Site - Name string `json:"name"` - Status string `json:"status"` - Dir string `json:"dir"` - Arch string `json:"arch"` - Cpus int `json:"cpus"` - Memory int `json:"memory"` - Disk int `json:"disk"` - SshLocalPort int `json:"sshLocalPort,omitempty"` - VMType string `json:"vmType,omitempty"` - MountType string `json:"-"` - Config Config `json:"config"` - Username string `json:"username,omitempty"` + Name string `json:"name"` + Status string `json:"status"` + Dir string `json:"dir"` + Arch string `json:"arch"` + Cpus int `json:"cpus"` + Memory int `json:"memory"` + Disk int `json:"disk"` + SshLocalPort int `json:"sshLocalPort,omitempty"` + VMType string `json:"vmType,omitempty"` + MountType string `json:"-"` + Config Config `json:"config"` + Network []Network `json:"network,omitempty"` + Username string `json:"username,omitempty"` } func (i *Instance) ConfigFile() string { @@ -136,6 +144,16 @@ func (i *Instance) IP() (ip string, err error) { output, err := command.Cmd("limactl", args).CombinedOutput() + // Lima's cloud-init config renames the vzNAT interface to lima0, but on some + // images (e.g. Ubuntu 26.04) the rename fails on first boot and the + // interface keeps its predictable name (enp0s2). Find it by MAC instead. + if err != nil && i.VMType != "qemu" && strings.Contains(string(output), `Cannot find device "lima0"`) { + if name := i.interfaceByMAC(); name != "" { + args = []string{"shell", "--workdir", "/", i.Name, "ip", "route", "show", "dev", name} + output, err = command.Cmd("limactl", args).CombinedOutput() + } + } + if err != nil { return "", fmt.Errorf("%w: %v\n%s", IpErr, err, string(output)) } @@ -157,6 +175,39 @@ func (i *Instance) IP() (ip string, err error) { return matches[1], nil } +/* +Gets the guest interface name for the lima0 network by its MAC address using +the output of `ip -o link show`: + + 3: enp0s2: mtu 1500 ... link/ether 52:55:55:2f:cb:a6 brd ff:ff:ff:ff:ff:ff +*/ +func (i *Instance) interfaceByMAC() string { + mac := "" + for _, n := range i.Network { + if n.Interface == "lima0" && n.MACAddress != "" { + mac = n.MACAddress + break + } + } + + if mac == "" { + return "" + } + + output, err := command.Cmd("limactl", []string{"shell", "--workdir", "/", i.Name, "ip", "-o", "link", "show"}).Output() + if err != nil { + return "" + } + + re := regexp.MustCompile(`(?mi)^\d+:\s+([^:@\s]+)[:@].*link/ether\s+` + regexp.QuoteMeta(mac) + `\b`) + matches := re.FindStringSubmatch(string(output)) + if len(matches) < 2 { + return "" + } + + return matches[1] +} + func (i *Instance) Running() bool { return i.Status == "Running" } diff --git a/pkg/lima/instance_test.go b/pkg/lima/instance_test.go index 9a7f0bbb..bbba84be 100644 --- a/pkg/lima/instance_test.go +++ b/pkg/lima/instance_test.go @@ -378,6 +378,51 @@ func TestIP(t *testing.T) { } } +func TestIPWithoutLima0Interface(t *testing.T) { + instance := &Instance{ + Name: "test", + Network: []Network{ + {VZNAT: true, MACAddress: "52:55:55:2f:cb:a6", Interface: "lima0"}, + }, + } + + linkOutput := `1: lo: mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000\ link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 +2: enp0s1: mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000\ link/ether 52:55:55:29:df:82 brd ff:ff:ff:ff:ff:ff +3: enp0s2: mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000\ link/ether 52:55:55:2f:cb:a6 brd ff:ff:ff:ff:ff:ff +` + routeOutput := `default via 192.168.65.1 proto dhcp src 192.168.65.28 metric 1024 +192.168.65.0/24 proto kernel scope link src 192.168.65.28 metric 1024 +` + commands := []command.MockCommand{ + { + Command: "limactl", + Args: []string{"shell", "--workdir", "/", instance.Name, "ip", "route", "show", "dev", "lima0"}, + Output: `Cannot find device "lima0"`, + ExitCode: 1, + }, + { + Command: "limactl", + Args: []string{"shell", "--workdir", "/", instance.Name, "ip", "-o", "link", "show"}, + Output: linkOutput, + }, + { + Command: "limactl", + Args: []string{"shell", "--workdir", "/", instance.Name, "ip", "route", "show", "dev", "enp0s2"}, + Output: routeOutput, + }, + } + defer command.MockExecCommands(t, commands)() + + ip, err := instance.IP() + if err != nil { + t.Fatal(err) + } + + if ip != "192.168.65.28" { + t.Errorf("expected 192.168.65.28\ngot %s", ip) + } +} + func TestIPQemu(t *testing.T) { instance := &Instance{ Name: "test", diff --git a/pkg/lima/ubuntu.go b/pkg/lima/ubuntu.go index 53aaa5b6..2d6b2747 100644 --- a/pkg/lima/ubuntu.go +++ b/pkg/lima/ubuntu.go @@ -25,4 +25,16 @@ var UbuntuImages = map[string][]Image{ Arch: "aarch64", }, }, + "26.04": { + { + Alias: "resolute", + Location: "https://cloud-images.ubuntu.com/resolute/current/resolute-server-cloudimg-amd64.img", + Arch: "x86_64", + }, + { + Alias: "resolute", + Location: "https://cloud-images.ubuntu.com/resolute/current/resolute-server-cloudimg-arm64.img", + Arch: "aarch64", + }, + }, } diff --git a/pkg/server/types/types.go b/pkg/server/types/types.go index 04bcde6d..46658c6b 100644 --- a/pkg/server/types/types.go +++ b/pkg/server/types/types.go @@ -17,13 +17,13 @@ func SupportedProviders() []ProviderName { return []ProviderName{ProviderDigitalOcean, ProviderHetzner} } -// DefaultImage returns the default Ubuntu 24.04 image slug for each provider. +// DefaultImage returns the default Ubuntu 26.04 image slug for each provider. func DefaultImage(provider ProviderName) string { switch provider { case ProviderDigitalOcean: - return "ubuntu-24-04-x64" + return "ubuntu-26-04-x64" case ProviderHetzner: - return "ubuntu-24.04" + return "ubuntu-26.04" default: return "" } diff --git a/trellis/trellis.go b/trellis/trellis.go index aa93894d..0461f9e3 100644 --- a/trellis/trellis.go +++ b/trellis/trellis.go @@ -39,7 +39,7 @@ var DefaultCliConfig = cli_config.Config{ Vm: cli_config.VmConfig{ Manager: "auto", HostsResolver: "hosts_file", - Ubuntu: "24.04", + Ubuntu: "26.04", InstanceName: "", ForwardHttpPort: true, },