cli: Add setup and firstboot commands
This adds two new top-level commands: setup and firstboot. Firstboot is pure-golang implementation of a service that runs some commands once when a system first boots. You need to install this service, and put the scripts to run in a special directory. This is inspired by the virt-builder --firstboot mechanism. Setup is a general purpose command that makes it easy to setup certain facilities on a new machine. These include the mgmt package dependencies it might need, a service to run it from, and the necessary service to use the mgmt firstboot service as well. All of this has been built to facilitate handoff between provisioning a new machine and running configuration management on it.
This commit is contained in:
12
cli/cli.go
12
cli/cli.go
@@ -119,6 +119,10 @@ type Args struct {
|
|||||||
|
|
||||||
DeployCmd *DeployArgs `arg:"subcommand:deploy" help:"deploy code into a cluster"`
|
DeployCmd *DeployArgs `arg:"subcommand:deploy" help:"deploy code into a cluster"`
|
||||||
|
|
||||||
|
SetupCmd *SetupArgs `arg:"subcommand:setup" help:"setup some bootstrapping tasks"`
|
||||||
|
|
||||||
|
FirstbootCmd *FirstbootArgs `arg:"subcommand:firstboot" help:"run some tasks on first boot"`
|
||||||
|
|
||||||
// This never runs, it gets preempted in the real main() function.
|
// 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?
|
// 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"`
|
EtcdCmd *EtcdArgs `arg:"subcommand:etcd" help:"run standalone etcd"`
|
||||||
@@ -155,6 +159,14 @@ func (obj *Args) Run(ctx context.Context, data *cliUtil.Data) (bool, error) {
|
|||||||
return cmd.Run(ctx, data)
|
return cmd.Run(ctx, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cmd := obj.SetupCmd; cmd != nil {
|
||||||
|
return cmd.Run(ctx, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd := obj.FirstbootCmd; cmd != nil {
|
||||||
|
return cmd.Run(ctx, data)
|
||||||
|
}
|
||||||
|
|
||||||
// NOTE: we could return true, fmt.Errorf("...") if more than one did
|
// NOTE: we could return true, fmt.Errorf("...") if more than one did
|
||||||
return false, nil // nobody activated
|
return false, nil // nobody activated
|
||||||
}
|
}
|
||||||
|
|||||||
151
cli/firstboot.go
Normal file
151
cli/firstboot.go
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2024+ 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/firstboot"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FirstbootArgs is the CLI parsing structure and type of the parsed result.
|
||||||
|
// This particular one contains all the common flags for the `firstboot`
|
||||||
|
// subcommand.
|
||||||
|
type FirstbootArgs struct {
|
||||||
|
firstboot.Config // embedded config (can't be a pointer) https://github.com/alexflint/go-arg/issues/240
|
||||||
|
|
||||||
|
FirstbootStart *cliUtil.FirstbootStartArgs `arg:"subcommand:start" help:"start firstboot service"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 `firstboot` subcommand. The firstboot command as a
|
||||||
|
// service that lets you run commands once on the first boot of a system.
|
||||||
|
func (obj *FirstbootArgs) 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.FirstbootStart; cmd != nil {
|
||||||
|
name = cliUtil.LookupSubcommand(obj, cmd) // "pkg"
|
||||||
|
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 firstboot.API
|
||||||
|
|
||||||
|
if cmd := obj.FirstbootStart; cmd != nil {
|
||||||
|
api = &firstboot.Start{
|
||||||
|
FirstbootStartArgs: args.(*cliUtil.FirstbootStartArgs),
|
||||||
|
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 setup 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
|
||||||
|
}
|
||||||
180
cli/setup.go
Normal file
180
cli/setup.go
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2024+ 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/setup"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetupArgs is the CLI parsing structure and type of the parsed result. This
|
||||||
|
// particular one contains all the common flags for the `setup` subcommand.
|
||||||
|
type SetupArgs struct {
|
||||||
|
setup.Config // embedded config (can't be a pointer) https://github.com/alexflint/go-arg/issues/240
|
||||||
|
|
||||||
|
SetupPkg *cliUtil.SetupPkgArgs `arg:"subcommand:pkg" help:"setup packages"`
|
||||||
|
SetupSvc *cliUtil.SetupSvcArgs `arg:"subcommand:svc" help:"setup services"`
|
||||||
|
SetupFirstboot *cliUtil.SetupFirstbootArgs `arg:"subcommand:firstboot" help:"setup firstboot"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 `setup` subcommand. The setup command does some
|
||||||
|
// bootstrap work to help get things going.
|
||||||
|
func (obj *SetupArgs) 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.SetupPkg; cmd != nil {
|
||||||
|
name = cliUtil.LookupSubcommand(obj, cmd) // "pkg"
|
||||||
|
args = cmd
|
||||||
|
}
|
||||||
|
if cmd := obj.SetupSvc; cmd != nil {
|
||||||
|
name = cliUtil.LookupSubcommand(obj, cmd) // "svc"
|
||||||
|
args = cmd
|
||||||
|
}
|
||||||
|
if cmd := obj.SetupFirstboot; cmd != nil {
|
||||||
|
name = cliUtil.LookupSubcommand(obj, cmd) // "firstboot"
|
||||||
|
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 setup.API
|
||||||
|
|
||||||
|
if cmd := obj.SetupPkg; cmd != nil {
|
||||||
|
api = &setup.Pkg{
|
||||||
|
SetupPkgArgs: args.(*cliUtil.SetupPkgArgs),
|
||||||
|
Config: obj.Config,
|
||||||
|
Program: data.Program,
|
||||||
|
Version: data.Version,
|
||||||
|
Debug: data.Flags.Debug,
|
||||||
|
Logf: Logf,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cmd := obj.SetupSvc; cmd != nil {
|
||||||
|
api = &setup.Svc{
|
||||||
|
SetupSvcArgs: args.(*cliUtil.SetupSvcArgs),
|
||||||
|
Config: obj.Config,
|
||||||
|
Program: data.Program,
|
||||||
|
Version: data.Version,
|
||||||
|
Debug: data.Flags.Debug,
|
||||||
|
Logf: Logf,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cmd := obj.SetupFirstboot; cmd != nil {
|
||||||
|
api = &setup.Firstboot{
|
||||||
|
SetupFirstbootArgs: args.(*cliUtil.SetupFirstbootArgs),
|
||||||
|
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 setup 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
|
||||||
|
}
|
||||||
@@ -149,3 +149,41 @@ type LangPuppetArgs struct {
|
|||||||
|
|
||||||
// end LangArgs
|
// end LangArgs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetupPkgArgs is the setup service CLI parsing structure and type of the
|
||||||
|
// parsed result.
|
||||||
|
type SetupPkgArgs struct {
|
||||||
|
Distro string `arg:"--distro" help:"build for this distro"`
|
||||||
|
Sudo bool `arg:"--sudo" help:"include sudo in the command"`
|
||||||
|
Exec bool `arg:"--exec" help:"actually run these commands"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupSvcArgs is the setup service CLI parsing structure and type of the
|
||||||
|
// parsed result.
|
||||||
|
type SetupSvcArgs struct {
|
||||||
|
BinaryPath string `arg:"--binary-path" help:"path to the binary"`
|
||||||
|
Install bool `arg:"--install" help:"install the systemd mgmt service"`
|
||||||
|
Start bool `arg:"--start" help:"start the mgmt service"`
|
||||||
|
Enable bool `arg:"--enable" help:"enable the mgmt service"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupFirstbootArgs is the setup service CLI parsing structure and type of the
|
||||||
|
// parsed result.
|
||||||
|
type SetupFirstbootArgs struct {
|
||||||
|
BinaryPath string `arg:"--binary-path" help:"path to the binary"`
|
||||||
|
Mkdir bool `arg:"--mkdir" help:"make the necessary firstboot dirs"`
|
||||||
|
Install bool `arg:"--install" help:"install the systemd firstboot service"`
|
||||||
|
Start bool `arg:"--start" help:"start the firstboot service (typically not used)"`
|
||||||
|
Enable bool `arg:"--enable" help:"enable the firstboot service"`
|
||||||
|
|
||||||
|
FirstbootStartArgs // Include these options if we want to specify them.
|
||||||
|
}
|
||||||
|
|
||||||
|
// FirstbootStartArgs is the firstboot service CLI parsing structure and type of
|
||||||
|
// the parsed result.
|
||||||
|
type FirstbootStartArgs struct {
|
||||||
|
LockFilePath string `arg:"--lock-file-path" help:"path to the lock file"`
|
||||||
|
ScriptsDir string `arg:"--scripts-dir" help:"path to the scripts dir"`
|
||||||
|
DoneDir string `arg:"--done-dir" help:"dir to move done scripts to"`
|
||||||
|
LoggingDir string `arg:"--logging-dir" help:"directory to store logs in"`
|
||||||
|
}
|
||||||
|
|||||||
53
firstboot/firstboot.go
Normal file
53
firstboot/firstboot.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2024+ 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 firstboot provides a service that runs scripts once on first boot.
|
||||||
|
// You can easily test this with:
|
||||||
|
//
|
||||||
|
// mkdir /tmp/mgmt-firstboot/ ; echo 'echo hello' > /tmp/mgmt-firstboot/foo.sh
|
||||||
|
// ./mgmt firstboot start --lock-file-path=/tmp/flock.lock \
|
||||||
|
// --scripts-dir=/tmp/mgmt-firstboot/ --logging-dir=/tmp/
|
||||||
|
package firstboot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// API is the simple interface we expect for any setup items.
|
||||||
|
type API interface {
|
||||||
|
// Main runs everything for this setup item.
|
||||||
|
Main(context.Context) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config is a struct of all the configuration values which are shared by all of
|
||||||
|
// the setup 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_FIRSTBOOT_FOO" help:"Foo..."` // TODO: foo
|
||||||
|
}
|
||||||
290
firstboot/start.go
Normal file
290
firstboot/start.go
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2024+ 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 firstboot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
cliUtil "github.com/purpleidea/mgmt/cli/util"
|
||||||
|
"github.com/purpleidea/mgmt/util"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// LockFilePath is where we store a lock file to prevent more than one
|
||||||
|
// copy of this service running at the same time.
|
||||||
|
// TODO: Is there a better place to put this?
|
||||||
|
LockFilePath string = "/var/lib/mgmt/firstboot.lock"
|
||||||
|
|
||||||
|
// ScriptsDir is the directory where firstboot scripts can be found. You
|
||||||
|
// can put binaries in here too. Contents must be executable.
|
||||||
|
ScriptsDir string = "/var/lib/mgmt/firstboot/"
|
||||||
|
|
||||||
|
// StyleSuffix is what we append to executables to specify a style file.
|
||||||
|
StyleSuffix = ".yaml"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Start is the standalone entry program for the firstboot start component.
|
||||||
|
type Start struct {
|
||||||
|
*cliUtil.FirstbootStartArgs // 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 setup item.
|
||||||
|
func (obj *Start) Main(ctx context.Context) error {
|
||||||
|
if err := obj.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := obj.Run(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate verifies that the structure has acceptable data stored within.
|
||||||
|
func (obj *Start) Validate() error {
|
||||||
|
if obj == nil {
|
||||||
|
return fmt.Errorf("data is nil")
|
||||||
|
}
|
||||||
|
if obj.Program == "" {
|
||||||
|
return fmt.Errorf("program is empty")
|
||||||
|
}
|
||||||
|
if obj.Version == "" {
|
||||||
|
return fmt.Errorf("version is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run performs the desired actions. This runs a list of scripts and then
|
||||||
|
// removes them. This is useful for a "firstboot" service. If there exists a
|
||||||
|
// file with the same name as an executable script or binary, but which has a
|
||||||
|
// .yaml extension, that will be parsed to look for command modifiers.
|
||||||
|
func (obj *Start) Run(ctx context.Context) error {
|
||||||
|
lockFile := LockFilePath // default
|
||||||
|
if s := obj.FirstbootStartArgs.LockFilePath; s != "" && !strings.HasSuffix(s, "/") {
|
||||||
|
lockFile = s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the directory exists.
|
||||||
|
d := filepath.Dir(lockFile)
|
||||||
|
if err := os.MkdirAll(d, 0750); err != nil {
|
||||||
|
return fmt.Errorf("could not make lockfile dir at: %s", d)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure only one copy of this service is running at a time.
|
||||||
|
unlock, err := util.NewFlock(lockFile).TryLock()
|
||||||
|
if err != nil {
|
||||||
|
return err // can't get lock
|
||||||
|
}
|
||||||
|
if unlock == nil {
|
||||||
|
return fmt.Errorf("already running")
|
||||||
|
}
|
||||||
|
// now we're locked!
|
||||||
|
defer unlock()
|
||||||
|
|
||||||
|
scriptsDir := ScriptsDir // default
|
||||||
|
if s := obj.FirstbootStartArgs.ScriptsDir; s != "" && strings.HasSuffix(s, "/") {
|
||||||
|
scriptsDir = s
|
||||||
|
}
|
||||||
|
obj.Logf("scripts dir: %s", scriptsDir)
|
||||||
|
|
||||||
|
// Loop through all the entries and execute what we can...
|
||||||
|
entries, err := os.ReadDir(scriptsDir) // ([]os.DirEntry, error)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() { // skip dirs
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !entry.Type().IsRegular() { // skip weird things
|
||||||
|
// TODO: We may wish to relax this constraint eventually.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Why is entry.Type() always empty?
|
||||||
|
//fmt.Printf("???: %+v\n", entry.Type())
|
||||||
|
//if m := entry.Type(); m&0100 == 0 { // owner bit is not executable
|
||||||
|
// continue
|
||||||
|
//}
|
||||||
|
fi, err := entry.Info()
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
continue // we might have deleted a style file
|
||||||
|
} else if err != nil {
|
||||||
|
return err // TODO: continue instead?
|
||||||
|
}
|
||||||
|
if m := fi.Mode(); m&0100 == 0 { // owner bit is not executable
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
p := filepath.Clean(scriptsDir + entry.Name())
|
||||||
|
obj.Logf("found: %s", p)
|
||||||
|
|
||||||
|
styleFile := p + StyleSuffix // maybe it exists!
|
||||||
|
|
||||||
|
style := &Style{ // set defaults here
|
||||||
|
DoneDir: obj.FirstbootStartArgs.DoneDir,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: check style file is _not_ executable to avoid chains?
|
||||||
|
data, err := os.ReadFile(styleFile)
|
||||||
|
if err != nil && !os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
if err := yaml.Unmarshal(data, style); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
style.exists = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Found one!
|
||||||
|
if err := obj.process(ctx, p, style); err != nil {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// process runs the command, logs, and deletes the script if needed.
|
||||||
|
func (obj *Start) process(ctx context.Context, name string, style *Style) error {
|
||||||
|
logOutput := ""
|
||||||
|
if !style.NoLog {
|
||||||
|
loggingDir := "/root/"
|
||||||
|
if s := obj.FirstbootStartArgs.LoggingDir; s != "" && strings.HasSuffix(s, "/") {
|
||||||
|
loggingDir = s
|
||||||
|
}
|
||||||
|
logOutput = fmt.Sprintf("%smgmt-firstboot-%v.log", loggingDir, time.Now().UnixNano())
|
||||||
|
}
|
||||||
|
if err := util.AppendFile(logOutput, []byte(name+"\n"), 0600); err != nil {
|
||||||
|
obj.Logf("error: %v", err)
|
||||||
|
}
|
||||||
|
opts := &util.SimpleCmdOpts{
|
||||||
|
Debug: obj.Debug,
|
||||||
|
Logf: obj.Logf,
|
||||||
|
LogOutput: logOutput,
|
||||||
|
}
|
||||||
|
args := []string{}
|
||||||
|
err := util.SimpleCmd(ctx, name, args, opts)
|
||||||
|
errStr := fmt.Sprintf("error: %v\n", err)
|
||||||
|
if err := util.AppendFile(logOutput, []byte(errStr), 0600); err != nil {
|
||||||
|
obj.Logf("error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil && style.KeepOnFail {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if style.DoneDir != "" && strings.HasSuffix(style.DoneDir, "/") {
|
||||||
|
dest := func(p string) string { // dest path from input full path
|
||||||
|
return style.DoneDir + filepath.Base(p)
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(style.DoneDir, 0750); err != nil { // convenience
|
||||||
|
obj.Logf("error: %v", err)
|
||||||
|
//return err // let the real final error be seen...
|
||||||
|
}
|
||||||
|
// Move files!
|
||||||
|
if err := os.Rename(name, dest(name)); err != nil {
|
||||||
|
obj.Logf("error: %v", err)
|
||||||
|
} else {
|
||||||
|
obj.Logf("moved: %s", name)
|
||||||
|
}
|
||||||
|
if style.exists {
|
||||||
|
if err := os.Rename(name+StyleSuffix, dest(name+StyleSuffix)); err != nil {
|
||||||
|
obj.Logf("error: %v", err)
|
||||||
|
} else {
|
||||||
|
obj.Logf("moved: %s", name+StyleSuffix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove files!
|
||||||
|
if err := os.Remove(name); err != nil {
|
||||||
|
obj.Logf("error: %v", err)
|
||||||
|
} else {
|
||||||
|
obj.Logf("removed: %s", name)
|
||||||
|
}
|
||||||
|
if style.exists {
|
||||||
|
if err := os.Remove(name + StyleSuffix); err != nil {
|
||||||
|
obj.Logf("error: %v", err)
|
||||||
|
} else {
|
||||||
|
obj.Logf("removed: %s", name+StyleSuffix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Style are some values that are used to specify how each command runs.
|
||||||
|
type Style struct {
|
||||||
|
// TODO: Is this easy to implement?
|
||||||
|
// DeleteFirst should be true to delete the script before it runs. This
|
||||||
|
// is useful to prevent scenarios where a read-only filesystem would
|
||||||
|
// cause the script to run again and again.
|
||||||
|
//DeleteFirst bool `yaml:"delete-first"`
|
||||||
|
|
||||||
|
// DoneDir specifies the dir to move files to instead of deleting them.
|
||||||
|
DoneDir string `yaml:"done-dir"`
|
||||||
|
|
||||||
|
// KeepOnFail should be true to preserve the script if it fails. This
|
||||||
|
// means it will likely run again the next time the service runs.
|
||||||
|
KeepOnFail bool `yaml:"keep-on-fail"`
|
||||||
|
|
||||||
|
// NoLog should be true to skip logging the output of this command.
|
||||||
|
NoLog bool `yaml:"no-log"`
|
||||||
|
|
||||||
|
// exists specifies if the style file exists on disk. (And as a result,
|
||||||
|
// should it be removed at the end?)
|
||||||
|
exists bool
|
||||||
|
}
|
||||||
175
setup/firstboot.go
Normal file
175
setup/firstboot.go
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2024+ 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 setup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
cliUtil "github.com/purpleidea/mgmt/cli/util"
|
||||||
|
"github.com/purpleidea/mgmt/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Firstboot is the standalone entry program for the firstboot setup component.
|
||||||
|
type Firstboot struct {
|
||||||
|
*cliUtil.SetupFirstbootArgs // 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 setup item.
|
||||||
|
func (obj *Firstboot) Main(ctx context.Context) error {
|
||||||
|
if err := obj.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := obj.Run(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate verifies that the structure has acceptable data stored within.
|
||||||
|
func (obj *Firstboot) Validate() error {
|
||||||
|
if obj == nil {
|
||||||
|
return fmt.Errorf("data is nil")
|
||||||
|
}
|
||||||
|
if obj.Program == "" {
|
||||||
|
return fmt.Errorf("program is empty")
|
||||||
|
}
|
||||||
|
if obj.Version == "" {
|
||||||
|
return fmt.Errorf("version is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run performs the desired actions. This templates and installs a systemd
|
||||||
|
// service and enables and starts it if so desired.
|
||||||
|
func (obj *Firstboot) Run(ctx context.Context) error {
|
||||||
|
cmdNameSystemctl := "/usr/bin/systemctl"
|
||||||
|
opts := &util.SimpleCmdOpts{
|
||||||
|
Debug: obj.Debug,
|
||||||
|
Logf: obj.Logf,
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.SetupFirstbootArgs.Mkdir {
|
||||||
|
// TODO: Should we also make LoggingDir and LockFilePath's dir?
|
||||||
|
if s := obj.SetupFirstbootArgs.ScriptsDir; s != "" {
|
||||||
|
if err := os.MkdirAll(s, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
obj.Logf("mkdir: %s", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.SetupFirstbootArgs.Install {
|
||||||
|
binaryPath := "/usr/bin/mgmt" // default
|
||||||
|
if s := obj.SetupFirstbootArgs.BinaryPath; s != "" {
|
||||||
|
binaryPath = s
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{}
|
||||||
|
if s := obj.SetupFirstbootArgs.LockFilePath; s != "" {
|
||||||
|
arg := fmt.Sprintf("--lock-file-path=%s", s)
|
||||||
|
args = append(args, arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s := obj.SetupFirstbootArgs.ScriptsDir; s != "" {
|
||||||
|
arg := fmt.Sprintf("--scripts-dir=%s", s)
|
||||||
|
args = append(args, arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s := obj.SetupFirstbootArgs.DoneDir; s != "" {
|
||||||
|
arg := fmt.Sprintf("--done-dir=%s", s)
|
||||||
|
args = append(args, arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s := obj.SetupFirstbootArgs.LoggingDir; s != "" {
|
||||||
|
arg := fmt.Sprintf("--logging-dir=%s", s)
|
||||||
|
args = append(args, arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
unit := &util.UnitData{
|
||||||
|
Description: "Mgmt firstboot service",
|
||||||
|
Documentation: "https://github.com/purpleidea/mgmt/",
|
||||||
|
After: []string{"network.target"}, // TODO: systemd-networkd.service ?
|
||||||
|
|
||||||
|
Type: "oneshot",
|
||||||
|
ExecStart: fmt.Sprintf("%s firstboot start %s", binaryPath, strings.Join(args, " ")),
|
||||||
|
|
||||||
|
RemainAfterExit: true,
|
||||||
|
StandardOutput: "journal+console",
|
||||||
|
StandardError: "inherit",
|
||||||
|
|
||||||
|
WantedBy: []string{"multi-user.target"},
|
||||||
|
}
|
||||||
|
unitData, err := unit.Template()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
unitPath := "/etc/systemd/system/mgmt-firstboot.service"
|
||||||
|
|
||||||
|
if err := os.WriteFile(unitPath, []byte(unitData), 0644); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
obj.Logf("wrote file to: %s", unitPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.SetupFirstbootArgs.Start {
|
||||||
|
cmdArgs := []string{"start", "mgmt-firsboot.service"}
|
||||||
|
if err := util.SimpleCmd(ctx, cmdNameSystemctl, cmdArgs, opts); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.SetupFirstbootArgs.Enable {
|
||||||
|
cmdArgs := []string{"enable", "mgmt-firstboot.service"}
|
||||||
|
if err := util.SimpleCmd(ctx, cmdNameSystemctl, cmdArgs, opts); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
144
setup/pkg.go
Normal file
144
setup/pkg.go
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2024+ 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 setup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
cliUtil "github.com/purpleidea/mgmt/cli/util"
|
||||||
|
"github.com/purpleidea/mgmt/util"
|
||||||
|
distroUtil "github.com/purpleidea/mgmt/util/distro"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Pkg is the standalone entry program for the pkg setup component.
|
||||||
|
type Pkg struct {
|
||||||
|
*cliUtil.SetupPkgArgs // 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 setup item.
|
||||||
|
func (obj *Pkg) Main(ctx context.Context) error {
|
||||||
|
if err := obj.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := obj.Run(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate verifies that the structure has acceptable data stored within.
|
||||||
|
func (obj *Pkg) Validate() error {
|
||||||
|
if obj == nil {
|
||||||
|
return fmt.Errorf("data is nil")
|
||||||
|
}
|
||||||
|
if obj.Program == "" {
|
||||||
|
return fmt.Errorf("program is empty")
|
||||||
|
}
|
||||||
|
if obj.Version == "" {
|
||||||
|
return fmt.Errorf("version is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.SetupPkgArgs.Distro == "" {
|
||||||
|
return fmt.Errorf("distro is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run performs the desired actions. This generates a list of bash commands to
|
||||||
|
// run since we might not be able to run this binary to install these packages!
|
||||||
|
// The output (stdout) of this command can be run from a shell.
|
||||||
|
func (obj *Pkg) Run(ctx context.Context) error {
|
||||||
|
cmdName := ""
|
||||||
|
|
||||||
|
packages, exists := distroUtil.ToBootstrapPackages(obj.SetupPkgArgs.Distro)
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("unknown distro")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Consider moving cmdName into the util/distro package.
|
||||||
|
if obj.SetupPkgArgs.Distro == "fedora" {
|
||||||
|
cmdName = "/usr/bin/dnf --assumeyes install"
|
||||||
|
}
|
||||||
|
if obj.SetupPkgArgs.Distro == "debian" {
|
||||||
|
cmdName = "/usr/bin/apt --yes install"
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmdName == "" {
|
||||||
|
return fmt.Errorf("no command name found")
|
||||||
|
}
|
||||||
|
if len(packages) == 0 {
|
||||||
|
return nil // nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdArgs := []string{}
|
||||||
|
cmdArgs = append(cmdArgs, packages...)
|
||||||
|
|
||||||
|
if !obj.SetupPkgArgs.Exec { // print, don't exec
|
||||||
|
cmd := ""
|
||||||
|
if obj.SetupPkgArgs.Sudo {
|
||||||
|
cmd += "sudo" + " "
|
||||||
|
}
|
||||||
|
cmd += cmdName + " "
|
||||||
|
|
||||||
|
cmd += strings.Join(cmdArgs, " ")
|
||||||
|
|
||||||
|
fmt.Printf("%s\n", cmd)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split off any bonus elements to the command...
|
||||||
|
realCmdName := strings.Split(cmdName, " ")
|
||||||
|
realCmdArgs := []string{}
|
||||||
|
realCmdArgs = append(realCmdArgs, realCmdName[1:]...)
|
||||||
|
realCmdArgs = append(realCmdArgs, cmdArgs...)
|
||||||
|
opts := &util.SimpleCmdOpts{
|
||||||
|
Debug: obj.Debug,
|
||||||
|
Logf: obj.Logf,
|
||||||
|
}
|
||||||
|
return util.SimpleCmd(ctx, realCmdName[0], realCmdArgs, opts)
|
||||||
|
}
|
||||||
48
setup/setup.go
Normal file
48
setup/setup.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2024+ 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 setup provides some simple facilities to help bootstrap things.
|
||||||
|
package setup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// API is the simple interface we expect for any setup items.
|
||||||
|
type API interface {
|
||||||
|
// Main runs everything for this setup item.
|
||||||
|
Main(context.Context) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config is a struct of all the configuration values which are shared by all of
|
||||||
|
// the setup 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_FIRSTBOOT_FOO" help:"Foo..."` // TODO: foo
|
||||||
|
}
|
||||||
137
setup/svc.go
Normal file
137
setup/svc.go
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2024+ 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 setup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
cliUtil "github.com/purpleidea/mgmt/cli/util"
|
||||||
|
"github.com/purpleidea/mgmt/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Svc is the standalone entry program for the svc setup component.
|
||||||
|
type Svc struct {
|
||||||
|
*cliUtil.SetupSvcArgs // 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 setup item.
|
||||||
|
func (obj *Svc) Main(ctx context.Context) error {
|
||||||
|
if err := obj.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := obj.Run(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate verifies that the structure has acceptable data stored within.
|
||||||
|
func (obj *Svc) Validate() error {
|
||||||
|
if obj == nil {
|
||||||
|
return fmt.Errorf("data is nil")
|
||||||
|
}
|
||||||
|
if obj.Program == "" {
|
||||||
|
return fmt.Errorf("program is empty")
|
||||||
|
}
|
||||||
|
if obj.Version == "" {
|
||||||
|
return fmt.Errorf("version is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run performs the desired actions. This templates and installs a systemd
|
||||||
|
// service and enables and starts it if so desired.
|
||||||
|
func (obj *Svc) Run(ctx context.Context) error {
|
||||||
|
cmdNameSystemctl := "/usr/bin/systemctl"
|
||||||
|
opts := &util.SimpleCmdOpts{
|
||||||
|
Debug: obj.Debug,
|
||||||
|
Logf: obj.Logf,
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.SetupSvcArgs.Install {
|
||||||
|
binaryPath := "/usr/bin/mgmt" // default
|
||||||
|
if s := obj.SetupSvcArgs.BinaryPath; s != "" {
|
||||||
|
binaryPath = s
|
||||||
|
}
|
||||||
|
|
||||||
|
unit := &util.UnitData{
|
||||||
|
Description: "Mgmt configuration management service",
|
||||||
|
Documentation: "https://github.com/purpleidea/mgmt/",
|
||||||
|
ExecStart: fmt.Sprintf("%s run empty $OPTS", binaryPath),
|
||||||
|
RestartSec: "5s",
|
||||||
|
Restart: "always",
|
||||||
|
WantedBy: []string{"multi-user.target"},
|
||||||
|
}
|
||||||
|
unitData, err := unit.Template()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
unitPath := "/etc/systemd/system/mgmt.service"
|
||||||
|
|
||||||
|
if err := os.WriteFile(unitPath, []byte(unitData), 0644); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
obj.Logf("wrote file to: %s", unitPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.SetupSvcArgs.Start {
|
||||||
|
cmdArgs := []string{"start", "mgmt.service"}
|
||||||
|
if err := util.SimpleCmd(ctx, cmdNameSystemctl, cmdArgs, opts); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.SetupSvcArgs.Enable {
|
||||||
|
cmdArgs := []string{"enable", "mgmt.service"}
|
||||||
|
if err := util.SimpleCmd(ctx, cmdNameSystemctl, cmdArgs, opts); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user