diff --git a/Makefile b/Makefile index e561cc6..cceb63c 100644 --- a/Makefile +++ b/Makefile @@ -18,6 +18,7 @@ bin/fioconfig-%: FORCE GOARCH=$(shell echo $* | cut -f2 -d\-) \ go build -tags vpn $(LDFLAGS) -o $@ main.go +.PHONY: bin/fioconfig-nopkcs11 bin/fioconfig-nopkcs11: CGO_ENABLED=0 go build -tags vpn,disable_pkcs11 $(LDFLAGS) -o $@ main.go diff --git a/contrib/actions/diag b/contrib/actions/diag new file mode 100755 index 0000000..67ef906 --- /dev/null +++ b/contrib/actions/diag @@ -0,0 +1,4 @@ +#!/bin/sh -ex + +/usr/sbin/fio-diag.sh +dmesg > $ARTIFACTS/dmesg.log diff --git a/contrib/actions/reboot b/contrib/actions/reboot new file mode 100755 index 0000000..2492f73 --- /dev/null +++ b/contrib/actions/reboot @@ -0,0 +1,5 @@ +#!/bin/sh -ex + +# Schedule a reboot in one minute to give fioconfig the chance to create a +# TestResult on the backend so the operator will know the device is rebooting. +systemctl reboot --when="+5 seconds" diff --git a/contrib/fioconfig-oneshot b/contrib/fioconfig-oneshot new file mode 100755 index 0000000..eb7f622 --- /dev/null +++ b/contrib/fioconfig-oneshot @@ -0,0 +1,63 @@ +#!/bin/sh -e + +# This is an OnChanged handler that can handle requests to run a command +# one time, capture the output, and report it back to the device-gateway. + +[ -z "${CONFIG_FILE}" ] && (echo "No CONFIG_FILE specified"; exit 1) +[ -f "${CONFIG_FILE}" ] || (echo "${CONFIG_FILE} does not exist. Assuming config file was deleted"; exit 0) + +ACTIONS_DIR=${ACTIONS_DIR-"/usr/share/fioconfig/actions"} +[ -d "${ACTIONS_DIR}" ] || (echo "${ACTIONS_DIR} does not exist"; exit 1) + +cmd_id=$(cat ${CONFIG_FILE} | grep COMMAND_ID | cut -d= -f2) +[ -z "${cmd_id}" ] && (echo "No COMMAND_ID found in config file"; exit 1) +echo "Command ID is ${cmd_id}" + +capture=$(cat ${CONFIG_FILE} | grep COMMAND_CAPTURE | cut -d= -f2) + +action=$1 +active_file="${STORAGE_DIR}/oneshot.${action}.state" +completed_file="${active_file}.completed" + +echo "Action is ${action}" +cmd="${ACTIONS_DIR}/${action}" +if [ -x "${cmd}" ] ; then + if [ "${capture}" = "1" ]; then + artifacts_dir=$(mktemp -d fioconfig-oneshot.XXXXXX) + trap 'rm -rf ${artifacts_dir}' EXIT + + echo "Capturing stdout/stderr" + cmd="${FIOCONFIG_BIN} run-and-report --artifacts-dir ${artifacts_dir} --id ${cmd_id} --name ${action} ${cmd}" + fi +else + echo "Action not found in ${ACTIONS_DIR} or is not executable" + echo -n "${cmd_id}" > ${completed_file} + if [ "${capture}" = "1" ]; then + # report something back to the operator + ${FIOCONFIG_BIN} run-and-report --id ${cmd_id} --name ${action} /bin/sh -c "echo Action(${cmd}) not found; exit 1" + fi + exit 1 +fi + +# We'll execute under two conditions: +# 1) We've never been run (oneshot.state.completed does not exist) +# 3) We have a new command ($cmd_id != oneshot.state.completed's COMMANDID) + +runcmd() { + ${cmd} ; echo -n "${cmd_id}" > ${completed_file} + rm -rf ${artifacts_dir} + exit +} + +if [ ! -f "${completed_file}" ] ; then + echo "Completion file, ${completed_file}, does not exist" + runcmd +fi + +completed_id=$(cat ${completed_file}) +if [ "${completed_id}" != "${cmd_id}" ] ; then + echo "Running command: COMMAND_ID has changed from ${completed_id} -> ${cmd_id}" + runcmd +fi + +echo "Command not needed. Current COMMAND_ID is ${cmd_id}" diff --git a/internal/remote_actions.go b/internal/remote_actions.go new file mode 100644 index 0000000..06f6f91 --- /dev/null +++ b/internal/remote_actions.go @@ -0,0 +1,65 @@ +//go:build !disable_remoteactions + +package internal + +import ( + "bytes" + "errors" + "log/slog" + "os" + "path/filepath" + "strings" +) + +type raInitCallback struct { + newActions []byte +} + +func (r *raInitCallback) ConfigFiles(app *App) []ConfigFileReq { + const actionsDir = "/usr/share/fioconfig/actions" + var actions []string + entries, err := os.ReadDir(actionsDir) + if err != nil && !errors.Is(err, os.ErrNotExist) { + // This is a bug - we can't read the directory for some reason + slog.Error("Unable to initialize remote actions", "error", err) + return nil + } else if err == nil { + for _, entry := range entries { + actions = append(actions, entry.Name()) + } + } + actionsStr := strings.Join(actions, ",") + + content, err := os.ReadFile(filepath.Join(app.SecretsDir, "fio-remote-actions")) + if err != nil { + if os.IsNotExist(err) { + content = nil + } else { + slog.Warn("Unable to read configured remote actions", "error", err) + return nil + } + } + + if !bytes.Equal(content, []byte(actionsStr)) { + slog.Info("Configured remote actions changed", "old", string(content), "new", actionsStr) + file := ConfigFileReq{ + Name: "fio-remote-actions", + Unencrypted: true, + Value: actionsStr, + } + r.newActions = []byte(actionsStr) + return []ConfigFileReq{file} + } + + // prevent init logic from calling OnComplete by removing ourselves + delete(initCallbacks, "remote-actions") + return nil +} + +func (r raInitCallback) OnComplete(app *App) { + delete(initCallbacks, "remote-actions") +} + +func init() { + initCallbacks["remote-actions"] = &raInitCallback{} +}