Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
4 changes: 2 additions & 2 deletions cli_config/cli_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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" {
Expand Down
4 changes: 2 additions & 2 deletions cmd/server_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down Expand Up @@ -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
Expand Down
75 changes: 63 additions & 12 deletions pkg/lima/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"os"
"path/filepath"
"regexp"
"strings"
"text/template"

"github.com/roots/trellis-cli/command"
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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))
}
Expand All @@ -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: <BROADCAST,MULTICAST,UP,LOWER_UP> 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"
}
Expand Down
45 changes: 45 additions & 0 deletions pkg/lima/instance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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: <LOOPBACK,UP,LOWER_UP> 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: <BROADCAST,MULTICAST,UP,LOWER_UP> 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: <BROADCAST,MULTICAST,UP,LOWER_UP> 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",
Expand Down
12 changes: 12 additions & 0 deletions pkg/lima/ubuntu.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
},
}
6 changes: 3 additions & 3 deletions pkg/server/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""
}
Expand Down
2 changes: 1 addition & 1 deletion trellis/trellis.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down
Loading