2
0
mirror of https://github.com/linka-cloud/d2vm.git synced 2024-11-22 15:56:24 +00:00

chore: bootloader abtraction

Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
This commit is contained in:
Adphi 2023-09-11 13:44:12 +02:00
parent ec33a7ad74
commit a003e176f5
Signed by: adphi
GPG Key ID: F2159213400E50AB
8 changed files with 292 additions and 173 deletions

2
.gitignore vendored
View File

@ -11,6 +11,8 @@ dist/
images images
/d2vm /d2vm
/examples/build /examples/build
/examples/full/demo-magic
/examples/full/inside
.goreleaser.yaml .goreleaser.yaml
docs/build docs/build
docs-src docs-src

View File

@ -87,7 +87,7 @@ install: docker-build
.build: .build:
@go generate ./... @go generate ./...
@go build -o d2vm -ldflags "-s -w -X '$(MODULE).Version=$(VERSION)' -X '$(MODULE).BuildDate=$(shell date)'" ./cmd/d2vm @CGO_ENABLED=0 go build -o d2vm -ldflags "-s -w -X '$(MODULE).Version=$(VERSION)' -X '$(MODULE).BuildDate=$(shell date)'" ./cmd/d2vm
.PHONY: build-snapshot .PHONY: build-snapshot
build-snapshot: bin build-snapshot: bin
@ -116,7 +116,7 @@ completions: .build
.PHONY: examples .PHONY: examples
examples: build-dev examples: build-dev
@mkdir -p examples/build @mkdir -p examples/build
@for f in $$(find examples -type f -name '*Dockerfile' -maxdepth 1); do \ @for f in $$(find examples -maxdepth 1 -type f -name '*Dockerfile'); do \
echo "Building $$f"; \ echo "Building $$f"; \
./d2vm build -o examples/build/$$(basename $$f|cut -d'.' -f1).qcow2 -p root -f $$f examples --force; \ ./d2vm build -o examples/build/$$(basename $$f|cut -d'.' -f1).qcow2 -p root -f $$f examples --force; \
done done

42
bootloader.go Normal file
View File

@ -0,0 +1,42 @@
// Copyright 2023 Linka Cloud All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package d2vm
import (
"context"
"fmt"
)
var bootloaderProviders = map[string]BootloaderProvider{}
func RegisterBootloaderProvider(provider BootloaderProvider) {
bootloaderProviders[provider.Name()] = provider
}
func BootloaderByName(name string) (BootloaderProvider, error) {
if p, ok := bootloaderProviders[name]; ok {
return p, nil
}
return nil, fmt.Errorf("bootloader provider %s not found", name)
}
type BootloaderProvider interface {
New(c Config) (Bootloader, error)
Name() string
}
type Bootloader interface {
Setup(ctx context.Context, raw, path, cmdline string) error
}

View File

@ -41,72 +41,10 @@ ff02::1 ip6-allnodes
ff02::2 ip6-allrouters ff02::2 ip6-allrouters
ff02::3 ip6-allhosts ff02::3 ip6-allhosts
` `
syslinuxCfgUbuntu = `DEFAULT linux
SAY Now booting the kernel from SYSLINUX...
LABEL linux
KERNEL /boot/vmlinuz
APPEND ro root=UUID=%s initrd=/boot/initrd.img net.ifnames=0 console=tty0 console=ttyS0,115200n8 %s
`
syslinuxCfgDebian = `DEFAULT linux
SAY Now booting the kernel from SYSLINUX...
LABEL linux
KERNEL /vmlinuz
APPEND ro root=UUID=%s initrd=/initrd.img net.ifnames=0 console=tty0 console=ttyS0,115200n8 %s
`
syslinuxCfgAlpine = `DEFAULT linux
SAY Now booting the kernel from SYSLINUX...
LABEL linux
KERNEL /boot/vmlinuz-virt
APPEND ro root=UUID=%s rootfstype=ext4 initrd=/boot/initramfs-virt console=ttyS0,115200 %s
`
syslinuxCfgCentOS = `DEFAULT linux
SAY Now booting the kernel from SYSLINUX...
LABEL linux
KERNEL /boot/vmlinuz
APPEND ro root=UUID=%s initrd=/boot/initrd.img net.ifnames=0 console=tty0 console=ttyS0,115200n8 %s
`
)
var (
formats = []string{"qcow2", "qed", "raw", "vdi", "vhd", "vmdk"}
mbrPaths = []string{
// debian path
"/usr/lib/syslinux/mbr/mbr.bin",
// ubuntu path
"/usr/lib/EXTLINUX/mbr.bin",
// alpine path
"/usr/share/syslinux/mbr.bin",
// centos path
"/usr/share/syslinux/mbr.bin",
// archlinux path
"/usr/lib/syslinux/bios/mbr.bin",
}
)
const (
perm os.FileMode = 0644 perm os.FileMode = 0644
) )
func sysconfig(osRelease OSRelease) (string, error) { var formats = []string{"qcow2", "qed", "raw", "vdi", "vhd", "vmdk"}
switch osRelease.ID {
case ReleaseUbuntu:
if osRelease.VersionID < "20.04" {
return syslinuxCfgDebian, nil
}
return syslinuxCfgUbuntu, nil
case ReleaseDebian:
return syslinuxCfgDebian, nil
case ReleaseKali:
return syslinuxCfgDebian, nil
case ReleaseAlpine:
return syslinuxCfgAlpine, nil
case ReleaseCentOS:
return syslinuxCfgCentOS, nil
default:
return "", fmt.Errorf("%s: distribution not supported", osRelease.ID)
}
}
type Builder interface { type Builder interface {
Build(ctx context.Context) (err error) Build(ctx context.Context) (err error)
@ -115,6 +53,8 @@ type Builder interface {
type builder struct { type builder struct {
osRelease OSRelease osRelease OSRelease
config Config
bootloader Bootloader
src string src string
img *image img *image
@ -128,8 +68,6 @@ type builder struct {
splitBoot bool splitBoot bool
bootSize uint64 bootSize uint64
mbrPath string
loDevice string loDevice string
bootPart string bootPart string
rootPart string rootPart string
@ -145,7 +83,7 @@ type builder struct {
cmdLineExtra string cmdLineExtra string
} }
func NewBuilder(ctx context.Context, workdir, imgTag, disk string, size uint64, osRelease OSRelease, format string, cmdLineExtra string, splitBoot bool, bootSize uint64, luksPassword string) (Builder, error) { func NewBuilder(ctx context.Context, workdir, imgTag, disk string, size uint64, osRelease OSRelease, format string, cmdLineExtra string, splitBoot bool, bootSize uint64, luksPassword string, bootLoader string) (Builder, error) {
if err := checkDependencies(); err != nil { if err := checkDependencies(); err != nil {
return nil, err return nil, err
} }
@ -176,16 +114,24 @@ func NewBuilder(ctx context.Context, workdir, imgTag, disk string, size uint64,
return nil, fmt.Errorf("boot partition size must be less than the disk size") return nil, fmt.Errorf("boot partition size must be less than the disk size")
} }
mbrBin := "" if bootLoader == "" {
for _, v := range mbrPaths { bootLoader = "syslinux"
if _, err := os.Stat(v); err == nil {
mbrBin = v
break
} }
config, err := osRelease.Config()
if err != nil {
return nil, err
} }
if mbrBin == "" {
return nil, fmt.Errorf("unable to find syslinux's mbr.bin path") blp, err := BootloaderByName(bootLoader)
if err != nil {
return nil, err
} }
bl, err := blp.New(config)
if err != nil {
return nil, err
}
if size == 0 { if size == 0 {
size = 10 * uint64(datasize.GB) size = 10 * uint64(datasize.GB)
} }
@ -207,12 +153,13 @@ func NewBuilder(ctx context.Context, workdir, imgTag, disk string, size uint64,
// } // }
b := &builder{ b := &builder{
osRelease: osRelease, osRelease: osRelease,
config: config,
bootloader: bl,
img: img, img: img,
diskRaw: filepath.Join(workdir, disk+".d2vm.raw"), diskRaw: filepath.Join(workdir, disk+".d2vm.raw"),
diskOut: filepath.Join(workdir, disk+"."+format), diskOut: filepath.Join(workdir, disk+"."+format),
format: f, format: f,
size: size, size: size,
mbrPath: mbrBin,
mntPoint: filepath.Join(workdir, "/mnt"), mntPoint: filepath.Join(workdir, "/mnt"),
cmdLineExtra: cmdLineExtra, cmdLineExtra: cmdLineExtra,
splitBoot: splitBoot, splitBoot: splitBoot,
@ -259,9 +206,6 @@ func (b *builder) Build(ctx context.Context) (err error) {
if err = b.unmountImg(ctx); err != nil { if err = b.unmountImg(ctx); err != nil {
return err return err
} }
if err = b.setupMBR(ctx); err != nil {
return err
}
if err = b.convert2Img(ctx); err != nil { if err = b.convert2Img(ctx); err != nil {
return err return err
} }
@ -490,45 +434,27 @@ func (b *builder) setupRootFS(ctx context.Context) (err error) {
} }
} }
func (b *builder) installKernel(ctx context.Context) error { func (b *builder) cmdline(_ context.Context) string {
logrus.Infof("installing linux kernel")
if err := exec.Run(ctx, "extlinux", "--install", b.chPath("/boot")); err != nil {
return err
}
sysconfig, err := sysconfig(b.osRelease)
if err != nil {
return err
}
var cfg string
if b.isLuksEnabled() { if b.isLuksEnabled() {
switch b.osRelease.ID { switch b.osRelease.ID {
case ReleaseAlpine: case ReleaseAlpine:
cfg = fmt.Sprintf(sysconfig, b.rootUUID, fmt.Sprintf("%s root=/dev/mapper/root cryptdm=root", b.cmdLineExtra)) return b.config.Cmdline(RootUUID(b.rootUUID), "root=/dev/mapper/root", "cryptdm=root", "cryptroot=UUID="+b.cryptUUID, b.cmdLineExtra)
cfg = strings.Replace(cfg, "root=UUID="+b.rootUUID, "cryptroot=UUID="+b.cryptUUID, 1)
case ReleaseCentOS: case ReleaseCentOS:
cfg = fmt.Sprintf(sysconfig, b.rootUUID, fmt.Sprintf("%s rd.luks.name=UUID=%s rd.luks.uuid=%s rd.luks.crypttab=0", b.cmdLineExtra, b.rootUUID, b.cryptUUID)) return b.config.Cmdline(RootUUID(b.rootUUID), "rd.luks.name=UUID="+b.rootUUID+" rd.luks.uuid="+b.cryptUUID+" rd.luks.crypttab=0", b.cmdLineExtra)
default: default:
// for some versions of debian, the cryptopts parameter MUST contain all the following: target,source,key,opts... // for some versions of debian, the cryptopts parameter MUST contain all the following: target,source,key,opts...
// see https://salsa.debian.org/cryptsetup-team/cryptsetup/-/blob/debian/buster/debian/functions // see https://salsa.debian.org/cryptsetup-team/cryptsetup/-/blob/debian/buster/debian/functions
// and https://cryptsetup-team.pages.debian.net/cryptsetup/README.initramfs.html // and https://cryptsetup-team.pages.debian.net/cryptsetup/README.initramfs.html
cfg = fmt.Sprintf(sysconfig, b.rootUUID, fmt.Sprintf("%s root=/dev/mapper/root cryptopts=target=root,source=UUID=%s,key=none,luks", b.cmdLineExtra, b.cryptUUID)) return b.config.Cmdline(nil, "root=/dev/mapper/root", "cryptopts=target=root,source=UUID="+b.cryptUUID+",key=none,luks", b.cmdLineExtra)
cfg = strings.Replace(cfg, "root=UUID="+b.rootUUID, "", 1)
} }
} else { } else {
cfg = fmt.Sprintf(sysconfig, b.rootUUID, b.cmdLineExtra) return b.config.Cmdline(RootUUID(b.rootUUID), b.cmdLineExtra)
} }
if err := b.chWriteFile("/boot/syslinux.cfg", cfg, perm); err != nil {
return err
}
return nil
} }
func (b *builder) setupMBR(ctx context.Context) error { func (b *builder) installKernel(ctx context.Context) error {
logrus.Infof("writing MBR") logrus.Infof("installing linux kernel")
if err := exec.Run(ctx, "dd", fmt.Sprintf("if=%s", b.mbrPath), fmt.Sprintf("of=%s", b.diskRaw), "bs=440", "count=1", "conv=notrunc"); err != nil { return b.bootloader.Setup(ctx, b.diskRaw, b.chPath("/boot"), b.cmdline(ctx))
return err
}
return nil
} }
func (b *builder) convert2Img(ctx context.Context) error { func (b *builder) convert2Img(ctx context.Context) error {

89
config.go Normal file
View File

@ -0,0 +1,89 @@
// Copyright 2023 Linka Cloud All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package d2vm
import (
"fmt"
"strings"
)
var (
configUbuntu = Config{
Kernel: "/boot/vmlinuz",
Initrd: "/boot/initrd.img",
}
configDebian = Config{
Kernel: "/vmlinuz",
Initrd: "/initrd.img",
}
configAlpine = Config{
Kernel: "/boot/vmlinuz-virt",
Initrd: "/boot/initramfs-virt",
}
configCentOS = Config{
Kernel: "/boot/vmlinuz",
Initrd: "/boot/initrd.img",
}
)
type Root interface {
String() string
}
type RootUUID string
func (r RootUUID) String() string {
return "UUID=" + string(r)
}
type RootPath string
func (r RootPath) String() string {
return string(r)
}
type Config struct {
Kernel string
Initrd string
}
func (c Config) Cmdline(root Root, args ...string) string {
var r string
if root != nil {
r = fmt.Sprintf("root=%s", root.String())
}
return fmt.Sprintf("ro initrd=%s %s net.ifnames=0 rootfstype=ext4 console=tty0 console=ttyS0,115200n8 %s", c.Initrd, r, strings.Join(args, " "))
}
func (r OSRelease) Config() (Config, error) {
switch r.ID {
case ReleaseUbuntu:
if r.VersionID < "20.04" {
return configDebian, nil
}
return configUbuntu, nil
case ReleaseDebian:
return configDebian, nil
case ReleaseKali:
return configDebian, nil
case ReleaseAlpine:
return configAlpine, nil
case ReleaseCentOS:
return configCentOS, nil
default:
return Config{}, fmt.Errorf("%s: distribution not supported", r.ID)
}
}

View File

@ -23,14 +23,13 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.linka.cloud/d2vm/pkg/docker" "go.linka.cloud/d2vm/pkg/docker"
"go.linka.cloud/d2vm/pkg/exec" "go.linka.cloud/d2vm/pkg/exec"
) )
func testSysconfig(t *testing.T, ctx context.Context, img, sysconf, kernel, initrd string) { func testConfig(t *testing.T, ctx context.Context, img string, config Config) {
require.NoError(t, docker.Pull(ctx, img)) require.NoError(t, docker.Pull(ctx, img))
tmpPath := filepath.Join(os.TempDir(), "d2vm-tests", strings.NewReplacer(":", "-", ".", "-").Replace(img)) tmpPath := filepath.Join(os.TempDir(), "d2vm-tests", strings.NewReplacer(":", "-", ".", "-").Replace(img))
require.NoError(t, os.MkdirAll(tmpPath, 0755)) require.NoError(t, os.MkdirAll(tmpPath, 0755))
@ -39,9 +38,6 @@ func testSysconfig(t *testing.T, ctx context.Context, img, sysconf, kernel, init
r, err := FetchDockerImageOSRelease(ctx, img, tmpPath) r, err := FetchDockerImageOSRelease(ctx, img, tmpPath)
require.NoError(t, err) require.NoError(t, err)
defer docker.Remove(ctx, img) defer docker.Remove(ctx, img)
sys, err := sysconfig(r)
require.NoError(t, err)
assert.Equal(t, sysconf, sys)
d, err := NewDockerfile(r, img, "root", "", false) d, err := NewDockerfile(r, img, "root", "", false)
require.NoError(t, err) require.NoError(t, err)
logrus.Infof("docker image based on %s", d.Release.Name) logrus.Infof("docker image based on %s", d.Release.Name)
@ -55,95 +51,67 @@ func testSysconfig(t *testing.T, ctx context.Context, img, sysconf, kernel, init
logrus.Infof("building kernel enabled image") logrus.Infof("building kernel enabled image")
require.NoError(t, docker.Build(ctx, imgUUID, p, dir)) require.NoError(t, docker.Build(ctx, imgUUID, p, dir))
defer docker.Remove(ctx, imgUUID) defer docker.Remove(ctx, imgUUID)
require.NoError(t, docker.RunAndRemove(ctx, imgUUID, "test", "-f", kernel)) require.NoError(t, docker.RunAndRemove(ctx, imgUUID, "test", "-f", config.Kernel))
require.NoError(t, docker.RunAndRemove(ctx, imgUUID, "test", "-f", initrd)) require.NoError(t, docker.RunAndRemove(ctx, imgUUID, "test", "-f", config.Initrd))
} }
func TestSyslinuxCfg(t *testing.T) { func TestConfig(t *testing.T) {
t.Parallel() t.Parallel()
tests := []struct { tests := []struct {
image string image string
kernel string config Config
initrd string
sysconfig string
}{ }{
{ {
image: "ubuntu:18.04", image: "ubuntu:18.04",
kernel: "/vmlinuz", config: configDebian,
initrd: "/initrd.img",
sysconfig: syslinuxCfgDebian,
}, },
{ {
image: "ubuntu:20.04", image: "ubuntu:20.04",
kernel: "/boot/vmlinuz", config: configUbuntu,
initrd: "/boot/initrd.img",
sysconfig: syslinuxCfgUbuntu,
}, },
{ {
image: "ubuntu:22.04", image: "ubuntu:22.04",
kernel: "/boot/vmlinuz", config: configUbuntu,
initrd: "/boot/initrd.img",
sysconfig: syslinuxCfgUbuntu,
}, },
{ {
image: "ubuntu:latest", image: "ubuntu:latest",
kernel: "/boot/vmlinuz", config: configUbuntu,
initrd: "/boot/initrd.img",
sysconfig: syslinuxCfgUbuntu,
}, },
{ {
image: "debian:9", image: "debian:9",
kernel: "/vmlinuz", config: configDebian,
initrd: "/initrd.img",
sysconfig: syslinuxCfgDebian,
}, },
{ {
image: "debian:10", image: "debian:10",
kernel: "/vmlinuz", config: configDebian,
initrd: "/initrd.img",
sysconfig: syslinuxCfgDebian,
}, },
{ {
image: "debian:11", image: "debian:11",
kernel: "/vmlinuz", config: configDebian,
initrd: "/initrd.img",
sysconfig: syslinuxCfgDebian,
}, },
{ {
image: "debian:latest", image: "debian:latest",
kernel: "/vmlinuz", config: configDebian,
initrd: "/initrd.img",
sysconfig: syslinuxCfgDebian,
}, },
{ {
image: "kalilinux/kali-rolling:latest", image: "kalilinux/kali-rolling:latest",
kernel: "/vmlinuz", config: configDebian,
initrd: "/initrd.img",
sysconfig: syslinuxCfgDebian,
}, },
{ {
image: "alpine:3.16", image: "alpine:3.16",
kernel: "/boot/vmlinuz-virt", config: configAlpine,
initrd: "/boot/initramfs-virt",
sysconfig: syslinuxCfgAlpine,
}, },
{ {
image: "alpine", image: "alpine",
kernel: "/boot/vmlinuz-virt", config: configAlpine,
initrd: "/boot/initramfs-virt",
sysconfig: syslinuxCfgAlpine,
}, },
{ {
image: "centos:8", image: "centos:8",
kernel: "/boot/vmlinuz", config: configCentOS,
initrd: "/boot/initrd.img",
sysconfig: syslinuxCfgCentOS,
}, },
{ {
image: "centos:latest", image: "centos:latest",
kernel: "/boot/vmlinuz", config: configCentOS,
initrd: "/boot/initrd.img",
sysconfig: syslinuxCfgCentOS,
}, },
} }
exec.SetDebug(true) exec.SetDebug(true)
@ -154,7 +122,7 @@ func TestSyslinuxCfg(t *testing.T) {
t.Parallel() t.Parallel()
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
testSysconfig(t, ctx, test.image, test.sysconfig, test.kernel, test.initrd) testConfig(t, ctx, test.image, test.config)
}) })
} }
} }

View File

@ -88,7 +88,7 @@ func Convert(ctx context.Context, img string, opts ...ConvertOption) error {
if format == "" { if format == "" {
format = "raw" format = "raw"
} }
b, err := NewBuilder(ctx, tmpPath, imgUUID, "", o.size, r, format, o.cmdLineExtra, o.splitBoot, o.bootSize, o.luksPassword) b, err := NewBuilder(ctx, tmpPath, imgUUID, "", o.size, r, format, o.cmdLineExtra, o.splitBoot, o.bootSize, o.luksPassword, "")
if err != nil { if err != nil {
return err return err
} }

92
syslinux.go Normal file
View File

@ -0,0 +1,92 @@
// Copyright 2023 Linka Cloud All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package d2vm
import (
"context"
"fmt"
"os"
"path/filepath"
"github.com/sirupsen/logrus"
"go.linka.cloud/d2vm/pkg/exec"
)
const syslinuxCfg = `DEFAULT linux
SAY Now booting the kernel from SYSLINUX...
LABEL linux
KERNEL %s
APPEND %s
`
var mbrPaths = []string{
// debian path
"/usr/lib/syslinux/mbr/mbr.bin",
// ubuntu path
"/usr/lib/EXTLINUX/mbr.bin",
// alpine path
"/usr/share/syslinux/mbr.bin",
// centos path
"/usr/share/syslinux/mbr.bin",
// archlinux path
"/usr/lib/syslinux/bios/mbr.bin",
}
type syslinux struct {
c Config
mbrBin string
}
func (s syslinux) Setup(ctx context.Context, raw, path string, cmdline string) error {
if err := exec.Run(ctx, "extlinux", "--install", path); err != nil {
return err
}
if err := os.WriteFile(filepath.Join(path, "syslinux.cfg"), []byte(fmt.Sprintf(syslinuxCfg, s.c.Kernel, cmdline)), perm); err != nil {
return err
}
logrus.Infof("writing MBR")
if err := exec.Run(ctx, "dd", fmt.Sprintf("if=%s", s.mbrBin), fmt.Sprintf("of=%s", raw), "bs=440", "count=1", "conv=notrunc"); err != nil {
return err
}
return nil
}
type syslinuxProvider struct{}
func (s syslinuxProvider) New(c Config) (Bootloader, error) {
mbrBin := ""
for _, v := range mbrPaths {
if _, err := os.Stat(v); err == nil {
mbrBin = v
break
}
}
if mbrBin == "" {
return nil, fmt.Errorf("unable to find syslinux's mbr.bin path")
}
return &syslinux{
c: c,
mbrBin: mbrBin,
}, nil
}
func (s syslinuxProvider) Name() string {
return "syslinux"
}
func init() {
RegisterBootloaderProvider(syslinuxProvider{})
}