asteris-llc/converge

View on GitHub
resource/systemd/unit/resource.go

Summary

Maintainability
A
3 hrs
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 unit

import (
    "fmt"

    "github.com/pkg/errors"

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

// Resource is the resource struct for systemd.unit.
//
// Many of the exported fields are derived from the systemd dbus api; see
// https://www.freedesktop.org/wiki/Software/systemd/dbus/ for a full
// description of the potential values for this fields and their meanings.
type Resource struct {
    // The name of the unit, including the unit type.
    Name string `export:"unit"`

    // The desired state of the unit as configured by the user. It will be one of
    // `running`, `stopped`, or `restarted` if it was configured by the user, and
    // an empty string otherwise.
    State string `export:"state"`

    // This field is set to true if the reload flag was configured by the user.
    Reload bool `export:"reload"`

    // The human-readable name of a unix signal that will be sent to the process.
    // If this is set the name will match the field set in SignalNumber.  See the
    // man pages for `signal(3)` on BSD/Darwin or `signal(7)` on GNU Linux for a
    // full explanation of these signals.
    SignalName string `export:"signal_name"`

    // The numeric identifier of a unix signal that will be sent to the process.
    // If this is set it will match the field set in SignalName.  See the man
    // pages for `signal(3)` on BSD/Darwin or `signal(7)` on GNU Linux for a full
    // explanation of these signals.
    SignalNumber uint `export:"signal_number"`

    // The full path to the unit file on disk. This field will be empty if the
    // unit was not started from a systemd unit file on disk.
    Path string `export:"path"`

    // Description of the services. This field will be empty unless a description
    // has been provided in the systemd unit file.
    Description string `export:"description"`

    // The active state of the unit. It will always be one of: `active`,
    // `reloading`, `inactive`, `failed`, `activating`, `deactivating`.
    ActiveState string `export:"activestate"`

    // The load state of the unit.
    LoadState string `export:"loadstate"`

    // The type of the unit as an enumerated value.  See TypeStr for a human
    // readable type.
    Type UnitType `export:"type"`

    // The type of the unit as a human readable string.  See the man page for
    // `systemd(1)` for a full description of the types and their meaning.
    TypeStr string `export:"typestr"`

    // The status represents the current status of the process.  It will be
    // initialized during planning and updated after apply to reflect any changes.
    Status string `export:"status"`

    // Properties are the global systemd unit properties and will be set for all
    // unit types. See the [systemd_Properties]({{< ref "properties.md" >}}) docs
    // for more information.
    Properties Properties `re-export-as:"global_properties"`

    // ServiceProperties contain properties specific to Service unit types. This
    // field is only exported when the unit type is `service`. See the
    // [systemd_ServiceTypeProperties]({{< ref "service_properties.md" >}}) docs
    // for more information.
    ServiceProperties *ServiceTypeProperties `re-export-as:"service_properties"`

    // SocketProperties contain properties specific to Socket unit types. This
    // field is only exported when the unit type is `socket`. See the
    // [systemd_SocketTypeProperties]({{< ref "socket_properties.md" >}}) docs for
    // more information.
    SocketProperties *SocketTypeProperties `re-export-as:"socket_properties"`

    // DeviceProperties contain properties specific to Device unit types. This
    // field is only exported when the unit type is `device`. See the
    // [systemd_DeviceTypeProperties]({{< ref "device_properties.md" >}}) docs for
    // more information.
    DeviceProperties *DeviceTypeProperties `re-export-as:"device_properties"`

    // MountProperties contain properties specific to Mount unit types. This field
    // is only exported when the unit type is `mount`. See the
    // [systemd_MountTypeProperties]({{< ref "mount_properties.md" >}}) docs for
    // more information.
    MountProperties *MountTypeProperties `re-export-as:"mount_properties"`

    // AutomountProperties contain properties specific to Autoumount unit types.
    // This field is only exported when the unit type is`automount`. See the
    // [systemd_AutomountTypeProperties]({{< ref "automount_properties.md" >}})
    // docs for more information.
    AutomountProperties *AutomountTypeProperties `re-export-as:"automount_properties"`

    // SwapProperties contain properties specific to Swap unit types. This field
    // is only exported when the unit type is `swap`. See the
    // [systemd_SwapTypeProperties]({{< ref "swap_properties.md" >}}) docs for
    // more information.
    SwapProperties *SwapTypeProperties `re-export-as:"swap_properties"`

    // PathProperties contain properties specific to Path unit types. This field
    // is only exported when the unit type is `path`. See the
    // [systemd_PathTypeProperties]({{< ref "path_properties.md" >}}) docs for
    // more information.
    PathProperties *PathTypeProperties `re-export-as:"path_properties"`

    // TimerProperties contain properties specific to Timer unit types. This field
    // is only exported when the unit type is `timer`. See the
    // [systemd_TimerTypeProperties]({{< ref "timer_properties.md" >}}) docs for
    // more information.
    TimerProperties *TimerTypeProperties `re-export-as:"timer_properties"`

    // SliceProperties contain properties specific to Slice unit types. This field
    // is only exported when the unit type is `slice`. See the
    // [systemd_SliceTypeProperties]({{< ref "slice_properties.md" >}}) docs for
    // more information.
    SliceProperties *SliceTypeProperties `re-export-as:"slice_properties"`

    // ScopeProperties contain properties specific to Scope unit types. This field
    // is only exported when the unit type is `scope`. See the
    // [systemd_ScopeTypeProperties]({{< ref "scope_properties.md" >}}) docs for
    // more information.
    ScopeProperties *ScopeTypeProperties `re-export-as:"scope_properties"`

    sendSignal      bool
    systemdExecutor SystemdExecutor
    hasRun          bool
}

type response struct {
    status resource.TaskStatus
    err    error
}

func wrapCall(f func() (resource.TaskStatus, error)) <-chan response {
    resp := make(chan response)
    go func() {
        st, err := f()
        resp <- response{st, err}
    }()
    return resp
}

// Check implements resource.Task
func (r *Resource) Check(ctx context.Context, _ resource.Renderer) (resource.TaskStatus, error) {
    ch := wrapCall(r.runCheck)
    select {
    case <-ctx.Done():
        return nil, errors.New("context was cancelled")
    case results := <-ch:
        return results.status, results.err
    }
}

// Apply implemnts resource.Task
func (r *Resource) Apply(ctx context.Context) (resource.TaskStatus, error) {
    ch := wrapCall(r.runApply)
    select {
    case <-ctx.Done():
        return nil, errors.New("context was cancelled")
    case results := <-ch:
        return results.status, results.err
    }
}

func (r *Resource) runCheck() (resource.TaskStatus, error) {
    status := resource.NewStatus()
    u, err := r.systemdExecutor.QueryUnit(r.Name, false)
    if err != nil {
        status.AddMessage("query unit returned an error: " + err.Error())
        return nil, err
    }
    r.populateFromUnit(u)
    if r.sendSignal && !r.hasRun {
        status.RaiseLevel(resource.StatusWillChange)
        status.AddMessage(fmt.Sprintf("Sending signal `%s` to unit", r.SignalName))
    }
    if r.Reload && !r.hasRun {
        status.RaiseLevel(resource.StatusWillChange)
        status.AddMessage("Reloading unit configuration")
        status.AddDifference("state", u.ActiveState, "reloaded", "")
    }
    switch r.State {
    case "restarted":
        status.RaiseLevel(resource.StatusWillChange)
        status.AddMessage("Restarting unit")
        status.AddDifference("state", u.ActiveState, "restarted", "")
    case "running":
        r.shouldStart(u, status)
    case "stopped":
        r.shouldStop(u, status)
    }
    r.hasRun = true
    return status, nil
}

func (r *Resource) runApply() (resource.TaskStatus, error) {
    log.WithField("Unit Name: ", r.Name).Infof("calling runApply()....")
    status := resource.NewStatus()
    tempStatus := resource.NewStatus()
    u, err := r.systemdExecutor.QueryUnit(r.Name, false)
    if err != nil {
        return nil, err
    }

    if u.ActiveState == "unknown" {
        log.Infof("unable to query information about the unit. " +
            "Making a best guess based on configured data")
        u.Name = r.Name
    }

    if r.sendSignal {
        status.AddMessage(fmt.Sprintf("Sending signal `%s` to unit", r.SignalName))
        r.systemdExecutor.SendSignal(u, Signal(r.SignalNumber))
    }
    if r.Reload {
        status.AddMessage("Reloading unit configuration")
        status.AddDifference("state", u.ActiveState, "reloaded", "")
        if err := r.systemdExecutor.ReloadUnit(u); err != nil {
            return nil, err
        }
    }

    var runstateErr error
    switch r.State {
    case "running":
        if r.shouldStart(u, tempStatus) {
            runstateErr = r.systemdExecutor.StartUnit(u)
        }
    case "stopped":
        if r.shouldStop(u, tempStatus) {
            runstateErr = r.systemdExecutor.StopUnit(u)
        }
    case "restarted":
        runstateErr = r.systemdExecutor.RestartUnit(u)
    }
    return status, runstateErr
}

// We copy data from the unit into the resource to make the UX nicer for users
// who want to access systemd information.
func (r *Resource) populateFromUnit(u *Unit) {
    r.Description = u.Description
    r.Path = u.Path
    r.Type = u.Type
    r.TypeStr = u.Type.String()
    r.Status = u.ActiveState
    r.Properties = u.Properties
    r.ServiceProperties = u.ServiceProperties
    r.SocketProperties = u.SocketProperties
    r.DeviceProperties = u.DeviceProperties
    r.MountProperties = u.MountProperties
    r.AutomountProperties = u.AutomountProperties
    r.SwapProperties = u.SwapProperties
    r.PathProperties = u.PathProperties
    r.TimerProperties = u.TimerProperties
    r.SliceProperties = u.SliceProperties
    r.ScopeProperties = u.ScopeProperties
}

func (r *Resource) shouldStart(u *Unit, st *resource.Status) bool {
    switch u.ActiveState {
    case "active":
        st.AddMessage("already running")
        return false
    case "reloading":
        st.AddMessage("unit is reloading, will re-check status during apply")
        st.RaiseLevel(resource.StatusMayChange)
        return true
    case "inactive":
        st.RaiseLevel(resource.StatusWillChange)
        st.AddDifference("state", "inactive", "active", "")
        return true
    case "failed":
        st.AddMessage("unit has failed; will attempt to restart")
        if reason, err := getFailedReason(u); err != nil {
            st.AddMessage(fmt.Sprintf("cannot determine root cause of failure: %v", err))
        } else {
            st.AddMessage(fmt.Sprintf("unit previously failed, the message was: %s", reason))
        }
        st.RaiseLevel(resource.StatusWillChange)
        st.AddDifference("state", "failed", "active", "")
        return true
    case "activating":
        st.AddMessage("unit is alread activating, will re-check status during apply")
        st.RaiseLevel(resource.StatusMayChange)
        return true
    case "deactivating":
        st.AddMessage("unit is currently deactivating, will re-check status during apply")
        st.RaiseLevel(resource.StatusMayChange)
        st.AddDifference("state", "deactivating", "active", "")
        return true
    case "unknown":
        st.AddMessage("unit was in an unknown state, will attempt to start")
        st.RaiseLevel(resource.StatusMayChange)
        st.AddDifference("state", "unknown", "active", "")
        return true
    }
    return true
}

func (r *Resource) shouldStop(u *Unit, st *resource.Status) bool {
    switch u.ActiveState {
    case "active":
        st.AddDifference("state", "active", "inactive", "")
        st.RaiseLevel(resource.StatusWillChange)
        return true
    case "reloading":
        st.AddMessage("unit is reloading; will re-check status during apply")
        st.AddDifference("state", "reloading", "inactive", "")
        st.RaiseLevel(resource.StatusMayChange)
        return true
    case "inactive":
        st.AddMessage("unit is already inactive")
        return false
    case "failed":
        st.AddMessage("unit is not running because it has failed.  Will not restart")
        if reason, err := getFailedReason(u); err != nil {
            st.AddMessage(fmt.Sprintf("cannot determine root cause of failure: %v", err))
        } else {
            st.AddMessage(fmt.Sprintf("unit previously failed, the message was: %s", reason))
        }
        return false
    case "activating":
        st.AddDifference("state", "active", "inactive", "")
        st.RaiseLevel(resource.StatusMayChange)
        return true
    case "deactivating":
        st.AddMessage("unit is deactivating.  Will re-check status during apply")
        st.RaiseLevel(resource.StatusMayChange)
        return true
    case "unknown":
        st.AddMessage("unit was in an unknown state, will attempt to stop")
        st.RaiseLevel(resource.StatusMayChange)
        st.AddDifference("state", "unknown", "inactive", "")
        return true
    }
    return true
}

func getFailedReason(u *Unit) (string, error) {
    err := errors.New("unable to determine cause of failure: no properties available")
    var reason string
    switch u.Type {
    case UnitTypeService:
        if u.ServiceProperties == nil {
            return "", err
        }
        reason = u.ServiceProperties.Result
    case UnitTypeSocket:
        if u.SocketProperties == nil {
            return "", err
        }
        reason = u.SocketProperties.Result
    case UnitTypeMount:
        if u.MountProperties == nil {
            return "", err
        }
        reason = u.MountProperties.Result
    case UnitTypeAutoMount:
        if u.AutomountProperties == nil {
            return "", err
        }
        reason = u.AutomountProperties.Result
    case UnitTypeSwap:
        if u.SwapProperties == nil {
            return "", err
        }
        reason = u.SwapProperties.Result
    case UnitTypeTimer:
        if u.TimerProperties == nil {
            return "", err
        }
        reason = u.TimerProperties.Result
    }
    switch reason {
    case "success":
        return "the unit was activated successfully", nil
    case "resources":
        return "not enough resources available to create the process", nil
    case "timeout":
        return "a timeout occurred while starting the unit", nil
    case "exit-code":
        return "unit exited with a non-zero exit code", nil
    case "signal":
        return "unit exited due to an unhandled signal", nil
    case "core-dump":
        return "unit exited and dumped core", nil
    case "watchdog":
        return "watchdog terminated the service due to slow or missing responses", nil
    case "start-limit":
        return "process has been restarted too many times", nil
    case "service-failed-permanent":
        return "continual failure of this socket", nil
    }
    return "unknown reason", nil
}