diff --git a/cmd/control/main.go b/cmd/control/main.go new file mode 100644 index 0000000..a79e6d0 --- /dev/null +++ b/cmd/control/main.go @@ -0,0 +1,22 @@ +package main + +import ( + "fmt" + "os" + + "gitea.sunhpc.com/kelvin/sunhpc" +) + +func main() { + + root := sunhpc.GetRootCommand() + + err := root.Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "ERROR: %s\n", err) + if sunhpc.DebugFlag { + fmt.Printf("\nSTACK TRACE: %+v\n", err) + } + os.Exit(255) + } +} \ No newline at end of file diff --git a/cmd/shctl/main.go b/cmd/shctl/main.go deleted file mode 100644 index a6c5079..0000000 --- a/cmd/shctl/main.go +++ /dev/null @@ -1,10 +0,0 @@ -package main - -import ( - "fmt" - "os" -) - -func main() { - fmt.Printfn("hello world.") -} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7dcc783 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module gitea.sunhpc.com/kelvin/sunhpc.git + +go 1.25.8 + +require gitea.sunhpc.com/kelvin/sunhpc v0.0.0-20260428114043-53d56ec6e2ba // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1afe100 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +gitea.sunhpc.com/kelvin/sunhpc v0.0.0-20260428114043-53d56ec6e2ba h1:NKGusycgJgYD/ljVamRqkLm8+jqT2h09xVjZf7NBAqc= +gitea.sunhpc.com/kelvin/sunhpc v0.0.0-20260428114043-53d56ec6e2ba/go.mod h1:qQ5hwUVNDDEjeZRE35FzMpsobqly7h0tRcBGCcvzgrc= diff --git a/internal/app/control/root.go b/internal/app/control/root.go new file mode 100644 index 0000000..29092a7 --- /dev/null +++ b/internal/app/control/root.go @@ -0,0 +1,40 @@ +package control + +import ( + "os" + + "github.com/spf13/cobra" + "gitea.sunhpc.com/kelvin/sunhpc/internal/app/control/version" + "gitea.sunhpc.com/kelvin/sunhpc/internal/pkg/help" + "gitea.sunhpc.com/kelvin/sunhpc/internal/pkg/splog" +) + +var ( + rootCmd = &cobra.Command{ + DisableFlagsInUseLine: true, + Use: "sunhpc control COMMAND [options] [FLAGS]", + Short: "Sunhpc Control", + Long: `Control interface to the SunHPC Cluster Provisioning System.`, + PersistentPreRunE: rootPersistentPreRunE, + SilenceUsage: true, + SilenceErrors: true, + Args: cobra.NoArgs, + } + verboseArg bool + DebugFlag bool + SunhpcConfArg string + AllowEmptyConf bool +) + +func init() { + rootCmd.CompletionOptions.HiddenDefaultCmd = true + rootCmd.PersistentFlags().BoolVarP(&verboseArg, "verbose", "v", false, "verbose output") + rootCmd.PersistentFlags().BoolVarP(&DebugFlag, "debug", "d", false, "debug output") + rootCmd.PersistentFlags().IntVar(&LogLevel, "loglevel", splog.INFO, "Set log level to given string") + _ = rootCmd.PersistentFlags().MarkHidden("loglevel") + rootCmd.PersistentFlags().StringVar(&SunhpcConfArg, "sunhpcconf", "", "Set the sunhpc configuration file") + rootCmd.PersistentFlags().BoolVar(&AllowEmptyConf, "emptyconf", false, "Allow empty configuration") + _ = rootCmd.PersistentFlags().MarkHidden("emptyconf") + rootCmd.SetUsageTemplate(help.UsageTemplate) + rootCmd.SetHelpTemplate(help.HelpTemplate) +} \ No newline at end of file diff --git a/internal/pkg/help/help.go b/internal/pkg/help/help.go new file mode 100644 index 0000000..8ce94c9 --- /dev/null +++ b/internal/pkg/help/help.go @@ -0,0 +1,36 @@ +package help + +// UsageTemplate replaces the default usage template from cobra +const UsageTemplate string = `Usage: +{{if .Runnable}} {{.UseLine}} +{{end}}{{if .HasAvailableSubCommands}} {{.CommandPath}} COMMAND [OPTIONS] +{{end}}{{if gt (len .Aliases) 0}} +Aliases: + {{.NameAndAliases}} +{{end}}{{if .HasExample}} +Examples: + {{.Example}} +{{end}}{{if .HasAvailableSubCommands}} +Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}} + {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}} +{{end}}{{if .HasAvailableLocalFlags}} +Options: +{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}} +{{end}}{{if .HasAvailableInheritedFlags}} +Global Options: +{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}} +{{end}}{{if .HasHelpSubCommands}} +Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}} + {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}} +{{end}}{{if .HasAvailableSubCommands}} +Use "{{.CommandPath}} COMMAND --help" for more information about a COMMAND. +{{end}} +` + +// End UsageTemplate + +// HelpTemplate replaces the default help template from cobra +const HelpTemplate string = ` +{{with (or .Long .Short)}}{{. | trimTrailingWhitespaces}} +{{end}} +{{if or .Runnable .HasSubCommands}}{{.UsageString}}{{end}}` diff --git a/internal/pkg/splog/splog.go b/internal/pkg/splog/splog.go new file mode 100644 index 0000000..1471fe8 --- /dev/null +++ b/internal/pkg/splog/splog.go @@ -0,0 +1,310 @@ +package wwlog + +import ( + "fmt" + "io" + "os" + "reflect" + "runtime" + "sort" + "strings" + "time" +) + +type LogRecord struct { + Level int + Err error + Msg string + Args []interface{} + Pc uintptr + File string + Line int + Time time.Time +} + +/* +Format a log message from a record +Only called if rec.level >= logLevel +*/ +type LogFormatter func(logLevel int, rec *LogRecord) string + +var ( + SECCRITICAL = SetLevelName(51, "SECCRITICAL") + CRITICAL = SetLevelName(50, "CRITICAL") + DENIED = SetLevelName(42, "DENIED") + SECERROR = SetLevelName(41, "SECERROR") + ERROR = SetLevelName(40, "ERROR") + SECWARN = SetLevelName(31, "SECWARN") + WARN = SetLevelName(30, "WARN") + ERROUT = SetLevelName(29, "ERROUT") + SERV = SetLevelName(25, "SERV") + OUT = SetLevelName(22, "OUT") + SECINFO = SetLevelName(21, "SECINFO") + INFO = SetLevelName(20, "INFO") + SECVERBOSE = SetLevelName(16, "SECVERBOSE") + VERBOSE = SetLevelName(15, "VERBOSE") + SECDEBUG = SetLevelName(11, "SECDEBUG") + DEBUG = SetLevelName(10, "DEBUG") +) + +var ( + levelNums = []int{0} + levelNames = []string{"NOTSET"} + logLevel = INFO + logErr io.Writer = os.Stderr + logOut io.Writer = os.Stdout + logFormatter LogFormatter = DefaultFormatter +) + +func LevelNameEff(level int) (int, int, string) { + n := len(levelNums) + idx := sort.SearchInts(levelNums, level) + + if idx >= n { + idx = n - 1 + } + + eff_level := levelNums[idx] + eff_name := levelNames[idx] + + return idx, eff_level, eff_name +} + +func LevelName(level int) string { + _, _, name := LevelNameEff(level) + return name +} + +func SetLevelName(level int, name string) int { + n := len(levelNums) + idx := sort.SearchInts(levelNums, level) + + if idx < n && levelNums[idx] == level { + levelNames[idx] = name + } else { + + levelNums = append(levelNums, level) + levelNames = append(levelNames, name) + + if idx < n { + copy(levelNums[idx+1:], levelNums[idx:]) + copy(levelNames[idx+1:], levelNames[idx:]) + + levelNums[idx] = level + levelNames[idx] = name + } + } + + return level +} + +func DefaultFormatter(logLevel int, rec *LogRecord) string { + message := fmt.Sprintf(rec.Msg, rec.Args...) + + if !strings.HasSuffix(message, "\n") { + // ensure written messages are separated by at least one newline + message += "\n" + } + + if rec.Err != nil { + if logLevel < VERBOSE { + // when debugging errors, add file and line number, and any stack trace + message += fmt.Sprintf("%s:%d\n%+v\n", rec.File, rec.Line, rec.Err) + } else { + message += fmt.Sprintf("%v\n", rec.Err) + } + } + + if (rec.Level == INFO || rec.Level == OUT) && logLevel == INFO { + // NOTE: this is a bit strange, but for user-friendliness it makes sense + // to not pollute the messages unless something bad happens (by default). + // if logLevel > INFO, then level == INFO messages should not be printed anyway, + // and if logLevel < INFO, then it seems like all messages should get prefixed + return message + } + + name := LevelName(rec.Level) + + if len(name) <= 7 { + return fmt.Sprintf("%-7s: %s", name, message) + } + + return fmt.Sprintf("%-11s: %s", name, message) +} + +func EnabledForLevel(level int) bool { + return level >= logLevel +} + +/* +Set the central log level. Uneven values are security related +lof entries. +*/ +func SetLogLevel(level int) { + logLevel = level + + Debug("Set log level to: %d, %s", logLevel, LevelName(logLevel)) +} + +func GetLogLevel() int { + return logLevel +} + +/* +Set the log output writer +By default they are set to output writer +*/ +func SetLogWriter(newOut io.Writer) { + logErr = newOut + logOut = newOut +} + +/* +Set the log ofr info only +*/ +func SetLogWriterInfo(newOut io.Writer) { + logOut = newOut +} + +/* +Set the log writer for errors +*/ +func SetLogWriterErr(newOut io.Writer) { + logErr = newOut +} + +/* +Set the log record formatter +By default this is set to DefaultFormatter +*/ +func SetLogFormatter(formatter LogFormatter) { + logFormatter = formatter + Debug("Set log formatter: %s", runtime.FuncForPC(reflect.ValueOf(formatter).Pointer()).Name()) +} + +/* +Internal method to create a log record +*/ +func LogCaller(level int, skip int, err error, message string, a ...interface{}) { + if EnabledForLevel(level) { + pc, file, line, ok := runtime.Caller(skip + 1) + if !ok { + file = "[unknown]" + } + + rec := LogRecord{ + Level: level, + Err: err, + Msg: message, + Args: a, + Pc: pc, + File: file, + Line: line, + Time: time.Now(), + } + + message = logFormatter(logLevel, &rec) + if level == INFO || level == OUT { + _, _ = fmt.Fprint(logOut, message) + } else { + _, _ = fmt.Fprint(logErr, message) + + } + } +} + +/* +****************************************************************************** +Named log level functions +*/ +func Log(level int, message string, a ...interface{}) { + LogCaller(level, 1, nil, message, a...) +} + +func LogExc(level int, err error, message string, a ...interface{}) { + LogCaller(level, 1, err, message, a...) +} + +func Debug(message string, a ...interface{}) { + LogCaller(DEBUG, 1, nil, message, a...) +} + +func DebugExc(err error, message string, a ...interface{}) { + LogCaller(DEBUG, 1, err, message, a...) +} + +func SecDebug(message string, a ...interface{}) { + LogCaller(SECDEBUG, 1, nil, message, a...) +} + +func Verbose(message string, a ...interface{}) { + LogCaller(VERBOSE, 1, nil, message, a...) +} + +func VerboseExc(err error, message string, a ...interface{}) { + LogCaller(VERBOSE, 1, err, message, a...) +} + +func SecVerbose(message string, a ...interface{}) { + LogCaller(SECVERBOSE, 1, nil, message, a...) +} + +func Info(message string, a ...interface{}) { + LogCaller(INFO, 1, nil, message, a...) +} + +func Output(message string, a ...interface{}) { + LogCaller(OUT, 1, nil, message, a...) +} + +func InfoExc(err error, message string, a ...interface{}) { + LogCaller(INFO, 1, err, message, a...) +} + +func SecInfo(message string, a ...interface{}) { + LogCaller(SECINFO, 1, nil, message, a...) +} + +func Serv(message string, a ...interface{}) { + LogCaller(SERV, 1, nil, message, a...) +} + +func Warn(message string, a ...interface{}) { + LogCaller(WARN, 1, nil, message, a...) +} + +func WarnExc(err error, message string, a ...interface{}) { + LogCaller(WARN, 1, err, message, a...) +} + +func SecWarn(message string, a ...interface{}) { + LogCaller(SECWARN, 1, nil, message, a...) +} + +func Error(message string, a ...interface{}) { + LogCaller(ERROR, 1, nil, message, a...) +} + +func ErrorExc(err error, message string, a ...interface{}) { + LogCaller(ERROR, 1, err, message, a...) +} + +func SecError(message string, a ...interface{}) { + LogCaller(SECERROR, 1, nil, message, a...) +} + +func Denied(message string, a ...interface{}) { + LogCaller(DENIED, 1, nil, message, a...) +} + +func Critical(message string, a ...interface{}) { + LogCaller(CRITICAL, 1, nil, message, a...) +} + +func CriticalExc(err error, message string, a ...interface{}) { + LogCaller(CRITICAL, 1, err, message, a...) +} + +func SecCritical(message string, a ...interface{}) { + LogCaller(SECCRITICAL, 1, nil, message, a...) +} diff --git a/sunhpc b/sunhpc new file mode 100755 index 0000000..96b33a0 Binary files /dev/null and b/sunhpc differ