Copyright (c) 2024 Huawei Technologies Co., Ltd.
openFuyao is licensed under Mulan PSL v2.
You can use this software according to the terms and conditions of the Mulan PSL v2.
You may obtain a copy of Mulan PSL v2 at:
http://license.coscl.org.cn/MulanPSL2
THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
See the Mulan PSL v2 for more details.
*/
package main
import (
"flag"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"time"
"openfuyao.com/oscheck/internal/checker"
"openfuyao.com/oscheck/internal/conf"
"openfuyao.com/oscheck/internal/report"
"openfuyao.com/oscheck/internal/utils"
)
type Params map[string][]string
const (
numParmsKv = 2
)
func (p *Params) Set(param string) error {
parts := strings.SplitN(param, "=", numParmsKv)
if len(parts) != numParmsKv {
return fmt.Errorf("invalid parameter format: %s, expected key1=value1[,value2,...]", param)
}
key := parts[0]
values := parts[1]
if _, ok := (*p)[key]; !ok {
(*p)[key] = []string{}
}
start := 0
for i := 0; i < len(values); i++ {
if values[i] == '\\' {
if i+1 < len(values) && values[i+1] == '\\' {
i++
} else if i+1 < len(values) && values[i+1] == ',' {
i++
}
} else if values[i] == ',' {
(*p)[key] = append((*p)[key], values[start:i])
start = i + 1
} else if i == len(values)-1 {
(*p)[key] = append((*p)[key], values[start:])
}
}
return nil
}
func (p *Params) String() string {
return fmt.Sprintf("%v", *p)
}
func main() {
utils.InitLogFile(filepath.Join(utils.GetStartDir(), "log", "oscheck.log"))
config, err := conf.LoadConf()
if err != nil {
utils.GetDefaultLogger().Printf("Failed to initialize configuration: %s", err.Error())
utils.PromptMsg("Error: Failed to initialize configuration")
os.Exit(1)
}
if os.Getuid() != 0 {
utils.PromptMsg("Error: This tool requires root privileges to run. Please run it with sudo.")
os.Exit(1)
}
params, err := getParams(config)
if err != nil {
os.Exit(1)
}
if params.help {
return
}
utils.PromptMsg("Start to execute checks...")
checkResult, ok := executeChecks(params)
if !ok {
utils.PromptMsg("Error: Failed to execute checks.")
os.Exit(1)
}
reportPath, err := params.reporter.GenerateReport(checkResult, params.reportPath)
if err != nil {
utils.PromptMsg("Error: Failed to generate report: %s", err.Error())
os.Exit(1)
}
if reportPath != "" {
utils.PromptMsg("Report generated successfully: %s", reportPath)
}
if checkResult.Result != checker.ResultValid {
os.Exit(1)
}
}
func getParams(config *conf.CheckConfig) (runParams, error) {
var checkSet, outputFormat, reportPath, rootPath string
var params Params = make(map[string][]string)
help := false
flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
flag.StringVar(&checkSet, "s", "All",
"Specify the check set to use, if not specified, all check items in the conf/CheckItem directory will be used.")
flag.StringVar(&outputFormat, "t", "plain", "Specify the output format: yaml, csv, plain,"+
" if not specified, plain will be used.")
flag.Var(¶ms, "p", `Specify the parameters, format: key1=value1[,value2,...],`+
`This parameter can be bound multiple times.`)
flag.StringVar(&reportPath, "o", "", "Specify the report output path."+
" The report file will be generated under this path: CheckReport-{NodeName}-{Time}.format.")
flag.BoolVar(&help, "h", false, "Print help information")
flag.Usage = func() {
utils.PromptMsg("This tool is used to check " +
"whether the OS configuration is consistent with a specific check set.")
utils.PromptMsg("Usage: %s [-s check-set] [-o yaml|csv|plain] [-r report-path]"+
" [-p key1=value1[,value2,...]] [-h]", flag.CommandLine.Name())
flag.VisitAll(func(f *flag.Flag) {
printFlag(config, f)
})
utils.PromptMsg("\nExample:")
utils.PromptMsg(" %s -t plain -o ./reports", flag.CommandLine.Name())
}
flag.Parse()
if help {
flag.Usage()
return runParams{help: true}, nil
}
paramsChecked, ok := checkAndInitParam(config, checkSet, outputFormat, reportPath, rootPath)
paramsChecked.itemParams = params
if !ok {
utils.PromptMsg("Error: Invalid command arguments. Use '-h' for usage instructions.")
return runParams{}, fmt.Errorf("invalid command arguments")
}
return paramsChecked, nil
}
func printFlag(config *conf.CheckConfig, f *flag.Flag) {
utils.PromptMsg("\t-%s: %s", f.Name, f.Usage)
switch f.Name {
case "p":
hasPromptParams := false
for _, item := range config.CheckItems {
if len(item.Params) > 0 {
if !hasPromptParams {
hasPromptParams = true
utils.PromptMsg("\t\t Optional parameters:")
}
for _, param := range item.Params {
utils.PromptMsg("\t\t\t %s: %s", param.Name, param.Desc)
}
}
}
if !hasPromptParams {
utils.PromptMsg("\t\t No optional parameters.")
}
case "s":
if len(config.CheckSets) > 0 {
utils.PromptMsg("\t\t Optional check sets:")
} else {
utils.PromptMsg("\t\t No check set available.")
}
for _, checkset := range config.CheckSets {
utils.PromptMsg("\t\t\t %s: %s", checkset.Name, checkset.Desc)
}
default:
return
}
}
func executeChecks(p runParams) (checker.Result, bool) {
chanConc := make(chan struct{}, 10)
defer close(chanConc)
chanRslt := make(chan checker.ItemRslt, len(p.checkItems))
var wg sync.WaitGroup
hasErr := false
result := checker.Result{
CheckSet: p.checkSet,
Result: checker.ResultValid,
StartTime: time.Now(),
Items: make([]checker.ItemRslt, 0, len(p.checkItems)),
}
checkers, hasErr := getCheckers(p)
if hasErr {
return result, false
}
for _, checker := range checkers {
wg.Add(1)
copiedChecker := checker
go func() {
defer wg.Done()
chanConc <- struct{}{}
rslt := copiedChecker.Check()
chanRslt <- rslt
<-chanConc
}()
}
wg.Wait()
close(chanRslt)
for item := range chanRslt {
if int(result.Result) < int(item.Result) {
result.Result = item.Result
}
result.Items = append(result.Items, item)
}
result.HostName = getHostName(p.isInPods, p.rootPath)
result.EndTime = time.Now()
return result, true
}
func getCheckers(p runParams) ([]checker.Checker, bool) {
hasErr := false
checkers := make([]checker.Checker, 0, len(p.checkItems))
ctx := checker.Context{
RootPath: p.rootPath,
}
for _, checkItem := range p.checkItems {
if len(checkItem.Params) > 0 {
checkItem.FillParamValues(p.itemParams)
}
checker, err := checker.GetChecker(checkItem.Kind)
if err != nil {
utils.PromptMsg(`Unsupported Check kind "%s" in file: %s`, checkItem.Kind, checkItem.FilePath)
hasErr = true
continue
}
checkInitErr := checker.Init(ctx, checkItem)
if checkInitErr != nil {
if len(checkInitErr.ErrField) > 0 {
utils.PromptMsg("Check item %s has configuration error, error fields: %s",
checkItem.FilePath, strings.Join(checkInitErr.ErrField, ","))
} else {
utils.PromptMsg("Check item %s has configuration error.", checkItem.FilePath)
}
hasErr = true
continue
}
checkers = append(checkers, checker)
}
return checkers, hasErr
}
func getHostName(isInPods bool, rootPath string) string {
hostName := ""
if isInPods {
hostName = os.Getenv("NODE_NAME")
if hostName != "" {
return hostName
}
bytes, err := os.ReadFile(filepath.Join(rootPath, "proc/sys/kernel/hostname"))
if err == nil {
hostName = string(bytes)
} else {
utils.GetDefaultLogger().Printf("Failed get host name in pods, because: %s", err.Error())
}
}
if hostName == "" {
var err error
hostName, err = os.Hostname()
if err != nil {
utils.GetDefaultLogger().Printf("Failed get host name in pods, because: %s", err.Error())
}
}
if hostName == "" {
hostName = "unknown"
}
return hostName
}
type runParams struct {
rootPath string
checkSet string
reporter report.Reporter
reportPath string
checkItems map[string]conf.CheckItem
isInPods bool
help bool
itemParams map[string][]string
}
func checkAndInitParam(config *conf.CheckConfig, checkset, outputFormat,
reportPath, rootPath string) (runParams, bool) {
p := runParams{
checkSet: "All",
reportPath: "",
checkItems: make(map[string]conf.CheckItem),
}
ok := true
ok = ok && checkAndInitRootPath(rootPath, &p)
ok = ok && checkAndInitOutput(outputFormat, &p)
ok = ok && checkAndInitReportPath(reportPath, &p)
ok = ok && checkAndInitCheckSet(config, checkset, &p)
return p, ok
}
func checkAndInitCheckSet(config *conf.CheckConfig, checkSetName string, p *runParams) bool {
if checkSetName == "All" {
p.checkItems = config.CheckItems
} else {
checkSet, exist := config.CheckSets[checkSetName]
if !exist {
checksetNames := make([]string, 0, len(config.CheckSets))
for name := range config.CheckSets {
checksetNames = append(checksetNames, name)
}
utils.PromptMsg("CheckSet %s does not exist, please choose from: %v", checkSetName, checksetNames)
return false
}
p.checkItems = checkSet.Items
p.checkSet = checkSet.Name
}
return true
}
func checkAndInitRootPath(rootPath string, p *runParams) bool {
rootPath = strings.TrimSpace(rootPath)
if rootPath != "" {
if fileInfo, err := os.Stat(rootPath); err != nil || fileInfo == nil {
utils.PromptMsg("Error: Root path %s cannot access, err: %s", rootPath, err.Error())
return false
} else if !fileInfo.IsDir() {
utils.PromptMsg("Error: Root path %s is not a directory", rootPath)
return false
} else {
hostnamePath := filepath.Join(rootPath, "/proc/sys/kernel/hostname")
if _, err := os.Stat(hostnamePath); err != nil {
utils.PromptMsg("Error: Root path %s is not a valid root path, err: %s", rootPath, err.Error())
return false
}
}
p.rootPath = rootPath
}
if utils.IsPodEnv() {
p.isInPods = true
if p.rootPath == "" {
utils.PromptMsg("Error: Root path must be specified when running in a container")
return false
}
}
return true
}
func checkAndInitOutput(outputFormat string, p *runParams) bool {
reporter, err := report.GetReporter(outputFormat)
if err != nil {
utils.PromptMsg("Unsupported report type: %s", outputFormat)
return false
}
p.reporter = reporter
return true
}
func checkAndInitReportPath(reportPath string, p *runParams) bool {
if reportPath == "" {
return true
}
if stat, err := os.Stat(reportPath); os.IsNotExist(err) {
if err := os.MkdirAll(reportPath, utils.DirModeOwnerReadWriteExec); err != nil {
utils.PromptMsg("Failed to create output directory: %s, error: %s", reportPath, err.Error())
return false
}
} else if !stat.IsDir() {
utils.PromptMsg("Output path %s is not a directory", reportPath)
return false
}
p.reportPath = reportPath
return true
}