vorteil/vorteil

View on GitHub
pkg/xva/xva.go

Summary

Maintainability
A
0 mins
Test Coverage
package xva

/**
 * SPDX-License-Identifier: Apache-2.0
 * Copyright 2020 vorteil.io Pty Ltd
 */

import (
    "archive/tar"
    "bytes"
    "crypto/sha1"
    "encoding/hex"
    "errors"
    "fmt"
    "hash"
    "io"
    "path/filepath"
    "strings"
    "time"

    "github.com/vorteil/vorteil/pkg/vcfg"
    "github.com/vorteil/vorteil/pkg/vio"
)

// Sizer is an interface that shouldn't exist in a vacuum, but does because our
// other image formats follow a similar patten and need more information. A
// Sizer should return the true and final RAW size of the image and be callable
// before the first byte of data is written to the Writer. Note that our
// vimg.Builder implements this interface and is the intended argument in most
// cases.
type Sizer interface {
    Size() int64
}

// Writer implements io.Closer, io.Writer, and io.Seeker interfaces. Creating an
// XVA image is as simple as getting one of these writers and copying a raw
// image into it.
type Writer struct {
    tw  *tar.Writer
    h   Sizer
    cfg *vcfg.VCFG

    hdr    *tar.Header
    hasher hash.Hash
    buffer *bytes.Buffer
    cursor int64
}

// NewWriter returns a Writer to which a RAW image can be copied in order to
// create an XVA format disk image. The Sizer 'h' must accurately return the
// true and final RAW size of the image.
func NewWriter(w io.Writer, h Sizer, cfg *vcfg.VCFG) (*Writer, error) {

    xw := new(Writer)
    xw.h = h
    xw.cfg = cfg
    xw.tw = tar.NewWriter(w)

    err := xw.writeOVAXML()
    if err != nil {
        _ = xw.tw.Close()
        return nil, err
    }

    xw.hasher = sha1.New()
    xw.buffer = new(bytes.Buffer)

    return xw, nil

}

func (w *Writer) writeOVAXML() error {

    // timestamp := src.ModTime()
    timestamp := time.Now()

    hdr := &tar.Header{
        ModTime:    timestamp,
        AccessTime: timestamp,
        ChangeTime: timestamp,
    }
    w.hdr = hdr

    // write ova.xml
    hdr.Name = "ova.xml"
    ova := w.ovaXML()
    hdr.Size = int64(len(ova))

    err := w.tw.WriteHeader(hdr)
    if err != nil {
        return err
    }

    _, err = io.Copy(w.tw, strings.NewReader(ova))
    if err != nil {
        return err
    }

    return nil

}

const mib = 0x100000
const emptyChunkChecksum = "3b71f43ff30f4b15b5cd85dd9e95ebc7e84eb5a3"

// Write implements io.Writer.
func (w *Writer) Write(p []byte) (n int, err error) {

    var total int

    for {
        chunkSpace := mib - w.cursor%mib
        if int64(len(p)) < chunkSpace {
            n, err = w.buffer.Write(p)
            w.cursor += int64(n)
            total += n
            return total, err
        }

        this := p[:chunkSpace]
        next := p[chunkSpace:]
        n, err = w.buffer.Write(this)
        w.cursor += int64(n)
        total += n
        if err != nil {
            return total, err
        }

        err = w.flushBuffer()
        if err != nil {
            return total, err
        }

        if len(next) > 0 {
            p = next
            continue
        }

        break
    }

    return total, err

}

func (w *Writer) flushChunkHeader(chunk int64) error {

    w.hdr.Name = filepath.Join("Ref:4", fmt.Sprintf("%08d", chunk))
    w.hdr.Size = int64(mib)
    err := w.tw.WriteHeader(w.hdr)
    if err != nil {
        return err
    }

    _, err = io.Copy(w.tw, bytes.NewReader(w.buffer.Bytes()))
    if err != nil {
        return err
    }

    return nil

}

func (w *Writer) flushChunkData(checksum string) error {

    w.hdr.Name += ".checksum"
    w.hdr.Size = 40
    err := w.tw.WriteHeader(w.hdr)
    if err != nil {
        return err
    }

    _, err = io.Copy(w.tw, strings.NewReader(checksum))
    if err != nil {
        return err
    }

    return nil

}

func (w *Writer) flushBuffer() error {

    chunk := w.cursor/mib - 1
    checksum := hex.EncodeToString(w.hasher.Sum(nil))
    if checksum != emptyChunkChecksum {
        err := w.flushChunkHeader(chunk)
        if err != nil {
            return err
        }

        err = w.flushChunkData(checksum)
        if err != nil {
            return err
        }
    }

    w.buffer.Reset()
    w.hasher.Reset()
    return nil

}

// Seek implements io.Seeker
func (w *Writer) Seek(offset int64, whence int) (int64, error) {

    var abs int64
    switch whence {
    case io.SeekStart:
        abs = offset
    case io.SeekCurrent:
        abs = w.cursor + offset
    case io.SeekEnd:
        abs = w.h.Size() + offset
    default:
        panic("bad seek whence")
    }

    if abs < w.cursor {
        return w.cursor, errors.New("xva archive writer cannot seek backwards")
    }

    // TODO: make this faster using HolePredictor
    delta := abs - w.cursor
    _, err := io.CopyN(w, vio.Zeroes, delta)
    if err != nil {
        return w.cursor, err
    }

    return w.cursor, nil

}

// Close implements io.Closer
func (w *Writer) Close() error {

    if w.cursor < w.h.Size() {
        return errors.New("xva archive expected more raw image data than was received")
    }

    err := w.tw.Close()
    if err != nil {
        return err
    }

    return nil

}

func (w *Writer) ovaXML() string {

    cfg := w.cfg

    name := cfg.Info.Name
    if name == "" {
        name = "Vorteil App"
    }
    description := cfg.Info.Summary
    if description == "" {
        description = "Created by Vorteil"
    }
    mem := int(cfg.VM.RAM)
    cpus := int(cfg.VM.CPUs)

    var networkVIFs string
    var networkSettings string
    for i := range cfg.Networks {
        vifID := 2*i + 8
        netID := 2*i + 9
        if i == 0 {
            vifID = 1
            netID = 2
        }
        mtu := cfg.Networks[i].MTU
        if mtu == 0 {
            mtu = 1500
        }
        networkVIFs += fmt.Sprintf(networkVIFTemplate, vifID)
        networkSettings += fmt.Sprintf(networkSettingsTemplate, vifID, i, netID, mtu, netID, vifID, mtu)
    }

    s := fmt.Sprintf(ovaXMLTemplate, name, description, mem, mem, mem, mem, cpus, cpus, networkVIFs, networkSettings, w.h.Size())

    lines := strings.Split(s, "\n")
    for i := 0; i < len(lines); i++ {
        lines[i] = strings.TrimSpace(lines[i])
    }
    s = strings.Join(lines, "")

    return s

}

const networkVIFTemplate = `<value>Ref:%d</value>`