diff --git a/examples/aws.yml b/examples/aws.yml index af3dc67e2..2da4d8cbc 100644 --- a/examples/aws.yml +++ b/examples/aws.yml @@ -13,7 +13,7 @@ onboot: image: linuxkit/dhcpcd:2f8a9b670aa6e96a09db56ec45c9f07ef2a811ee command: ["/sbin/dhcpcd", "--nobackground", "-f", "/dhcpcd.conf", "-1"] - name: metadata - image: linuxkit/metadata:04ce7519c2ea2eaf99bbdc76bb01fc036eed7ab0 + image: linuxkit/metadata:dc5bcfa45946053145391ceabe33729c8b9507a1 services: - name: rngd image: linuxkit/rngd:7fab61cca793113280397dcee8159f35dc37adcb diff --git a/examples/docker-for-mac.yml b/examples/docker-for-mac.yml index 35bcfa897..feb251d9f 100644 --- a/examples/docker-for-mac.yml +++ b/examples/docker-for-mac.yml @@ -11,7 +11,7 @@ init: onboot: # support metadata for optional config in /run/config - name: metadata - image: linuxkit/metadata:04ce7519c2ea2eaf99bbdc76bb01fc036eed7ab0 + image: linuxkit/metadata:dc5bcfa45946053145391ceabe33729c8b9507a1 - name: sysctl image: linuxkit/sysctl:541f60fe3676611328e89e8bac251fc636b1a6aa - name: sysfs diff --git a/examples/gcp.yml b/examples/gcp.yml index 39f1b833e..c037e4fe9 100644 --- a/examples/gcp.yml +++ b/examples/gcp.yml @@ -13,7 +13,7 @@ onboot: image: linuxkit/dhcpcd:2f8a9b670aa6e96a09db56ec45c9f07ef2a811ee command: ["/sbin/dhcpcd", "--nobackground", "-f", "/dhcpcd.conf", "-1"] - name: metadata - image: linuxkit/metadata:04ce7519c2ea2eaf99bbdc76bb01fc036eed7ab0 + image: linuxkit/metadata:dc5bcfa45946053145391ceabe33729c8b9507a1 services: - name: getty image: linuxkit/getty:48f66df198981e692084bf70ab72b9fe2be0f880 diff --git a/examples/hetzner.yml b/examples/hetzner.yml index 19f956fea..b20c30164 100644 --- a/examples/hetzner.yml +++ b/examples/hetzner.yml @@ -18,7 +18,7 @@ onboot: image: linuxkit/dhcpcd:2f8a9b670aa6e96a09db56ec45c9f07ef2a811ee command: ["/sbin/dhcpcd", "--nobackground", "-f", "/dhcpcd.conf", "-1"] - name: metadata - image: linuxkit/metadata:04ce7519c2ea2eaf99bbdc76bb01fc036eed7ab0 + image: linuxkit/metadata:dc5bcfa45946053145391ceabe33729c8b9507a1 command: ["/usr/bin/metadata", "hetzner"] services: - name: rngd diff --git a/examples/openstack.yml b/examples/openstack.yml index 8823c944a..7fc6741ad 100644 --- a/examples/openstack.yml +++ b/examples/openstack.yml @@ -13,7 +13,7 @@ onboot: image: linuxkit/dhcpcd:2f8a9b670aa6e96a09db56ec45c9f07ef2a811ee command: ["/sbin/dhcpcd", "--nobackground", "-f", "/dhcpcd.conf", "-1"] - name: metadata - image: linuxkit/metadata:04ce7519c2ea2eaf99bbdc76bb01fc036eed7ab0 + image: linuxkit/metadata:dc5bcfa45946053145391ceabe33729c8b9507a1 command: ["/usr/bin/metadata", "openstack"] services: - name: rngd diff --git a/examples/packet.yml b/examples/packet.yml index e118cac5a..42001bb68 100644 --- a/examples/packet.yml +++ b/examples/packet.yml @@ -18,7 +18,7 @@ onboot: image: linuxkit/dhcpcd:2f8a9b670aa6e96a09db56ec45c9f07ef2a811ee command: ["/sbin/dhcpcd", "--nobackground", "-f", "/dhcpcd.conf", "-1"] - name: metadata - image: linuxkit/metadata:04ce7519c2ea2eaf99bbdc76bb01fc036eed7ab0 + image: linuxkit/metadata:dc5bcfa45946053145391ceabe33729c8b9507a1 command: ["/usr/bin/metadata", "packet"] services: - name: rngd diff --git a/examples/scaleway.yml b/examples/scaleway.yml index cc86a3791..89b895b7b 100644 --- a/examples/scaleway.yml +++ b/examples/scaleway.yml @@ -16,7 +16,7 @@ onboot: image: linuxkit/dhcpcd:2f8a9b670aa6e96a09db56ec45c9f07ef2a811ee command: ["/sbin/dhcpcd", "--nobackground", "-f", "/dhcpcd.conf", "-1"] - name: metadata - image: linuxkit/metadata:04ce7519c2ea2eaf99bbdc76bb01fc036eed7ab0 + image: linuxkit/metadata:dc5bcfa45946053145391ceabe33729c8b9507a1 services: - name: getty image: linuxkit/getty:48f66df198981e692084bf70ab72b9fe2be0f880 diff --git a/examples/vultr.yml b/examples/vultr.yml index 06bcf6fe6..f25c499d3 100644 --- a/examples/vultr.yml +++ b/examples/vultr.yml @@ -13,7 +13,7 @@ onboot: image: linuxkit/dhcpcd:2f8a9b670aa6e96a09db56ec45c9f07ef2a811ee command: ["/sbin/dhcpcd", "--nobackground", "-f", "/dhcpcd.conf", "-1"] - name: metadata - image: linuxkit/metadata:04ce7519c2ea2eaf99bbdc76bb01fc036eed7ab0 + image: linuxkit/metadata:dc5bcfa45946053145391ceabe33729c8b9507a1 command: ["/usr/bin/metadata", "vultr"] services: - name: getty diff --git a/pkg/metadata/.gitignore b/pkg/metadata/.gitignore index db2b4ca32..a2643b665 100644 --- a/pkg/metadata/.gitignore +++ b/pkg/metadata/.gitignore @@ -1,4 +1,3 @@ dev proc -sys usr diff --git a/pkg/metadata/main.go b/pkg/metadata/main.go index 9bb9106db..afb859313 100644 --- a/pkg/metadata/main.go +++ b/pkg/metadata/main.go @@ -2,13 +2,15 @@ package main import ( "encoding/json" + "flag" "io/ioutil" - "log" "os" "path" "strconv" "strings" "syscall" + + log "github.com/sirupsen/logrus" ) const ( @@ -26,6 +28,22 @@ const ( metaDataURL = "http://169.254.169.254/latest/meta-data/" ) +var ( + defaultLogFormatter = &log.TextFormatter{} +) + +// infoFormatter overrides the default format for Info() log events to +// provide an easier to read output +type infoFormatter struct { +} + +func (f *infoFormatter) Format(entry *log.Entry) ([]byte, error) { + if entry.Level == log.InfoLevel { + return append([]byte(entry.Message), '\n'), nil + } + return defaultLogFormatter.Format(entry) +} + // Provider is a generic interface for metadata/userdata providers. type Provider interface { // String should return a unique name for the Provider @@ -49,9 +67,21 @@ var cdromProviders []Provider var fileProviders []Provider func main() { + log.SetFormatter(new(infoFormatter)) + log.SetLevel(log.InfoLevel) + flagVerbose := flag.Bool("v", false, "Verbose execution") + + flag.Parse() + if *flagVerbose { + // Switch back to the standard formatter + log.SetFormatter(defaultLogFormatter) + log.SetLevel(log.DebugLevel) + } + providers := []string{"aws", "gcp", "hetzner", "openstack", "scaleway", "vultr", "packet", "cdrom"} - if len(os.Args) > 1 { - providers = os.Args[1:] + args := flag.Args() + if len(args) > 0 { + providers = args } for _, p := range providers { switch { diff --git a/pkg/metadata/provider_cdrom.go b/pkg/metadata/provider_cdrom.go index 34b9608fe..d4b1d9de9 100644 --- a/pkg/metadata/provider_cdrom.go +++ b/pkg/metadata/provider_cdrom.go @@ -5,7 +5,11 @@ import ( "io/ioutil" "path" "path/filepath" + "strings" "syscall" + + "github.com/diskfs/go-diskfs" + log "github.com/sirupsen/logrus" ) const ( @@ -13,6 +17,7 @@ const ( userdataFile = "user-data" userdataFallback = "config" cdromDevs = "/dev/sr[0-9]*" + blockDevs = "/sys/class/block/*" ) var ( @@ -35,6 +40,14 @@ func ListCDROMs() []Provider { // Glob can only error on invalid pattern panic(fmt.Sprintf("Invalid glob pattern: %s", cdromDevs)) } + log.Debugf("cdrom devices to be checked: %v", cdroms) + // get the devices that match the cloud-init spec + cidevs := FindCIs() + log.Debugf("CIDATA devices to be checked: %v", cidevs) + // merge the two, ensuring that the list is unique + cdroms = append(cidevs, cdroms...) + cdroms = uniqueString(cdroms) + log.Debugf("unique devices to be checked: %v", cdroms) providers := []Provider{} for _, device := range cdroms { providers = append(providers, NewCDROM(device)) @@ -42,6 +55,49 @@ func ListCDROMs() []Provider { return providers } +// FindCIs goes through all known devices. Returns any that are either fat32 or +// iso9660 and have a filesystem label "CIDATA" or "cidata", per the spec +// here https://github.com/canonical/cloud-init/blob/master/doc/rtd/topics/datasources/nocloud.rst +func FindCIs() []string { + devs, err := filepath.Glob(blockDevs) + log.Debugf("block devices found: %v", devs) + if err != nil { + // Glob can only error on invalid pattern + panic(fmt.Sprintf("Invalid glob pattern: %s", blockDevs)) + } + foundDevices := []string{} + for _, device := range devs { + // get the base device name + dev := filepath.Base(device) + // ignore loop and ram devices + if strings.HasPrefix(dev, "loop") || strings.HasPrefix(dev, "ram") { + log.Debugf("ignoring loop or ram device: %s", dev) + continue + } + dev = fmt.Sprintf("/dev/%s", dev) + log.Debugf("checking device: %s", dev) + // open readonly, ignore errors + disk, err := diskfs.OpenWithMode(dev, diskfs.ReadOnly) + if err != nil { + log.Debugf("failed to open device read-only: %s: %v", dev, err) + continue + } + fs, err := disk.GetFilesystem(0) + if err != nil { + log.Debugf("failed to get filesystem on partition 0 for device: %s: %v", dev, err) + continue + } + // get the label + label := strings.TrimSpace(fs.Label()) + log.Debugf("found trimmed filesystem label for device: %s: '%s'", dev, label) + if label == "cidata" || label == "CIDATA" { + log.Debugf("adding device: %s", dev) + foundDevices = append(foundDevices, dev) + } + } + return foundDevices +} + // NewCDROM returns a new ProviderCDROM func NewCDROM(device string) *ProviderCDROM { mountPoint, err := ioutil.TempDir("", "cd") @@ -97,3 +153,18 @@ func (p *ProviderCDROM) mount() error { func (p *ProviderCDROM) unmount() { _ = syscall.Unmount(p.mountPoint, 0) } + +// uniqueString returns a unique subset of the string slice provided. +func uniqueString(input []string) []string { + u := make([]string, 0, len(input)) + m := make(map[string]bool) + + for _, val := range input { + if _, ok := m[val]; !ok { + m[val] = true + u = append(u, val) + } + } + + return u +} diff --git a/pkg/metadata/vendor.conf b/pkg/metadata/vendor.conf index 29c809ff9..c52157621 100644 --- a/pkg/metadata/vendor.conf +++ b/pkg/metadata/vendor.conf @@ -1,3 +1,9 @@ +github.com/diskfs/go-diskfs 29b62ddcc13e0d45cf3ce57e586585b0998bcac1 +gopkg.in/djherbis/times.v1 v1.2.0 +github.com/google/uuid v1.1.1 github.com/packethost/packngo 131798f2804a1b3e895ca98047d56f0d7e094e2a +github.com/sirupsen/logrus v1.0.3 github.com/vishvananda/netlink f5a6f697a596c788d474984a38a0ac4ba0719e93 github.com/vishvananda/netns 86bef332bfc3b59b7624a600bd53009ce91a9829 +golang.org/x/crypto 1a580b3eff7814fc9b40602fd35256c63b50f491 +golang.org/x/sys 1957bb5e6d1f523308b49060df02171d06ddfc77 diff --git a/pkg/metadata/vendor/github.com/diskfs/go-diskfs/LICENSE b/pkg/metadata/vendor/github.com/diskfs/go-diskfs/LICENSE new file mode 100644 index 000000000..7f4b860b5 --- /dev/null +++ b/pkg/metadata/vendor/github.com/diskfs/go-diskfs/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Avi Deitcher + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/pkg/metadata/vendor/github.com/diskfs/go-diskfs/README.md b/pkg/metadata/vendor/github.com/diskfs/go-diskfs/README.md new file mode 100644 index 000000000..6dd2a51c6 --- /dev/null +++ b/pkg/metadata/vendor/github.com/diskfs/go-diskfs/README.md @@ -0,0 +1,158 @@ +# go-diskfs +go-diskfs is a [go](https://golang.org) library for performing manipulation of disks, disk images and filesystems natively in go. + +You can do nearly everything that go-diskfs provides using shell tools like gdisk/fdisk/mkfs.vfat/mtools/sgdisk/sfdisk/dd. However, these have the following limitations: + +* they need to be installed on your system +* you need to fork/exec to the command (and possibly a shell) to run them +* some are difficult to run without mounting disks, which may not be possible or may be risky in your environment, and almost certainly will require root privileges +* you do not want to launch a VM to run the excellent [libguestfs](https://libguestfs.org) and it may not be installed + +go-diskfs performs all modifications _natively_ in go, without mounting any disks. + +## Usage +Note: detailed go documentation is available at [godoc.org](https://godoc.org/github.com/diskfs/go-diskfs). + +### Concepts +`go-diskfs` has a few basic concepts: + +* Disk +* Partition +* Filesystem + +#### Disk +A disk represents either a file or block device that you access and manipulate. With access to the disk, you can: + +* read, modify or create a partition table +* open an existing or create a new filesystem + +#### Partition +A partition is a slice of a disk, beginning at one point and ending at a later one. You can have multiple partitions on a disk, and a partition table that describes how partitions are laid out on the disk. + +#### Filesystem +A filesystem is a construct that gives you access to create, read and write directories and files. + +You do *not* need a partitioned disk to work with a filesystem; filesystems can be an entire `disk`, just as they can be an entire block device. However, they also can be in a partition in a `disk` + +### Working With a Disk +Before you can do anything with a disk - partitions or filesystems - you need to access it. + +* If you have an existing disk or image file, you `Open()` it +* If you are creating a new one, usually just disk image files, you `Create()` it + +The disk will be opened read-write, with exclusive access. If it cannot do either, it will fail. + +Once you have a `Disk`, you can work with partitions or filesystems in it. + +#### Partitions on a Disk + +The following are the partition actions you can take on a disk: + +* `GetPartitionTable()` - if one exists. Will report the table layout and type. +* `Partition()` - partition the disk, overwriting any previous table if it exists + +As of this writing, supported partition formats are Master Boot Record (`mbr`) and GUID Partition Table (`gpt`). + +#### Filesystems on a Disk +Once you have a valid disk, and optionally partition, you can access filesystems on that disk image or partition. + +* `CreateFilesystem()` - create a filesystem in an individual partition or the entire disk +* `GetFilesystem()` - access an existing filesystem in a partition or the entire disk + +As of this writing, supported filesystems include `FAT32` and `ISO9660` (a.k.a. `.iso`). + +With a filesystem in hand, you can create, access and modify directories and files. + +* `Mkdir()` - make a directory in a filesystem +* `Readdir()` - read all of the entries in a directory +* `OpenFile()` - open a file for read, optionally write, create and append + +Note that `OpenFile()` is intended to match [os.OpenFile](https://golang.org/pkg/os/#OpenFile) and returns a `godiskfs.File` that closely matches [os.File](https://golang.org/pkg/os/#File) + +With a `File` in hand, you then can: + +* `Write(p []byte)` to the file +* `Read(b []byte)` from the file +* `Seek(offset int64, whence int)` to set the next read or write to an offset in the file + +### Read-Only Filesystems +Some filesystem types are intended to be created once, after which they are read-only, for example `ISO9660`/`.iso` and `squashfs`. + +`godiskfs` recognizes read-only filesystems and limits working with them to the following: + +* You can `GetFilesystem()` a read-only filesystem and do all read activities, but cannot write to them. Any attempt to `Mkdir()` or `OpenFile()` in write/append/create modes or `Write()` to the file will result in an error. +* You can `CreateFilesystem()` a read-only filesystem and write anything to it that you want. It will do all of its work in a "scratch" area, or temporary "workspace" directory on your local filesystem. When you are ready to complete it, you call `Finalize()`, after which it becomes read-only. If you forget to `Finalize()` it, you get... nothing. The `Finalize()` function exists only on read-only filesystems. + +### Example + +There are examples in the [examples/](./examples/) directory. Here is one to get you started. + +The following example will create a fully bootable EFI disk image. It assumes you have a bootable EFI file (any modern Linux kernel compiled with `CONFIG_EFI_STUB=y` will work) available. + +```go +import diskfs "github.com/diskfs/goi-diskfs" + +espSize int := 100*1024*1024 // 100 MB +diskSize int := espSize + 4*1024*1024 // 104 MB + + +// create a disk image +diskImg := "/tmp/disk.img" +disk := diskfs.Create(diskImg, diskSize, diskfs.Raw) +// create a partition table +blkSize int := 512 +partitionSectors int := espSize / blkSize +partitionStart int := 2048 +partitionEnd int := partitionSectors - partitionStart + 1 +table := PartitionTable{ + type: partition.GPT, + partitions:[ + Partition{Start: partitionStart, End: partitionEnd, Type: partition.EFISystemPartition, Name: "EFI System"} + ] +} +// apply the partition table +err = disk.Partition(table) + + +/* + * create an ESP partition with some contents + */ +kernel, err := ioutil.ReadFile("/some/kernel/file") + +fs, err := disk.CreateFilesystem(0, diskfs.TypeFat32) + +// make our directories +err = fs.Mkdir("/EFI/BOOT") +rw, err := fs.OpenFile("/EFI/BOOT/BOOTX64.EFI", os.O_CREATE|os.O_RDRWR) + +err = rw.Write(kernel) + +``` + +## Tests +There are two ways to run tests: unit and integration (somewhat loosely defined). + +* Unit: these tests run entirely within the go process, primarily test unexported and some exported functions, and may use pre-defined test fixtures in a directory's `testdata/` subdirectory. By default, these are run by running `go test ./...` or just `make unit_test`. +* Integration: these test the exported functions and their ability to create or manipulate correct files. They are validated by running a [docker](https://docker.com) container with the right utilities to validate the output. These are run by running `TEST_IMAGE=diskfs/godiskfs go test ./...` or just `make test`. The value of `TEST_IMAGE` will be the image to use to run tests. + +For integration tests to work, the correct docker image must be available. You can create it by running `make image`. Check the [Makefile](./Makefile) to see the `docker build` command used to create it. Running `make test` automatically creates the image for you. + +### Integration Test Image +The integration test image contains the various tools necessary to test images: `mtools`, `fdisk`, `gdisk`, etc. It works on precisely one file at a time. In order to avoid docker volume mounting limitations with various OSes, instead of mounting the image `-v`, it expects to receive the image as a `stdin` stream, and saves it internally to the container as `/file.img`. + +For example, to test the existence of directory `/abc` on file `$PWD/foo.img`: + +``` +cat $PWD/foo.img | docker run -i --rm $INT_IMAGE mdir -i /file.img /abc +``` + + +## Plans +Future plans are to add the following: + +* embed boot code in `mbr` e.g. `altmbr.bin` (no need for `gpt` since an ESP with `/EFI/BOOT/BOOT.EFI` will boot) +* `ext4` filesystem +* `Joliet` extensions to `iso9660` +* `Rock Ridge` sparse file support - supports the flag, but not yet reading or writing +* `squashfs` filesystem +* `qcow` disk format diff --git a/pkg/metadata/vendor/github.com/diskfs/go-diskfs/disk/disk.go b/pkg/metadata/vendor/github.com/diskfs/go-diskfs/disk/disk.go new file mode 100644 index 000000000..82e74eba0 --- /dev/null +++ b/pkg/metadata/vendor/github.com/diskfs/go-diskfs/disk/disk.go @@ -0,0 +1,233 @@ +// Package disk provides utilities for working directly with a disk +// +// Most of the provided functions are intelligent wrappers around implementations of +// github.com/diskfs/go-diskfs/partition and github.com/diskfs/go-diskfs/filesystem +package disk + +import ( + "errors" + "fmt" + "io" + "os" + + log "github.com/sirupsen/logrus" + + "github.com/diskfs/go-diskfs/filesystem" + "github.com/diskfs/go-diskfs/filesystem/fat32" + "github.com/diskfs/go-diskfs/filesystem/iso9660" + "github.com/diskfs/go-diskfs/partition" +) + +// Disk is a reference to a single disk block device or image that has been Create() or Open() +type Disk struct { + File *os.File + Info os.FileInfo + Type Type + Size int64 + LogicalBlocksize int64 + PhysicalBlocksize int64 + Table partition.Table + Writable bool + DefaultBlocks bool +} + +// Type represents the type of disk this is +type Type int + +const ( + // File is a file-based disk image + File Type = iota + // Device is an OS-managed block device + Device +) + +var ( + errIncorrectOpenMode = errors.New("disk file or device not open for write") +) + +// GetPartitionTable retrieves a PartitionTable for a Disk +// +// returns an error if the Disk is invalid or does not exist, or the partition table is unknown +func (d *Disk) GetPartitionTable() (partition.Table, error) { + return partition.Read(d.File, int(d.LogicalBlocksize), int(d.PhysicalBlocksize)) +} + +// Partition applies a partition.Table implementation to a Disk +// +// The Table can have zero, one or more Partitions, each of which is unique to its +// implementation. E.g. MBR partitions in mbr.Table look different from GPT partitions in gpt.Table +// +// Actual writing of the table is delegated to the individual implementation +func (d *Disk) Partition(table partition.Table) error { + if !d.Writable { + return errIncorrectOpenMode + } + // fill in the uuid + err := table.Write(d.File, d.Size) + if err != nil { + return fmt.Errorf("Failed to write partition table: %v", err) + } + d.Table = table + // the partition table needs to be re-read only if + // the disk file is an actual block device + if d.Type == Device { + err = d.ReReadPartitionTable() + if err != nil { + return fmt.Errorf("Unable to re-read the partition table. Kernel still uses old partition table: %v", err) + } + } + return nil +} + +// WritePartitionContents writes the contents of an io.Reader to a given partition +// +// if successful, returns the number of bytes written +// +// returns an error if there was an error writing to the disk, reading from the reader, the table +// is invalid, or the partition is invalid +func (d *Disk) WritePartitionContents(partition int, reader io.Reader) (int64, error) { + if !d.Writable { + return -1, errIncorrectOpenMode + } + if d.Table == nil { + return -1, fmt.Errorf("cannot write contents of a partition on a disk without a partition table") + } + if partition < 0 { + return -1, fmt.Errorf("cannot write contents of a partition without specifying a partition") + } + partitions := d.Table.GetPartitions() + // API indexes from 1, but slice from 0 + if partition > len(partitions) { + return -1, fmt.Errorf("cannot write contents of partition %d which is greater than max partition %d", partition, len(partitions)) + } + written, err := partitions[partition-1].WriteContents(d.File, reader) + return int64(written), err +} + +// ReadPartitionContents reads the contents of a partition to an io.Writer +// +// if successful, returns the number of bytes read +// +// returns an error if there was an error reading from the disk, writing to the writer, the table +// is invalid, or the partition is invalid +func (d *Disk) ReadPartitionContents(partition int, writer io.Writer) (int64, error) { + if d.Table == nil { + return -1, fmt.Errorf("cannot read contents of a partition on a disk without a partition table") + } + if partition < 0 { + return -1, fmt.Errorf("cannot read contents of a partition without specifying a partition") + } + partitions := d.Table.GetPartitions() + // API indexes from 1, but slice from 0 + if partition > len(partitions) { + return -1, fmt.Errorf("cannot read contents of partition %d which is greater than max partition %d", partition, len(partitions)) + } + return partitions[partition-1].ReadContents(d.File, writer) +} + +// FilesystemSpec represents the specification of a filesystem to be created +type FilesystemSpec struct { + Partition int + FSType filesystem.Type + VolumeLabel string +} + +// CreateFilesystem creates a filesystem on a disk image, the equivalent of mkfs. +// +// Required: +// * desired partition number, or 0 to create the filesystem on the entire block device or +// disk image, +// * the filesystem type from github.com/diskfs/go-diskfs/filesystem +// +// Optional: +// * volume label for those filesystems that support it; under Linux this shows +// in '/dev/disks/by-label/