diff options
-rw-r--r-- | README.md | 109 | ||||
-rw-r--r-- | action.go | 27 | ||||
-rw-r--r-- | actions/debootstrap_action.go | 31 | ||||
-rw-r--r-- | actions/image_partition_action.go | 55 | ||||
-rw-r--r-- | actions/ostree_commit_action.go | 12 | ||||
-rw-r--r-- | actions/pack_action.go | 2 | ||||
-rw-r--r-- | actions/run_action.go | 10 | ||||
-rw-r--r-- | cmd/debos/debos.go | 68 | ||||
-rw-r--r-- | commands.go | 8 | ||||
-rw-r--r-- | doc/examples/example.yaml | 34 | ||||
-rw-r--r-- | doc/examples/overlays/sudo/etc/sudoers.d/user | 1 | ||||
-rwxr-xr-x | doc/examples/setup-user.sh | 10 | ||||
-rwxr-xr-x | doc/man/create_manpage.sh | 28 | ||||
-rw-r--r-- | doc/man/debos.1 | 132 | ||||
-rw-r--r-- | filesystem.go | 13 |
15 files changed, 481 insertions, 59 deletions
@@ -1,13 +1,102 @@ -debos -===== +# debos - Debian OS images builder -Debian OS builder. debos is a tool to make creation of various debian based os -"images" simpler. While most other tools focus on specific use-case, debos is -more meant as a toolchain to make comon actions trivial while providing enough -rope to do whatever tweaking that might be required behind the scene. +## Sypnosis -debos expects a yaml file as input, syntax description can be found at: - https://godoc.org/github.com/go-debos/debos/actions + debos [options] <recipe file in YAML> + debos [--help] + +Application Options: + + --artifactdir= + -t, --template-var= Template variables + --debug-shell Fall into interactive shell on error + -s, --shell= Redefine interactive shell binary (default: bash) + --scratchsize= Size of disk backed scratch space + + +## Description + +debos is a tool to make the creation of various Debian-based OS images +simpler. While most other tools focus on specific use-cases, debos is +more meant as a tool-chain to make common actions trivial while providing +enough rope to do whatever tweaking that might be required behind the scene. + +debos expects a YAML file as input and will run the actions listed in the +file sequentially. These actions should be self-contained and independent +of each other. + +Some of the actions provided by debos to customize and produce images are: + +* apt: install packages and their dependencies with 'apt' +* debootstrap: construct the target rootfs with debootstrap +* download: download a single file from the internet +* filesystem-deploy: deploy a root filesystem to an image previously created +* image-partition: create an image file, make partitions and format them +* ostree-commit: create an OSTree commit from rootfs +* ostree-deploy: deploy an OSTree branch to the image +* overlay: do a recursive copy of directories or files to the target filesystem +* pack: create a tarball with the target filesystem +* raw: directly write a file to the output image at a given offset +* run: allows to run a command or script in the filesystem or in the host +* unpack: unpack files from archive in the filesystem + +A full syntax description of all the debos actions can be found at: +https://godoc.org/github.com/go-debos/debos/actions + +## Installation (under Debian) + + sudo apt install golang + sudo apt install libglib2.0-dev libostree-dev + export GOPATH=/opt/src/gocode # or whatever suites your needs + go get -u github.com/go-debos/debos/cmd/debos + /opt/src/gocode/bin/debos --help + +## Simple example + +The following example will create a arm64 image, install several +packages in it, change the file /etc/hostname to "debian" and finally +make a tarball. + + {{- $image := or .image "debian.tgz" -}} + + architecture: arm64 + + actions: + - action: debootstrap + suite: "buster" + components: + - main + - non-free + mirror: https://deb.debian.org/debian + variant: minbase + + - action: apt + packages: [ sudo, openssh-server, adduser, systemd-sysv, firmware-linux ] + + - action: run + chroot: true + command: echo debian > /etc/hostname + + - action: pack + file: {{ $image }} + compression: gz + +To run it, create a file named `example.yaml` and run: + + debos example.yaml + +The final tarball will be named "debian.tgz" if you would like to modify +this name, you can provided a different name for the variable image like +this: + + debos -t image:"debian-arm64.tgz" example.yaml + +## Other examples + +This example builds a customized image for a Raspberry Pi 3. +https://github.com/go-debos/debos-recipes + + +## See also +fakemachine at https://github.com/go-debos/fakemachine -and examples are to be found at: - https://github.com/go-debos/debos-recipes @@ -6,6 +6,14 @@ import ( "log" ) +type DebosState int + +// Represent the current state of Debos +const ( + Success DebosState = iota + Failed +) + // Mapping from partition name as configured in the image-partition action to // device path for usage by other actions type Partition struct { @@ -27,6 +35,7 @@ type DebosContext struct { Architecture string DebugShell string Origins map[string]string + State DebosState } type Action interface { @@ -35,8 +44,13 @@ type Action interface { PreMachine(context *DebosContext, m *fakemachine.Machine, args *[]string) error PreNoMachine(context *DebosContext) error Run(context *DebosContext) error - Cleanup(context DebosContext) error - PostMachine(context DebosContext) error + // Cleanup() method gets called only if the Run for an action + // was started and in the same machine (host or fake) as Run has run + Cleanup(context *DebosContext) error + PostMachine(context *DebosContext) error + // PostMachineCleanup() gets called for all actions if Pre*Machine() method + // has run for Action. This method is always executed on the host with user's permissions. + PostMachineCleanup(context *DebosContext) error String() string } @@ -55,10 +69,11 @@ func (b *BaseAction) PreMachine(context *DebosContext, args *[]string) error { return nil } -func (b *BaseAction) PreNoMachine(context *DebosContext) error { return nil } -func (b *BaseAction) Run(context *DebosContext) error { return nil } -func (b *BaseAction) Cleanup(context DebosContext) error { return nil } -func (b *BaseAction) PostMachine(context DebosContext) error { return nil } +func (b *BaseAction) PreNoMachine(context *DebosContext) error { return nil } +func (b *BaseAction) Run(context *DebosContext) error { return nil } +func (b *BaseAction) Cleanup(context *DebosContext) error { return nil } +func (b *BaseAction) PostMachine(context *DebosContext) error { return nil } +func (b *BaseAction) PostMachineCleanup(context *DebosContext) error { return nil } func (b *BaseAction) String() string { if b.Description == "" { return b.Action diff --git a/actions/debootstrap_action.go b/actions/debootstrap_action.go index 4f30ee6..bfbf3dd 100644 --- a/actions/debootstrap_action.go +++ b/actions/debootstrap_action.go @@ -10,6 +10,7 @@ Yaml syntax: components: <list of components> variant: "name" keyring-package: + keyring-file: Mandatory properties: @@ -17,6 +18,8 @@ Mandatory properties: Optional properties: +- check-gpg -- verify GPG signatures on Release files, true by default + - mirror -- URL with Debian-compatible repository - variant -- name of the bootstrap script variant to use @@ -25,7 +28,9 @@ Optional properties: Example: components: [ main, contrib ] -- keyring-package -- keyring for packages validation. Currently ignored. +- keyring-package -- keyring for package validation. + +- keyring-file -- keyring file for repository validation. - merged-usr -- use merged '/usr' filesystem, true by default. */ @@ -47,14 +52,18 @@ type DebootstrapAction struct { Mirror string Variant string KeyringPackage string `yaml:"keyring-package"` + KeyringFile string `yaml:"keyring-file"` Components []string MergedUsr bool `yaml:"merged-usr"` + CheckGpg bool `yaml:"check-gpg"` } func NewDebootstrapAction() *DebootstrapAction { d := DebootstrapAction{} // Use filesystem with merged '/usr' by default d.MergedUsr = true + // Be secure by default + d.CheckGpg = true return &d } @@ -74,17 +83,31 @@ func (d *DebootstrapAction) RunSecondStage(context debos.DebosContext) error { // Can't use nspawn for debootstrap as it wants to create device nodes c.ChrootMethod = debos.CHROOT_METHOD_CHROOT - return c.Run("Debootstrap (stage 2)", cmdline...) + err := c.Run("Debootstrap (stage 2)", cmdline...) + + if (err != nil) { + log := path.Join(context.Rootdir, "debootstrap/debootstrap.log") + _ = debos.Command{}.Run("debootstrap.log", "cat", log) + } + + return err } func (d *DebootstrapAction) Run(context *debos.DebosContext) error { d.LogStart() - cmdline := []string{"debootstrap", "--no-check-gpg"} + cmdline := []string{"debootstrap"} if d.MergedUsr { cmdline = append(cmdline, "--merged-usr") } + if !d.CheckGpg { + cmdline = append(cmdline, fmt.Sprintf("--no-check-gpg")) + } else if d.KeyringFile != "" { + path := debos.CleanPathAt(d.KeyringFile, context.RecipeDir) + cmdline = append(cmdline, fmt.Sprintf("--keyring=%s", path)) + } + if d.KeyringPackage != "" { cmdline = append(cmdline, fmt.Sprintf("--include=%s", d.KeyringPackage)) } @@ -115,6 +138,8 @@ func (d *DebootstrapAction) Run(context *debos.DebosContext) error { err := debos.Command{}.Run("Debootstrap", cmdline...) if err != nil { + log := path.Join(context.Rootdir, "debootstrap/debootstrap.log") + _ = debos.Command{}.Run("debootstrap.log", "cat", log) return err } diff --git a/actions/image_partition_action.go b/actions/image_partition_action.go index 5054e6d..cd832ac 100644 --- a/actions/image_partition_action.go +++ b/actions/image_partition_action.go @@ -116,6 +116,7 @@ import ( "os" "os/exec" "path" + "path/filepath" "strings" "syscall" @@ -183,19 +184,23 @@ func (i *ImagePartitionAction) generateKernelRoot(context *debos.DebosContext) e } func (i ImagePartitionAction) getPartitionDevice(number int, context debos.DebosContext) string { + /* Always look up canonical device as udev might not generate the by-id + * symlinks while there is an flock on /dev/vda */ + device, _ := filepath.EvalSymlinks(context.Image) + suffix := "p" /* Check partition naming first: if used 'by-id'i naming convention */ - if strings.Contains(context.Image, "/disk/by-id/") { + if strings.Contains(device, "/disk/by-id/") { suffix = "-part" } /* If the iamge device has a digit as the last character, the partition * suffix is p<number> else it's just <number> */ - last := context.Image[len(context.Image)-1] + last := device[len(device)-1] if last >= '0' && last <= '9' { - return fmt.Sprintf("%s%s%d", context.Image, suffix, number) + return fmt.Sprintf("%s%s%d", device, suffix, number) } else { - return fmt.Sprintf("%s%d", context.Image, number) + return fmt.Sprintf("%s%d", device, number) } } @@ -247,7 +252,7 @@ func (i ImagePartitionAction) formatPartition(p *Partition, context debos.DebosC return nil } -func (i ImagePartitionAction) PreNoMachine(context *debos.DebosContext) error { +func (i *ImagePartitionAction) PreNoMachine(context *debos.DebosContext) error { img, err := os.OpenFile(i.ImageName, os.O_WRONLY|os.O_CREATE, 0666) if err != nil { @@ -274,11 +279,28 @@ func (i ImagePartitionAction) PreNoMachine(context *debos.DebosContext) error { func (i ImagePartitionAction) Run(context *debos.DebosContext) error { i.LogStart() + /* Exclusively Lock image device file to prevent udev from triggering + * partition rescans, which cause confusion as some time asynchronously the + * partition device might disappear and reappear due to that! */ + imageFD, err := os.Open(context.Image) + if err != nil { + return err + } + /* Defer will keep the fd open until the function returns, at which points + * the filesystems will have been mounted protecting from more udev funnyness + */ + defer imageFD.Close() + + err = syscall.Flock(int(imageFD.Fd()), syscall.LOCK_EX) + if err != nil { + return err + } + command := []string{"parted", "-s", context.Image, "mklabel", i.PartitionType} if len(i.GptGap) > 0 { command = append(command, i.GptGap) } - err := debos.Command{}.Run("parted", command...) + err = debos.Command{}.Run("parted", command...) if err != nil { return err } @@ -317,12 +339,6 @@ func (i ImagePartitionAction) Run(context *debos.DebosContext) error { } devicePath := i.getPartitionDevice(p.number, *context) - // Give a chance for udevd to create proper symlinks - err = debos.Command{}.Run("udevadm", "udevadm", "settle", "-t", "5", - "-E", devicePath) - if err != nil { - return err - } err = i.formatPartition(p, *context) if err != nil { @@ -358,7 +374,7 @@ func (i ImagePartitionAction) Run(context *debos.DebosContext) error { return nil } -func (i ImagePartitionAction) Cleanup(context debos.DebosContext) error { +func (i ImagePartitionAction) Cleanup(context *debos.DebosContext) error { for idx := len(i.Mountpoints) - 1; idx >= 0; idx-- { m := i.Mountpoints[idx] mntpath := path.Join(context.ImageMntDir, m.Mountpoint) @@ -372,6 +388,19 @@ func (i ImagePartitionAction) Cleanup(context debos.DebosContext) error { return nil } +func (i ImagePartitionAction) PostMachineCleanup(context *debos.DebosContext) error { + image := path.Join(context.Artifactdir, i.ImageName) + /* Remove the image in case of any action failure */ + if context.State != debos.Success { + if _, err := os.Stat(image); !os.IsNotExist(err) { + if err = os.Remove(image); err != nil { + return err + } + } + } + return nil +} + func (i *ImagePartitionAction) Verify(context *debos.DebosContext) error { if len(i.GptGap) > 0 { log.Println("WARNING: special version of parted is needed for 'gpt_gap' option") diff --git a/actions/ostree_commit_action.go b/actions/ostree_commit_action.go index 6d41b89..a0d8333 100644 --- a/actions/ostree_commit_action.go +++ b/actions/ostree_commit_action.go @@ -45,9 +45,17 @@ type OstreeCommitAction struct { func emptyDir(dir string) { d, _ := os.Open(dir) defer d.Close() - files, _ := d.Readdirnames(-1) + + files, err := d.Readdirnames(-1) + if err != nil { + log.Fatal(err) + } + for _, f := range files { - os.RemoveAll(f) + err := os.RemoveAll(path.Join(dir, f)) + if err != nil { + log.Fatalf("Failed to remove file: %v", err) + } } } diff --git a/actions/pack_action.go b/actions/pack_action.go index a90cb1d..1cb1af0 100644 --- a/actions/pack_action.go +++ b/actions/pack_action.go @@ -10,7 +10,7 @@ Yaml syntax: Mandatory properties: -- file -- name of the output tarball. +- file -- name of the output tarball, relative to the artifact directory. - compression -- compression type to use. Only 'gz' is supported at the moment. diff --git a/actions/run_action.go b/actions/run_action.go index 90e4572..c6115cc 100644 --- a/actions/run_action.go +++ b/actions/run_action.go @@ -21,8 +21,10 @@ host's or chrooted environment -- depending on 'chroot' property. Optional properties: - chroot -- run script or command in target filesystem if set to true. -In other case the command or script is executed within the build process, with -access to the filesystem and the image. In both cases it is run with root privileges. +Otherwise the command or script is executed within the build process, with +access to the filesystem ($ROOTDIR), the image if any ($IMAGE), the +recipe directory ($RECIPEDIR) and the artifact directory ($ARTIFACTDIR). +In both cases it is run with root privileges. - postprocess -- if set script or command is executed after all other commands and has access to the image file. @@ -126,9 +128,9 @@ func (run *RunAction) Run(context *debos.DebosContext) error { return run.doRun(*context) } -func (run *RunAction) PostMachine(context debos.DebosContext) error { +func (run *RunAction) PostMachine(context *debos.DebosContext) error { if !run.PostProcess { return nil } - return run.doRun(context) + return run.doRun(*context) } diff --git a/cmd/debos/debos.go b/cmd/debos/debos.go index 0d1f7ff..f0f0ce3 100644 --- a/cmd/debos/debos.go +++ b/cmd/debos/debos.go @@ -14,25 +14,29 @@ import ( "github.com/jessevdk/go-flags" ) -func checkError(context debos.DebosContext, err error, a debos.Action, stage string) int { +func checkError(context *debos.DebosContext, err error, a debos.Action, stage string) int { if err == nil { return 0 } + context.State = debos.Failed log.Printf("Action `%s` failed at stage %s, error: %s", a, stage, err) - debos.DebugShell(context) + debos.DebugShell(*context) return 1 } func main() { var context debos.DebosContext var options struct { - ArtifactDir string `long:"artifactdir"` + ArtifactDir string `long:"artifactdir" description:"Directory for packed archives and ostree repositories (default: current directory)"` InternalImage string `long:"internal-image" hidden:"true"` - TemplateVars map[string]string `short:"t" long:"template-var" description:"Template variables"` + TemplateVars map[string]string `short:"t" long:"template-var" description:"Template variables (use -t VARIABLE:VALUE syntax)"` DebugShell bool `long:"debug-shell" description:"Fall into interactive shell on error"` Shell string `short:"s" long:"shell" description:"Redefine interactive shell binary (default: bash)" optionsl:"" default:"/bin/bash"` ScratchSize string `long:"scratchsize" description:"Size of disk backed scratch space"` + CPUs int `short:"c" long:"cpus" description:"Number of CPUs to use for build VM (default: 2)"` + Memory string `short:"m" long:"memory" description:"Amount of memory for build VM (default: 2048MB)"` + ShowBoot bool `long:"show-boot" description:"Show boot/console messages from the fake machine"` } var exitcode int = 0 @@ -111,9 +115,11 @@ func main() { context.Architecture = r.Architecture + context.State = debos.Success + for _, a := range r.Actions { err = a.Verify(&context) - if exitcode = checkError(context, err, a, "Verify"); exitcode != 0 { + if exitcode = checkError(&context, err, a, "Verify"); exitcode != 0 { return } } @@ -122,6 +128,24 @@ func main() { m := fakemachine.NewMachine() var args []string + if options.Memory == "" { + // Set default memory size for fakemachine + options.Memory = "2Gb" + } + memsize, err := units.RAMInBytes(options.Memory) + if err != nil { + fmt.Printf("Couldn't parse memory size: %v\n", err) + exitcode = 1 + return + } + m.SetMemory(int(memsize / 1024 / 1024)) + + if options.CPUs == 0 { + // Set default CPU count for fakemachine + options.CPUs = 2 + } + m.SetNumCPUs(options.CPUs) + if options.ScratchSize != "" { size, err := units.FromHumanSize(options.ScratchSize) if err != nil { @@ -132,6 +156,8 @@ func main() { m.SetScratch(size, "") } + m.SetShowBoot(options.ShowBoot) + m.AddVolume(context.Artifactdir) args = append(args, "--artifactdir", context.Artifactdir) @@ -148,8 +174,11 @@ func main() { } for _, a := range r.Actions { + // Stack PostMachineCleanup methods + defer a.PostMachineCleanup(&context) + err = a.PreMachine(&context, m, &args) - if exitcode = checkError(context, err, a, "PreMachine"); exitcode != 0 { + if exitcode = checkError(&context, err, a, "PreMachine"); exitcode != 0 { return } } @@ -161,12 +190,13 @@ func main() { } if exitcode != 0 { + context.State = debos.Failed return } for _, a := range r.Actions { - err = a.PostMachine(context) - if exitcode = checkError(context, err, a, "Postmachine"); exitcode != 0 { + err = a.PostMachine(&context) + if exitcode = checkError(&context, err, a, "Postmachine"); exitcode != 0 { return } } @@ -177,8 +207,11 @@ func main() { if !fakemachine.InMachine() { for _, a := range r.Actions { + // Stack PostMachineCleanup methods + defer a.PostMachineCleanup(&context) + err = a.PreNoMachine(&context) - if exitcode = checkError(context, err, a, "PreNoMachine"); exitcode != 0 { + if exitcode = checkError(&context, err, a, "PreNoMachine"); exitcode != 0 { return } } @@ -195,22 +228,21 @@ func main() { for _, a := range r.Actions { err = a.Run(&context) - if exitcode = checkError(context, err, a, "Run"); exitcode != 0 { - return - } - } - for _, a := range r.Actions { - err = a.Cleanup(context) - if exitcode = checkError(context, err, a, "Cleanup"); exitcode != 0 { + // This does not stop the call of stacked Cleanup methods for other Actions + // Stack Cleanup methods + defer a.Cleanup(&context) + + // Check the state of Run method + if exitcode = checkError(&context, err, a, "Run"); exitcode != 0 { return } } if !fakemachine.InMachine() { for _, a := range r.Actions { - err = a.PostMachine(context) - if exitcode = checkError(context, err, a, "PostMachine"); exitcode != 0 { + err = a.PostMachine(&context) + if exitcode = checkError(&context, err, a, "PostMachine"); exitcode != 0 { return } } diff --git a/commands.go b/commands.go index 557b636..96e2de1 100644 --- a/commands.go +++ b/commands.go @@ -176,6 +176,14 @@ func newQemuHelper(c Command) qemuHelper { q.qemusrc = "/usr/bin/qemu-arm-static" case "arm64": q.qemusrc = "/usr/bin/qemu-aarch64-static" + case "mips": + q.qemusrc = "/usr/bin/qemu-mips-static" + case "mipsel": + q.qemusrc = "/usr/bin/qemu-mipsel-static" + case "mips64el": + q.qemusrc = "/usr/bin/qemu-mips64el-static" + case "riscv64": + q.qemusrc = "/usr/bin/qemu-riscv64-static" case "amd64", "i386": /* Dummy, no qemu */ default: diff --git a/doc/examples/example.yaml b/doc/examples/example.yaml new file mode 100644 index 0000000..c0b6f55 --- /dev/null +++ b/doc/examples/example.yaml @@ -0,0 +1,34 @@ +{{- $architecture := or .architecture "arm64" -}} +{{- $suite := or .suite "stretch" -}} +{{ $image := or .image (printf "debian-%s-%s.tgz" $suite $architecture) }} + +architecture: {{ $architecture }} + +actions: + - action: debootstrap + suite: "buster" + components: + - main + - contrib + - non-free + mirror: https://deb.debian.org/debian + variant: minbase + + - action: apt + description: Install some packages + packages: [ sudo, openssh-server, adduser, systemd-sysv, firmware-linux ] + + - action: run + chroot: true + script: setup-user.sh + + - action: overlay + source: overlays/sudo + + - action: run + chroot: true + command: echo debian > /etc/hostname + + - action: pack + file: {{ $image }} + compression: gz diff --git a/doc/examples/overlays/sudo/etc/sudoers.d/user b/doc/examples/overlays/sudo/etc/sudoers.d/user new file mode 100644 index 0000000..125f22b --- /dev/null +++ b/doc/examples/overlays/sudo/etc/sudoers.d/user @@ -0,0 +1 @@ +%sudo ALL=(ALL) NOPASSWD: /usr/bin/su diff --git a/doc/examples/setup-user.sh b/doc/examples/setup-user.sh new file mode 100755 index 0000000..da155d1 --- /dev/null +++ b/doc/examples/setup-user.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +set -e + +echo "I: create user" +adduser --gecos User user + +echo "I: set user password" +echo "user:user" | chpasswd +adduser user sudo diff --git a/doc/man/create_manpage.sh b/doc/man/create_manpage.sh new file mode 100755 index 0000000..acac83a --- /dev/null +++ b/doc/man/create_manpage.sh @@ -0,0 +1,28 @@ +#!/bin/bash +# Create a manpage from the README.md + +# Add header +echo '''% debos(1) + +# NAME + + debos - Debian OS images builder +''' > debos.md + +# Add README.md +tail -n +2 ../../README.md >> debos.md + +# Some tweaks to the markdown +# Uppercase titles +sed -i 's/^\(##.*\)$/\U\1/' debos.md + +# Remove double # +sed -i 's/^\##/#/' debos.md + +# Create the manpage +pandoc -s -t man debos.md -o debos.1 + +# Resulting manpage can be browsed with groff: +#groff -man -Tascii debos.1 + + diff --git a/doc/man/debos.1 b/doc/man/debos.1 new file mode 100644 index 0000000..6e58a40 --- /dev/null +++ b/doc/man/debos.1 @@ -0,0 +1,132 @@ +.\" Automatically generated by Pandoc 2.2.1 +.\" +.TH "debos" "1" "" "" "" +.hy +.SH NAME +.IP +.nf +\f[C] +debos\ \-\ \ Debian\ OS\ images\ builder +\f[] +.fi +.SH SYPNOSIS +.IP +.nf +\f[C] +debos\ [options]\ <recipe\ file\ in\ YAML> +debos\ [\-\-help] +\f[] +.fi +.PP +Application Options: +.IP +.nf +\f[C] +\ \ \ \ \ \ \-\-artifactdir= +\ \ \-t,\ \-\-template\-var=\ \ \ Template\ variables +\ \ \ \ \ \ \-\-debug\-shell\ \ \ \ \ Fall\ into\ interactive\ shell\ on\ error +\ \ \-s,\ \-\-shell=\ \ \ \ \ \ \ \ \ \ Redefine\ interactive\ shell\ binary\ (default:\ bash) +\ \ \ \ \ \ \-\-scratchsize=\ \ \ \ Size\ of\ disk\ backed\ scratch\ space +\f[] +.fi +.SH DESCRIPTION +.PP +debos is a tool to make the creation of various Debian\-based OS images +simpler. +While most other tools focus on specific use\-cases, debos is more meant +as a tool\-chain to make common actions trivial while providing enough +rope to do whatever tweaking that might be required behind the scene. +.PP +debos expects a YAML file as input and will run the actions listed in +the file sequentially. +These actions should be self\-contained and independent of each other. +.PP +Some of the actions provided by debos to customize and produce images +are: +.IP \[bu] 2 +apt: install packages and their dependencies with `apt' +.IP \[bu] 2 +debootstrap: construct the target rootfs with debootstrap +.IP \[bu] 2 +download: download a single file from the internet +.IP \[bu] 2 +filesystem\-deploy: deploy a root filesystem to an image previously +created +.IP \[bu] 2 +image\-partition: create an image file, make partitions and format them +.IP \[bu] 2 +ostree\-commit: create an OSTree commit from rootfs +.IP \[bu] 2 +ostree\-deploy: deploy an OSTree branch to the image +.IP \[bu] 2 +overlay: do a recursive copy of directories or files to the target +filesystem +.IP \[bu] 2 +pack: create a tarball with the target filesystem +.IP \[bu] 2 +raw: directly write a file to the output image at a given offset +.IP \[bu] 2 +run: allows to run a command or script in the filesystem or in the host +.IP \[bu] 2 +unpack: unpack files from archive in the filesystem +.PP +A full syntax description of all the debos actions can be found at: +https://godoc.org/github.com/go\-debos/debos/actions +.SH SIMPLE EXAMPLE +.PP +The following example will create a arm64 image, install several +packages in it, change the file /etc/hostname to \[lq]debian\[rq] and +finally make a tarball. +.IP +.nf +\f[C] +{{\-\ $image\ :=\ or\ .image\ "debian.tgz"\ \-}} + +architecture:\ arm64 + +actions: +\ \ \-\ action:\ debootstrap +\ \ \ \ suite:\ "buster" +\ \ \ \ components: +\ \ \ \ \ \ \-\ main +\ \ \ \ \ \ \-\ non\-free +\ \ \ \ mirror:\ https://deb.debian.org/debian +\ \ \ \ variant:\ minbase + +\ \ \-\ action:\ apt +\ \ \ \ packages:\ [\ sudo,\ openssh\-server,\ adduser,\ systemd\-sysv,\ firmware\-linux\ ] + +\ \ \-\ action:\ run +\ \ \ \ chroot:\ true +\ \ \ \ command:\ echo\ debian\ >\ /etc/hostname + +\ \ \-\ action:\ pack +\ \ \ \ file:\ {{\ $image\ }} +\ \ \ \ compression:\ gz +\f[] +.fi +.PP +To run it, create a file named \f[C]example.yaml\f[] and run: +.IP +.nf +\f[C] +debos\ example.yaml +\f[] +.fi +.PP +The final tarball will be named \[lq]debian.tgz\[rq] if you would like +to modify this name, you can provided a different name for the variable +image like this: +.IP +.nf +\f[C] +debos\ \-t\ image:"debian\-arm64.tgz"\ example.yaml +\f[] +.fi +.SH OTHER EXAMPLES +.PP +This example builds a customized image for a Raspberry Pi 3. +https://github.com/go\-debos/debos\-recipes +.SH SEE ALSO +.PP +fakemachine at https://github.com/go\-debos/fakemachine diff --git a/filesystem.go b/filesystem.go index a6e5239..b5c41fa 100644 --- a/filesystem.go +++ b/filesystem.go @@ -48,7 +48,13 @@ func CopyFile(src, dst string, mode os.FileMode) error { os.Remove(tmp.Name()) return err } - return os.Rename(tmp.Name(), dst) + + if err = os.Rename(tmp.Name(), dst); err != nil { + os.Remove(tmp.Name()) + return err + } + + return nil } func CopyTree(sourcetree, desttree string) error { @@ -63,7 +69,10 @@ func CopyTree(sourcetree, desttree string) error { target := path.Join(desttree, suffix) switch info.Mode() & os.ModeType { case 0: - CopyFile(p, target, info.Mode()) + err := CopyFile(p, target, info.Mode()) + if err != nil { + log.Panicf("Failed to copy file %s: %v", p, err) + } case os.ModeDir: os.Mkdir(target, info.Mode()) case os.ModeSymlink: |