asteris-llc/converge

View on GitHub
resource/lvm/fs/fs.go

Summary

Maintainability
A
35 mins
Test Coverage
// Copyright © 2016 Asteris, LLC
//
// 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 fs

import (
    "bytes"
    "fmt"
    "strings"
    "text/template"

    log "github.com/Sirupsen/logrus"
    "github.com/asteris-llc/converge/resource"
    "github.com/asteris-llc/converge/resource/lvm/lowlevel"
    "github.com/pkg/errors"
    "golang.org/x/net/context"
)

type resourceFS struct {
    mount           *Mount
    lvm             lowlevel.LVM
    unitFileName    string
    unitFileContent string
    unitNeedUpdate  bool
    mountNeedUpdate bool
    needMkfs        bool
}

// Mount is a structure for holding values to be rendered as .mount unit for systemd
type Mount struct {
    What       string
    Where      string
    Type       string
    Before     string
    WantedBy   string
    RequiredBy string
}

// NB: RequiredBy statement should issued only when non-empty
// Related issue: https://github.com/asteris-llc/converge/issues/452
const unitTemplate = `[Unit]
Before=local-fs.target {{.Before}}

[Mount]
What={{.What}}
Where={{.Where}}
Type={{.Type}}

[Install]
WantedBy=local-fs.target {{.WantedBy}}
RequiredBy={{.RequiredBy}}`

func (r *resourceFS) Check(context.Context, resource.Renderer) (resource.TaskStatus, error) {
    status := &resource.Status{}

    if err := r.lvm.CheckFilesystemTools(r.mount.Type); err != nil {
        return nil, errors.Wrapf(err, "filesystem prerequisites for %s", r.mount.Type)
    }

    if err := r.checkBlkid(status); err != nil {
        return nil, err
    }

    if err := r.checkUnit(status); err != nil {
        return nil, err
    }

    if err := r.checkMountpoint(status); err != nil {
        return nil, err
    }

    status.RaiseLevelForDiffs()

    return status, nil
}

func (r *resourceFS) Apply(context.Context) (resource.TaskStatus, error) {
    if r.needMkfs {
        if err := r.lvm.Mkfs(r.mount.What, r.mount.Type); err != nil {
            return nil, errors.Wrapf(err, "mkfs")
        }
    }

    if r.unitNeedUpdate {
        if err := r.lvm.UpdateUnit(r.unitFileName, r.unitFileContent); err != nil {
            return nil, errors.Wrapf(err, "updating unit file %s", r.unitFileName)
        }
    }

    if r.mountNeedUpdate {
        service := r.unitServiceName()
        if err := r.lvm.StartUnit(service); err != nil {
            return nil, errors.Wrapf(err, "starting service %s", service)
        }
    }

    r.unitNeedUpdate = false
    r.mountNeedUpdate = false
    return &resource.Status{}, nil
}

// NewResourceFS create new resource.Task node for create/mount FileSystem.
func NewResourceFS(lvm lowlevel.LVM, m *Mount) (resource.Task, error) {
    var err error
    r := &resourceFS{
        lvm:   lvm,
        mount: m,
    }
    r.unitFileName = r.unitName()
    r.unitFileContent, err = r.renderUnitFile()
    if err != nil {
        return nil, err
    }
    return r, nil
}

func (r *resourceFS) checkBlkid(status *resource.Status) error {
    fs, err := r.lvm.Blkid(r.mount.What)
    if err != nil {
        return errors.Wrapf(err, "retrieving current FS type of %s", r.mount.What)
    }
    log.Debugf("blkid detect following fstype: %s, planned fstype: %s", fs, r.mount.Type)
    if fs == r.mount.Type {
        r.needMkfs = false
    } else if fs == "" {
        r.needMkfs = true
        status.AddDifference("format", "<unformatted>", r.mount.Type, "")
    } else {
        return fmt.Errorf("%s already contain other filesystem with different type %s", r.mount.What, fs)
    }
    return nil
}

// NB: Here we need to ensure, that r. mount.Where is exists, and have proper permissions
// Related issue: https://github.com/asteris-llc/converge/issues/449
//
// NB: We need to ensure, that no other filesystems mounted to Where
// (now we check only if it mountpoint or not)
// Related issue: https://github.com/asteris-llc/converge/issues/450
func (r *resourceFS) checkMountpoint(status *resource.Status) error {
    ok, err := r.lvm.Mountpoint(r.mount.Where)
    if err != nil {
        return errors.Wrapf(err, "querying mountpoint %s", r.mount.Where)
    }
    r.mountNeedUpdate = r.unitNeedUpdate || !ok

    if r.mountNeedUpdate {
        status.AddDifference(r.mount.Where, "<none>", fmt.Sprintf("mount %s", r.mount.Where), "")
    }
    return nil
}

func (r *resourceFS) checkUnit(status *resource.Status) error {
    ok, err := r.lvm.CheckUnit(r.unitFileName, r.unitFileContent)
    if err != nil {
        return err
    }
    if ok {
        r.unitNeedUpdate = true
        status.AddDifference(r.unitFileName, "<none>", r.unitFileContent, "")
    }
    return nil
}

func (r *resourceFS) escapedMountpoint() string {
    // NB: proper systemd' escaping of r.mountpoint should be implemented here
    // Related issue: https://github.com/asteris-llc/converge/issues/451
    return strings.Replace(strings.Trim(r.mount.Where, "/"), "/", "-", -1)
}

func (r *resourceFS) unitServiceName() string {
    return fmt.Sprintf("%s.mount", r.escapedMountpoint())
}

func (r *resourceFS) unitName() string {
    return fmt.Sprintf("/etc/systemd/system/%s", r.unitServiceName())
}

func (r *resourceFS) renderUnitFile() (string, error) {
    var b bytes.Buffer
    tmpl, err := template.New("unit.mount").Parse(unitTemplate)
    if err != nil {
        return "", errors.Wrapf(err, "renderUnitFile(): template syntax")
    }
    err = tmpl.Execute(&b, r.mount)
    if err != nil {
        return "", errors.Wrapf(err, "renderUnitFile(): template executing")
    }
    return b.String(), nil
}