cli, tools, util, modules: Add a grow util and module
This builds in some functionality for growing the filesystem for new machines. It also gets wrapped with an mcl module for ease of use.
This commit is contained in:
@@ -125,6 +125,8 @@ type Args struct {
|
||||
|
||||
DocsCmd *DocsGenerateArgs `arg:"subcommand:docs" help:"generate documentation"`
|
||||
|
||||
ToolsCmd *ToolsArgs `arg:"subcommand:tools" help:"collection of useful tools"`
|
||||
|
||||
// This never runs, it gets preempted in the real main() function.
|
||||
// XXX: Can we do it nicely with the new arg parser? can it ignore all args?
|
||||
EtcdCmd *EtcdArgs `arg:"subcommand:etcd" help:"run standalone etcd"`
|
||||
@@ -173,6 +175,10 @@ func (obj *Args) Run(ctx context.Context, data *cliUtil.Data) (bool, error) {
|
||||
return cmd.Run(ctx, data)
|
||||
}
|
||||
|
||||
if cmd := obj.ToolsCmd; cmd != nil {
|
||||
return cmd.Run(ctx, data)
|
||||
}
|
||||
|
||||
// NOTE: we could return true, fmt.Errorf("...") if more than one did
|
||||
return false, nil // nobody activated
|
||||
}
|
||||
|
||||
150
cli/tools.go
Normal file
150
cli/tools.go
Normal file
@@ -0,0 +1,150 @@
|
||||
// Mgmt
|
||||
// Copyright (C) James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
//
|
||||
// Additional permission under GNU GPL version 3 section 7
|
||||
//
|
||||
// If you modify this program, or any covered work, by linking or combining it
|
||||
// with embedded mcl code and modules (and that the embedded mcl code and
|
||||
// modules which link with this program, contain a copy of their source code in
|
||||
// the authoritative form) containing parts covered by the terms of any other
|
||||
// license, the licensors of this program grant you additional permission to
|
||||
// convey the resulting work. Furthermore, the licensors of this program grant
|
||||
// the original author, James Shubin, additional permission to update this
|
||||
// additional permission if he deems it necessary to achieve the goals of this
|
||||
// additional permission.
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
cliUtil "github.com/purpleidea/mgmt/cli/util"
|
||||
"github.com/purpleidea/mgmt/tools"
|
||||
)
|
||||
|
||||
// ToolsArgs is the CLI parsing structure and type of the parsed result. This
|
||||
// particular one contains all the common flags for the `tools` subcommand.
|
||||
type ToolsArgs struct {
|
||||
tools.Config // embedded config (can't be a pointer) https://github.com/alexflint/go-arg/issues/240
|
||||
|
||||
ToolsGrow *cliUtil.ToolsGrowArgs `arg:"subcommand:grow" help:"tools for growing storage"`
|
||||
}
|
||||
|
||||
// Run executes the correct subcommand. It errors if there's ever an error. It
|
||||
// returns true if we did activate one of the subcommands. It returns false if
|
||||
// we did not. This information is used so that the top-level parser can return
|
||||
// usage or help information if no subcommand activates. This particular Run is
|
||||
// the run for the main `tools` subcommand. The tools command provides some
|
||||
// functionality which can be helpful with provisioning and config management.
|
||||
func (obj *ToolsArgs) Run(ctx context.Context, data *cliUtil.Data) (bool, error) {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
var name string
|
||||
var args interface{}
|
||||
if cmd := obj.ToolsGrow; cmd != nil {
|
||||
name = cliUtil.LookupSubcommand(obj, cmd) // "grow"
|
||||
args = cmd
|
||||
}
|
||||
_ = name
|
||||
|
||||
Logf := func(format string, v ...interface{}) {
|
||||
// Don't block this globally...
|
||||
//if !data.Flags.Debug {
|
||||
// return
|
||||
//}
|
||||
data.Flags.Logf("main: "+format, v...)
|
||||
}
|
||||
|
||||
var api tools.API
|
||||
|
||||
if cmd := obj.ToolsGrow; cmd != nil {
|
||||
api = &tools.Grow{
|
||||
ToolsGrowArgs: args.(*cliUtil.ToolsGrowArgs),
|
||||
Config: obj.Config,
|
||||
Program: data.Program,
|
||||
Version: data.Version,
|
||||
Debug: data.Flags.Debug,
|
||||
Logf: Logf,
|
||||
}
|
||||
}
|
||||
|
||||
if api == nil {
|
||||
return false, nil // nothing found (display help!)
|
||||
}
|
||||
|
||||
// We don't use these for the tools command in normal operation.
|
||||
if data.Flags.Debug {
|
||||
cliUtil.Hello(data.Program, data.Version, data.Flags) // say hello!
|
||||
defer Logf("goodbye!")
|
||||
}
|
||||
|
||||
// install the exit signal handler
|
||||
wg := &sync.WaitGroup{}
|
||||
defer wg.Wait()
|
||||
exit := make(chan struct{})
|
||||
defer close(exit)
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer cancel()
|
||||
defer wg.Done()
|
||||
// must have buffer for max number of signals
|
||||
signals := make(chan os.Signal, 3+1) // 3 * ^C + 1 * SIGTERM
|
||||
signal.Notify(signals, os.Interrupt) // catch ^C
|
||||
//signal.Notify(signals, os.Kill) // catch signals
|
||||
signal.Notify(signals, syscall.SIGTERM)
|
||||
var count uint8
|
||||
for {
|
||||
select {
|
||||
case sig := <-signals: // any signal will do
|
||||
if sig != os.Interrupt {
|
||||
data.Flags.Logf("interrupted by signal")
|
||||
return
|
||||
}
|
||||
|
||||
switch count {
|
||||
case 0:
|
||||
data.Flags.Logf("interrupted by ^C")
|
||||
cancel()
|
||||
case 1:
|
||||
data.Flags.Logf("interrupted by ^C (fast pause)")
|
||||
cancel()
|
||||
case 2:
|
||||
data.Flags.Logf("interrupted by ^C (hard interrupt)")
|
||||
cancel()
|
||||
}
|
||||
count++
|
||||
|
||||
case <-exit:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if err := api.Main(ctx); err != nil {
|
||||
if data.Flags.Debug {
|
||||
data.Flags.Logf("main: %+v", err)
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
@@ -204,3 +204,11 @@ type DocsGenerateArgs struct {
|
||||
NoResources bool `arg:"--no-resources" help:"skip resource doc generation"`
|
||||
NoFunctions bool `arg:"--no-functions" help:"skip function doc generation"`
|
||||
}
|
||||
|
||||
// ToolsGrowArgs is the util tool CLI parsing structure and type of the parsed
|
||||
// result.
|
||||
type ToolsGrowArgs struct {
|
||||
Mount string `arg:"--mount,required" help:"root mount point to start with"`
|
||||
Exec bool `arg:"--exec" help:"actually run these commands"`
|
||||
Done string `arg:"--done" help:"create this file when done, skip if it exists"`
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
# additional permission if he deems it necessary to achieve the goals of this
|
||||
# additional permission.
|
||||
|
||||
import "deploy"
|
||||
import "fmt"
|
||||
import "golang"
|
||||
import "golang/strings" as golang_strings
|
||||
@@ -403,3 +404,34 @@ class copr_enable($name) {
|
||||
user => "root",
|
||||
}
|
||||
}
|
||||
|
||||
# For some reason, newly provisioned Fedora machines, don't have their root
|
||||
# partitions consuming 100% of the available space. Fix that. Make sure that
|
||||
# this runs on firstboot and *before* we change any LUKS password, as this only
|
||||
# works with the empty password.
|
||||
class grow($mount) {
|
||||
pkg "cloud-utils-growpart" {
|
||||
state => "installed",
|
||||
}
|
||||
|
||||
$done = "/root/.mgmt_grow" # must be absolute
|
||||
|
||||
# The xfs_growfs, resize2fs, cryptsetup, and findmnt tools should
|
||||
# already be installed on any resonable Linux system by default.
|
||||
exec "mgmt grow ${mount}" {
|
||||
cmd => deploy.binary_path(), # this is mgmt
|
||||
args => [
|
||||
"tools", # tools is a subcommand of mgmt
|
||||
"grow",
|
||||
"--mount",
|
||||
"${mount}", # typically root, eg: /
|
||||
"--exec", # !noop
|
||||
"--done",
|
||||
"${done}",
|
||||
],
|
||||
creates => $done, # TODO: is there something better?
|
||||
user => "root", # need root to be able to run normally
|
||||
|
||||
Depend => Pkg["cloud-utils-growpart"],
|
||||
}
|
||||
}
|
||||
|
||||
68
tools/grow.go
Normal file
68
tools/grow.go
Normal file
@@ -0,0 +1,68 @@
|
||||
// Mgmt
|
||||
// Copyright (C) James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
//
|
||||
// Additional permission under GNU GPL version 3 section 7
|
||||
//
|
||||
// If you modify this program, or any covered work, by linking or combining it
|
||||
// with embedded mcl code and modules (and that the embedded mcl code and
|
||||
// modules which link with this program, contain a copy of their source code in
|
||||
// the authoritative form) containing parts covered by the terms of any other
|
||||
// license, the licensors of this program grant you additional permission to
|
||||
// convey the resulting work. Furthermore, the licensors of this program grant
|
||||
// the original author, James Shubin, additional permission to update this
|
||||
// additional permission if he deems it necessary to achieve the goals of this
|
||||
// additional permission.
|
||||
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
cliUtil "github.com/purpleidea/mgmt/cli/util"
|
||||
growUtil "github.com/purpleidea/mgmt/util/grow"
|
||||
)
|
||||
|
||||
// Grow is the standalone entry program for the grow tools component.
|
||||
type Grow struct {
|
||||
*cliUtil.ToolsGrowArgs // embedded config
|
||||
Config // embedded Config
|
||||
|
||||
// Program is the name of this program, usually set at compile time.
|
||||
Program string
|
||||
|
||||
// Version is the version of this program, usually set at compile time.
|
||||
Version string
|
||||
|
||||
// Debug represents if we're running in debug mode or not.
|
||||
Debug bool
|
||||
|
||||
// Logf is a logger which should be used.
|
||||
Logf func(format string, v ...interface{})
|
||||
}
|
||||
|
||||
// Main runs everything for this tools item.
|
||||
func (obj *Grow) Main(ctx context.Context) error {
|
||||
|
||||
g := &growUtil.Grow{
|
||||
Noop: !obj.Exec,
|
||||
Done: obj.Done,
|
||||
Debug: obj.Debug,
|
||||
Logf: obj.Logf,
|
||||
}
|
||||
|
||||
return g.Run(ctx, obj.Mount)
|
||||
}
|
||||
48
tools/tools.go
Normal file
48
tools/tools.go
Normal file
@@ -0,0 +1,48 @@
|
||||
// Mgmt
|
||||
// Copyright (C) James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
//
|
||||
// Additional permission under GNU GPL version 3 section 7
|
||||
//
|
||||
// If you modify this program, or any covered work, by linking or combining it
|
||||
// with embedded mcl code and modules (and that the embedded mcl code and
|
||||
// modules which link with this program, contain a copy of their source code in
|
||||
// the authoritative form) containing parts covered by the terms of any other
|
||||
// license, the licensors of this program grant you additional permission to
|
||||
// convey the resulting work. Furthermore, the licensors of this program grant
|
||||
// the original author, James Shubin, additional permission to update this
|
||||
// additional permission if he deems it necessary to achieve the goals of this
|
||||
// additional permission.
|
||||
|
||||
// Package tools provides some simple standalone tools to help with things.
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// API is the simple interface we expect for any tools items.
|
||||
type API interface {
|
||||
// Main runs everything for this tools item.
|
||||
Main(context.Context) error
|
||||
}
|
||||
|
||||
// Config is a struct of all the configuration values which are shared by all of
|
||||
// the tools utilities. By including this as a separate struct, it can be used
|
||||
// as part of the API if we want.
|
||||
type Config struct {
|
||||
//Foo string `arg:"--foo,env:MGMT_TOOLS_FOO" help:"Foo..."` // TODO: foo
|
||||
}
|
||||
408
util/grow/grow.go
Normal file
408
util/grow/grow.go
Normal file
@@ -0,0 +1,408 @@
|
||||
// Mgmt
|
||||
// Copyright (C) James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
//
|
||||
// Additional permission under GNU GPL version 3 section 7
|
||||
//
|
||||
// If you modify this program, or any covered work, by linking or combining it
|
||||
// with embedded mcl code and modules (and that the embedded mcl code and
|
||||
// modules which link with this program, contain a copy of their source code in
|
||||
// the authoritative form) containing parts covered by the terms of any other
|
||||
// license, the licensors of this program grant you additional permission to
|
||||
// convey the resulting work. Furthermore, the licensors of this program grant
|
||||
// the original author, James Shubin, additional permission to update this
|
||||
// additional permission if he deems it necessary to achieve the goals of this
|
||||
// additional permission.
|
||||
|
||||
// Package grow is a utility for growing storage.
|
||||
package grow
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/purpleidea/mgmt/util"
|
||||
)
|
||||
|
||||
const (
|
||||
// DevDir is where linux system devices are found.
|
||||
DevDir = "/dev/"
|
||||
)
|
||||
|
||||
// Grow is a utility that grows the underlying partition, luks device (if
|
||||
// encrypted) and then finally the partition. It makes many assumptions about
|
||||
// the luks device being encrypted with an empty password, and that nobody uses
|
||||
// lvm anymore. This utility is useful when provisioning new machines which
|
||||
// don't get their maximum disk utilization by default. (All the Fedora machines
|
||||
// whether physical or virtual seem to have this problem.)
|
||||
//
|
||||
// This whole utility should only be run by the Run entrypoint if you want to
|
||||
// receive the benefit of the various options, such as Noop and Done. If you
|
||||
// bypass those then you're on your own.
|
||||
//
|
||||
// Patches are welcome to expand the functionality of this software, however it
|
||||
// was intended as the minimally viable piece of reliable code to solve the
|
||||
// specific grow problem commonly seen, without adding unused features.
|
||||
type Grow struct {
|
||||
// Noop is set to true to prevent any actual grow operation.
|
||||
Noop bool
|
||||
|
||||
// Done specifies a file path to create once the operations complete
|
||||
// successfully. If this file exists, these operations won't run.
|
||||
Done string
|
||||
|
||||
// Debug represents if we're running in debug mode or not.
|
||||
Debug bool
|
||||
|
||||
// Logf is a logger which should be used.
|
||||
Logf func(format string, v ...interface{})
|
||||
}
|
||||
|
||||
// Run the whole sequence. This is probably the only function you want to run.
|
||||
func (obj *Grow) Run(ctx context.Context, mount string) (reterr error) {
|
||||
if obj.Done != "" {
|
||||
if !strings.HasPrefix(obj.Done, "/") {
|
||||
return fmt.Errorf("done must be absolute")
|
||||
}
|
||||
if strings.HasSuffix(obj.Done, "/") {
|
||||
return fmt.Errorf("done must be a file")
|
||||
}
|
||||
|
||||
_, err := os.Stat(obj.Done)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err // permission err
|
||||
}
|
||||
if err == nil { // exists!
|
||||
obj.Logf("done mode")
|
||||
return nil
|
||||
}
|
||||
defer func() {
|
||||
// Store error in case we have a problem here.
|
||||
reterr = os.WriteFile(obj.Done, []byte(fmt.Sprintf("%s\n", mount)), 0600)
|
||||
}()
|
||||
}
|
||||
|
||||
fsList, err := obj.RunFindMnt(ctx, mount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(fsList) == 0 {
|
||||
return fmt.Errorf("no mount found")
|
||||
}
|
||||
if len(fsList) != 1 {
|
||||
return fmt.Errorf("ambiguous mount found, expected one")
|
||||
}
|
||||
|
||||
if target := fsList[0].Target; target != mount {
|
||||
return fmt.Errorf("unexpected target: %s", target)
|
||||
}
|
||||
|
||||
fsType := fsList[0].FsType
|
||||
|
||||
if err := obj.IsValidFsType(fsType); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If we're using LUKS, this will be something like:
|
||||
// /dev/mapper/luks-<UUID> but keep in mind that `cryptsetup isLuks` on
|
||||
// that device will be FALSE because that's the unencrypted
|
||||
// device-mapper device, not the actual encrypted block device.
|
||||
source := fsList[0].Source
|
||||
if source == "" {
|
||||
return fmt.Errorf("empty source")
|
||||
}
|
||||
if source == "/" {
|
||||
return fmt.Errorf("invalid source")
|
||||
}
|
||||
|
||||
// Source is probably a device mapper device. Let's resolve it.
|
||||
// /usr/bin/realpath --relative-to /dev /dev/mapper/luks-<UUID>
|
||||
//
|
||||
// It might just be something like /dev/sda3 which means this is a noop.
|
||||
if !strings.HasPrefix(source, "/") {
|
||||
return fmt.Errorf("source is not absolute")
|
||||
}
|
||||
dev, err := filepath.EvalSymlinks(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// This should output something like /dev/dm-0 or maybe not if it's not
|
||||
// using LUKS.
|
||||
|
||||
if !strings.HasPrefix(dev, DevDir) {
|
||||
return fmt.Errorf("unexpected resolved link of: %s", dev)
|
||||
}
|
||||
|
||||
// This is now dm-0 if we're using luks/device mapper or maybe sda3.
|
||||
|
||||
luks := "" // assume false
|
||||
if err := obj.IsDeviceMapper(dev); err == nil {
|
||||
|
||||
// XXX: Is there a better way to resolve device-mapper? I know I
|
||||
// can cryptsetup status directly on the /dev/mapper/luks-<UUID>
|
||||
// but I'd like a more flexible approach where I do it stepwise.
|
||||
dm := strings.TrimPrefix(dev, DevDir)
|
||||
s := fmt.Sprintf("/sys/block/%s/slaves/", dm)
|
||||
dirs, err := os.ReadDir(s) // ([]DirEntry, error)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if l := len(dirs); l != 1 {
|
||||
return fmt.Errorf("unexpected number of device-mapper slaves: %d", l)
|
||||
}
|
||||
|
||||
//luks = source // this would work
|
||||
luks = dev // this feels more accurate
|
||||
|
||||
// This is the underlying device. You could also get this from
|
||||
// cryptsetup status $dev or $source but since the cryptsetup
|
||||
// command seems to be a bit sloppy in what it expects, we just
|
||||
// look up the device mapper stuff in the kernel /sys/ dirs...
|
||||
dev = DevDir + dirs[0].Name() // eg: /dev/ + nvme0n1p3
|
||||
}
|
||||
|
||||
if obj.Debug {
|
||||
obj.Logf("source: %s", source)
|
||||
obj.Logf("luks: %s", luks)
|
||||
obj.Logf("dev: %s", dev)
|
||||
}
|
||||
|
||||
if obj.Noop {
|
||||
obj.Logf("noop mode")
|
||||
}
|
||||
|
||||
if err := obj.GrowPart(ctx, dev); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if luks != "" {
|
||||
if err := obj.GrowLUKS(ctx, luks); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := obj.GrowFs(ctx, source, fsType); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GrowPart runs the "growpart" command from the "cloud-utils-growpart" package.
|
||||
func (obj *Grow) GrowPart(ctx context.Context, part string) error {
|
||||
if part == "" {
|
||||
return fmt.Errorf("empty part")
|
||||
}
|
||||
|
||||
base, part, err := obj.GrowPartArgs(part)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if obj.Debug {
|
||||
obj.Logf("base: %s", base)
|
||||
obj.Logf("part: %s", part)
|
||||
}
|
||||
|
||||
cmd, err := exec.LookPath("growpart")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmdArgs := []string{base, part}
|
||||
obj.Logf("cmd: %s %s", cmd, strings.Join(cmdArgs, " "))
|
||||
if obj.Noop {
|
||||
return nil
|
||||
}
|
||||
return util.SimpleCmd(ctx, cmd, cmdArgs, obj.cmdOpts())
|
||||
}
|
||||
|
||||
// GrowLUKS grows the luks device that has an empty password.
|
||||
func (obj *Grow) GrowLUKS(ctx context.Context, luks string) error {
|
||||
if luks == "" {
|
||||
return fmt.Errorf("empty luks")
|
||||
}
|
||||
|
||||
// XXX: this hack is necessary because cryptsetup is dumb
|
||||
empty := "/tmp/empty"
|
||||
if !obj.Noop {
|
||||
if err := os.WriteFile(empty, []byte{}, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
cmd, err := exec.LookPath("cryptsetup")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmdArgs := []string{"resize", fmt.Sprintf("--key-file=%s", empty), luks}
|
||||
obj.Logf("cmd: %s %s", cmd, strings.Join(cmdArgs, " "))
|
||||
if obj.Noop {
|
||||
return nil
|
||||
}
|
||||
return util.SimpleCmd(ctx, cmd, cmdArgs, obj.cmdOpts())
|
||||
}
|
||||
|
||||
// GrowFs expands the filesystem while it's online.
|
||||
func (obj *Grow) GrowFs(ctx context.Context, dev, fsType string) error {
|
||||
if dev == "" {
|
||||
return fmt.Errorf("empty dev")
|
||||
}
|
||||
|
||||
if fsType == "ext4" {
|
||||
cmd, err := exec.LookPath("resize2fs")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmdArgs := []string{dev}
|
||||
obj.Logf("cmd: %s %s", cmd, strings.Join(cmdArgs, " "))
|
||||
if obj.Noop {
|
||||
return nil
|
||||
}
|
||||
return util.SimpleCmd(ctx, cmd, cmdArgs, obj.cmdOpts())
|
||||
}
|
||||
|
||||
if fsType == "xfs" {
|
||||
cmd, err := exec.LookPath("xfs_growfs")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmdArgs := []string{dev}
|
||||
obj.Logf("cmd: %s %s", cmd, strings.Join(cmdArgs, " "))
|
||||
if obj.Noop {
|
||||
return nil
|
||||
}
|
||||
return util.SimpleCmd(ctx, cmd, cmdArgs, obj.cmdOpts())
|
||||
}
|
||||
|
||||
// XXX: btrfs filesystem resize max /path
|
||||
|
||||
return obj.IsValidFsType(fsType)
|
||||
}
|
||||
|
||||
// GrowPartArgs get the special args needed for the `growpart` command.
|
||||
func (obj *Grow) GrowPartArgs(dev string) (string, string, error) {
|
||||
if dev == "" {
|
||||
return "", "", fmt.Errorf("empty dev")
|
||||
}
|
||||
if !strings.HasPrefix(dev, DevDir) {
|
||||
return "", "", fmt.Errorf("missing prefix for dev in: %s", dev)
|
||||
}
|
||||
|
||||
// Simple parsing for /dev/sda3 and friends... Walk backwards until we
|
||||
// encounter a non-number, and if it's a weird nvme thing, remove the p.
|
||||
i := len(dev) - 1
|
||||
for i >= 0 && dev[i] >= '0' && dev[i] <= '9' { // is a number
|
||||
i--
|
||||
}
|
||||
if i == len(dev)-1 {
|
||||
return "", "", fmt.Errorf("no partition number found")
|
||||
}
|
||||
base := dev[:i+1]
|
||||
part := dev[i+1:]
|
||||
|
||||
// fancy parsing for /dev/nvme0n1p3 which has the "p".
|
||||
if strings.HasPrefix(dev, DevDir+"nvme") && dev[i] == 'p' {
|
||||
base = dev[:i]
|
||||
}
|
||||
if base == "" || part == "" {
|
||||
return "", "", fmt.Errorf("unexpected empty result")
|
||||
}
|
||||
|
||||
return base, part, nil
|
||||
}
|
||||
|
||||
// IsDeviceMapper runs a simple check to see if we're something like:
|
||||
// `/dev/dm-0`.
|
||||
func (obj *Grow) IsDeviceMapper(name string) error {
|
||||
if !strings.HasPrefix(name, "/dev/dm-") {
|
||||
return fmt.Errorf("missing device mapper prefix")
|
||||
}
|
||||
s := strings.TrimPrefix(name, "/dev/dm-")
|
||||
|
||||
if _, err := strconv.Atoi(s); err != nil {
|
||||
return fmt.Errorf("missing device mapper number")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsValidFsType are the types of filesystems we currently know how to grow.
|
||||
func (obj *Grow) IsValidFsType(fsType string) error {
|
||||
if fsType == "ext4" || fsType == "xfs" {
|
||||
return nil
|
||||
}
|
||||
// TODO: add btrfs support
|
||||
//if fsType == "btrfs" {
|
||||
// return nil
|
||||
//}
|
||||
|
||||
return fmt.Errorf("can't grow fstype: %s", fsType)
|
||||
}
|
||||
|
||||
// RunFindMnt runs the linux util `findmnt` command.
|
||||
func (obj *Grow) RunFindMnt(ctx context.Context, p string) ([]*MountInfo, error) {
|
||||
cmd, err := exec.LookPath("findmnt")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmdArgs := []string{"--json", "--all", "--mountpoint", p}
|
||||
b, err := util.SimpleCmdOut(ctx, cmd, cmdArgs, obj.cmdOpts())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var st FindMnt
|
||||
if err := json.Unmarshal(b, &st); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return st.Filesystems, nil
|
||||
}
|
||||
|
||||
// cmdOpts is just a helper to return the same struct repeatedly.
|
||||
func (obj *Grow) cmdOpts() *util.SimpleCmdOpts {
|
||||
logf := func(format string, v ...interface{}) {
|
||||
if !obj.Debug { // ignore the noisy "always on" log messages...
|
||||
return
|
||||
}
|
||||
obj.Logf(format, v...)
|
||||
}
|
||||
return &util.SimpleCmdOpts{
|
||||
Debug: obj.Debug,
|
||||
Logf: logf,
|
||||
}
|
||||
}
|
||||
|
||||
// FindMnt is the --json output of the `findmnt` command.
|
||||
type FindMnt struct {
|
||||
Filesystems []*MountInfo `json:"filesystems"`
|
||||
}
|
||||
|
||||
// MountInfo is the type of each entry in the FindMnt Filesystems field.
|
||||
type MountInfo struct {
|
||||
Target string `json:"target"`
|
||||
Source string `json:"source"`
|
||||
FsType string `json:"fstype"`
|
||||
Options string `json:"options"`
|
||||
}
|
||||
Reference in New Issue
Block a user