202 lines
7.5 KiB
Go
202 lines
7.5 KiB
Go
package main
|
|
|
|
// Taking over a hand-written fstab line (novox/hq ADR 0263 rule 10, to-be 52 §5).
|
|
//
|
|
// The line is the operator's, so it is taken over only on a person's word (`confirm`, with why) and only
|
|
// once the module's own unit pair for the same mount point exists and its automount is armed: the
|
|
// replacement works first, then the line goes. It is commented out, not deleted, with a dated marker; a
|
|
// password in it is redacted in the commented line, and the whole file as it was is kept beside it,
|
|
// readable by root only, because it holds that password.
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// AdoptedPrefix opens a line the module took over.
|
|
const AdoptedPrefix = "# adopted by the mesh (module mounts) on "
|
|
|
|
// A password's value runs to the next single comma or blank: mount.cifs writes a comma inside it as two.
|
|
var passwordValue = regexp.MustCompile(`(?i)\b(password2?|pass|passwd)=(?:,,+[^,\s]*|[^,\s])*`)
|
|
|
|
// userPassword is user=name%password, where mount.cifs takes the password after the percent sign.
|
|
var userPassword = regexp.MustCompile(`(?i)\b(user(?:name)?=[^,\s%]*)%(?:,,+[^,\s]*|[^,\s])*`)
|
|
|
|
// RedactLine replaces every credential's value in a raw fstab line.
|
|
func RedactLine(line string) string {
|
|
line = passwordValue.ReplaceAllString(line, "${1}="+Redacted)
|
|
return userPassword.ReplaceAllString(line, "${1}%"+Redacted)
|
|
}
|
|
|
|
// Adoption is what taking a line over does, or would do.
|
|
type Adoption struct {
|
|
Mountpoint string `json:"mountpoint"`
|
|
Line int `json:"line"`
|
|
Was string `json:"was"`
|
|
Becomes string `json:"becomes"`
|
|
Copy string `json:"copy,omitempty"`
|
|
Done bool `json:"done"`
|
|
Said string `json:"said"`
|
|
}
|
|
|
|
// PlanAdoption finds the one uncommented fstab line for a mount point and what it becomes. `was` is
|
|
// redacted; the new text keeps every other line byte for byte.
|
|
func PlanAdoption(fstab, mountpoint string, now time.Time) (string, Adoption, error) {
|
|
lines := strings.Split(fstab, "\n")
|
|
hit := -1
|
|
for i, raw := range lines {
|
|
t := strings.TrimSpace(raw)
|
|
if t == "" || strings.HasPrefix(t, "#") {
|
|
continue
|
|
}
|
|
f := strings.Fields(t)
|
|
if len(f) >= 2 && unescape(f[1]) == mountpoint {
|
|
if hit >= 0 {
|
|
return "", Adoption{}, fmt.Errorf("/etc/fstab has more than one line for %s (lines %d and %d); take it over by hand", mountpoint, hit+1, i+1)
|
|
}
|
|
hit = i
|
|
}
|
|
}
|
|
if hit < 0 {
|
|
return "", Adoption{}, fmt.Errorf("/etc/fstab has no line for %s", mountpoint)
|
|
}
|
|
becomes := AdoptedPrefix + now.Format("2006-01-02") + ": " + RedactLine(lines[hit])
|
|
a := Adoption{Mountpoint: mountpoint, Line: hit + 1, Was: RedactLine(lines[hit]), Becomes: becomes}
|
|
lines[hit] = becomes
|
|
return strings.Join(lines, "\n"), a, nil
|
|
}
|
|
|
|
// replacementArmed says whether the module's own pair for a mount point is written and its automount
|
|
// armed, with why not.
|
|
func (m *Machine) replacementArmed(ctx context.Context, mountpoint string) error {
|
|
base := EscapePath(mountpoint)
|
|
for _, unit := range []string{base + ".mount", base + ".automount"} {
|
|
if !IsOwnUnit(m, filepath.Join(UnitDir, unit)) {
|
|
return fmt.Errorf("the module has no unit pair for %s yet (%s is not its own): it takes a line over only once its replacement is in place", mountpoint, unit)
|
|
}
|
|
}
|
|
shown, err := m.Run(ctx, "systemctl", "show", "-p", "ActiveState", "--value", base+".automount")
|
|
if err != nil {
|
|
return fmt.Errorf("the service manager did not say whether %s.automount is armed: %v", base, err)
|
|
}
|
|
if strings.TrimSpace(shown) != "active" {
|
|
return fmt.Errorf("%s.automount is not armed (%s): the replacement does not work yet, so the line stays", base, strings.TrimSpace(shown))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// AdoptDryRun is what taking the line over would do, changing nothing.
|
|
func (m *Machine) AdoptDryRun(ctx context.Context, mountpoint string, now time.Time) (Adoption, error) {
|
|
if err := CheckMountpoint(mountpoint); err != nil {
|
|
return Adoption{}, err
|
|
}
|
|
fstab, err := m.read("/etc/fstab")
|
|
if err != nil {
|
|
return Adoption{}, errors.New(unreadable("/etc/fstab", err))
|
|
}
|
|
_, a, err := PlanAdoption(fstab, mountpoint, now)
|
|
if err != nil {
|
|
return Adoption{}, err
|
|
}
|
|
if err := m.replacementArmed(ctx, mountpoint); err != nil {
|
|
a.Said = "Would be refused now: " + err.Error()
|
|
return a, nil
|
|
}
|
|
a.Said = fmt.Sprintf("A dry run: line %d of /etc/fstab would be commented out and a copy of the file kept beside it. Nothing changed; call again with confirm and why.", a.Line)
|
|
return a, nil
|
|
}
|
|
|
|
// AdoptAsRoot takes the line over: the process's half, run as root through sudo by the tool. It checks
|
|
// everything again, since the tool's look was the operator account's.
|
|
func (m *Machine) AdoptAsRoot(ctx context.Context, mountpoint string, now time.Time) (Adoption, error) {
|
|
if err := CheckMountpoint(mountpoint); err != nil {
|
|
return Adoption{}, err
|
|
}
|
|
if err := m.replacementArmed(ctx, mountpoint); err != nil {
|
|
return Adoption{}, err
|
|
}
|
|
full := filepath.Join(m.Root, "etc/fstab")
|
|
raw, err := os.ReadFile(full)
|
|
if err != nil {
|
|
return Adoption{}, errors.New(unreadable("/etc/fstab", err))
|
|
}
|
|
info, err := os.Stat(full)
|
|
if err != nil {
|
|
return Adoption{}, err
|
|
}
|
|
next, a, err := PlanAdoption(string(raw), mountpoint, now)
|
|
if err != nil {
|
|
return Adoption{}, err
|
|
}
|
|
a.Copy = "/etc/fstab.mesh-adopted-" + now.Format("20060102-150405")
|
|
if err := os.WriteFile(filepath.Join(m.Root, a.Copy), raw, 0o600); err != nil {
|
|
return Adoption{}, fmt.Errorf("the copy of /etc/fstab was not kept, so nothing changed: %w", err)
|
|
}
|
|
tmp := full + ".mesh-new"
|
|
if err := os.WriteFile(tmp, []byte(next), info.Mode().Perm()); err != nil {
|
|
return Adoption{}, fmt.Errorf("/etc/fstab was not written, so nothing changed: %w", err)
|
|
}
|
|
if err := os.Rename(tmp, full); err != nil {
|
|
os.Remove(tmp)
|
|
return Adoption{}, fmt.Errorf("/etc/fstab was not written, so nothing changed: %w", err)
|
|
}
|
|
a.Done = true
|
|
a.Said = fmt.Sprintf("Line %d of /etc/fstab, for %s, is commented out; the file as it was is kept at %s (root only, as it may hold a password).", a.Line, mountpoint, a.Copy)
|
|
if _, err := m.Run(ctx, "systemctl", "daemon-reload"); err != nil {
|
|
a.Said += " The service manager did not reload: " + err.Error()
|
|
}
|
|
return a, nil
|
|
}
|
|
|
|
// Adopt is the verb: a dry run without confirm; with it, the root half through sudo.
|
|
func (m *Machine) Adopt(ctx context.Context, mountpoint string, confirm bool, why string, now time.Time) (Adoption, error) {
|
|
if !confirm {
|
|
return m.AdoptDryRun(ctx, mountpoint, now)
|
|
}
|
|
if strings.TrimSpace(why) == "" {
|
|
return Adoption{}, errors.New("say why the line is taken over (`why`): it is the operator's line, and the act is recorded")
|
|
}
|
|
self, err := os.Executable()
|
|
if err != nil {
|
|
return Adoption{}, err
|
|
}
|
|
out, err := m.Escalate(ctx, self, "adopt", mountpoint)
|
|
var a Adoption
|
|
if jerr := json.Unmarshal([]byte(strings.TrimSpace(out)), &a); jerr != nil {
|
|
if err != nil {
|
|
return Adoption{}, err
|
|
}
|
|
return Adoption{}, fmt.Errorf("the root half answered no adoption: %v", jerr)
|
|
}
|
|
if !a.Done && a.Said != "" {
|
|
return a, errors.New(a.Said)
|
|
}
|
|
a.Said += " Why: " + strings.TrimSpace(why) + "."
|
|
return a, nil
|
|
}
|
|
|
|
// RunAdopt is `mounts adopt <mountpoint>`, as root: the answer on stdout as JSON, also when refused.
|
|
func RunAdopt(args []string) int {
|
|
if len(args) != 1 {
|
|
fmt.Fprintln(os.Stderr, "usage: mounts adopt <mountpoint>")
|
|
return 2
|
|
}
|
|
a, err := Here().AdoptAsRoot(context.Background(), args[0], time.Now())
|
|
if err != nil {
|
|
a = Adoption{Mountpoint: args[0], Said: err.Error()}
|
|
}
|
|
raw, _ := json.Marshal(a)
|
|
fmt.Println(string(raw))
|
|
if err != nil {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|