From cadc4c92875956da6276c9ee921bf94f2f8b943e Mon Sep 17 00:00:00 2001 From: AngelaBriel Date: Fri, 10 Jun 2022 16:00:22 +0200 Subject: [PATCH 01/12] change 'saptune status' 'configured Solution' -> 'enabled Solution (NOTE LIST)' add 'applied Solution (applied NOTE LIST)' --- .github/workflows/saptune-ut.yml | 4 +-- actions/serviceacts.go | 43 ++++++++++++++-------- actions/serviceacts_test.go | 62 ++++++++++++++++---------------- actions/solutionacts.go | 15 ++------ app/app.go | 12 +++++++ ospackage/man/saptune.8 | 14 +++++--- 6 files changed, 86 insertions(+), 64 deletions(-) diff --git a/.github/workflows/saptune-ut.yml b/.github/workflows/saptune-ut.yml index b579b055..6b7b45cc 100644 --- a/.github/workflows/saptune-ut.yml +++ b/.github/workflows/saptune-ut.yml @@ -4,7 +4,7 @@ name: saptuneUnittst on: # Triggers the workflow on pull request events but only for the master and sle-12 branch push: - branches: [ saptune_test ] + branches: [ master, saptune_test ] pull_request: branches: [ master ] @@ -54,7 +54,7 @@ jobs: - name: Code Climate report coverage run: ./cc-test-reporter after-build --debug --prefix ${{ env.CC_PREFIX }} --exit-code $? - if: github.event_name != 'pull_request' + if: ${{ env.CC_TEST_REPORTER_ID }} - name: Stop and remove Docker Image run: | diff --git a/actions/serviceacts.go b/actions/serviceacts.go index 262293ad..f252192f 100644 --- a/actions/serviceacts.go +++ b/actions/serviceacts.go @@ -470,7 +470,7 @@ func getInfoTxt(action string, state bool) (string, string, bool, bool) { // printSapconfStatus prints status of sapconf.service func printSapconfStatus(writer io.Writer, jstat *system.JStatusServs) bool { scenabled := false - fmt.Fprintf(writer, "sapconf.service: ") + fmt.Fprintf(writer, "sapconf.service: ") if system.IsServiceAvailable(SapconfService) { stat := "" enabled, err := system.SystemctlIsEnabled(SapconfService) @@ -498,7 +498,7 @@ func printSapconfStatus(writer io.Writer, jstat *system.JStatusServs) bool { // printTunedStatus prints status of tuned.service func printTunedStatus(writer io.Writer, jstat *system.JStatusServs) { - fmt.Fprintf(writer, "tuned.service: ") + fmt.Fprintf(writer, "tuned.service: ") if system.IsServiceAvailable(TunedService) { stat := "" enabled, err := system.SystemctlIsEnabled(TunedService) @@ -532,13 +532,26 @@ func printTunedStatus(writer io.Writer, jstat *system.JStatusServs) { // printNoteAndSols prints all enabled/active notes and solutions func printNoteAndSols(writer io.Writer, tuneApp *app.App, jstat *system.JStatus) bool { notTuned := true - fmt.Fprintf(writer, "configured Solution: ") + fmt.Fprintf(writer, "enabled Solution: ") if len(tuneApp.TuneForSolutions) > 0 { - fmt.Fprintf(writer, "%s", tuneApp.TuneForSolutions[0]) + solName := tuneApp.TuneForSolutions[0] + fmt.Fprintf(writer, "%s (%s)", solName, strings.Join(tuneApp.AllSolutions[solName], ", ")) notTuned = false } fmt.Fprintf(writer, "\n") - fmt.Fprintf(writer, "manually enabled Notes: ") + fmt.Fprintf(writer, "applied Solution: ") + appliedSol := tuneApp.AppliedSolution() + if appliedSol != "" { + appliedSolNotes := []string{} + for _, note := range tuneApp.AllSolutions[appliedSol] { + if _, ok := tuneApp.IsNoteApplied(note); ok { + appliedSolNotes = append(appliedSolNotes, note) + } + } + fmt.Fprintf(writer, "%s (%s)", appliedSol, strings.Join(appliedSolNotes, ", ")) + } + fmt.Fprintf(writer, "\n") + fmt.Fprintf(writer, "additional enabled Notes: ") if len(tuneApp.TuneForNotes) > 0 { for _, noteID := range tuneApp.TuneForNotes { fmt.Fprintf(writer, noteID+" ") @@ -546,12 +559,12 @@ func printNoteAndSols(writer io.Writer, tuneApp *app.App, jstat *system.JStatus) notTuned = false } fmt.Fprintf(writer, "\n") - fmt.Fprintf(writer, "enabled Notes: ") + fmt.Fprintf(writer, "enabled Notes: ") if len(tuneApp.NoteApplyOrder) != 0 { fmt.Fprintf(writer, "%s", strings.Join(tuneApp.NoteApplyOrder, " ")) } fmt.Fprintf(writer, "\n") - fmt.Fprintf(writer, "applied Notes: ") + fmt.Fprintf(writer, "applied Notes: ") appliedNotes := tuneApp.AppliedNotes() fmt.Fprintf(writer, "%s", appliedNotes) fmt.Fprintf(writer, "\n") @@ -570,19 +583,19 @@ func printSaptuneVers(writer io.Writer, saptuneVersion string, jstat *system.JSt // because of the need of 'reproducible' builds, we can not use a // build date in the 'official' saptune binary, so 'RPMDate' will // report 'undef' - fmt.Fprintf(writer, "saptune package: '%s'", RPMVersion) + fmt.Fprintf(writer, "saptune package: '%s'", RPMVersion) if RPMDate != "undef" { fmt.Fprintf(writer, " (%s)", RPMDate) } fmt.Fprintf(writer, "\n") - fmt.Fprintf(writer, "configured version: '%s'\n", saptuneVersion) + fmt.Fprintf(writer, "configured version: '%s'\n", saptuneVersion) jstat.RPMVersion = RPMVersion jstat.SaptuneVersion = saptuneVersion } // printStagingStatus prints the status of the staging area func printStagingStatus(writer io.Writer, jstage *system.JStatusStaging) { - fmt.Fprintf(writer, "staging: ") + fmt.Fprintf(writer, "staging: ") stagingSwitch := getStagingFromConf() if stagingSwitch { fmt.Fprintf(writer, "enabled\n") @@ -590,8 +603,8 @@ func printStagingStatus(writer io.Writer, jstage *system.JStatusStaging) { fmt.Fprintf(writer, "disabled\n") } stNotes, stSols := listStageNotesAndSols() - fmt.Fprintf(writer, "staged Notes: %s\n", strings.Join(stNotes, " ")) - fmt.Fprintf(writer, "staged Solutions: %s\n", strings.Join(stSols, " ")) + fmt.Fprintf(writer, "staged Notes: %s\n", strings.Join(stNotes, " ")) + fmt.Fprintf(writer, "staged Solutions: %s\n", strings.Join(stSols, " ")) jstage.StagingEnabled = stagingSwitch jstage.StagedNotes = stNotes jstage.StagedSols = stSols @@ -603,7 +616,7 @@ func printSaptuneStatus(writer io.Writer, jstat *system.JStatusServs) (bool, boo remember := false saptuneStopped := false stenabled := false - fmt.Fprintf(writer, "saptune.service: ") + fmt.Fprintf(writer, "saptune.service: ") if system.IsServiceAvailable(SaptuneService) { stat := "" enabled, err := system.SystemctlIsEnabled(SaptuneService) @@ -637,7 +650,7 @@ func printSaptuneStatus(writer io.Writer, jstat *system.JStatusServs) (bool, boo func printSystemStatus(writer io.Writer, jstat *system.JStatus) bool { chkHint := false state, err := system.GetSystemState() - fmt.Fprintf(writer, "system state: %s\n", state) + fmt.Fprintf(writer, "system state: %s\n", state) jstat.SystemState = state if err != nil { chkHint = true @@ -649,7 +662,7 @@ func printSystemStatus(writer io.Writer, jstat *system.JStatus) bool { func printVirtStatus(writer io.Writer, jstat *system.JStatus) { vtype := system.GetVirtStatus() system.InfoLog("Following virtualized environment was detected: %s", vtype) - fmt.Fprintf(writer, "virtualization: %s\n", vtype) + fmt.Fprintf(writer, "virtualization: %s\n", vtype) jstat.VirtEnv = vtype } diff --git a/actions/serviceacts_test.go b/actions/serviceacts_test.go index a2f86058..ae779379 100644 --- a/actions/serviceacts_test.go +++ b/actions/serviceacts_test.go @@ -64,21 +64,22 @@ func TestDaemonActions(t *testing.T) { // Test DaemonActionStatus t.Run("DaemonActionStatus", func(t *testing.T) { daemonStatusMatchText := fmt.Sprintf(` -saptune.service: disabled/active -saptune package: 'undef' -configured version: '3' -configured Solution: sol1 -manually enabled Notes: 2205917 -enabled Notes: 2205917 -applied Notes: -staging: disabled -staged Notes: -staged Solutions: - -sapconf.service: not available -tuned.service: disabled/active (profile: '%s') -system state: running -virtualization: %s +saptune.service: disabled/active +saptune package: 'undef' +configured version: '3' +enabled Solution: sol1 +applied Solution: +additional enabled Notes: 2205917 +enabled Notes: 2205917 +applied Notes: +staging: disabled +staged Notes: +staged Solutions: + +sapconf.service: not available +tuned.service: disabled/active (profile: '%s') +system state: running +virtualization: %s Remember: if you wish to automatically activate the note's and solution's tuning options after a reboot, you must enable saptune.service by running: 'saptune service enable'. @@ -194,21 +195,22 @@ func TestServiceActions(t *testing.T) { // Test ServiceActionStatus t.Run("ServiceActionStatus", func(t *testing.T) { serviceStatusMatchText := fmt.Sprintf(` -saptune.service: disabled/active -saptune package: 'undef' -configured version: '3' -configured Solution: sol1 -manually enabled Notes: 2205917 -enabled Notes: 2205917 -applied Notes: -staging: disabled -staged Notes: -staged Solutions: - -sapconf.service: not available -tuned.service: disabled/active (profile: '%s') -system state: running -virtualization: %s +saptune.service: disabled/active +saptune package: 'undef' +configured version: '3' +enabled Solution: sol1 +applied Solution: +additional enabled Notes: 2205917 +enabled Notes: 2205917 +applied Notes: +staging: disabled +staged Notes: +staged Solutions: + +sapconf.service: not available +tuned.service: disabled/active (profile: '%s') +system state: running +virtualization: %s Remember: if you wish to automatically activate the note's and solution's tuning options after a reboot, you must enable saptune.service by running: 'saptune service enable'. diff --git a/actions/solutionacts.go b/actions/solutionacts.go index d0ceca62..5cc510de 100644 --- a/actions/solutionacts.go +++ b/actions/solutionacts.go @@ -220,18 +220,9 @@ func SolutionActionEnabled(writer io.Writer, tuneApp *app.App) { // SolutionActionApplied prints out the applied solution func SolutionActionApplied(writer io.Writer, tuneApp *app.App) { - solName := "" - if len(tuneApp.TuneForSolutions) != 0 { - solName = tuneApp.TuneForSolutions[0] - if state, ok := tuneApp.IsSolutionApplied(solName); ok { - if state == "partial" { - fmt.Fprintf(writer, "%s (partial)", solName) - } else { - fmt.Fprintf(writer, "%s", solName) - } - } - } - system.Jcollect(strings.Split(solName, " ")) + solApplied := tuneApp.AppliedSolution() + fmt.Fprintf(writer, "%s", solApplied) + system.Jcollect(strings.Split(solApplied, " ")) } // SolutionActionCustomise creates an override file and allows to editing the diff --git a/app/app.go b/app/app.go index a0110563..d41fe872 100644 --- a/app/app.go +++ b/app/app.go @@ -205,6 +205,18 @@ func (app *App) IsSolutionApplied(sol string) (string, bool) { return state, ret } +// AppliedSolution returns the currently applied Solution +func (app *App) AppliedSolution() string { + var solApplied string + if len(app.TuneForSolutions) != 0 { + solName := app.TuneForSolutions[0] + if _, ok := app.IsSolutionApplied(solName); ok { + solApplied = solName + } + } + return solApplied +} + // NoteSanityCheck checks, if for all notes listed in // NoteApplyOrder and TuneForNotes a note definition file exists. // if not, remove the NoteID from the variables, save the new config and diff --git a/ospackage/man/saptune.8 b/ospackage/man/saptune.8 index 4a206c22..7a3c65f7 100644 --- a/ospackage/man/saptune.8 +++ b/ospackage/man/saptune.8 @@ -120,17 +120,21 @@ saptune package version .IP \[bu] configured saptune major version (from \fI/etc/sysconfig/saptune\fP) .IP \[bu] -configured Solution +enabled Solution .br -The entry 'configured Solution' shows the Solution, which was manually applied by '\fIsaptune solution apply \fP'. +The entry 'enabled Solution' shows the Solution, which was manually applied by '\fIsaptune solution apply \fP' and it's related Notes. .IP \[bu] -manually enabled Notes, sorted lexicographically +applied Solution .br -The entry 'manually enabled Notes' shows all Notes, which were manually applied by '\fIsaptune note apply \fP'. They are \fBone\fP part of the list of notes in the entry 'applied Notes' and 'enabled Notes'. +The entry 'applied Solution' shows the Solution, which is currently applied and it's related and applied Notes. +.IP \[bu] +additional enabled Notes, sorted lexicographically +.br +The entry 'additional enabled Notes' shows all Notes, which were additional manually applied by '\fIsaptune note apply \fP'. They are \fBone\fP part of the list of notes in the entry 'applied Notes' and 'enabled Notes'. .IP \[bu] all selected Notes in applied order .br -The list of 'enabled Notes' includes all Notes from 'manually enabled Notes' and additional all the Notes related to the 'configured Solution' too. The list shows the order in which these Notes were applied and will be re-applied after a system reboot, if the \fBsaptune.service\fP is enabled. +The list of 'enabled Notes' includes all Notes from 'additional enabled Notes' and additional all the Notes related to the 'enabled Solution' too. The list shows the order in which these Notes were applied and will be re-applied after a system reboot, if the \fBsaptune.service\fP is enabled. .IP \[bu] all currently applied Notes in applied order .br From 39f80a7637e477de686c8fc88c7dc7fa3aafeb67 Mon Sep 17 00:00:00 2001 From: AngelaBriel Date: Fri, 10 Jun 2022 16:13:53 +0200 Subject: [PATCH 02/12] adapt unittest for 'saptune status' --- actions/serviceacts_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/actions/serviceacts_test.go b/actions/serviceacts_test.go index ae779379..1efb0a8c 100644 --- a/actions/serviceacts_test.go +++ b/actions/serviceacts_test.go @@ -67,7 +67,7 @@ func TestDaemonActions(t *testing.T) { saptune.service: disabled/active saptune package: 'undef' configured version: '3' -enabled Solution: sol1 +enabled Solution: sol1 (simpleNote) applied Solution: additional enabled Notes: 2205917 enabled Notes: 2205917 @@ -198,7 +198,7 @@ func TestServiceActions(t *testing.T) { saptune.service: disabled/active saptune package: 'undef' configured version: '3' -enabled Solution: sol1 +enabled Solution: sol1 (simpleNote) applied Solution: additional enabled Notes: 2205917 enabled Notes: 2205917 From 91035316e3194b6e4f961b0788e885b1ea2d2e46 Mon Sep 17 00:00:00 2001 From: AngelaBriel Date: Fri, 10 Jun 2022 17:51:49 +0200 Subject: [PATCH 03/12] add json output for 'saptune note verify' and 'saptune solution verify' and adjust 'saptune status' --- actions/actions.go | 7 ++++- actions/noteacts.go | 11 +++++-- actions/serviceacts.go | 20 ++++++++++-- actions/solutionacts.go | 6 ++-- actions/table.go | 68 ++++++++++++++++++++++++++++++++++++----- system/json.go | 59 ++++++++++++++++++++++++++++++++--- 6 files changed, 152 insertions(+), 19 deletions(-) diff --git a/actions/actions.go b/actions/actions.go index d92a878d..716d4792 100644 --- a/actions/actions.go +++ b/actions/actions.go @@ -139,6 +139,7 @@ func rememberMessage(writer io.Writer) { // VerifyAllParameters Verify that all system parameters do not deviate from any of the enabled solutions/notes. func VerifyAllParameters(writer io.Writer, tuneApp *app.App) { + result := system.JPNotes{} if len(tuneApp.NoteApplyOrder) == 0 { fmt.Fprintf(writer, "No notes or solutions enabled, nothing to verify.\n") } else { @@ -146,8 +147,12 @@ func VerifyAllParameters(writer io.Writer, tuneApp *app.App) { if err != nil { system.ErrorExit("Failed to inspect the current system: %v", err) } - PrintNoteFields(writer, "NONE", comparisons, true) + PrintNoteFields(writer, "NONE", comparisons, true, &result) tuneApp.PrintNoteApplyOrder(writer) + result.NotesOrder = tuneApp.NoteApplyOrder + sysComp := len(unsatisfiedNotes) == 0 + result.SysCompliance = &sysComp + system.Jcollect(result) if len(unsatisfiedNotes) == 0 { fmt.Fprintf(writer, "%s%sThe running system is currently well-tuned according to all of the enabled notes.%s%s\n", setGreenText, setBoldText, resetBoldText, resetTextColor) } else { diff --git a/actions/noteacts.go b/actions/noteacts.go index 5864fb43..4c62a5c1 100644 --- a/actions/noteacts.go +++ b/actions/noteacts.go @@ -151,6 +151,8 @@ func NoteActionVerify(writer io.Writer, noteID string, tuneApp *app.App) { if noteID == "" { VerifyAllParameters(writer, tuneApp) } else { + result := system.JPNotes{} + // Check system parameters against the specified note, no matter the note has been tuned for or not. conforming, comparisons, _, err := tuneApp.VerifyNote(noteID) if err != nil { @@ -158,8 +160,11 @@ func NoteActionVerify(writer io.Writer, noteID string, tuneApp *app.App) { } noteComp := make(map[string]map[string]note.FieldComparison) noteComp[noteID] = comparisons - PrintNoteFields(writer, "HEAD", noteComp, true) + PrintNoteFields(writer, "HEAD", noteComp, true, &result) tuneApp.PrintNoteApplyOrder(writer) + result.NotesOrder = tuneApp.NoteApplyOrder + result.SysCompliance = &conforming + system.Jcollect(result) if !conforming { system.ErrorExit("The parameters listed above have deviated from the specified note.\n", "colorPrint", setRedText, setBoldText, resetBoldText, resetTextColor) } else { @@ -171,6 +176,7 @@ func NoteActionVerify(writer io.Writer, noteID string, tuneApp *app.App) { // NoteActionSimulate shows all changes that will be applied to the system if // the Note will be applied. func NoteActionSimulate(writer io.Writer, noteID string, tuneApp *app.App) { + result := system.JPNotes{} if noteID == "" { PrintHelpAndExit(writer, 1) } @@ -181,7 +187,8 @@ func NoteActionSimulate(writer io.Writer, noteID string, tuneApp *app.App) { fmt.Fprintf(writer, "If you run `saptune note apply %s`, the following changes will be applied to your system:\n", noteID) noteComp := make(map[string]map[string]note.FieldComparison) noteComp[noteID] = comparisons - PrintNoteFields(writer, "HEAD", noteComp, false) + PrintNoteFields(writer, "HEAD", noteComp, false, &result) + result.SysCompliance = nil } } diff --git a/actions/serviceacts.go b/actions/serviceacts.go index f252192f..3ea3022f 100644 --- a/actions/serviceacts.go +++ b/actions/serviceacts.go @@ -533,16 +533,17 @@ func printTunedStatus(writer io.Writer, jstat *system.JStatusServs) { func printNoteAndSols(writer io.Writer, tuneApp *app.App, jstat *system.JStatus) bool { notTuned := true fmt.Fprintf(writer, "enabled Solution: ") + solName := "" if len(tuneApp.TuneForSolutions) > 0 { - solName := tuneApp.TuneForSolutions[0] + solName = tuneApp.TuneForSolutions[0] fmt.Fprintf(writer, "%s (%s)", solName, strings.Join(tuneApp.AllSolutions[solName], ", ")) notTuned = false } fmt.Fprintf(writer, "\n") fmt.Fprintf(writer, "applied Solution: ") appliedSol := tuneApp.AppliedSolution() + appliedSolNotes := []string{} if appliedSol != "" { - appliedSolNotes := []string{} for _, note := range tuneApp.AllSolutions[appliedSol] { if _, ok := tuneApp.IsNoteApplied(note); ok { appliedSolNotes = append(appliedSolNotes, note) @@ -571,7 +572,22 @@ func printNoteAndSols(writer io.Writer, tuneApp *app.App, jstat *system.JStatus) if appliedNotes != "" { jstat.AppliedNotes = strings.Split(appliedNotes, " ") } + if appliedSol != "" { + jstat.AppliedSol = strings.Split(appliedSol, " ") + appSolNotes := system.JSol{ + SolName: appliedSol, + NotesList: appliedSolNotes, + } + jstat.AppliedSolNotes = append(jstat.AppliedSolNotes, appSolNotes) + } jstat.ConfiguredSol = tuneApp.TuneForSolutions + if solName != "" { + confSolNotes := system.JSol{ + SolName: solName, + NotesList: tuneApp.AllSolutions[solName], + } + jstat.ConfSolNotes = append(jstat.ConfSolNotes, confSolNotes) + } jstat.ConfiguredNotes = tuneApp.TuneForNotes jstat.EnabledNotes = tuneApp.NoteApplyOrder return notTuned diff --git a/actions/solutionacts.go b/actions/solutionacts.go index 5cc510de..d08f5a43 100644 --- a/actions/solutionacts.go +++ b/actions/solutionacts.go @@ -160,12 +160,13 @@ func SolutionActionVerify(writer io.Writer, solName string, tuneApp *app.App) { if solName == "" { VerifyAllParameters(writer, tuneApp) } else { + result := system.JPNotes{} // Check system parameters against the specified solution, no matter the solution has been tuned for or not. unsatisfiedNotes, comparisons, err := tuneApp.VerifySolution(solName) if err != nil { system.ErrorExit("Failed to test the current system against the specified SAP solution: %v", err) } - PrintNoteFields(writer, "NONE", comparisons, true) + PrintNoteFields(writer, "NONE", comparisons, true, &result) if len(unsatisfiedNotes) == 0 { fmt.Fprintf(writer, "%s%sThe system fully conforms to the tuning guidelines of the specified SAP solution.%s%s\n", setGreenText, setBoldText, resetBoldText, resetTextColor) } else { @@ -177,6 +178,7 @@ func SolutionActionVerify(writer io.Writer, solName string, tuneApp *app.App) { // SolutionActionSimulate shows all changes that will be applied to the system if // the solution will be applied. func SolutionActionSimulate(writer io.Writer, solName string, tuneApp *app.App) { + result := system.JPNotes{} if solName == "" { PrintHelpAndExit(writer, 1) } @@ -185,7 +187,7 @@ func SolutionActionSimulate(writer io.Writer, solName string, tuneApp *app.App) system.ErrorExit("Failed to test the current system against the specified note: %v", err) } else { fmt.Fprintf(writer, "If you run `saptune solution apply %s`, the following changes will be applied to your system:\n", solName) - PrintNoteFields(writer, "NONE", comparisons, false) + PrintNoteFields(writer, "NONE", comparisons, false, &result) } } diff --git a/actions/table.go b/actions/table.go index dd7d2f04..4a7600bb 100644 --- a/actions/table.go +++ b/actions/table.go @@ -12,8 +12,7 @@ import ( ) // PrintNoteFields Print mismatching fields in the note comparison result. -func PrintNoteFields(writer io.Writer, header string, noteComparisons map[string]map[string]note.FieldComparison, printComparison bool) { - +func PrintNoteFields(writer io.Writer, header string, noteComparisons map[string]map[string]note.FieldComparison, printComparison bool, result *system.JPNotes) { // initialise colFormat := "" colCompliant := "" @@ -25,8 +24,9 @@ func PrintNoteFields(writer io.Writer, header string, noteComparisons map[string override := "" comment := "" hasDiff := false - pAct := "NA" pExp := "" + noteLine := system.JPNotesLine{} + noteList := []system.JPNotesLine{} colorScheme := getColorScheme() @@ -65,11 +65,13 @@ func PrintNoteFields(writer io.Writer, header string, noteComparisons map[string } // print table body + // define variable pAct here and not at the beginning as we need + // the 'address' of the variable some lines below + pAct := strings.Replace(comparison.ActualValueJS, "\t", " ", -1) if comparison.ActualValueJS == "PNA" { pAct = "NA" - } else { - pAct = strings.Replace(comparison.ActualValueJS, "\t", " ", -1) } + noteLine.ActValue = &pAct pExp = strings.Replace(comparison.ExpectedValueJS, "\t", " ", -1) if printComparison { // verify @@ -83,9 +85,57 @@ func PrintNoteFields(writer io.Writer, header string, noteComparisons map[string // simulate fmt.Fprintf(writer, format, comparison.ReflectMapKey, pAct, pExp, override, comment) } + noteLine = collectMRO(noteLine, compliant, noteID, noteComparisons, comparison, pExp, override, printComparison, comment, footnote, pAct) + noteList = append(noteList, noteLine) } + // print footer - printTableFooter(writer, header, footnote, reminder, hasDiff) + reminderList := []system.JPNotesRemind{} + printTableFooter(writer, header, footnote, reminder, hasDiff, &reminderList) + result.Verifications = noteList + result.Attentions = reminderList +} + +// collectMRO collects the data for machine readable output +// parameter: noteLine, compliant, noteID, noteComparisons, comparison, +// pExp, override, printComparison, comment, footnote, pAct +// Attention - order of parameter important! +func collectMRO(stuff ...interface{}) system.JPNotesLine { + nLine := stuff[0].(system.JPNotesLine) + noteComp := !strings.Contains(stuff[1].(string), "no") + nLine.NoteID = stuff[2].(string) + nLine.NoteVers = txtparser.GetINIFileVersionSectionEntry(stuff[3].(map[string]map[string]note.FieldComparison)[stuff[2].(string)]["ConfFilePath"].ActualValue.(string), "version") + nLine.Parameter = stuff[4].(note.FieldComparison).ReflectMapKey + nLine.ExpValue = stuff[5].(string) + nLine.OverValue = stuff[6].(string) + nLine.Compliant = ¬eComp + + if strings.Contains(stuff[1].(string), "-") { + nLine.Compliant = nil + } + if stuff[7].(bool) { + // verify + noteFNs := []system.JFootNotes{} + fns := system.JFootNotes{} + for _, fn := range strings.Fields(stuff[8].(string)) { + indx := fn[1:len(fn)-1] + idx, _ := strconv.Atoi(indx) + if idx > 0 { + fns.FNoteNumber = idx + fns.FNoteTxt = stuff[9].([]string)[idx - 1] + } + noteFNs = append(noteFNs, fns) + } + nLine.Footnotes = noteFNs + nLine.Comment = "" + } else { + // simulate + nLine.Comment = stuff[8].(string) + } + if stuff[10].(string) == "NA" || stuff[10].(string) == "PNA" || stuff[10].(string) == "all:none" { + nLine.ActValue = nil + } + return nLine } // sortNoteComparisonsOutput sorts the output of the Note comparison @@ -201,7 +251,7 @@ func printTableHeader(writer io.Writer, format string, col0, col1, col2, col3, c // printTableFooter prints the footer of the table // footnotes and reminder section -func printTableFooter(writer io.Writer, header string, footnote []string, reminder map[string]string, hasDiff bool) { +func printTableFooter(writer io.Writer, header string, footnote []string, reminder map[string]string, hasDiff bool, noteReminder *[]system.JPNotesRemind) { if header != "NONE" && !hasDiff { fmt.Fprintf(writer, "\n (no change)\n") } @@ -211,11 +261,15 @@ func printTableFooter(writer io.Writer, header string, footnote []string, remind } } fmt.Fprintf(writer, "\n\n") + noteRem := system.JPNotesRemind{} for noteID, reminde := range reminder { if reminde != "" { reminderHead := fmt.Sprintf("Attention for SAP Note %s:\nHints or values not yet handled by saptune. So please read carefully, check and set manually, if needed:\n", noteID) fmt.Fprintf(writer, "%s\n", setRedText+reminderHead+reminde+resetTextColor) + noteRem.NoteID = noteID + noteRem.NoteReminder = reminderHead+reminde } + *noteReminder = append(*noteReminder, noteRem) } } diff --git a/system/json.go b/system/json.go index c49c75c8..50971961 100644 --- a/system/json.go +++ b/system/json.go @@ -9,7 +9,7 @@ import ( ) var schemaDir = "file:///usr/share/saptune/schemas/1.0/" -var supportedRAC = map[string]bool{"daemon start": false, "daemon status": true, "daemon stop": false, "service start": false, "service status": true, "service stop": false, "service restart": false, "service takeover": false, "service enable": false, "service disable": false, "service enablestart": false, "service disablestop": false, "note list": true, "note revertall": false, "note enabled": true, "note applied": true, "note apply": false, "note simulate": false, "note customise": false, "note create": false, "note edit": false, "note revert": false, "note show": false, "note delete": false, "note verify": false, "note rename": false, "solution list": true, "solution verify": false, "solution enabled": true, "solution applied": true, "solution apply": false, "solution simulate": false, "solution customise": false, "solution create": false, "solution edit": false, "solution revert": false, "solution show": false, "solution delete": false, "solution rename": false, "staging status": false, "staging enable": false, "staging disable": false, "staging is-enabled": false, "staging list": false, "staging diff": false, "staging analysis": false, "staging release": false, "revert all": false, "lock remove": false, "check": false, "status": true, "version": true, "help": false} +var supportedRAC = map[string]bool{"daemon start": false, "daemon status": true, "daemon stop": false, "service start": false, "service status": true, "service stop": false, "service restart": false, "service takeover": false, "service enable": false, "service disable": false, "service enablestart": false, "service disablestop": false, "note list": true, "note revertall": false, "note enabled": true, "note applied": true, "note apply": false, "note simulate": false, "note customise": false, "note create": false, "note edit": false, "note revert": false, "note show": false, "note delete": false, "note verify": true, "note rename": false, "solution list": true, "solution verify": true, "solution enabled": true, "solution applied": true, "solution apply": false, "solution simulate": false, "solution customise": false, "solution create": false, "solution edit": false, "solution revert": false, "solution show": false, "solution delete": false, "solution rename": false, "staging status": false, "staging enable": false, "staging disable": false, "staging is-enabled": false, "staging list": false, "staging diff": false, "staging analysis": false, "staging release": false, "revert all": false, "lock remove": false, "check": false, "status": true, "version": true, "help": false} // jentry is the json entry to display var jentry JEntry @@ -78,7 +78,7 @@ type configuredSol struct { } //type configuredSol struct { - //ConfiguredSol JObj `json:"enabled Solution"` +//ConfiguredSol JObj `json:"enabled Solution"` //} // appliedSol is for 'saptune solution applied' @@ -117,6 +117,48 @@ type JNoteList struct { Msg string `json:"remember message"` } +// JPNotesLine one row of 'saptune note verify|simulate' +// from PrintNoteFields +type JPNotesLine struct { + NoteID string `json:"Note ID,omitempty"` + NoteVers string `json:"Note version,omitempty"` + Parameter string `json:"parameter"` + ExpValue string `json:"expected value,omitempty"` + OverValue string `json:"override value,omitempty"` + ActValue *string `json:"actual value,omitempty"` + Compliant *bool `json:"compliant,omitempty"` + Comment string `json:"comment,omitempty"` + Footnotes []JFootNotes `json:"amendments,omitempty"` +} + +// JFootNotes collects the footnotes per parameter +type JFootNotes struct { + FNoteNumber int `json:"index,omitempty"` + FNoteTxt string `json:"text,omitempty"` +} + +// JPNotesRemind is the reminder section +type JPNotesRemind struct { + NoteID string `json:"Note ID,omitempty"` + NoteReminder string `json:"attention,omitempty"` +} + +// JPrintNotes is the whole 'PrintNoteFields' function +// if we need to differ between 'verify' and 'simulate' this +// can be done in PrintNoteFields' or in jcollect. +type JPNotes struct { + Verifications []JPNotesLine `json:"verifications"` + Attentions []JPNotesRemind `json:"attentions,omitempty"` + NotesOrder []string `json:"enabled Notes,omitempty"` + SysCompliance *bool `json:"system compliance,omitempty"` +} + +// JSol - Solution name and related Note list +type JSol struct { + SolName string `json:"Solution name"` + NotesList []string `json:"Note list"` +} + // JStatus is the whole 'saptune status' type JStatus struct { Services JStatusServs `json:"services"` @@ -124,7 +166,10 @@ type JStatus struct { VirtEnv string `json:"virtualization"` SaptuneVersion string `json:"configured version"` RPMVersion string `json:"package version"` - ConfiguredSol []string `json:"enabled Solution"` + ConfiguredSol []string `json:"Solution enabled"` + ConfSolNotes []JSol `json:"Notes enabled by Solution"` + AppliedSol []string `json:"Solution applied"` + AppliedSolNotes []JSol `json:"Notes applied by Solution"` ConfiguredNotes []string `json:"Notes configured"` EnabledNotes []string `json:"Notes enabled"` AppliedNotes []string `json:"Notes applied"` @@ -258,9 +303,13 @@ func Jcollect(data interface{}) { if rac == "solution applied" { jentry.CmdResult = appliedSol{AppliedSol: res} } - case JSolList, JNoteList, JStatus: + case JSolList, JNoteList, JStatus, JPNotes: switch rac { - case "solution list", "note list", "status", "daemon status", "service status": + case "note simulate": + // if we add 'note simulate' we need to switch off/skip + // some fields in JPNotes + default: + //"solution list", "note list", "status", "daemon status", "service status", "note verify": jentry.CmdResult = res } default: From 4d6378d6f81066df15b2528a57918273cd09bf91 Mon Sep 17 00:00:00 2001 From: AngelaBriel Date: Fri, 10 Jun 2022 18:08:55 +0200 Subject: [PATCH 04/12] fix unittest for PrintNoteFields --- actions/serviceacts.go | 2 +- actions/table.go | 6 +++--- actions/table_test.go | 8 ++++---- system/json.go | 24 ++++++++++++------------ 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/actions/serviceacts.go b/actions/serviceacts.go index 3ea3022f..e0eb4777 100644 --- a/actions/serviceacts.go +++ b/actions/serviceacts.go @@ -583,7 +583,7 @@ func printNoteAndSols(writer io.Writer, tuneApp *app.App, jstat *system.JStatus) jstat.ConfiguredSol = tuneApp.TuneForSolutions if solName != "" { confSolNotes := system.JSol{ - SolName: solName, + SolName: solName, NotesList: tuneApp.AllSolutions[solName], } jstat.ConfSolNotes = append(jstat.ConfSolNotes, confSolNotes) diff --git a/actions/table.go b/actions/table.go index 4a7600bb..944ca133 100644 --- a/actions/table.go +++ b/actions/table.go @@ -118,11 +118,11 @@ func collectMRO(stuff ...interface{}) system.JPNotesLine { noteFNs := []system.JFootNotes{} fns := system.JFootNotes{} for _, fn := range strings.Fields(stuff[8].(string)) { - indx := fn[1:len(fn)-1] + indx := fn[1 : len(fn)-1] idx, _ := strconv.Atoi(indx) if idx > 0 { fns.FNoteNumber = idx - fns.FNoteTxt = stuff[9].([]string)[idx - 1] + fns.FNoteTxt = stuff[9].([]string)[idx-1] } noteFNs = append(noteFNs, fns) } @@ -267,7 +267,7 @@ func printTableFooter(writer io.Writer, header string, footnote []string, remind reminderHead := fmt.Sprintf("Attention for SAP Note %s:\nHints or values not yet handled by saptune. So please read carefully, check and set manually, if needed:\n", noteID) fmt.Fprintf(writer, "%s\n", setRedText+reminderHead+reminde+resetTextColor) noteRem.NoteID = noteID - noteRem.NoteReminder = reminderHead+reminde + noteRem.NoteReminder = reminderHead + reminde } *noteReminder = append(*noteReminder, noteRem) } diff --git a/actions/table_test.go b/actions/table_test.go index 19ee79af..c7207b10 100644 --- a/actions/table_test.go +++ b/actions/table_test.go @@ -193,25 +193,25 @@ func TestPrintNoteFields(t *testing.T) { t.Run("verify with header", func(t *testing.T) { buffer := bytes.Buffer{} - PrintNoteFields(&buffer, "HEAD", noteComp, true) + PrintNoteFields(&buffer, "HEAD", noteComp, true, nil) txt := buffer.String() checkCorrectMessage(t, txt, printMatchText1) }) t.Run("simulate with header", func(t *testing.T) { buffer := bytes.Buffer{} - PrintNoteFields(&buffer, "HEAD", noteComp, false) + PrintNoteFields(&buffer, "HEAD", noteComp, false, nil) txt := buffer.String() checkCorrectMessage(t, txt, printMatchText2) }) t.Run("verify without header", func(t *testing.T) { buffer := bytes.Buffer{} - PrintNoteFields(&buffer, "NONE", noteComp, true) + PrintNoteFields(&buffer, "NONE", noteComp, true, nil) txt := buffer.String() checkCorrectMessage(t, txt, printMatchText3) }) t.Run("simulate without header", func(t *testing.T) { buffer := bytes.Buffer{} - PrintNoteFields(&buffer, "NONE", noteComp, false) + PrintNoteFields(&buffer, "NONE", noteComp, false, nil) txt := buffer.String() checkCorrectMessage(t, txt, printMatchText4) }) diff --git a/system/json.go b/system/json.go index 50971961..9e048de5 100644 --- a/system/json.go +++ b/system/json.go @@ -120,15 +120,15 @@ type JNoteList struct { // JPNotesLine one row of 'saptune note verify|simulate' // from PrintNoteFields type JPNotesLine struct { - NoteID string `json:"Note ID,omitempty"` - NoteVers string `json:"Note version,omitempty"` - Parameter string `json:"parameter"` - ExpValue string `json:"expected value,omitempty"` - OverValue string `json:"override value,omitempty"` - ActValue *string `json:"actual value,omitempty"` - Compliant *bool `json:"compliant,omitempty"` - Comment string `json:"comment,omitempty"` - Footnotes []JFootNotes `json:"amendments,omitempty"` + NoteID string `json:"Note ID,omitempty"` + NoteVers string `json:"Note version,omitempty"` + Parameter string `json:"parameter"` + ExpValue string `json:"expected value,omitempty"` + OverValue string `json:"override value,omitempty"` + ActValue *string `json:"actual value,omitempty"` + Compliant *bool `json:"compliant,omitempty"` + Comment string `json:"comment,omitempty"` + Footnotes []JFootNotes `json:"amendments,omitempty"` } // JFootNotes collects the footnotes per parameter @@ -143,7 +143,7 @@ type JPNotesRemind struct { NoteReminder string `json:"attention,omitempty"` } -// JPrintNotes is the whole 'PrintNoteFields' function +// JPNotes is the whole 'PrintNoteFields' function // if we need to differ between 'verify' and 'simulate' this // can be done in PrintNoteFields' or in jcollect. type JPNotes struct { @@ -155,8 +155,8 @@ type JPNotes struct { // JSol - Solution name and related Note list type JSol struct { - SolName string `json:"Solution name"` - NotesList []string `json:"Note list"` + SolName string `json:"Solution name"` + NotesList []string `json:"Note list"` } // JStatus is the whole 'saptune status' From 34ceef1e4f91509a9abb18c982c62d692709573b Mon Sep 17 00:00:00 2001 From: AngelaBriel Date: Fri, 10 Jun 2022 18:16:45 +0200 Subject: [PATCH 05/12] fix unittest for PrintNoteFields --- actions/table.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/actions/table.go b/actions/table.go index 944ca133..2782a861 100644 --- a/actions/table.go +++ b/actions/table.go @@ -92,8 +92,10 @@ func PrintNoteFields(writer io.Writer, header string, noteComparisons map[string // print footer reminderList := []system.JPNotesRemind{} printTableFooter(writer, header, footnote, reminder, hasDiff, &reminderList) - result.Verifications = noteList - result.Attentions = reminderList + if result != nil { + result.Verifications = noteList + result.Attentions = reminderList + } } // collectMRO collects the data for machine readable output From cc69568f9615d0747108cd9ce0f69424e46b9c41 Mon Sep 17 00:00:00 2001 From: AngelaBriel Date: Tue, 5 Jul 2022 16:31:45 +0200 Subject: [PATCH 06/12] fix the json output for 'saptune service reload', 'saptune service apply', 'saptune service revert' and 'saptune help' (jsc#SLE-23696) --- main.go | 1 + system/json.go | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index 2dbcecd3..e5b4fe79 100644 --- a/main.go +++ b/main.go @@ -45,6 +45,7 @@ func main() { system.ErrorExit("", 0) } if arg1 == "help" || system.IsFlagSet("help") { + system.JnotSupportedYet() actions.PrintHelpAndExit(os.Stdout, 0) } if arg1 == "" { diff --git a/system/json.go b/system/json.go index 9e048de5..9b6a17eb 100644 --- a/system/json.go +++ b/system/json.go @@ -9,7 +9,7 @@ import ( ) var schemaDir = "file:///usr/share/saptune/schemas/1.0/" -var supportedRAC = map[string]bool{"daemon start": false, "daemon status": true, "daemon stop": false, "service start": false, "service status": true, "service stop": false, "service restart": false, "service takeover": false, "service enable": false, "service disable": false, "service enablestart": false, "service disablestop": false, "note list": true, "note revertall": false, "note enabled": true, "note applied": true, "note apply": false, "note simulate": false, "note customise": false, "note create": false, "note edit": false, "note revert": false, "note show": false, "note delete": false, "note verify": true, "note rename": false, "solution list": true, "solution verify": true, "solution enabled": true, "solution applied": true, "solution apply": false, "solution simulate": false, "solution customise": false, "solution create": false, "solution edit": false, "solution revert": false, "solution show": false, "solution delete": false, "solution rename": false, "staging status": false, "staging enable": false, "staging disable": false, "staging is-enabled": false, "staging list": false, "staging diff": false, "staging analysis": false, "staging release": false, "revert all": false, "lock remove": false, "check": false, "status": true, "version": true, "help": false} +var supportedRAC = map[string]bool{"daemon start": false, "daemon status": true, "daemon stop": false, "service apply": false, "service start": false, "service status": true, "service stop": false, "service restart": false, "service revert": false, "service reload": false, "service takeover": false, "service enable": false, "service disable": false, "service enablestart": false, "service disablestop": false, "note list": true, "note revertall": false, "note enabled": true, "note applied": true, "note apply": false, "note simulate": false, "note customise": false, "note create": false, "note edit": false, "note revert": false, "note show": false, "note delete": false, "note verify": true, "note rename": false, "solution list": true, "solution verify": true, "solution enabled": true, "solution applied": true, "solution apply": false, "solution simulate": false, "solution customise": false, "solution create": false, "solution edit": false, "solution revert": false, "solution show": false, "solution delete": false, "solution rename": false, "staging status": false, "staging enable": false, "staging disable": false, "staging is-enabled": false, "staging list": false, "staging diff": false, "staging analysis": false, "staging release": false, "revert all": false, "lock remove": false, "check": false, "status": true, "version": true, "help": false} // jentry is the json entry to display var jentry JEntry From 247cfbfb0c92a28e2535bb5a535cc5cfeb71bb0f Mon Sep 17 00:00:00 2001 From: AngelaBriel Date: Tue, 19 Jul 2022 15:57:59 +0200 Subject: [PATCH 07/12] adapt the latest json schema changes and related changes to the saptune actions --- actions/noteacts.go | 30 ++++++++++++++++++ actions/serviceacts.go | 23 +++++++++++--- actions/solutionacts.go | 39 ++++++++++++++++-------- actions/table.go | 38 +++++++++++++++-------- app/app.go | 63 +++++++++++++++++++++++++------------- system/json.go | 67 +++++++++++++++++++++++------------------ 6 files changed, 180 insertions(+), 80 deletions(-) diff --git a/actions/noteacts.go b/actions/noteacts.go index 4c62a5c1..92a8e94a 100644 --- a/actions/noteacts.go +++ b/actions/noteacts.go @@ -189,6 +189,7 @@ func NoteActionSimulate(writer io.Writer, noteID string, tuneApp *app.App) { noteComp[noteID] = comparisons PrintNoteFields(writer, "HEAD", noteComp, false, &result) result.SysCompliance = nil + system.Jcollect(result) } } @@ -418,6 +419,11 @@ func NoteActionRevert(writer io.Writer, noteID string, tuneApp *app.App) { if err := tuneApp.RevertNote(noteID, true); err != nil { system.ErrorExit("Failed to revert note %s: %v", noteID, err) } + // if a solution is enabled (available in the configuration), check, if + // this note is the last note in NoteApplyOrder, which is related to + // this solution. If yes, remove solution for the configuration. + solutionStillEnabled(tuneApp) + if ok { system.InfoLog("Parameters tuned by the note '%s' have been successfully reverted.", noteID) fmt.Fprintf(writer, "Parameters tuned by the note have been successfully reverted.\n") @@ -426,6 +432,30 @@ func NoteActionRevert(writer io.Writer, noteID string, tuneApp *app.App) { } } +// if a solution is enabled (available in the configuration), check, if +// this note is the last note in NoteApplyOrder, which is related to +// this solution. If yes, remove solution for the configuration. +func solutionStillEnabled(tuneApp *app.App) { + if len(tuneApp.TuneForSolutions) == 0 { + return + } + for _, sol := range tuneApp.TuneForSolutions { + solNoteAvail := false + for _, solNote := range tuneApp.AllSolutions[sol] { + if tuneApp.PositionInNoteApplyOrder(solNote) < 0 { + continue + } else { + solNoteAvail = true // sol weiter gültig + break + } + } + if !solNoteAvail { + system.InfoLog("The last, still enabled Note got reverted and removed from the configuration, so remove the enabled Solution from the configuration too.") + _ = tuneApp.RemoveSolFromConfig(sol) + } + } +} + // NoteActionEnabled lists all enabled Note definitions as list separated // by blanks func NoteActionEnabled(writer io.Writer, tuneApp *app.App) { diff --git a/actions/serviceacts.go b/actions/serviceacts.go index e0eb4777..024f67c5 100644 --- a/actions/serviceacts.go +++ b/actions/serviceacts.go @@ -532,6 +532,7 @@ func printTunedStatus(writer io.Writer, jstat *system.JStatusServs) { // printNoteAndSols prints all enabled/active notes and solutions func printNoteAndSols(writer io.Writer, tuneApp *app.App, jstat *system.JStatus) bool { notTuned := true + partial := false fmt.Fprintf(writer, "enabled Solution: ") solName := "" if len(tuneApp.TuneForSolutions) > 0 { @@ -541,7 +542,7 @@ func printNoteAndSols(writer io.Writer, tuneApp *app.App, jstat *system.JStatus) } fmt.Fprintf(writer, "\n") fmt.Fprintf(writer, "applied Solution: ") - appliedSol := tuneApp.AppliedSolution() + appliedSol, state := tuneApp.AppliedSolution() appliedSolNotes := []string{} if appliedSol != "" { for _, note := range tuneApp.AllSolutions[appliedSol] { @@ -549,7 +550,12 @@ func printNoteAndSols(writer io.Writer, tuneApp *app.App, jstat *system.JStatus) appliedSolNotes = append(appliedSolNotes, note) } } - fmt.Fprintf(writer, "%s (%s)", appliedSol, strings.Join(appliedSolNotes, ", ")) + if state == "partial" { + fmt.Fprintf(writer, "%s (%s -> %s)", appliedSol, strings.Join(appliedSolNotes, ", "), state) + partial = true + } else { + fmt.Fprintf(writer, "%s (%s)", appliedSol, strings.Join(appliedSolNotes, ", ")) + } } fmt.Fprintf(writer, "\n") fmt.Fprintf(writer, "additional enabled Notes: ") @@ -569,11 +575,20 @@ func printNoteAndSols(writer io.Writer, tuneApp *app.App, jstat *system.JStatus) appliedNotes := tuneApp.AppliedNotes() fmt.Fprintf(writer, "%s", appliedNotes) fmt.Fprintf(writer, "\n") + // initialise + jstat.ConfSolNotes = []system.JSol{} + jstat.AppliedNotes = []string{} + jstat.AppliedSol = []system.JAppliedSol{} + jstat.AppliedSolNotes = []system.JSol{} if appliedNotes != "" { jstat.AppliedNotes = strings.Split(appliedNotes, " ") } if appliedSol != "" { - jstat.AppliedSol = strings.Split(appliedSol, " ") + appSol := system.JAppliedSol{ + SolName: appliedSol, + Partial: partial, + } + jstat.AppliedSol = append(jstat.AppliedSol, appSol) appSolNotes := system.JSol{ SolName: appliedSol, NotesList: appliedSolNotes, @@ -667,7 +682,7 @@ func printSystemStatus(writer io.Writer, jstat *system.JStatus) bool { chkHint := false state, err := system.GetSystemState() fmt.Fprintf(writer, "system state: %s\n", state) - jstat.SystemState = state + jstat.SystemdSysState = state if err != nil { chkHint = true } diff --git a/actions/solutionacts.go b/actions/solutionacts.go index d08f5a43..7a611b74 100644 --- a/actions/solutionacts.go +++ b/actions/solutionacts.go @@ -107,7 +107,7 @@ func SolutionActionList(writer io.Writer, tuneApp *app.App) { format = " D" + format jsolutionListEntry.DepSol = true } - if i := sort.SearchStrings(tuneApp.TuneForSolutions, solName); i < len(tuneApp.TuneForSolutions) && tuneApp.TuneForSolutions[i] == solName { + if tuneApp.IsSolutionEnabled(solName) { // enabled solution format = " " + setGreenText + "*" + format jsolutionListEntry.SolEnabled = true @@ -188,6 +188,7 @@ func SolutionActionSimulate(writer io.Writer, solName string, tuneApp *app.App) } else { fmt.Fprintf(writer, "If you run `saptune solution apply %s`, the following changes will be applied to your system:\n", solName) PrintNoteFields(writer, "NONE", comparisons, false, &result) + system.Jcollect(result) } } @@ -222,9 +223,19 @@ func SolutionActionEnabled(writer io.Writer, tuneApp *app.App) { // SolutionActionApplied prints out the applied solution func SolutionActionApplied(writer io.Writer, tuneApp *app.App) { - solApplied := tuneApp.AppliedSolution() - fmt.Fprintf(writer, "%s", solApplied) - system.Jcollect(strings.Split(solApplied, " ")) + partial := false + solApplied, state := tuneApp.AppliedSolution() + if state == "partial" { + fmt.Fprintf(writer, "%s (partial)", solApplied) + partial = true + } else { + fmt.Fprintf(writer, "%s", solApplied) + } + appSol := system.JAppliedSol{ + SolName: solApplied, + Partial: partial, + } + system.Jcollect(appSol) } // SolutionActionCustomise creates an override file and allows to editing the @@ -262,8 +273,10 @@ func SolutionActionCustomise(writer io.Writer, customSol string, tuneApp *app.Ap } if changed { // check, if solution is active - applied - if i := sort.SearchStrings(tuneApp.TuneForSolutions, customSol); i < len(tuneApp.TuneForSolutions) && tuneApp.TuneForSolutions[i] == customSol { + if _, ok := tuneApp.IsSolutionApplied(customSol); ok { system.NoticeLog("Your just edited Solution is already applied. To get your changes to take effect, please 'revert' the Solution and apply again.\n") + } else if tuneApp.IsSolutionEnabled(customSol) { + system.NoticeLog("Your just edited Solution is enabled, but not applied yet. To get your changes to take effect, please apply the just edited Solution or start saptune.service\n") } else { system.NoticeLog("Do not forget to apply the just edited Solution to get your changes to take effect\n") } @@ -300,8 +313,10 @@ func SolutionActionEdit(writer io.Writer, customSol string, tuneApp *app.App) { } if changed { // check, if solution is active - applied - if i := sort.SearchStrings(tuneApp.TuneForSolutions, customSol); i < len(tuneApp.TuneForSolutions) && tuneApp.TuneForSolutions[i] == customSol { + if _, ok := tuneApp.IsSolutionApplied(customSol); ok { system.NoticeLog("Your just edited Solution is already applied. To get your changes to take effect, please 'revert' the Solution and apply again.\n") + } else if tuneApp.IsSolutionEnabled(customSol) { + system.NoticeLog("Your just edited Solution is enabled, but not applied yet. To get your changes to take effect, please apply the just edited Solution or start saptune.service\n") } else { system.NoticeLog("Do not forget to apply the just edited Solution to get your changes to take effect\n") } @@ -381,9 +396,9 @@ func SolutionActionDelete(reader io.Reader, writer io.Writer, solName string, tu fileName, extraSol := getFileName(solFName, SolutionSheets, ExtraTuningSheets) ovFileName, overrideSol := getovFile(solFName, OverrideTuningSheets) - // check, if solution is active - applied - if i := sort.SearchStrings(tuneApp.TuneForSolutions, solName); i < len(tuneApp.TuneForSolutions) && tuneApp.TuneForSolutions[i] == solName { - system.ErrorExit("The Solution file you want to delete is currently in use, which means the Solution is already applied.\nSo please 'revert' the Solution first and then try deleting again.") + // check, if solution is active - enabled + if tuneApp.IsSolutionEnabled(solName) { + system.ErrorExit("The Solution file you want to delete is currently in use, which means the Solution is already enabled/applied.\nSo please 'revert' the Solution first and then try deleting again.") } if !extraSol && !overrideSol { @@ -438,9 +453,9 @@ func SolutionActionRename(reader io.Reader, writer io.Writer, solName, newSolNam ovFileName, overrideSol := getovFile(solFName, OverrideTuningSheets) newovFileName := fmt.Sprintf("%s%s.sol", OverrideTuningSheets, newSolName) - // check, if solution is active - applied - if i := sort.SearchStrings(tuneApp.TuneForSolutions, solName); i < len(tuneApp.TuneForSolutions) && tuneApp.TuneForSolutions[i] == solName { - system.ErrorExit("The Solution definition file you want to rename is currently in use, which means the Solution is already applied.\nSo please 'revert' the Solution first and then try renaming again.") + // check, if solution is active - enabled + if tuneApp.IsSolutionEnabled(solName) { + system.ErrorExit("The Solution definition file you want to rename is currently in use, which means the Solution is already enabled/applied.\nSo please 'revert' the Solution first and then try renaming again.") } if extraSol && overrideSol { diff --git a/actions/table.go b/actions/table.go index 2782a861..6c6819eb 100644 --- a/actions/table.go +++ b/actions/table.go @@ -93,7 +93,15 @@ func PrintNoteFields(writer io.Writer, header string, noteComparisons map[string reminderList := []system.JPNotesRemind{} printTableFooter(writer, header, footnote, reminder, hasDiff, &reminderList) if result != nil { - result.Verifications = noteList + if printComparison { + // verify + result.Verifications = noteList + result.Simulations = []system.JPNotesLine{} + } else { + // simulate + result.Verifications = []system.JPNotesLine{} + result.Simulations = noteList + } result.Attentions = reminderList } } @@ -117,23 +125,27 @@ func collectMRO(stuff ...interface{}) system.JPNotesLine { } if stuff[7].(bool) { // verify - noteFNs := []system.JFootNotes{} - fns := system.JFootNotes{} - for _, fn := range strings.Fields(stuff[8].(string)) { - indx := fn[1 : len(fn)-1] - idx, _ := strconv.Atoi(indx) - if idx > 0 { - fns.FNoteNumber = idx - fns.FNoteTxt = stuff[9].([]string)[idx-1] - } - noteFNs = append(noteFNs, fns) - } - nLine.Footnotes = noteFNs nLine.Comment = "" } else { // simulate + nLine.NoteID = "" + nLine.NoteVers = "" nLine.Comment = stuff[8].(string) + nLine.Compliant = nil } + noteFNs := []system.JFootNotes{} + fns := system.JFootNotes{} + for _, fn := range strings.Fields(stuff[8].(string)) { + indx := fn[1 : len(fn)-1] + idx, _ := strconv.Atoi(indx) + if idx > 0 { + fns.FNoteNumber = idx + fns.FNoteTxt = stuff[9].([]string)[idx-1] + } + noteFNs = append(noteFNs, fns) + } + nLine.Footnotes = noteFNs + if stuff[10].(string) == "NA" || stuff[10].(string) == "PNA" || stuff[10].(string) == "all:none" { nLine.ActValue = nil } diff --git a/app/app.go b/app/app.go index d41fe872..029a573e 100644 --- a/app/app.go +++ b/app/app.go @@ -105,7 +105,7 @@ func (app *App) SaveConfig() error { // GetSortedSolutionEnabledNotes returns the number of all solution-enabled // SAP notes, sorted. func (app *App) GetSortedSolutionEnabledNotes() (allNoteIDs []string) { - allNoteIDs = make([]string, 0, 0) + allNoteIDs = make([]string, 0) for _, sol := range app.TuneForSolutions { for _, noteID := range app.AllSolutions[sol] { if i := sort.SearchStrings(allNoteIDs, noteID); !(i < len(allNoteIDs) && allNoteIDs[i] == noteID) { @@ -119,7 +119,7 @@ func (app *App) GetSortedSolutionEnabledNotes() (allNoteIDs []string) { // GetSortedAllNotes returns all SAP notes, sorted. func (app *App) GetSortedAllNotes() []string { - allNoteIDs := make([]string, 0, 0) + allNoteIDs := make([]string, 0) for noteID := range app.AllNotes { allNoteIDs = append(allNoteIDs, noteID) } @@ -178,6 +178,16 @@ func (app *App) IsNoteApplied(noteID string) (string, bool) { return rval, ret } +// IsSolutionEnabled returns true, if the solution is enabled or false, if not +// enbaled means - part of TuneForSolutions +func (app *App) IsSolutionEnabled(sol string) bool { + i := sort.SearchStrings(app.TuneForSolutions, sol) + if i < len(app.TuneForSolutions) && app.TuneForSolutions[i] == sol { + return true + } + return false +} + // IsSolutionApplied returns true, if the solution is (partial) applied // or false, if not func (app *App) IsSolutionApplied(sol string) (string, bool) { @@ -206,15 +216,17 @@ func (app *App) IsSolutionApplied(sol string) (string, bool) { } // AppliedSolution returns the currently applied Solution -func (app *App) AppliedSolution() string { - var solApplied string +func (app *App) AppliedSolution() (string, string) { + solApplied := "" + state := "" if len(app.TuneForSolutions) != 0 { solName := app.TuneForSolutions[0] - if _, ok := app.IsSolutionApplied(solName); ok { + if st, ok := app.IsSolutionApplied(solName); ok { solApplied = solName + state = st } } - return solApplied + return solApplied, state } // NoteSanityCheck checks, if for all notes listed in @@ -223,7 +235,7 @@ func (app *App) AppliedSolution() string { // inform the user func (app *App) NoteSanityCheck() error { // app.NoteApplyOrder, app.TuneForNotes - errs := make([]error, 0, 0) + errs := make([]error, 0) for _, note := range app.NoteApplyOrder { if _, exists := app.AllNotes[note]; exists { // note definition file for NoteID exists @@ -405,7 +417,7 @@ func (app *App) TuneNote(noteID string) error { // of tuned solution names. // If the solution covers any of the additional notes, those notes will be removed. func (app *App) TuneSolution(solName string) (removedExplicitNotes []string, err error) { - removedExplicitNotes = make([]string, 0, 0) + removedExplicitNotes = make([]string, 0) sol, err := app.GetSolutionByName(solName) if err != nil { return @@ -500,6 +512,18 @@ func (app *App) RevertNote(noteID string, permanent bool) error { return nil } +// RemoveSolFromConfig removes the given solution from the configuration +func (app *App) RemoveSolFromConfig(solName string) error { + i := sort.SearchStrings(app.TuneForSolutions, solName) + if i < len(app.TuneForSolutions) && app.TuneForSolutions[i] == solName { + app.TuneForSolutions = append(app.TuneForSolutions[0:i], app.TuneForSolutions[i+1:]...) + if err := app.SaveConfig(); err != nil { + return err + } + } + return nil +} + // RevertSolution permanently revert notes tuned by the solution and // clear their stored states. func (app *App) RevertSolution(solName string) error { @@ -508,13 +532,10 @@ func (app *App) RevertSolution(solName string) error { return err } // Remove from configuration - i := sort.SearchStrings(app.TuneForSolutions, solName) - if i < len(app.TuneForSolutions) && app.TuneForSolutions[i] == solName { - app.TuneForSolutions = append(app.TuneForSolutions[0:i], app.TuneForSolutions[i+1:]...) - if err := app.SaveConfig(); err != nil { - return err - } + if err := app.RemoveSolFromConfig(solName); err != nil { + return err } + // The tricky part: figure out which notes are to be reverted, do not revert manually enabled notes. notesDoNotRevert := make(map[string]struct{}) for _, noteID := range app.TuneForNotes { @@ -533,7 +554,7 @@ func (app *App) RevertSolution(solName string) error { } } // Now revert the (sol notes - manually enabled - other sol notes) - noteErrs := make([]error, 0, 0) + noteErrs := make([]error, 0) for _, noteID := range sol { if _, found := notesDoNotRevert[noteID]; found { continue // skip this one @@ -551,7 +572,7 @@ func (app *App) RevertSolution(solName string) error { // RevertAll revert all tuned parameters (both solutions and additional notes), // and clear stored states, but NOT NoteApplyOrder. func (app *App) RevertAll(permanent bool) error { - allErrs := make([]error, 0, 0) + allErrs := make([]error, 0) // Simply revert all notes from serialised states otherNotes, err := app.State.List() @@ -571,9 +592,9 @@ func (app *App) RevertAll(permanent bool) error { allErrs = append(allErrs, err) } if permanent { - app.TuneForNotes = make([]string, 0, 0) - app.TuneForSolutions = make([]string, 0, 0) - app.NoteApplyOrder = make([]string, 0, 0) + app.TuneForNotes = make([]string, 0) + app.TuneForSolutions = make([]string, 0) + app.NoteApplyOrder = make([]string, 0) if err := app.SaveConfig(); err != nil { allErrs = append(allErrs, err) } @@ -630,7 +651,7 @@ func (app *App) VerifyNote(noteID string) (conforming bool, comparisons map[stri // to all of the notes associated to the solution. // The note comparison results will always contain all fields from all notes. func (app *App) VerifySolution(solName string) (unsatisfiedNotes []string, comparisons map[string]map[string]note.FieldComparison, err error) { - unsatisfiedNotes = make([]string, 0, 0) + unsatisfiedNotes = make([]string, 0) comparisons = make(map[string]map[string]note.FieldComparison) sol, err := app.GetSolutionByName(solName) if err != nil { @@ -652,7 +673,7 @@ func (app *App) VerifySolution(solName string) (unsatisfiedNotes []string, compa // notes/solutions. // The note comparison results will always contain all fields from all notes. func (app *App) VerifyAll() (unsatisfiedNotes []string, comparisons map[string]map[string]note.FieldComparison, err error) { - unsatisfiedNotes = make([]string, 0, 0) + unsatisfiedNotes = make([]string, 0) comparisons = make(map[string]map[string]note.FieldComparison) for _, solName := range app.TuneForSolutions { // Collect field comparison results from solution notes diff --git a/system/json.go b/system/json.go index 9b6a17eb..613a2379 100644 --- a/system/json.go +++ b/system/json.go @@ -74,26 +74,33 @@ type versResults struct { // configuredSol is for 'saptune solution enabled' type configuredSol struct { - ConfiguredSol []string `json:"enabled Solution"` + ConfiguredSol []string `json:"Solution enabled"` } //type configuredSol struct { //ConfiguredSol JObj `json:"enabled Solution"` //} +// JAppliedSol is for 'Solution applied' in +// 'saptune status' and 'saptune solution applied' +type JAppliedSol struct { + SolName string `json:"Solution ID"` + Partial bool `json:"applied partially"` +} + // appliedSol is for 'saptune solution applied' type appliedSol struct { - AppliedSol []string `json:"applied Solution"` + AppliedSol []JAppliedSol `json:"Solution applied"` } // appliedNotes is for 'saptune note applied' type appliedNotes struct { - AppliedNotes []string `json:"applied Notes"` + AppliedNotes []string `json:"Notes applied"` } // notesOrder is for 'saptune note enabled' type notesOrder struct { - NotesOrder []string `json:"enabled Notes"` + NotesOrder []string `json:"Notes enabled"` } // JNoteListEntry is one line of 'saptune note list' @@ -112,8 +119,8 @@ type JNoteListEntry struct { // JNoteList is the whole 'saptune note list' type JNoteList struct { - NotesList []JNoteListEntry `json:"available Notes"` - NotesOrder []string `json:"enabled Notes"` + NotesList []JNoteListEntry `json:"Notes available"` + NotesOrder []string `json:"Notes enabled"` Msg string `json:"remember message"` } @@ -123,10 +130,10 @@ type JPNotesLine struct { NoteID string `json:"Note ID,omitempty"` NoteVers string `json:"Note version,omitempty"` Parameter string `json:"parameter"` + Compliant *bool `json:"compliant,omitempty"` ExpValue string `json:"expected value,omitempty"` OverValue string `json:"override value,omitempty"` ActValue *string `json:"actual value,omitempty"` - Compliant *bool `json:"compliant,omitempty"` Comment string `json:"comment,omitempty"` Footnotes []JFootNotes `json:"amendments,omitempty"` } @@ -134,7 +141,7 @@ type JPNotesLine struct { // JFootNotes collects the footnotes per parameter type JFootNotes struct { FNoteNumber int `json:"index,omitempty"` - FNoteTxt string `json:"text,omitempty"` + FNoteTxt string `json:"amendment,omitempty"` } // JPNotesRemind is the reminder section @@ -147,30 +154,31 @@ type JPNotesRemind struct { // if we need to differ between 'verify' and 'simulate' this // can be done in PrintNoteFields' or in jcollect. type JPNotes struct { - Verifications []JPNotesLine `json:"verifications"` + Verifications []JPNotesLine `json:"verifications,omitempty"` + Simulations []JPNotesLine `json:"simulations,omitempty"` Attentions []JPNotesRemind `json:"attentions,omitempty"` - NotesOrder []string `json:"enabled Notes,omitempty"` + NotesOrder []string `json:"Notes enabled,omitempty"` SysCompliance *bool `json:"system compliance,omitempty"` } // JSol - Solution name and related Note list type JSol struct { - SolName string `json:"Solution name"` + SolName string `json:"Solution ID"` NotesList []string `json:"Note list"` } // JStatus is the whole 'saptune status' type JStatus struct { Services JStatusServs `json:"services"` - SystemState string `json:"system state"` + SystemdSysState string `json:"systemd system state"` VirtEnv string `json:"virtualization"` SaptuneVersion string `json:"configured version"` RPMVersion string `json:"package version"` ConfiguredSol []string `json:"Solution enabled"` ConfSolNotes []JSol `json:"Notes enabled by Solution"` - AppliedSol []string `json:"Solution applied"` + AppliedSol []JAppliedSol `json:"Solution applied"` AppliedSolNotes []JSol `json:"Notes applied by Solution"` - ConfiguredNotes []string `json:"Notes configured"` + ConfiguredNotes []string `json:"Notes enabled additionally"` EnabledNotes []string `json:"Notes enabled"` AppliedNotes []string `json:"Notes applied"` Staging JStatusStaging `json:"staging"` @@ -179,9 +187,9 @@ type JStatus struct { // JStatusStaging contains the staging infos for 'saptune status' type JStatusStaging struct { - StagingEnabled bool `json:"enabled"` - StagedNotes []string `json:"staged Notes"` - StagedSols []string `json:"staged Solutions"` + StagingEnabled bool `json:"staging enabled"` + StagedNotes []string `json:"Notes staged"` + StagedSols []string `json:"Solutions staged"` } // JStatusServs are the mentioned systemd services in 'saptune status' @@ -194,7 +202,7 @@ type JStatusServs struct { // JSolListEntry is one line of 'saptune solution list' type JSolListEntry struct { - SolName string `json:"Solution name"` + SolName string `json:"Solution ID"` NotesList []string `json:"Note list"` SolEnabled bool `json:"Solution enabled"` SolOverride bool `json:"Solution override exists"` @@ -204,7 +212,7 @@ type JSolListEntry struct { // JSolList is the whole 'saptune solution list' type JSolList struct { - SolsList []JSolListEntry `json:"available Solutions"` + SolsList []JSolListEntry `json:"Solutions available"` Msg string `json:"remember message"` } @@ -300,18 +308,17 @@ func Jcollect(data interface{}) { if rac == "solution enabled" { jentry.CmdResult = configuredSol{ConfiguredSol: res} } - if rac == "solution applied" { - jentry.CmdResult = appliedSol{AppliedSol: res} - } + //if rac == "solution applied" { + // jentry.CmdResult = appliedSol{AppliedSol: res} + //} + case JAppliedSol: + // "saptune solution applied" + var appSol appliedSol + appSol.AppliedSol = append(appSol.AppliedSol, res) + jentry.CmdResult = appSol case JSolList, JNoteList, JStatus, JPNotes: - switch rac { - case "note simulate": - // if we add 'note simulate' we need to switch off/skip - // some fields in JPNotes - default: - //"solution list", "note list", "status", "daemon status", "service status", "note verify": - jentry.CmdResult = res - } + //"solution list", "note list", "status", "daemon status", "service status", "note verify", "solution verify", "note simulate", "solution simulate": + jentry.CmdResult = res default: WarningLog("Unknown data type '%T' for command '%s' in Jcollect, skipping", data, rac) } From 4a7015cef89701ae77c4b4c41ac2a6d26c70f6e7 Mon Sep 17 00:00:00 2001 From: AngelaBriel Date: Tue, 26 Jul 2022 16:19:58 +0200 Subject: [PATCH 08/12] fix various small bugs found during test --- actions/footnote.go | 7 +++++-- sap/solution/solution.go | 9 ++++---- system/argsAndFlags.go | 44 ++++++++++++++++++---------------------- system/cpu.go | 19 +++++++++++++---- system/daemon.go | 3 +-- system/logging.go | 1 + 6 files changed, 47 insertions(+), 36 deletions(-) diff --git a/actions/footnote.go b/actions/footnote.go index 34844248..985cb9cb 100644 --- a/actions/footnote.go +++ b/actions/footnote.go @@ -122,7 +122,9 @@ func setUsNa(actVal, compliant, comment string, footnote []string) (string, stri compliant = compliant + " [16]" compliant = strings.Replace(compliant, "no ", " - ", 1) comment = comment + " [16]" - footnote[15] = footnote16 + if !system.IsFlagSet("show-non-compliant") { + footnote[15] = footnote16 + } } return compliant, comment, footnote } @@ -165,7 +167,8 @@ func setSecBoot(mapKey, compliant, comment string, footnote []string) (string, s // setFLdiffs sets footnote for diffs in force_latency parameter func setFLdiffs(mapKey, compliant, comment, info string, footnote []string) (string, string, []string) { if mapKey == "force_latency" && info == "hasDiffs" { - compliant = "no [4]" + compliant = compliant + " [4]" + compliant = strings.Replace(compliant, " - ", "no ", 1) comment = comment + " [4]" footnote[3] = footnote4 } diff --git a/sap/solution/solution.go b/sap/solution/solution.go index b8df5321..4af102e3 100644 --- a/sap/solution/solution.go +++ b/sap/solution/solution.go @@ -60,9 +60,7 @@ func GetSolutionDefintion(solsDir, extraDir, noteDir string) map[string]map[stri solAllVals := getAllSolsFromDir(solsDir, "", "") // add custom solutions to the list custAllVals := getAllSolsFromDir(extraDir, noteDir, extraDir) - for _, p := range custAllVals { - solAllVals = append(solAllVals, p) - } + solAllVals = append(solAllVals, custAllVals...) for _, param := range solAllVals { if param.Section == "reminder" || param.Section == "version" { @@ -144,7 +142,10 @@ func GetOtherSolution(solsDir, noteFiles, extraFiles string) map[string]map[stri } // Do not allow customer/vendor to override built-in solutions if extra && IsShippedSolution(param.Key) { - system.WarningLog("extra solution '%s' will not override built-in solution implementation", param.Key) + // as the function most of the time is called + // before the logging is initialized use + // Fprintf instead to give customers a hint. + fmt.Fprintf(os.Stderr, "Warning: extra solution '%s' will not override built-in solution implementation\n",param.Key) continue } sol[param.Key] = strings.Split(param.Value, "\t") diff --git a/system/argsAndFlags.go b/system/argsAndFlags.go index adf4f4c5..fc20029b 100644 --- a/system/argsAndFlags.go +++ b/system/argsAndFlags.go @@ -118,7 +118,7 @@ func handleSimpleFlags(arg string, flags map[string]string) { // setUnsupportedFlag sets or appends a value to the unsupported Flag // collection of unsupported Flags func setUnsupportedFlag(val string, flags map[string]string) { - if flags["notSupported"] != "" { + if flags["notSupported"] == "" { flags["notSupported"] = flags["notSupported"] + val } else { flags["notSupported"] = flags["notSupported"] + " " + val @@ -156,14 +156,17 @@ func ChkCliSyntax() bool { // check command options if len(sArgs) < cmdOpt || (!IsFlagSet("force") && !IsFlagSet("dryrun") && !IsFlagSet("colorscheme") && !IsFlagSet("show-non-compliant")) { - // no command options set or too few options + // no command options set or too few options + // and/or non of the flags set, which need further checks + // so let the 'old' default checks (in main and/or actions) set + // the appropriate result return true } // saptune staging release [--force|--dry-run] [NOTE...|SOLUTION...|all] if !chkStagingReleaseSyntax(sArgs, realm, cmd, cmdOpt) { ret = false } - // saptune note verify [--coloscheme=] [--show-non-compliant] [NOTEID] + // saptune note verify [--colorscheme=] [--show-non-compliant] [NOTEID] if !chkNoteVerifySyntax(sArgs, realm, cmd, cmdOpt) { ret = false } @@ -188,7 +191,7 @@ func chkGlobalSyntax(stArgs []string, pglob, pcopt int) bool { } // check global option - // saptune -out=FORMAT + // saptune -format=FORMAT // && !strings.HasPrefix(sArgs[1], "-o=" if IsFlagSet("format") && !strings.Contains(stArgs[pglob], "--format") { ret = false @@ -202,7 +205,7 @@ func chkGlobalSyntax(stArgs []string, pglob, pcopt int) bool { func chkStagingReleaseSyntax(stArgs []string, prealm, pcmd, popt int) bool { ret := true if IsFlagSet("dryrun") || IsFlagSet("force") { - if stArgs[prealm] != "staging" && stArgs[pcmd] != "release" { + if !(stArgs[prealm] == "staging" && stArgs[pcmd] == "release") { ret = false } if stArgs[popt] != "--dry-run" && stArgs[popt] != "--force" { @@ -214,32 +217,25 @@ func chkStagingReleaseSyntax(stArgs []string, prealm, pcmd, popt int) bool { // chkNoteVerifySyntax checks the syntax of 'saptune note verify' command line // regarding command line options -// saptune note verify [--coloscheme=] [--show-non-compliant] [NOTEID] +// saptune note verify [--colorscheme=] [--show-non-compliant] [NOTEID] func chkNoteVerifySyntax(stArgs []string, prealm, pcmd, popt int) bool { ret := true - if IsFlagSet("colorscheme") { - if stArgs[prealm] != "note" && stArgs[pcmd] != "verify" { - ret = false - } - if IsFlagSet("show-non-compliant") { - if !strings.HasPrefix(stArgs[popt], "--colorscheme=") && !strings.HasPrefix(stArgs[popt+1], "--colorscheme=") { - ret = false - } - } else if !strings.HasPrefix(stArgs[popt], "--colorscheme=") { + if IsFlagSet("colorscheme") || IsFlagSet("show-non-compliant") { + if !(stArgs[prealm] == "note" && stArgs[pcmd] == "verify") { ret = false } } - if IsFlagSet("show-non-compliant") { - if stArgs[prealm] != "note" && stArgs[pcmd] != "verify" { - ret = false - } - if IsFlagSet("colorscheme") { - if stArgs[popt] != "--show-non-compliant" && stArgs[popt+1] != "--show-non-compliant" { - ret = false - } - } else if stArgs[popt] != "--show-non-compliant" { + if IsFlagSet("colorscheme") && IsFlagSet("show-non-compliant") { + // both flags set, check order + if !(strings.HasPrefix(stArgs[popt], "--colorscheme=") && stArgs[popt+1] == "--show-non-compliant") { ret = false } + } else if IsFlagSet("colorscheme") && !strings.HasPrefix(stArgs[popt], "--colorscheme=") { + // flag at wrong place in arg list + ret = false + } else if IsFlagSet("show-non-compliant") && stArgs[popt] != "--show-non-compliant" { + // flag at wrong place in arg list + ret = false } return ret } diff --git a/system/cpu.go b/system/cpu.go index 9ae916df..ac139a92 100644 --- a/system/cpu.go +++ b/system/cpu.go @@ -56,6 +56,9 @@ func GetPerfBias() string { str = str + fmt.Sprintf("cpu%d", k/2) case isPBias.MatchString(line): pb := strings.Split(line, ":") + if len(pb) < 2 { + continue + } if oldpb == "99" { oldpb = strings.TrimSpace(pb[1]) } @@ -81,6 +84,9 @@ func SetPerfBias(value string) error { cpu := "" for k, entry := range strings.Fields(value) { fields := strings.Split(entry, ":") + if len(fields) < 2 { + continue + } if fields[0] != "all" { cpu = strconv.Itoa(k) } else { @@ -218,6 +224,9 @@ func SetGovernor(value, info string) error { tst := "" for k, entry := range strings.Fields(value) { fields := strings.Split(entry, ":") + if len(fields) < 2 { + continue + } if fields[0] != "all" { cpu = strconv.Itoa(k) tst = cpu @@ -349,8 +358,8 @@ func SetForceLatency(value, savedStates, info string, revert bool) error { for _, entry := range dirCont { // cpu0 ... cpuXY if isCPU.MatchString(entry.Name()) { - cpudirCont, err := ioutil.ReadDir(path.Join(cpuDir, entry.Name(), "cpuidle")) - if err != nil { + cpudirCont, errns := ioutil.ReadDir(path.Join(cpuDir, entry.Name(), "cpuidle")) + if errns != nil { WarningLog("idle settings not supported for '%s'", entry.Name()) continue } @@ -364,8 +373,10 @@ func SetForceLatency(value, savedStates, info string, revert bool) error { // revert for _, ole := range strings.Fields(savedStates) { FLFields := strings.Split(ole, ":") - if FLFields[0] == entry.Name() && FLFields[1] == centry.Name() { - oldState = FLFields[2] + if len(FLFields) > 2 { + if FLFields[0] == entry.Name() && FLFields[1] == centry.Name() { + oldState = FLFields[2] + } } } if oldState != "" { diff --git a/system/daemon.go b/system/daemon.go index 4047108a..ef6474d3 100644 --- a/system/daemon.go +++ b/system/daemon.go @@ -211,8 +211,7 @@ func SystemctlIsRunning(thing string) (bool, error) { return match, nil } -// SystemctlIsActive return true only if systemctl suggests that the thing is -// running. +// SystemctlIsActive returns the output of 'systemctl is-active' func SystemctlIsActive(thing string) (string, error) { out, err := exec.Command(systemctlCmd, "is-active", thing).CombinedOutput() DebugLog("SystemctlIsActive - /usr/bin/systemctl is-active : '%+v %s'", err, string(out)) diff --git a/system/logging.go b/system/logging.go index e68bae52..a6d7bbcc 100644 --- a/system/logging.go +++ b/system/logging.go @@ -64,6 +64,7 @@ func WarningLog(txt string, stuff ...interface{}) { func ErrLog(txt string, stuff ...interface{}) { if errorLogger != nil { errorLogger.Printf(CalledFrom()+txt+"\n", stuff...) + jWriteMsg("ERROR", fmt.Sprintf(CalledFrom()+txt+"\n", stuff...)) } } From e3fd40230c5c92f6dcd6695210c3d27a339d1425 Mon Sep 17 00:00:00 2001 From: AngelaBriel Date: Mon, 1 Aug 2022 11:11:37 +0200 Subject: [PATCH 09/12] remove SAP Note 1410736 from solution 'SAP-ASE'. The best practice document (version 3.1) for ASE was changed and the SAP Note 1410736 was removed. Instead the parameter 'net.ipv4.tcp_keepalive_time' is set in SAP Note 1680803 (the ASE SAP Note) directly. --- ospackage/usr/share/saptune/sols/SAP-ASE.sol | 4 ++-- ospackage/usr/share/saptune/sols_15/SAP-ASE.sol | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ospackage/usr/share/saptune/sols/SAP-ASE.sol b/ospackage/usr/share/saptune/sols/SAP-ASE.sol index 6fb6afbd..75a38768 100644 --- a/ospackage/usr/share/saptune/sols/SAP-ASE.sol +++ b/ospackage/usr/share/saptune/sols/SAP-ASE.sol @@ -2,7 +2,7 @@ # SAP-NOTE=SAP-ASE CATEGORY=SOLUTION VERSION=1 DATE=07.07.2021 NAME="Definition of SAP-ASE solution for SLE12" [ArchX86] -941735 1410736 1680803 1771258 1984787 2993054 1656250 +941735 1680803 1771258 1984787 2993054 1656250 [ArchPPC64LE] -941735 1410736 1680803 1771258 1984787 2993054 1656250 +941735 1680803 1771258 1984787 2993054 1656250 diff --git a/ospackage/usr/share/saptune/sols_15/SAP-ASE.sol b/ospackage/usr/share/saptune/sols_15/SAP-ASE.sol index 9bef1941..5c5ed359 100644 --- a/ospackage/usr/share/saptune/sols_15/SAP-ASE.sol +++ b/ospackage/usr/share/saptune/sols_15/SAP-ASE.sol @@ -2,7 +2,7 @@ # SAP-NOTE=SAP-ASE CATEGORY=SOLUTION VERSION=1 DATE=07.07.2021 NAME="Definition of SAP-ASE solution for SLE15" [ArchX86] -941735 1410736 1680803 1771258 2578899 2993054 1656250 +941735 1680803 1771258 2578899 2993054 1656250 [ArchPPC64LE] -941735 1410736 1680803 1771258 2578899 2993054 1656250 +941735 1680803 1771258 2578899 2993054 1656250 From 885cc12b22aefbbdade1156742f25aa19c392611 Mon Sep 17 00:00:00 2001 From: AngelaBriel Date: Mon, 1 Aug 2022 11:13:32 +0200 Subject: [PATCH 10/12] fix output channel for the color printing of the function 'ErrorExit'. Now it's stderr, the same as for non colorized output. --- system/system.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/system.go b/system/system.go index 08ae037f..0bbd4753 100644 --- a/system/system.go +++ b/system/system.go @@ -155,7 +155,7 @@ func ErrorExit(template string, stuff ...interface{}) { if len(stuff) > 0 && stuff[0] == "colorPrint" { // color, bold, text/template, reset bold, reset color stuff = stuff[1:] - fmt.Printf("%s%sERROR: "+template+"%s%s\n", stuff...) + fmt.Fprintf(os.Stderr, "%s%sERROR: "+template+"%s%s\n", stuff...) if len(stuff) >= 4 { stuff = stuff[2 : len(stuff)-2] } From 331eced9cfbf9cf236678b649d4904b4c2985440 Mon Sep 17 00:00:00 2001 From: AngelaBriel Date: Mon, 1 Aug 2022 12:51:36 +0200 Subject: [PATCH 11/12] add new unit tests and adapt and fix exiting tests. add changes to please linters and other checkers --- actions/actions.go | 16 +- actions/actionsMatchtxt_test.go | 112 +++++ actions/actions_test.go | 84 +--- actions/noteacts.go | 44 +- actions/noteacts_test.go | 466 +++++++++++++++++- actions/serviceacts.go | 8 +- actions/solutionacts.go | 30 +- actions/solutionacts_test.go | 125 +---- actions/stagingacts.go | 44 +- actions/staticcheck.conf | 1 + actions/table_test.go | 315 +++++++++++- app/app_test.go | 217 +++++++- main.go | 2 +- main_test.go | 90 +++- ospackage/usr/share/saptune/notes/1680803 | 6 +- run_saptune_ci_tst.sh | 15 +- sap/note/db.go | 2 +- sap/note/db_test.go | 2 +- sap/note/ini.go | 2 +- sap/note/ini_test.go | 8 +- sap/note/note_test.go | 35 ++ sap/note/parameter.go | 2 +- sap/note/sectBlock.go | 14 +- sap/note/sectBlock_test.go | 23 +- sap/note/sectLimits.go | 5 +- sap/note/sectLimits_test.go | 11 + sap/note/sectMem.go | 3 +- sap/note/sectSysctl_test.go | 8 + sap/param/io_test.go | 47 ++ sap/solution/solution.go | 2 +- sap/solution/solution_test.go | 36 ++ staticcheck.conf | 1 + system/argsAndFlags_test.go | 282 ++++++++++- system/blockdev.go | 3 +- system/csp_test.go | 15 + system/daemon_test.go | 103 ++-- system/file_test.go | 63 +++ system/fs.go | 12 +- system/fs_test.go | 73 ++- system/limits.go | 6 +- system/lock.go | 5 +- system/logging_test.go | 6 +- system/rpm.go | 1 + system/service.go | 2 +- system/service_test.go | 2 +- system/sys_test.go | 3 + system/sysctl.go | 5 +- system/sysctl_test.go | 53 +- system/system.go | 23 +- system/system_test.go | 38 +- testdata/SAP_BWA | 0 testdata/etc/saptune/extra/900929.conf | 19 + .../etc/saptune/extra/extraTest2Note.conf | 1 + testdata/etc/saptune/extra/extraTestNote.conf | 1 + testdata/etc/saptune/override/1234567 | 9 + testdata/etc/saptune/override/extraTestNote | 1 + testdata/etc/saptune/override/testNote | 1 + testdata/etc/sysconfig/saptune | 11 + testdata/etc/sysconfig/saptune_tstorg | 45 ++ testdata/extra/900929.conf | 19 + testdata/extra/BWA.sol | 8 + testdata/fstest/fstab | 5 + testdata/fstest/procMounts | 35 ++ testdata/grub_and_service_test.ini | 15 + testdata/ini_all_test.ini | 6 + testdata/ini_missing_test.ini | 40 ++ testdata/ini_new_test.ini | 46 ++ testdata/ini_test.ini | 2 +- testdata/ini_wrong_test.ini | 46 ++ testdata/saptune_check | 3 + testdata/sol/sols/BWA.sol | 4 +- testdata/test2Note | 1 + testdata/testNote | 1 + testdata/tstedit | 2 + txtparser/ini_test.go | 74 ++- txtparser/section_test.go | 65 +++ txtparser/sysconfig.go | 11 +- txtparser/tags.go | 2 +- txtparser/tags_test.go | 37 ++ 79 files changed, 2583 insertions(+), 398 deletions(-) create mode 100644 actions/actionsMatchtxt_test.go create mode 100644 actions/staticcheck.conf create mode 100644 staticcheck.conf create mode 100644 testdata/SAP_BWA create mode 100644 testdata/etc/saptune/extra/900929.conf create mode 100644 testdata/etc/saptune/extra/extraTest2Note.conf create mode 100644 testdata/etc/saptune/extra/extraTestNote.conf create mode 100644 testdata/etc/saptune/override/1234567 create mode 100644 testdata/etc/saptune/override/extraTestNote create mode 100644 testdata/etc/saptune/override/testNote create mode 100644 testdata/etc/sysconfig/saptune_tstorg create mode 100644 testdata/extra/900929.conf create mode 100644 testdata/extra/BWA.sol create mode 100644 testdata/fstest/fstab create mode 100644 testdata/fstest/procMounts create mode 100644 testdata/grub_and_service_test.ini create mode 100644 testdata/ini_missing_test.ini create mode 100644 testdata/ini_new_test.ini create mode 100644 testdata/ini_wrong_test.ini create mode 100755 testdata/saptune_check create mode 100644 testdata/test2Note create mode 100644 testdata/testNote create mode 100755 testdata/tstedit create mode 100644 txtparser/section_test.go diff --git a/actions/actions.go b/actions/actions.go index 716d4792..c8b396bd 100644 --- a/actions/actions.go +++ b/actions/actions.go @@ -74,7 +74,7 @@ var dfltColorScheme = "full-red-noncmpl" // SelectAction selects the chosen action depending on the first command line // argument -func SelectAction(stApp *app.App, saptuneVers string) { +func SelectAction(writer io.Writer, stApp *app.App, saptuneVers string) { // switch off color and highlighting, if Stdout is not a terminal switchOffColor() system.JnotSupportedYet() @@ -86,21 +86,21 @@ func SelectAction(stApp *app.App, saptuneVers string) { switch system.CliArg(1) { case "daemon": - DaemonAction(os.Stdout, system.CliArg(2), saptuneVers, stApp) + DaemonAction(writer, system.CliArg(2), saptuneVers, stApp) case "service": - ServiceAction(system.CliArg(2), saptuneVers, stApp) + ServiceAction(writer, system.CliArg(2), saptuneVers, stApp) case "note": - NoteAction(system.CliArg(2), system.CliArg(3), system.CliArg(4), stApp) + NoteAction(writer, system.CliArg(2), system.CliArg(3), system.CliArg(4), stApp) case "solution": - SolutionAction(system.CliArg(2), system.CliArg(3), system.CliArg(4), stApp) + SolutionAction(writer, system.CliArg(2), system.CliArg(3), system.CliArg(4), stApp) case "revert": - RevertAction(os.Stdout, system.CliArg(2), stApp) + RevertAction(writer, system.CliArg(2), stApp) case "staging": StagingAction(system.CliArg(2), system.CliArgs(3), stApp) case "status": - ServiceAction("status", saptuneVers, stApp) + ServiceAction(writer, "status", saptuneVers, stApp) default: - PrintHelpAndExit(os.Stdout, 1) + PrintHelpAndExit(writer, 1) } } diff --git a/actions/actionsMatchtxt_test.go b/actions/actionsMatchtxt_test.go new file mode 100644 index 00000000..b8dfce48 --- /dev/null +++ b/actions/actionsMatchtxt_test.go @@ -0,0 +1,112 @@ +package actions + +import ( + "fmt" + "github.com/SUSE/saptune/system" +) + +var noteListMatchText = ` +All notes (+ denotes manually enabled notes, * denotes notes enabled by solutions, - denotes notes enabled by solutions but reverted manually later, O denotes override file exists for note, C denotes custom note): + 900929 Linux: STORAGE_PARAMETERS_WRONG_SET and 'mmap() failed' + Version 7 from 31.07.2017 + https://launchpad.support.sap.com/#/notes/900929 + NEWSOL2NOTE + extraNote Configuration drop in for extra tests + Version 0 from 04.06.2019 + oldFile Name_syntax + simpleNote Configuration drop in for simple tests + Version 1 from 09.07.2019 + wrongFileNamesyntax + +Remember: if you wish to automatically activate the solution's tuning options after a reboot, you must enable and start saptune.service by running: + saptune service enablestart +` + +var solutionListMatchText = ` +All solutions (* denotes enabled solution, O denotes override file exists for solution, C denotes custom solutions, D denotes deprecated solutions): + BWA - SAP_BWA + HANA - 941735 1771258 1980196 1984787 2205917 2382421 2534844 + MAXDB - 941735 1771258 1984787 + NETW - 941735 1771258 1980196 1984787 2534844 + +Remember: if you wish to automatically activate the solution's tuning options after a reboot, you must enable and start saptune.service by running: + saptune service enablestart +` + +var saptuneStatusMatchText = fmt.Sprintf(` +saptune.service: disabled/active +saptune package: 'undef' +configured version: '3' +enabled Solution: sol1 (simpleNote) +applied Solution: +additional enabled Notes: 2205917 +enabled Notes: 2205917 +applied Notes: +staging: disabled +staged Notes: +staged Solutions: + +sapconf.service: not available +tuned.service: disabled/active (profile: '%s') +system state: running +virtualization: %s + +Remember: if you wish to automatically activate the note's and solution's tuning options after a reboot, you must enable saptune.service by running: + 'saptune service enable'. + +`, system.GetTunedAdmProfile(), system.GetVirtStatus()) + +var saptuneStatMatchText = fmt.Sprintf(` +saptune.service: disabled/inactive +saptune package: 'undef' +configured version: '3' +enabled Solution: +applied Solution: +additional enabled Notes: +enabled Notes: +applied Notes: +staging: disabled +staged Notes: +staged Solutions: + +sapconf.service: not available +tuned.service: disabled/active (profile: '%s') +system state: running +virtualization: %s + +Remember: if you wish to automatically activate the note's and solution's tuning options after a reboot, you must enable saptune.service by running: + 'saptune service enablestart'. +Your system has not yet been tuned. Please visit `+"`"+`saptune note`+"`"+` and `+"`"+`saptune solution`+"`"+` to start tuning. + +`, system.GetTunedAdmProfile(), system.GetVirtStatus()) + +var PrintHelpAndExitMatchText = `saptune: Comprehensive system optimisation management for SAP solutions. +Daemon control: + saptune daemon [ start | status | stop ] ATTENTION: deprecated + saptune service [ start | status | stop | restart | takeover | enable | disable | enablestart | disablestop ] +Tune system according to SAP and SUSE notes: + saptune note [ list | revertall | enabled | applied ] + saptune note [ apply | simulate | customise | create | edit | revert | show | delete ] NoteID + saptune note verify [--colorscheme=] [--show-non-compliant] [NoteID] + saptune note rename NoteID newNoteID +Tune system for all notes applicable to your SAP solution: + saptune solution [ list | verify | enabled | applied ] + saptune solution [ apply | simulate | verify | customise | create | edit | revert | show | delete ] SolutionName + saptune solution rename SolutionName newSolutionName +Staging control: + saptune staging [ status | enable | disable | is-enabled | list | diff | analysis | release ] + saptune staging [ analysis | diff ] [ NoteID... | SolutionID... | all ] + saptune staging release [--force|--dry-run] [ NoteID... | SolutionID... | all ] +Revert all parameters tuned by the SAP notes or solutions: + saptune revert all +Remove the pending lock file from a former saptune call + saptune lock remove +Call external script '/usr/sbin/saptune_check' + saptune check +Print current saptune status: + saptune status +Print current saptune version: + saptune version +Print this message: + saptune help +` diff --git a/actions/actions_test.go b/actions/actions_test.go index 7a0ea985..a0ec6f24 100644 --- a/actions/actions_test.go +++ b/actions/actions_test.go @@ -16,9 +16,10 @@ import ( var SolutionSheetsInGOPATH = path.Join(os.Getenv("GOPATH"), "/src/github.com/SUSE/saptune/testdata/sol/sols") + "/" var ExtraFilesInGOPATH = path.Join(os.Getenv("GOPATH"), "/src/github.com/SUSE/saptune/testdata/extra") + "/" +var ExtraTstFilesInGOPATH = path.Join(os.Getenv("GOPATH"), "/src/github.com/SUSE/saptune/testdata/etc/saptune/extra") + "/" var OverTstFilesInGOPATH = path.Join(os.Getenv("GOPATH"), "/src/github.com/SUSE/saptune/testdata/etc/saptune/override") + "/" var DeprecFilesInGOPATH = path.Join(os.Getenv("GOPATH"), "/src/github.com/SUSE/saptune/testdata/sol/deprecated") + "/" -var TstFilesInGOPATH = path.Join(os.Getenv("GOPATH"), "/src/github.com/SUSE/saptune/testdata/") +var TstFilesInGOPATH = path.Join(os.Getenv("GOPATH"), "/src/github.com/SUSE/saptune/testdata") + "/" var AllTestSolutions = map[string]solution.Solution{ "sol1": {"simpleNote"}, @@ -40,6 +41,18 @@ var tstErrorExitOut = func(str string, out ...interface{}) error { return fmt.Errorf(str+"\n", out...) } +var switchOnColor = func(t *testing.T) { + setGreenText = "\033[32m" + setRedText = "\033[31m" + setYellowText = "\033[33m" + setBlueText = "\033[34m" + setBoldText = "\033[1m" + resetBoldText = "\033[22m" + setStrikeText = "\033[9m" + resetTextColor = "\033[0m" + dfltColorScheme = "full-red-noncmpl" +} + var checkOut = func(t *testing.T, got, want string) { t.Helper() if got != want { @@ -118,39 +131,9 @@ Parameters tuned by the notes and solutions have been successfully reverted. // this errExitMatchText differs from the 'real' text by the last 2 lines // because of test situation, the 'exit 1' in PrintHelpAndExit is not // executed (as designed for testing) - errExitMatchText := fmt.Sprintf(`saptune: Comprehensive system optimisation management for SAP solutions. -Daemon control: - saptune daemon [ start | status | stop ] ATTENTION: deprecated - saptune service [ start | status | stop | restart | takeover | enable | disable | enablestart | disablestop ] -Tune system according to SAP and SUSE notes: - saptune note [ list | revertall | enabled | applied ] - saptune note [ apply | simulate | customise | create | edit | revert | show | delete ] NoteID - saptune note verify [--colorscheme=] [--show-non-compliant] [NoteID] - saptune note rename NoteID newNoteID -Tune system for all notes applicable to your SAP solution: - saptune solution [ list | verify | enabled | applied ] - saptune solution [ apply | simulate | verify | customise | create | edit | revert | show | delete ] SolutionName - saptune solution rename SolutionName newSolutionName -Staging control: - saptune staging [ status | enable | disable | is-enabled | list | diff | analysis | release ] - saptune staging [ analysis | diff ] [ NoteID... | SolutionID... | all ] - saptune staging release [--force|--dry-run] [ NoteID... | SolutionID... | all ] -Revert all parameters tuned by the SAP notes or solutions: - saptune revert all -Remove the pending lock file from a former saptune call - saptune lock remove -Call external script '/usr/sbin/saptune_check' - saptune check -Print current saptune status: - saptune status -Print current saptune version: - saptune version -Print this message: - saptune help -Reverting all notes and solutions, this may take some time... + errExitMatchText := PrintHelpAndExitMatchText + `Reverting all notes and solutions, this may take some time... Parameters tuned by the notes and solutions have been successfully reverted. -`) - +` buffer.Reset() // reset tApp variables, which were deleted by 'revert all' tearDown(t) @@ -206,7 +189,7 @@ func TestGetFileName(t *testing.T) { // test with non-existing extra note nID = "hugo" getFnameMatchText := fmt.Sprintf("ERROR: Note %s not found in %s or %s.\n", nID, "", ExtraFilesInGOPATH) - fname, extra = getFileName(nID, "", ExtraFilesInGOPATH) + _, _ = getFileName(nID, "", ExtraFilesInGOPATH) if tstRetErrorExit != 1 { t.Errorf("error exit should be '1' and NOT '%v'\n", tstRetErrorExit) } @@ -215,7 +198,7 @@ func TestGetFileName(t *testing.T) { } func TestReadYesNo(t *testing.T) { - yesnoMatchText := fmt.Sprintf("Answer is [y/n]: ") + yesnoMatchText := "Answer is [y/n]: " buffer := bytes.Buffer{} input := "yes\n" if !readYesNo("Answer is", strings.NewReader(input), &buffer) { @@ -242,36 +225,7 @@ func TestPrintHelpAndExit(t *testing.T) { defer func() { system.ErrorExitOut = oldErrorExitOut }() system.ErrorExitOut = tstErrorExitOut - errExitMatchText := fmt.Sprintf(`saptune: Comprehensive system optimisation management for SAP solutions. -Daemon control: - saptune daemon [ start | status | stop ] ATTENTION: deprecated - saptune service [ start | status | stop | restart | takeover | enable | disable | enablestart | disablestop ] -Tune system according to SAP and SUSE notes: - saptune note [ list | revertall | enabled | applied ] - saptune note [ apply | simulate | customise | create | edit | revert | show | delete ] NoteID - saptune note verify [--colorscheme=] [--show-non-compliant] [NoteID] - saptune note rename NoteID newNoteID -Tune system for all notes applicable to your SAP solution: - saptune solution [ list | verify | enabled | applied ] - saptune solution [ apply | simulate | verify | customise | create | edit | revert | show | delete ] SolutionName - saptune solution rename SolutionName newSolutionName -Staging control: - saptune staging [ status | enable | disable | is-enabled | list | diff | analysis | release ] - saptune staging [ analysis | diff ] [ NoteID... | SolutionID... | all ] - saptune staging release [--force|--dry-run] [ NoteID... | SolutionID... | all ] -Revert all parameters tuned by the SAP notes or solutions: - saptune revert all -Remove the pending lock file from a former saptune call - saptune lock remove -Call external script '/usr/sbin/saptune_check' - saptune check -Print current saptune status: - saptune status -Print current saptune version: - saptune version -Print this message: - saptune help -`) + errExitMatchText := PrintHelpAndExitMatchText errExitbuffer := bytes.Buffer{} tstwriter = &errExitbuffer buffer := bytes.Buffer{} diff --git a/actions/noteacts.go b/actions/noteacts.go index 92a8e94a..20a82e9b 100644 --- a/actions/noteacts.go +++ b/actions/noteacts.go @@ -16,38 +16,38 @@ import ( var templateFile = "/usr/share/saptune/NoteTemplate.conf" // NoteAction Note actions like apply, revert, verify asm. -func NoteAction(actionName, noteID, newNoteID string, tuneApp *app.App) { +func NoteAction(writer io.Writer, actionName, noteID, newNoteID string, tuneApp *app.App) { switch actionName { case "apply": - NoteActionApply(os.Stdout, noteID, tuneApp) + NoteActionApply(writer, noteID, tuneApp) case "list": - NoteActionList(os.Stdout, tuneApp) + NoteActionList(writer, tuneApp) case "verify": - NoteActionVerify(os.Stdout, noteID, tuneApp) + NoteActionVerify(writer, noteID, tuneApp) case "simulate": - NoteActionSimulate(os.Stdout, noteID, tuneApp) + NoteActionSimulate(writer, noteID, tuneApp) case "customise", "customize": - NoteActionCustomise(os.Stdout, noteID, tuneApp) + NoteActionCustomise(writer, noteID, tuneApp) case "edit": - NoteActionEdit(os.Stdout, noteID, tuneApp) + NoteActionEdit(writer, noteID, tuneApp) case "create": - NoteActionCreate(noteID, tuneApp) + NoteActionCreate(writer, noteID, tuneApp) case "show": - NoteActionShow(os.Stdout, noteID, tuneApp) + NoteActionShow(writer, noteID, tuneApp) case "delete": - NoteActionDelete(os.Stdin, os.Stdout, noteID, tuneApp) + NoteActionDelete(os.Stdin, writer, noteID, tuneApp) case "rename": - NoteActionRename(os.Stdin, os.Stdout, noteID, newNoteID, tuneApp) + NoteActionRename(os.Stdin, writer, noteID, newNoteID, tuneApp) case "revert": - NoteActionRevert(os.Stdout, noteID, tuneApp) + NoteActionRevert(writer, noteID, tuneApp) case "revertall": - RevertAction(os.Stdout, "all", tuneApp) + RevertAction(writer, "all", tuneApp) case "applied": - NoteActionApplied(os.Stdout, tuneApp) + NoteActionApplied(writer, tuneApp) case "enabled": - NoteActionEnabled(os.Stdout, tuneApp) + NoteActionEnabled(writer, tuneApp) default: - PrintHelpAndExit(os.Stdout, 1) + PrintHelpAndExit(writer, 1) } } @@ -197,7 +197,7 @@ func NoteActionSimulate(writer io.Writer, noteID string, tuneApp *app.App) { // definition override file func NoteActionCustomise(writer io.Writer, noteID string, tuneApp *app.App) { if noteID == "" { - PrintHelpAndExit(os.Stdout, 1) + PrintHelpAndExit(writer, 1) } if _, err := tuneApp.GetNoteByID(noteID); err != nil { system.ErrorExit("%v", err) @@ -234,7 +234,7 @@ func NoteActionCustomise(writer io.Writer, noteID string, tuneApp *app.App) { // file and NOT the override file func NoteActionEdit(writer io.Writer, noteID string, tuneApp *app.App) { if noteID == "" { - PrintHelpAndExit(os.Stdout, 1) + PrintHelpAndExit(writer, 1) } if _, err := tuneApp.GetNoteByID(noteID); err != nil { system.ErrorExit("%v", err) @@ -265,9 +265,9 @@ func NoteActionEdit(writer io.Writer, noteID string, tuneApp *app.App) { } // NoteActionCreate helps the customer to create an own Note definition -func NoteActionCreate(noteID string, tuneApp *app.App) { +func NoteActionCreate(writer io.Writer, noteID string, tuneApp *app.App) { if noteID == "" { - PrintHelpAndExit(os.Stdout, 1) + PrintHelpAndExit(writer, 1) } if _, err := tuneApp.GetNoteByID(noteID); err == nil { system.ErrorExit("Note '%s' already exists. Please use 'saptune note customise %s' instead to create an override file or choose another NoteID.", noteID, noteID) @@ -433,8 +433,8 @@ func NoteActionRevert(writer io.Writer, noteID string, tuneApp *app.App) { } // if a solution is enabled (available in the configuration), check, if -// this note is the last note in NoteApplyOrder, which is related to -// this solution. If yes, remove solution for the configuration. +// there is a least one note in NoteApplyOrder, which is related to +// this solution. If no, remove solution for the configuration. func solutionStillEnabled(tuneApp *app.App) { if len(tuneApp.TuneForSolutions) == 0 { return diff --git a/actions/noteacts_test.go b/actions/noteacts_test.go index 82548035..b37bef6c 100644 --- a/actions/noteacts_test.go +++ b/actions/noteacts_test.go @@ -16,26 +16,76 @@ func TestNoteActions(t *testing.T) { // test setup setUp(t) + tstRetErrorExit = -1 + oldOSExit := system.OSExit + defer func() { system.OSExit = oldOSExit }() + system.OSExit = tstosExit + oldErrorExitOut := system.ErrorExitOut + defer func() { system.ErrorExitOut = oldErrorExitOut }() + system.ErrorExitOut = tstErrorExitOut + errExBuffer := bytes.Buffer{} + tstwriter = &errExBuffer + // Test NoteActionList t.Run("NoteActionList", func(t *testing.T) { - var listMatchText = ` + listMatchText := noteListMatchText + buffer := bytes.Buffer{} + NoteActionList(&buffer, tApp) + txt := buffer.String() + checkOut(t, txt, listMatchText) + + buffer.Reset() + var listMatchText2 = ` All notes (+ denotes manually enabled notes, * denotes notes enabled by solutions, - denotes notes enabled by solutions but reverted manually later, O denotes override file exists for note, C denotes custom note): - NEWSOL2NOTE - extraNote Configuration drop in for extra tests + C 900929 Linux: STORAGE_PARAMETERS_WRONG_SET and 'mmap() failed' + Version 7 from 31.07.2017 + https://launchpad.support.sap.com/#/notes/900929 + + NEWSOL2NOTE + - O extraNote Configuration drop in for extra tests Version 0 from 04.06.2019 - oldFile Name_syntax - simpleNote Configuration drop in for simple tests + oldFile Name_syntax + * simpleNote Configuration drop in for simple tests Version 1 from 09.07.2019 - wrongFileNamesyntax + wrongFileNamesyntax + +current order of enabled notes is: simpleNote NEWSOL2NOTE + Remember: if you wish to automatically activate the solution's tuning options after a reboot, you must enable and start saptune.service by running: saptune service enablestart ` - - buffer := bytes.Buffer{} + oldExtraTuningSheets := ExtraTuningSheets + defer func() { ExtraTuningSheets = oldExtraTuningSheets }() + ExtraTuningSheets = ExtraTstFilesInGOPATH + oldOverrideTuningSheets := OverrideTuningSheets + defer func() { OverrideTuningSheets = oldOverrideTuningSheets }() + OverrideTuningSheets = OverTstFilesInGOPATH + tApp.TuneForSolutions = []string{"sol12"} + tApp.TuneForNotes = []string{"NEWSOL2NOTE"} + tApp.NoteApplyOrder = []string{"simpleNote", "NEWSOL2NOTE"} + + buffer = bytes.Buffer{} NoteActionList(&buffer, tApp) - txt := buffer.String() - checkOut(t, txt, listMatchText) + txt = buffer.String() + checkOut(t, txt, listMatchText2) + + // test solutionStillEnabled as we currently have the needed + // test data available + solutionStillEnabled(tApp) + if strings.Join(tApp.TuneForSolutions, " ") != "sol12" { + t.Errorf("got: '%+v', expected: 'sol12'\n", strings.Join(tApp.TuneForSolutions, " ")) + } + tApp.NoteApplyOrder = []string{} + tApp.NoteApplyOrder = []string{"NEWSOL2NOTE"} + solutionStillEnabled(tApp) + if strings.Join(tApp.TuneForSolutions, " ") == "sol12" { + t.Errorf("got: '%+v', expected: ''\n", strings.Join(tApp.TuneForSolutions, " ")) + } + + tApp.TuneForSolutions = []string{} + tApp.TuneForNotes = []string{} + tApp.NoteApplyOrder = []string{} + }) // Test NoteActionSimulate @@ -62,6 +112,24 @@ Hints or values not yet handled by saptune. So please read carefully, check and NoteActionSimulate(&simBuf, nID, tApp) txt := simBuf.String() checkOut(t, txt, simulateMatchText) + + simBuf.Reset() + errExBuffer.Reset() + tstRetErrorExit = -1 + errMatchText := `ERROR: Failed to test the current system against the specified note: the Note ID "" is not recognised by saptune. +Run "saptune note list" for a complete list of supported notes. +and then please double check your input and /etc/sysconfig/saptune +` + errExitMatchText := PrintHelpAndExitMatchText + + NoteActionSimulate(&simBuf, "", tApp) + txt = simBuf.String() + checkOut(t, txt, errExitMatchText) + if tstRetErrorExit != 1 { + t.Errorf("error exit should be '1' and NOT '%v'\n", tstRetErrorExit) + } + errtxt := errExBuffer.String() + checkOut(t, errtxt, errMatchText) }) // Test NoteActionApply @@ -71,11 +139,30 @@ Hints or values not yet handled by saptune. So please read carefully, check and Remember: if you wish to automatically activate the solution's tuning options after a reboot, you must enable and start saptune.service by running: saptune service enablestart ` + buffer := bytes.Buffer{} nID := "simpleNote" NoteActionApply(&buffer, nID, tApp) txt := buffer.String() checkOut(t, txt, applyMatchText) + + buffer.Reset() + errExBuffer.Reset() + tstRetErrorExit = -1 + errMatchText := `ERROR: Failed to tune for note : the Note ID "" is not recognised by saptune. +Run "saptune note list" for a complete list of supported notes. +and then please double check your input and /etc/sysconfig/saptune +` + errExitMatchText := PrintHelpAndExitMatchText + applyMatchText + + NoteActionApply(&buffer, "", tApp) + txt = buffer.String() + checkOut(t, txt, errExitMatchText) + if tstRetErrorExit != 1 { + t.Errorf("error exit should be '1' and NOT '%v'\n", tstRetErrorExit) + } + errtxt := errExBuffer.String() + checkOut(t, errtxt, errMatchText) }) // Test VerifyAllParameters @@ -144,6 +231,16 @@ current order of enabled notes is: simpleNote checkOut(t, txt, enabledMatchText) }) + // Test NoteActionApplied + t.Run("NoteActionApplied", func(t *testing.T) { + appliedMatchText := "simpleNote" + + buffer := bytes.Buffer{} + NoteActionApplied(&buffer, tApp) + txt := buffer.String() + checkOut(t, txt, appliedMatchText) + }) + // Test NoteActionRevert t.Run("NoteActionRevert", func(t *testing.T) { var revertMatchText = `Parameters tuned by the note have been successfully reverted. @@ -183,6 +280,29 @@ net.ipv4.ip_local_port_range = 31768 61999 NoteActionShow(&buffer, nID, tApp) txt := buffer.String() checkOut(t, txt, showMatchText) + + buffer.Reset() + errExBuffer.Reset() + tstRetErrorExit = -1 + errMatchText := `ERROR: the Note ID "" is not recognised by saptune. +Run "saptune note list" for a complete list of supported notes. +and then please double check your input and /etc/sysconfig/saptune +ERROR: Note not found in or /home/ci_tst/gopath/src/github.com/SUSE/saptune/testdata/extra/. +ERROR: Failed to read file '' - open : no such file or directory +` + errExitMatchText := PrintHelpAndExitMatchText + ` +Content of Note : + +` + + NoteActionShow(&buffer, "", tApp) + txt = buffer.String() + checkOut(t, txt, errExitMatchText) + if tstRetErrorExit != 1 { + t.Errorf("error exit should be '1' and NOT '%v'\n", tstRetErrorExit) + } + errtxt := errExBuffer.String() + checkOut(t, errtxt, errMatchText) }) tearDown(t) @@ -200,30 +320,40 @@ func TestNoteActionCreate(t *testing.T) { tstwriter = &buffer oldEditor := os.Getenv("EDITOR") + defer func() { os.Setenv("EDITOR", oldEditor) }() os.Setenv("EDITOR", "/usr/bin/echo") newTuningOpts := note.GetTuningOptions("", ExtraFilesInGOPATH) nApp := app.InitialiseApp(TstFilesInGOPATH, "", newTuningOpts, AllTestSolutions) + createBuf := bytes.Buffer{} + // test with missing template file nID := "hugo" - createMatchText := fmt.Sprintf("ERROR: Problems while editing note definition file '/etc/saptune/extra/hugo.conf' - open /usr/share/saptune/NoteTemplate.conf: no such file or directory\n") - NoteActionCreate(nID, nApp) + createMatchText := "ERROR: Problems while editing note definition file '/etc/saptune/extra/hugo.conf' - open /usr/share/saptune/NoteTemplate.conf: no such file or directory\n" + cMatchText := "" + NoteActionCreate(&createBuf, nID, nApp) + txt := createBuf.String() + checkOut(t, txt, cMatchText) if tstRetErrorExit != 1 { t.Errorf("error exit should be '1' and NOT '%v'\n", tstRetErrorExit) } - txt := buffer.String() + txt = buffer.String() checkOut(t, txt, createMatchText) templateFile = path.Join(os.Getenv("GOPATH"), "/src/github.com/SUSE/saptune/ospackage/usr/share/saptune/NoteTemplate.conf") // test with available template file + createBuf.Reset() buffer.Reset() - createMatchText = fmt.Sprintf("") + createMatchText = "" + cMatchText = "" tstRetErrorExit = -1 oldExtraTuningSheets := ExtraTuningSheets defer func() { ExtraTuningSheets = oldExtraTuningSheets }() ExtraTuningSheets = ExtraFilesInGOPATH fname := fmt.Sprintf("%s%s.conf", ExtraTuningSheets, nID) - NoteActionCreate(nID, nApp) + NoteActionCreate(&createBuf, nID, nApp) + txt = createBuf.String() + checkOut(t, txt, cMatchText) if tstRetErrorExit != -1 { t.Errorf("error exit should be '-1' and NOT '%v'\n", tstRetErrorExit) } @@ -233,7 +363,24 @@ func TestNoteActionCreate(t *testing.T) { t.Errorf("found a created file '%s' even that no input was provided to the editor", fname) } os.Remove(fname) - os.Setenv("EDITOR", oldEditor) + + // test with empty noteID + createBuf.Reset() + buffer.Reset() + createMatchText = PrintHelpAndExitMatchText + cMatchText = "" + tstRetErrorExit = -1 + NoteActionCreate(&createBuf, "", nApp) + txt = createBuf.String() + checkOut(t, txt, createMatchText) + if tstRetErrorExit != 1 { + t.Errorf("error exit should be '1' and NOT '%v'\n", tstRetErrorExit) + } + txt = buffer.String() + checkOut(t, txt, cMatchText) + if _, err := os.Stat(fname); err == nil { + t.Errorf("found a created file '%s' even that no input was provided to the editor", fname) + } } func TestNoteActionRenameShowDelete(t *testing.T) { @@ -372,3 +519,290 @@ net.ipv4.ip_local_port_range = 31768 61999 t.Errorf("file '%s' still exists\n", newFileName) } } + +func TestNoteActionCustomise(t *testing.T) { + tstRetErrorExit = -1 + oldOSExit := system.OSExit + defer func() { system.OSExit = oldOSExit }() + system.OSExit = tstosExit + oldErrorExitOut := system.ErrorExitOut + defer func() { system.ErrorExitOut = oldErrorExitOut }() + system.ErrorExitOut = tstErrorExitOut + buffer := bytes.Buffer{} + tstwriter = &buffer + + oldEditor := os.Getenv("EDITOR") + defer func() { os.Setenv("EDITOR", oldEditor) }() + os.Setenv("EDITOR", "/usr/bin/echo") + + oldExtraTuningSheets := ExtraTuningSheets + defer func() { ExtraTuningSheets = oldExtraTuningSheets }() + ExtraTuningSheets = ExtraTstFilesInGOPATH + oldOverrideTuningSheets := OverrideTuningSheets + defer func() { OverrideTuningSheets = oldOverrideTuningSheets }() + OverrideTuningSheets = OverTstFilesInGOPATH + oldNoteTuningSheets := NoteTuningSheets + defer func() { NoteTuningSheets = oldNoteTuningSheets }() + NoteTuningSheets = TstFilesInGOPATH + newTuningOpts := note.GetTuningOptions(TstFilesInGOPATH, ExtraTstFilesInGOPATH) + cApp := app.InitialiseApp(TstFilesInGOPATH, "", newTuningOpts, AllTestSolutions) + + // test with empty noteID + custBuffer := bytes.Buffer{} + custMatchText := "" + // as we are in 'test mode' collect all errExit messages and continue, + // instead of exit function - so differ from real life + // test with missing note id + errMatchText := `ERROR: the Note ID "" is not recognised by saptune. +Run "saptune note list" for a complete list of supported notes. +and then please double check your input and /etc/sysconfig/saptune +ERROR: Problems while editing note definition file '/home/ci_tst/gopath/src/github.com/SUSE/saptune/testdata/etc/saptune/override/' - write /tmp/.sttemp: copy_file_range: is a directory +` + NoteActionCustomise(&custBuffer, "", cApp) + if tstRetErrorExit != 1 { + t.Errorf("error exit should be '1' and NOT '%v'\n", tstRetErrorExit) + } + txt := buffer.String() + checkOut(t, txt, errMatchText) + + // change EDITOR command + fakeEditorCommand := path.Join(TstFilesInGOPATH, "tstedit") + os.Setenv("EDITOR", fakeEditorCommand) + editorTxt := `Hello from test editor +` + + // test with existing override file - testNote (not applied) + buffer.Reset() + custBuffer.Reset() + errMatchText = "" + tstRetErrorExit = -1 + errMatchText = "" + overFileName := path.Join(OverTstFilesInGOPATH, "testNote") + NoteActionCustomise(&custBuffer, "testNote", cApp) + custTxt := custBuffer.String() + checkOut(t, custTxt, custMatchText) + cont, err := system.ReadConfigFile(overFileName, false) + if err != nil { + t.Error(err) + } + if string(cont) != editorTxt { + t.Errorf("got: '%+v', expected: '%s'\n", string(cont), editorTxt) + } + if tstRetErrorExit != -1 { + t.Errorf("error exit should be '-1' and NOT '%v'\n", tstRetErrorExit) + } + txt = buffer.String() + checkOut(t, txt, errMatchText) + + // test without override file - test2Note (applied Note) + buffer.Reset() + custBuffer.Reset() + errMatchText = "" + tstRetErrorExit = -1 + errMatchText = "" + // fake note applied + cApp.NoteApplyOrder = []string{"test2Note"} + emptySrc := path.Join(TstFilesInGOPATH, "saptune_NOEXIT") + stateFile := cApp.State.GetPathToNote("test2Note") + os.MkdirAll(path.Join(TstFilesInGOPATH, "/data/run/saptune/saved_state"), 0755) + if err := system.CopyFile(emptySrc, stateFile); err != nil { + t.Error(err) + } + defer os.RemoveAll(path.Join(TstFilesInGOPATH, "/data")) + + overFileName = path.Join(OverTstFilesInGOPATH, "test2Note") + NoteActionCustomise(&custBuffer, "test2Note", cApp) + custTxt = custBuffer.String() + checkOut(t, custTxt, custMatchText) + if _, err := os.Stat(overFileName); os.IsNotExist(err) { + t.Errorf("file '%s' does not exists\n", overFileName) + } + cont, err = system.ReadConfigFile(overFileName, false) + if err != nil { + t.Error(err) + } + if string(cont) != editorTxt { + t.Errorf("got: '%+v', expected: '%s'\n", string(cont), editorTxt) + } + if tstRetErrorExit != -1 { + t.Errorf("error exit should be '-1' and NOT '%v'\n", tstRetErrorExit) + } + txt = buffer.String() + checkOut(t, txt, errMatchText) + // remove testdata/etc/saptune/overrid/test2Note at the end + cApp.NoteApplyOrder = []string{} + os.Remove(overFileName) + + // test without override file and without changes - test2Note + os.Setenv("EDITOR", "/usr/bin/echo") + buffer.Reset() + custBuffer.Reset() + errMatchText = "" + tstRetErrorExit = -1 + errMatchText = "" + overFileName = path.Join(OverTstFilesInGOPATH, "test2Note") + NoteActionCustomise(&custBuffer, "test2Note", cApp) + custTxt = custBuffer.String() + checkOut(t, custTxt, custMatchText) + if tstRetErrorExit != -1 { + t.Errorf("error exit should be '-1' and NOT '%v'\n", tstRetErrorExit) + } + txt = buffer.String() + checkOut(t, txt, errMatchText) + + if _, err := os.Stat(overFileName); !os.IsNotExist(err) { + t.Errorf("file '%s' exists, even that we do not change it\n", overFileName) + cont, err = system.ReadConfigFile(overFileName, false) + if err != nil { + t.Error(err) + } else { + t.Errorf("content is '%+v'\n", string(cont)) + } + // remove testdata/etc/saptune/overrid/test2Note at the end + os.Remove(overFileName) + } +} + +func TestNoteActionEdit(t *testing.T) { + tstRetErrorExit = -1 + oldOSExit := system.OSExit + defer func() { system.OSExit = oldOSExit }() + system.OSExit = tstosExit + oldErrorExitOut := system.ErrorExitOut + defer func() { system.ErrorExitOut = oldErrorExitOut }() + system.ErrorExitOut = tstErrorExitOut + buffer := bytes.Buffer{} + tstwriter = &buffer + + oldEditor := os.Getenv("EDITOR") + defer func() { os.Setenv("EDITOR", oldEditor) }() + os.Setenv("EDITOR", "/usr/bin/echo") + + oldExtraTuningSheets := ExtraTuningSheets + defer func() { ExtraTuningSheets = oldExtraTuningSheets }() + ExtraTuningSheets = ExtraTstFilesInGOPATH + oldOverrideTuningSheets := OverrideTuningSheets + defer func() { OverrideTuningSheets = oldOverrideTuningSheets }() + OverrideTuningSheets = OverTstFilesInGOPATH + oldNoteTuningSheets := NoteTuningSheets + defer func() { NoteTuningSheets = oldNoteTuningSheets }() + NoteTuningSheets = TstFilesInGOPATH + newTuningOpts := note.GetTuningOptions(TstFilesInGOPATH, ExtraTstFilesInGOPATH) + eApp := app.InitialiseApp(TstFilesInGOPATH, "", newTuningOpts, AllTestSolutions) + + // test with empty noteID + editBuffer := bytes.Buffer{} + editMatchText := "" + // as we are in 'test mode' collect all errExit messages and continue, + // instead of exit function - so differ from real life + // test with missing note id + errMatchText := `ERROR: the Note ID "" is not recognised by saptune. +Run "saptune note list" for a complete list of supported notes. +and then please double check your input and /etc/sysconfig/saptune +ERROR: The Note definition file you want to edit is a saptune internal (shipped) Note and can NOT be edited. Use 'saptune note customise' instead. Exiting ... +ERROR: Problems while editing Note definition file '/home/ci_tst/gopath/src/github.com/SUSE/saptune/testdata/' - write /tmp/.sttemp: copy_file_range: is a directory +` + NoteActionEdit(&editBuffer, "", eApp) + if tstRetErrorExit != 1 { + t.Errorf("error exit should be '1' and NOT '%v'\n", tstRetErrorExit) + } + txt := buffer.String() + checkOut(t, txt, errMatchText) + + // change EDITOR command + fakeEditorCommand := path.Join(TstFilesInGOPATH, "tstedit") + os.Setenv("EDITOR", fakeEditorCommand) + editorTxt := `Hello from test editor +` + + // test with a system note - testNote + buffer.Reset() + editBuffer.Reset() + editMatchText = "" + //editMatchText = PrintHelpAndExitMatchText + errMatchText = `ERROR: The Note definition file you want to edit is a saptune internal (shipped) Note and can NOT be edited. Use 'saptune note customise' instead. Exiting ... +` + tstRetErrorExit = -1 + NoteActionEdit(&editBuffer, "testNote", eApp) + editTxt := editBuffer.String() + checkOut(t, editTxt, editMatchText) + if tstRetErrorExit != 1 { + t.Errorf("error exit should be '1' and NOT '%v'\n", tstRetErrorExit) + } + txt = buffer.String() + checkOut(t, txt, errMatchText) + + // test with existing override file - extraTestNote (not applied) + buffer.Reset() + editBuffer.Reset() + editMatchText = "" + errMatchText = "" + tstRetErrorExit = -1 + extraFileName := path.Join(ExtraTstFilesInGOPATH, "extraTestNote.conf") + NoteActionEdit(&editBuffer, "extraTestNote", eApp) + editTxt = editBuffer.String() + checkOut(t, editTxt, editMatchText) + cont, err := system.ReadConfigFile(extraFileName, false) + if err != nil { + t.Error(err) + } + if string(cont) != editorTxt { + t.Errorf("got: '%+v', expected: '%s'\n", string(cont), editorTxt) + } + if tstRetErrorExit != -1 { + t.Errorf("error exit should be '-1' and NOT '%v'\n", tstRetErrorExit) + } + txt = buffer.String() + checkOut(t, txt, errMatchText) + + // test without override file - extraTest2Note (applied Note) + buffer.Reset() + editBuffer.Reset() + errMatchText = "" + tstRetErrorExit = -1 + errMatchText = "" + // fake note applied + eApp.NoteApplyOrder = []string{"extraTest2Note"} + emptySrc := path.Join(TstFilesInGOPATH, "saptune_NOEXIT") + stateFile := eApp.State.GetPathToNote("extraTest2Note") + os.MkdirAll(path.Join(TstFilesInGOPATH, "/data/run/saptune/saved_state"), 0755) + if err := system.CopyFile(emptySrc, stateFile); err != nil { + t.Error(err) + } + defer os.RemoveAll(path.Join(TstFilesInGOPATH, "/data")) + + extraFileName = path.Join(ExtraTstFilesInGOPATH, "extraTest2Note.conf") + NoteActionEdit(&editBuffer, "extraTest2Note", eApp) + editTxt = editBuffer.String() + checkOut(t, editTxt, editMatchText) + cont, err = system.ReadConfigFile(extraFileName, false) + if err != nil { + t.Error(err) + } + if string(cont) != editorTxt { + t.Errorf("got: '%+v', expected: '%s'\n", string(cont), editorTxt) + } + if tstRetErrorExit != -1 { + t.Errorf("error exit should be '-1' and NOT '%v'\n", tstRetErrorExit) + } + txt = buffer.String() + checkOut(t, txt, errMatchText) + eApp.NoteApplyOrder = []string{} + + // test without changes - extraTest2Note + os.Setenv("EDITOR", "/usr/bin/echo") + buffer.Reset() + editBuffer.Reset() + errMatchText = "" + tstRetErrorExit = -1 + errMatchText = "" + NoteActionEdit(&editBuffer, "extraTest2Note", eApp) + editTxt = editBuffer.String() + checkOut(t, editTxt, editMatchText) + if tstRetErrorExit != -1 { + t.Errorf("error exit should be '-1' and NOT '%v'\n", tstRetErrorExit) + } + txt = buffer.String() + checkOut(t, txt, errMatchText) + +} diff --git a/actions/serviceacts.go b/actions/serviceacts.go index 024f67c5..6aa679f1 100644 --- a/actions/serviceacts.go +++ b/actions/serviceacts.go @@ -15,7 +15,7 @@ var ignoreFlag = "/run/.saptune.ignore" // ServiceAction handles service actions like start, stop, status, enable, disable // it controls the systemd saptune.service -func ServiceAction(actionName, saptuneVersion string, tApp *app.App) { +func ServiceAction(writer io.Writer, actionName, saptuneVersion string, tApp *app.App) { switch actionName { case "apply": // This action name is only used by saptune service, hence it is not advertised to end user. @@ -43,13 +43,13 @@ func ServiceAction(actionName, saptuneVersion string, tApp *app.App) { case "start": ServiceActionStart(false, tApp) case "status": - ServiceActionStatus(os.Stdout, tApp, saptuneVersion) + ServiceActionStatus(writer, tApp, saptuneVersion) case "stop": ServiceActionStop(false) case "takeover": ServiceActionTakeover(tApp) default: - PrintHelpAndExit(os.Stdout, 1) + PrintHelpAndExit(writer, 1) } } @@ -744,6 +744,6 @@ func DaemonAction(writer io.Writer, actionName, saptuneVersion string, tuneApp * // disablestop ServiceActionStop(true) default: - PrintHelpAndExit(os.Stdout, 1) + PrintHelpAndExit(writer, 1) } } diff --git a/actions/solutionacts.go b/actions/solutionacts.go index 7a611b74..61920c8d 100644 --- a/actions/solutionacts.go +++ b/actions/solutionacts.go @@ -18,36 +18,36 @@ import ( var solTemplate = "/usr/share/saptune/SolutionTemplate.conf" // SolutionAction Solution actions like apply, revert, verify asm. -func SolutionAction(actionName, solName, newSolName string, tuneApp *app.App) { +func SolutionAction(writer io.Writer, actionName, solName, newSolName string, tuneApp *app.App) { switch actionName { case "apply": - SolutionActionApply(os.Stdout, solName, tuneApp) + SolutionActionApply(writer, solName, tuneApp) case "list": - SolutionActionList(os.Stdout, tuneApp) + SolutionActionList(writer, tuneApp) case "verify": - SolutionActionVerify(os.Stdout, solName, tuneApp) + SolutionActionVerify(writer, solName, tuneApp) case "simulate": - SolutionActionSimulate(os.Stdout, solName, tuneApp) + SolutionActionSimulate(writer, solName, tuneApp) case "customise", "customize": - SolutionActionCustomise(os.Stdout, solName, tuneApp) + SolutionActionCustomise(writer, solName, tuneApp) case "edit": - SolutionActionEdit(os.Stdout, solName, tuneApp) + SolutionActionEdit(writer, solName, tuneApp) case "create": - SolutionActionCreate(os.Stdout, solName) + SolutionActionCreate(writer, solName) case "show": - SolutionActionShow(os.Stdout, solName) + SolutionActionShow(writer, solName) case "delete": - SolutionActionDelete(os.Stdin, os.Stdout, solName, tuneApp) + SolutionActionDelete(os.Stdin, writer, solName, tuneApp) case "rename": - SolutionActionRename(os.Stdin, os.Stdout, solName, newSolName, tuneApp) + SolutionActionRename(os.Stdin, writer, solName, newSolName, tuneApp) case "revert": - SolutionActionRevert(os.Stdout, solName, tuneApp) + SolutionActionRevert(writer, solName, tuneApp) case "applied": - SolutionActionApplied(os.Stdout, tuneApp) + SolutionActionApplied(writer, tuneApp) case "enabled": - SolutionActionEnabled(os.Stdout, tuneApp) + SolutionActionEnabled(writer, tuneApp) default: - PrintHelpAndExit(os.Stdout, 1) + PrintHelpAndExit(writer, 1) } } diff --git a/actions/solutionacts_test.go b/actions/solutionacts_test.go index 5656bca4..8aae760b 100644 --- a/actions/solutionacts_test.go +++ b/actions/solutionacts_test.go @@ -12,21 +12,10 @@ func TestSolutionActions(t *testing.T) { // Test SolutionActionList t.Run("SolutionActionList", func(t *testing.T) { - var listMatchText = ` -All solutions (* denotes enabled solution, O denotes override file exists for solution, C denotes custom solutions, D denotes deprecated solutions): - BWA - 941735 2534844 SAP_BWA - HANA - 941735 1771258 1980196 1984787 2205917 2382421 2534844 - MAXDB - 941735 1771258 1984787 - NETW - 941735 1771258 1980196 1984787 2534844 - -Remember: if you wish to automatically activate the solution's tuning options after a reboot, you must enable and start saptune.service by running: - saptune service enablestart -` - buffer := bytes.Buffer{} SolutionActionList(&buffer, tApp) txt := buffer.String() - checkOut(t, txt, listMatchText) + checkOut(t, txt, solutionListMatchText) }) // Test SolutionActionListCustomOverride @@ -36,7 +25,7 @@ Remember: if you wish to automatically activate the solution's tuning options af var listMatchText = ` All solutions (* denotes enabled solution, O denotes override file exists for solution, C denotes custom solutions, D denotes deprecated solutions): - BWA - 941735 2534844 SAP_BWA + C BWA - SAP_BWA O HANA - HANA1 NEWNOTE HANA2 D MAXDB - 941735 1771258 1984787 NETW - 941735 1771258 1980196 1984787 2534844 @@ -127,6 +116,16 @@ Hints or values not yet handled by saptune. So please read carefully, check and checkOut(t, txt, enabledMatchText) }) + // Test SolutionActionApplied + t.Run("SolutionActionApplied", func(t *testing.T) { + appliedMatchText := "sol1" + + buffer := bytes.Buffer{} + SolutionActionApplied(&buffer, tApp) + txt := buffer.String() + checkOut(t, txt, appliedMatchText) + }) + // Test SolutionActionRevert t.Run("SolutionActionRevert", func(t *testing.T) { var revertMatchText = `Parameters tuned by the notes referred by the SAP solution have been successfully reverted. @@ -227,40 +226,11 @@ Remember: if you wish to automatically activate the solution's tuning options af defer func() { system.ErrorExitOut = oldErrorExitOut }() system.ErrorExitOut = tstErrorExitOut - var errExitMatchText = `ERROR: Failed to test the current system against the specified note: solution name "" is not recognised by saptune. + errExitMatchText := `ERROR: Failed to test the current system against the specified note: solution name "" is not recognised by saptune. Run "saptune solution list" for a complete list of supported solutions, and then please double check your input and /etc/sysconfig/saptune ` - var simErrorMatchText = `saptune: Comprehensive system optimisation management for SAP solutions. -Daemon control: - saptune daemon [ start | status | stop ] ATTENTION: deprecated - saptune service [ start | status | stop | restart | takeover | enable | disable | enablestart | disablestop ] -Tune system according to SAP and SUSE notes: - saptune note [ list | revertall | enabled | applied ] - saptune note [ apply | simulate | customise | create | edit | revert | show | delete ] NoteID - saptune note verify [--colorscheme=] [--show-non-compliant] [NoteID] - saptune note rename NoteID newNoteID -Tune system for all notes applicable to your SAP solution: - saptune solution [ list | verify | enabled | applied ] - saptune solution [ apply | simulate | verify | customise | create | edit | revert | show | delete ] SolutionName - saptune solution rename SolutionName newSolutionName -Staging control: - saptune staging [ status | enable | disable | is-enabled | list | diff | analysis | release ] - saptune staging [ analysis | diff ] [ NoteID... | SolutionID... | all ] - saptune staging release [--force|--dry-run] [ NoteID... | SolutionID... | all ] -Revert all parameters tuned by the SAP notes or solutions: - saptune revert all -Remove the pending lock file from a former saptune call - saptune lock remove -Call external script '/usr/sbin/saptune_check' - saptune check -Print current saptune status: - saptune status -Print current saptune version: - saptune version -Print this message: - saptune help -` + simErrorMatchText := PrintHelpAndExitMatchText simBuf := bytes.Buffer{} errExitbuffer := bytes.Buffer{} @@ -285,46 +255,16 @@ Print this message: defer func() { system.ErrorExitOut = oldErrorExitOut }() system.ErrorExitOut = tstErrorExitOut - var errExitMatchText = `ERROR: There is already one solution applied. Applying another solution is NOT supported. + errExitMatchText := `ERROR: There is already one solution applied. Applying another solution is NOT supported. ERROR: Failed to tune for solution : solution name "" is not recognised by saptune. Run "saptune solution list" for a complete list of supported solutions, and then please double check your input and /etc/sysconfig/saptune ` - var applyErrorMatchText = `saptune: Comprehensive system optimisation management for SAP solutions. -Daemon control: - saptune daemon [ start | status | stop ] ATTENTION: deprecated - saptune service [ start | status | stop | restart | takeover | enable | disable | enablestart | disablestop ] -Tune system according to SAP and SUSE notes: - saptune note [ list | revertall | enabled | applied ] - saptune note [ apply | simulate | customise | create | edit | revert | show | delete ] NoteID - saptune note verify [--colorscheme=] [--show-non-compliant] [NoteID] - saptune note rename NoteID newNoteID -Tune system for all notes applicable to your SAP solution: - saptune solution [ list | verify | enabled | applied ] - saptune solution [ apply | simulate | verify | customise | create | edit | revert | show | delete ] SolutionName - saptune solution rename SolutionName newSolutionName -Staging control: - saptune staging [ status | enable | disable | is-enabled | list | diff | analysis | release ] - saptune staging [ analysis | diff ] [ NoteID... | SolutionID... | all ] - saptune staging release [--force|--dry-run] [ NoteID... | SolutionID... | all ] -Revert all parameters tuned by the SAP notes or solutions: - saptune revert all -Remove the pending lock file from a former saptune call - saptune lock remove -Call external script '/usr/sbin/saptune_check' - saptune check -Print current saptune status: - saptune status -Print current saptune version: - saptune version -Print this message: - saptune help -All tuning options for the SAP solution have been applied successfully. + applyErrorMatchText := PrintHelpAndExitMatchText + `All tuning options for the SAP solution have been applied successfully. Remember: if you wish to automatically activate the solution's tuning options after a reboot, you must enable and start saptune.service by running: saptune service enablestart ` - buffer := bytes.Buffer{} errExitbuffer := bytes.Buffer{} tstwriter = &errExitbuffer @@ -348,40 +288,11 @@ Remember: if you wish to automatically activate the solution's tuning options af defer func() { system.ErrorExitOut = oldErrorExitOut }() system.ErrorExitOut = tstErrorExitOut - var errExitMatchText = `ERROR: Failed to revert tuning for solution : solution name "" is not recognised by saptune. + errExitMatchText := `ERROR: Failed to revert tuning for solution : solution name "" is not recognised by saptune. Run "saptune solution list" for a complete list of supported solutions, and then please double check your input and /etc/sysconfig/saptune ` - var revertErrorMatchText = `saptune: Comprehensive system optimisation management for SAP solutions. -Daemon control: - saptune daemon [ start | status | stop ] ATTENTION: deprecated - saptune service [ start | status | stop | restart | takeover | enable | disable | enablestart | disablestop ] -Tune system according to SAP and SUSE notes: - saptune note [ list | revertall | enabled | applied ] - saptune note [ apply | simulate | customise | create | edit | revert | show | delete ] NoteID - saptune note verify [--colorscheme=] [--show-non-compliant] [NoteID] - saptune note rename NoteID newNoteID -Tune system for all notes applicable to your SAP solution: - saptune solution [ list | verify | enabled | applied ] - saptune solution [ apply | simulate | verify | customise | create | edit | revert | show | delete ] SolutionName - saptune solution rename SolutionName newSolutionName -Staging control: - saptune staging [ status | enable | disable | is-enabled | list | diff | analysis | release ] - saptune staging [ analysis | diff ] [ NoteID... | SolutionID... | all ] - saptune staging release [--force|--dry-run] [ NoteID... | SolutionID... | all ] -Revert all parameters tuned by the SAP notes or solutions: - saptune revert all -Remove the pending lock file from a former saptune call - saptune lock remove -Call external script '/usr/sbin/saptune_check' - saptune check -Print current saptune status: - saptune status -Print current saptune version: - saptune version -Print this message: - saptune help -` + revertErrorMatchText := PrintHelpAndExitMatchText buffer := bytes.Buffer{} errExitbuffer := bytes.Buffer{} diff --git a/actions/stagingacts.go b/actions/stagingacts.go index b579c2d6..28d8036e 100644 --- a/actions/stagingacts.go +++ b/actions/stagingacts.go @@ -220,12 +220,12 @@ func stagingActionRelease(reader io.Reader, writer io.Writer, sObject []string) system.ErrorExit("Flag 'dryrun' set, so staging action 'release' finished now without releasing anything", 0) } if !system.IsFlagSet("force") { - txtConfirm := fmt.Sprintf("Releasing is irreversible! Are you sure") + txtConfirm := "Releasing is irreversible! Are you sure" if !readYesNo(txtConfirm, reader, writer) { system.ErrorExit("Staging action 'release' aborted by user interaction", 0) } } - errs := make([]error, 0, 0) + errs := make([]error, 0) for _, stageName := range stgFiles.AllStageFiles { stagingFile = stgFiles.StageAttributes[stageName]["sfilename"] if _, err := os.Stat(stagingFile); err != nil { @@ -257,7 +257,7 @@ func stagingActionRelease(reader io.Reader, writer io.Writer, sObject []string) system.ErrorExit("Flag 'dryrun' set, so staging action 'release' finished now without releasing anything", 0) } if !system.IsFlagSet("force") { - txtConfirm := fmt.Sprintf("Releasing is irreversible! Are you sure") + txtConfirm := "Releasing is irreversible! Are you sure" if !readYesNo(txtConfirm, reader, writer) { system.ErrorExit("Staging action 'release' aborted by user interaction", 0) } @@ -328,19 +328,19 @@ func printSolAnalysis(writer io.Writer, stageName, txtPrefix, flag string) (bool } if stgFiles.StageAttributes[stageName]["override"] == "true" { - fmt.Fprintf(writer, txtOverrideExists) + fmt.Fprint(writer, txtOverrideExists) } if flag != "new" { if stgFiles.StageAttributes[stageName]["applied"] == "true" { - fmt.Fprintf(writer, txtSolApplied) + fmt.Fprint(writer, txtSolApplied) retVal = system.MaxI(retVal, 1) } else if stgFiles.StageAttributes[stageName]["enabled"] == "true" { - fmt.Fprintf(writer, txtSolEnabled) + fmt.Fprint(writer, txtSolEnabled) if flag == "deleted" { retVal = system.MaxI(retVal, 1) } } else { - fmt.Fprintf(writer, txtSolNotEnabled) + fmt.Fprint(writer, txtSolNotEnabled) } } @@ -376,7 +376,7 @@ func printSolAnalysis(writer io.Writer, stageName, txtPrefix, flag string) (bool } } if flag == "new" { - fmt.Fprintf(writer, txtSolNew) + fmt.Fprint(writer, txtSolNew) } return releaseable, retVal } @@ -408,19 +408,19 @@ func printNoteAnalysis(writer io.Writer, stageName, txtPrefix, flag string) (boo fmt.Fprintf(writer, txtDeleteNote, stageName) } if stgFiles.StageAttributes[stageName]["override"] == "true" { - fmt.Fprintf(writer, txtOverrideExists) + fmt.Fprint(writer, txtOverrideExists) } if flag != "new" { if stgFiles.StageAttributes[stageName]["applied"] == "true" { - fmt.Fprintf(writer, txtNoteApplied) + fmt.Fprint(writer, txtNoteApplied) retVal = system.MaxI(retVal, 1) } else if stgFiles.StageAttributes[stageName]["enabled"] == "true" { - fmt.Fprintf(writer, txtNoteEnabled) + fmt.Fprint(writer, txtNoteEnabled) if flag == "deleted" { retVal = system.MaxI(retVal, 1) } } else { - fmt.Fprintf(writer, txtNoteNotEnabled) + fmt.Fprint(writer, txtNoteNotEnabled) } } if stgFiles.StageAttributes[stageName]["inSolution"] != "" { @@ -449,7 +449,7 @@ func printNoteAnalysis(writer io.Writer, stageName, txtPrefix, flag string) (boo } } if flag == "new" { - fmt.Fprintf(writer, txtNoteNew) + fmt.Fprint(writer, txtNoteNew) } return releaseable, retVal } @@ -457,7 +457,7 @@ func printNoteAnalysis(writer io.Writer, stageName, txtPrefix, flag string) (boo // mvStageToWork moves a file from the staging area to the working area // or removes deleted files from the working area func mvStageToWork(stageName string) error { - errs := make([]error, 0, 0) + errs := make([]error, 0) stagingFile := stgFiles.StageAttributes[stageName]["sfilename"] workingFile := stgFiles.StageAttributes[stageName]["wfilename"] packageFile := stgFiles.StageAttributes[stageName]["pfilename"] @@ -520,11 +520,9 @@ func collectStageFileInfo(tuneApp *app.App) stageFiles { AllStageFiles: make([]string, 0, 64), StageAttributes: make(map[string]map[string]string), } - stageMap := make(map[string]string) - for _, stageName := range stagingOptions.GetSortedIDs() { // add new stage file - stageMap = make(map[string]string) + stageMap := make(map[string]string) // get Note/Solution Description and setup absolute filenames solStageName, name, workingFile, packageFile := getSolOrNoteEnv(stageName) @@ -580,7 +578,7 @@ func getSolOrNoteEnv(stgName string) (string, string, string, string) { // stage file is a solution file sName = strings.TrimSuffix(stgName, ".sol") if dName == "" { - dName = fmt.Sprintf("Definition of saptune solutions\n\t\t\tVersion 1") + dName = "Definition of saptune solutions\n\t\t\tVersion 1" } wFile = fmt.Sprintf("%s%s", SolutionSheets, stgName) pFile = fmt.Sprintf("%ssols/%s", PackageArea, stgName) @@ -816,10 +814,10 @@ func PrintStageFields(writer io.Writer, stageName string, comparison map[string] fmtdash, fmtplus, format, colwidth := setupStageTableFormat(comparison) // print table header - fmt.Fprintf(writer, fmtdash) + fmt.Fprint(writer, fmtdash) fmt.Fprintf(writer, format, stageName, headWork, headStage) fmt.Fprintf(writer, format, "", "(working area)", "(staging area)") - fmt.Fprintf(writer, fmtplus) + fmt.Fprint(writer, fmtplus) for _, skey := range sortkeys { // print table body if skey == "reminder" { @@ -857,7 +855,7 @@ func PrintStageFields(writer io.Writer, stageName string, comparison map[string] } } // print footer - fmt.Fprintf(writer, fmtdash) + fmt.Fprint(writer, fmtdash) fmt.Fprintf(writer, "\n") } @@ -875,9 +873,7 @@ func sortStageComparisonsOutput(noteCompare map[string]stageComparison) []string } } sort.Strings(skeys) - for _, rem := range rkeys { - skeys = append(skeys, rem) - } + skeys = append(skeys, rkeys...) return skeys } diff --git a/actions/staticcheck.conf b/actions/staticcheck.conf new file mode 100644 index 00000000..fb49f51a --- /dev/null +++ b/actions/staticcheck.conf @@ -0,0 +1 @@ +checks = ["inherit", "-ST1018"] diff --git a/actions/table_test.go b/actions/table_test.go index c7207b10..a551cdd6 100644 --- a/actions/table_test.go +++ b/actions/table_test.go @@ -6,6 +6,7 @@ import ( "github.com/SUSE/saptune/sap/note" "github.com/SUSE/saptune/system" "os" + "path" "testing" ) @@ -55,7 +56,7 @@ func TestSetWidthOfColums(t *testing.T) { } func TestPrintNoteFields(t *testing.T) { - os.Args = []string{"saptune", "note", "list", "--colorscheme=black", "--out=json", "--force", "--dryrun", "--help", "--version"} + os.Args = []string{"saptune", "note", "list", "--colorscheme=black", "--format=json", "--force", "--dryrun", "--help", "--version"} system.RereadArgs() footnote1 := " [1] setting is not supported by the system" @@ -74,9 +75,10 @@ func TestPrintNoteFields(t *testing.T) { 941735, 1 | IO_SCHEDULER_sdd | | | bfq | no [7] [10] 941735, 1 | IO_SCHEDULER_vda | noop | | all:none | - [1] [5] 941735, 1 | ShmFileSystemSizeMB | 1714 | | 488 | no - 941735, 1 | force_latency | 70 | | all:none | no [4] + 941735, 1 | force_latency | 70 | | all:none | no [1] [4] 941735, 1 | grub:intel_idle.max_cstate | 1 | | NA | no [2] [3] [6] 941735, 1 | kernel.shmmax | 18446744073709551615 | | 18446744073709551615 | yes + 941735, 1 | kernel.shmmni | | | NA | - [16] [7] ` + footnote1 + ` [2] setting is not available on the system @@ -86,6 +88,28 @@ func TestPrintNoteFields(t *testing.T) { [6] grub settings are mostly covered by other settings. See man page saptune-note(5) for details [7] parameter value is untouched by default [10] parameter is defined twice, see section [sys] 'sys:block.sdd.queue.scheduler' from the other applied notes + [16] parameter not available on the system, setting not possible + +` + var printMatchText1NoCompl = ` +941735 - Configuration drop in for simple tests + Version 1 from 09.07.2019 + + SAPNote, Version | Parameter | Expected | Override | Actual | Compliant +--------------------+----------------------------+----------------------+-----------+----------------------+----------- + 941735, 1 | IO_SCHEDULER_sdc | | | bfq | no [7] + 941735, 1 | IO_SCHEDULER_sdd | | | bfq | no [7] [10] + 941735, 1 | ShmFileSystemSizeMB | 1714 | | 488 | no + 941735, 1 | force_latency | 70 | | all:none | no [1] [4] + 941735, 1 | grub:intel_idle.max_cstate | 1 | | NA | no [2] [3] [6] + + ` + footnote1 + ` + [2] setting is not available on the system + [3] value is only checked, but NOT set + [4] cpu idle state settings differ + [6] grub settings are mostly covered by other settings. See man page saptune-note(5) for details + [7] parameter value is untouched by default + [10] parameter is defined twice, see section [sys] 'sys:block.sdd.queue.scheduler' from the other applied notes ` var printMatchText2 = ` @@ -102,6 +126,7 @@ func TestPrintNoteFields(t *testing.T) { force_latency | all:none | 70 | | [1] [4] grub:intel_idle.max_cstate | NA | 1 | | [2] [3] [6] kernel.shmmax | 18446744073709551615 | 18446744073709551615 | | + kernel.shmmni | NA | | | [16] [7] ` + footnote1 + ` [2] setting is not available on the system @@ -111,6 +136,7 @@ func TestPrintNoteFields(t *testing.T) { [6] grub settings are mostly covered by other settings. See man page saptune-note(5) for details [7] parameter value is untouched by default [10] parameter is defined twice, see section [sys] 'sys:block.sdd.queue.scheduler' from the other applied notes + [16] parameter not available on the system, setting not possible ` var printMatchText3 = ` @@ -121,9 +147,10 @@ func TestPrintNoteFields(t *testing.T) { 941735, 1 | IO_SCHEDULER_sdd | | | bfq | no [7] [10] 941735, 1 | IO_SCHEDULER_vda | noop | | all:none | - [1] [5] 941735, 1 | ShmFileSystemSizeMB | 1714 | | 488 | no - 941735, 1 | force_latency | 70 | | all:none | no [4] + 941735, 1 | force_latency | 70 | | all:none | no [1] [4] 941735, 1 | grub:intel_idle.max_cstate | 1 | | NA | no [2] [3] [6] 941735, 1 | kernel.shmmax | 18446744073709551615 | | 18446744073709551615 | yes + 941735, 1 | kernel.shmmni | | | NA | - [16] [7] ` + footnote1 + ` [2] setting is not available on the system @@ -133,6 +160,7 @@ func TestPrintNoteFields(t *testing.T) { [6] grub settings are mostly covered by other settings. See man page saptune-note(5) for details [7] parameter value is untouched by default [10] parameter is defined twice, see section [sys] 'sys:block.sdd.queue.scheduler' from the other applied notes + [16] parameter not available on the system, setting not possible ` var printMatchText4 = ` @@ -146,6 +174,7 @@ func TestPrintNoteFields(t *testing.T) { force_latency | all:none | 70 | | [1] [4] grub:intel_idle.max_cstate | NA | 1 | | [2] [3] [6] kernel.shmmax | 18446744073709551615 | 18446744073709551615 | | + kernel.shmmni | NA | | | [16] [7] ` + footnote1 + ` [2] setting is not available on the system @@ -155,6 +184,7 @@ func TestPrintNoteFields(t *testing.T) { [6] grub settings are mostly covered by other settings. See man page saptune-note(5) for details [7] parameter value is untouched by default [10] parameter is defined twice, see section [sys] 'sys:block.sdd.queue.scheduler' from the other applied notes + [16] parameter not available on the system, setting not possible ` checkCorrectMessage := func(t *testing.T, got, want string) { @@ -187,8 +217,9 @@ func TestPrintNoteFields(t *testing.T) { fcomp14 := note.FieldComparison{ReflectFieldName: "Inform", ReflectMapKey: "IO_SCHEDULER_sdc", ActualValue: "", ExpectedValue: "bfq", ActualValueJS: "", ExpectedValueJS: "bfq", MatchExpectation: false} fcomp15 := note.FieldComparison{ReflectFieldName: "SysctlParams", ReflectMapKey: "IO_SCHEDULER_sdd", ActualValue: "bfq", ExpectedValue: "", ActualValueJS: "bfq", ExpectedValueJS: "", MatchExpectation: false} fcomp16 := note.FieldComparison{ReflectFieldName: "Inform", ReflectMapKey: "IO_SCHEDULER_sdd", ActualValue: "", ExpectedValue: "[sys] 'sys:block.sdd.queue.scheduler' from the other applied notes", ActualValueJS: "", ExpectedValueJS: "[sys] 'sys:block.sdd.queue.scheduler' from the other applied notes", MatchExpectation: false} + fcomp17 := note.FieldComparison{ReflectFieldName: "SysctlParams", ReflectMapKey: "kernel.shmmni", ActualValue: "PNA", ExpectedValue: "", ActualValueJS: "PNA", ExpectedValueJS: "", MatchExpectation: false} - map941735 := map[string]note.FieldComparison{"ConfFilePath": fcomp1, "ID": fcomp2, "DescriptiveName": fcomp3, "SysctlParams[ShmFileSystemSizeMB]": fcomp4, "SysctlParams[kernel.shmmax]": fcomp5, "SysctlParams[IO_SCHEDULER_vda]": fcomp6, "SysctlParams[grub:intel_idle.max_cstate]": fcomp7, "SysctlParams[force_latency]": fcomp8, "Inform[force_latency]": fcomp9, "Inform[IO_SCHEDULER_vda]": fcomp10, "SysctlParams[IO_SCHEDULER_sdb]": fcomp11, "Inform[IO_SCHEDULER_sdb]": fcomp12, "SysctlParams[IO_SCHEDULER_sdc]": fcomp13, "Inform[IO_SCHEDULER_sdc]": fcomp14, "SysctlParams[IO_SCHEDULER_sdd]": fcomp15, "Inform[IO_SCHEDULER_sdd]": fcomp16} + map941735 := map[string]note.FieldComparison{"ConfFilePath": fcomp1, "ID": fcomp2, "DescriptiveName": fcomp3, "SysctlParams[ShmFileSystemSizeMB]": fcomp4, "SysctlParams[kernel.shmmax]": fcomp5, "SysctlParams[IO_SCHEDULER_vda]": fcomp6, "SysctlParams[grub:intel_idle.max_cstate]": fcomp7, "SysctlParams[force_latency]": fcomp8, "Inform[force_latency]": fcomp9, "Inform[IO_SCHEDULER_vda]": fcomp10, "SysctlParams[IO_SCHEDULER_sdb]": fcomp11, "Inform[IO_SCHEDULER_sdb]": fcomp12, "SysctlParams[IO_SCHEDULER_sdc]": fcomp13, "Inform[IO_SCHEDULER_sdc]": fcomp14, "SysctlParams[IO_SCHEDULER_sdd]": fcomp15, "Inform[IO_SCHEDULER_sdd]": fcomp16, "SysctlParams[kernel.shmmni]": fcomp17} noteComp := map[string]map[string]note.FieldComparison{"941735": map941735} t.Run("verify with header", func(t *testing.T) { @@ -215,4 +246,280 @@ func TestPrintNoteFields(t *testing.T) { txt := buffer.String() checkCorrectMessage(t, txt, printMatchText4) }) + + t.Run("verify with header and show-non-compliant", func(t *testing.T) { + os.Args = []string{"saptune", "note", "list", "--colorscheme=black", "--show-non-compliant", "--format=json", "--force", "--dryrun", "--help", "--version"} + system.RereadArgs() + + buffer := bytes.Buffer{} + PrintNoteFields(&buffer, "HEAD", noteComp, true, nil) + txt := buffer.String() + checkCorrectMessage(t, txt, printMatchText1NoCompl) + }) +} + +func TestGetColorScheme(t *testing.T) { + os.Args = []string{"saptune", "status"} + system.RereadArgs() + saptuneSysconfig = path.Join(os.Getenv("GOPATH"), "/src/github.com/SUSE/saptune/testdata/etc/sysconfig/saptune") + expColorScheme := "full-green-zebra" + colorScheme := getColorScheme() + if colorScheme != expColorScheme { + t.Errorf("got: %+v, expected: %+v\n", colorScheme, expColorScheme) + } +} + +func TestColorPrint(t *testing.T) { + format := ` %-16s | %-26s | %-15s | %-11s | %-14s | %2s +` + colorScheme := "full-green-zebra" + compliant := "yes" + expFormat := ` %-16s | %-26s | %-15s | %-11s | %-14s | %2s +` + cFormat, cCompl := colorPrint(format, compliant, colorScheme) + if cFormat != expFormat { + t.Errorf("got: %+v, expected: %+v\n", cFormat, expFormat) + } + if cCompl != compliant { + t.Errorf("got: %+v, expected: %+v\n", cCompl, compliant) + } + + compliant = "no [2] [3] [6]" + expFormat = ` %-16s | %-26s | %-15s | %-11s | %-14s | %2s +` + cFormat, cCompl = colorPrint(format, compliant, colorScheme) + if cFormat != expFormat { + t.Errorf("got: %+v, expected: %+v\n", cFormat, expFormat) + } + if cCompl != compliant { + t.Errorf("got: %+v, expected: %+v\n", cCompl, compliant) + } + + compliant = " - [1]" + cFormat, cCompl = colorPrint(format, compliant, colorScheme) + if cFormat != format { + t.Errorf("got: %+v, expected: %+v\n", cFormat, format) + } + if cCompl != compliant { + t.Errorf("got: %+v, expected: %+v\n", cCompl, compliant) + } + + colorScheme = "cmpl-green-zebra" + compliant = "yes" + expCompl := `yes` + cFormat, cCompl = colorPrint(format, compliant, colorScheme) + if cFormat != format { + t.Errorf("got: %+v, expected: %+v\n", cFormat, format) + } + if cCompl != expCompl { + t.Errorf("got: %+v, expected: %+v\n", cCompl, expCompl) + } + + compliant = "no [2] [3] [6]" + expCompl = `no [2] [3] [6]` + cFormat, cCompl = colorPrint(format, compliant, colorScheme) + if cFormat != format { + t.Errorf("got: %+v, expected: %+v\n", cFormat, format) + } + if cCompl != expCompl { + t.Errorf("got: %+v, expected: %+v\n", cCompl, expCompl) + } + + compliant = " - [1]" + cFormat, cCompl = colorPrint(format, compliant, colorScheme) + if cFormat != format { + t.Errorf("got: %+v, expected: %+v\n", cFormat, format) + } + if cCompl != compliant { + t.Errorf("got: %+v, expected: %+v\n", cCompl, compliant) + } + + colorScheme = "full-blue-zebra" + compliant = "yes" + expFormat = ` %-16s | %-26s | %-15s | %-11s | %-14s | %2s +` + cFormat, cCompl = colorPrint(format, compliant, colorScheme) + if cFormat != expFormat { + t.Errorf("got: %+v, expected: %+v\n", cFormat, expFormat) + } + if cCompl != compliant { + t.Errorf("got: %+v, expected: %+v\n", cCompl, compliant) + } + + compliant = "no [2] [3] [6]" + expFormat = ` %-16s | %-26s | %-15s | %-11s | %-14s | %2s +` + cFormat, cCompl = colorPrint(format, compliant, colorScheme) + if cFormat != expFormat { + t.Errorf("got: %+v, expected: %+v\n", cFormat, expFormat) + } + if cCompl != compliant { + t.Errorf("got: %+v, expected: %+v\n", cCompl, compliant) + } + + compliant = " - [1]" + cFormat, cCompl = colorPrint(format, compliant, colorScheme) + if cFormat != format { + t.Errorf("got: %+v, expected: %+v\n", cFormat, format) + } + if cCompl != compliant { + t.Errorf("got: %+v, expected: %+v\n", cCompl, compliant) + } + + colorScheme = "cmpl-blue-zebra" + compliant = "yes" + expCompl = `yes` + cFormat, cCompl = colorPrint(format, compliant, colorScheme) + if cFormat != format { + t.Errorf("got: %+v, expected: %+v\n", cFormat, format) + } + if cCompl != expCompl { + t.Errorf("got: %+v, expected: %+v\n", cCompl, expCompl) + } + + compliant = "no [2] [3] [6]" + expCompl = `no [2] [3] [6]` + cFormat, cCompl = colorPrint(format, compliant, colorScheme) + if cFormat != format { + t.Errorf("got: %+v, expected: %+v\n", cFormat, format) + } + if cCompl != expCompl { + t.Errorf("got: %+v, expected: %+v\n", cCompl, expCompl) + } + + compliant = " - [1]" + cFormat, cCompl = colorPrint(format, compliant, colorScheme) + if cFormat != format { + t.Errorf("got: %+v, expected: %+v\n", cFormat, format) + } + if cCompl != compliant { + t.Errorf("got: %+v, expected: %+v\n", cCompl, compliant) + } + + colorScheme = "full-red-noncmpl" + compliant = "yes" + cFormat, cCompl = colorPrint(format, compliant, colorScheme) + if cFormat != format { + t.Errorf("got: %+v, expected: %+v\n", cFormat, format) + } + if cCompl != compliant { + t.Errorf("got: %+v, expected: %+v\n", cCompl, compliant) + } + + compliant = "no [2] [3] [6]" + expFormat = ` %-16s | %-26s | %-15s | %-11s | %-14s | %2s +` + cFormat, cCompl = colorPrint(format, compliant, colorScheme) + if cFormat != expFormat { + t.Errorf("got: %+v, expected: %+v\n", cFormat, expFormat) + } + if cCompl != compliant { + t.Errorf("got: %+v, expected: %+v\n", cCompl, compliant) + } + + compliant = " - [1]" + cFormat, cCompl = colorPrint(format, compliant, colorScheme) + if cFormat != format { + t.Errorf("got: %+v, expected: %+v\n", cFormat, format) + } + if cCompl != compliant { + t.Errorf("got: %+v, expected: %+v\n", cCompl, compliant) + } + + colorScheme = "red-noncmpl" + compliant = "yes" + cFormat, cCompl = colorPrint(format, compliant, colorScheme) + if cFormat != format { + t.Errorf("got: %+v, expected: %+v\n", cFormat, format) + } + if cCompl != compliant { + t.Errorf("got: %+v, expected: %+v\n", cCompl, compliant) + } + + compliant = "no [2] [3] [6]" + expCompl = `no [2] [3] [6]` + cFormat, cCompl = colorPrint(format, compliant, colorScheme) + if cFormat != format { + t.Errorf("got: %+v, expected: %+v\n", cFormat, format) + } + if cCompl != expCompl { + t.Errorf("got: %+v, expected: %+v\n", cCompl, expCompl) + } + + compliant = " - [1]" + cFormat, cCompl = colorPrint(format, compliant, colorScheme) + if cFormat != format { + t.Errorf("got: %+v, expected: %+v\n", cFormat, format) + } + if cCompl != compliant { + t.Errorf("got: %+v, expected: %+v\n", cCompl, compliant) + } + + colorScheme = "full-yellow-noncmpl" + compliant = "yes" + cFormat, cCompl = colorPrint(format, compliant, colorScheme) + if cFormat != format { + t.Errorf("got: %+v, expected: %+v\n", cFormat, format) + } + if cCompl != compliant { + t.Errorf("got: %+v, expected: %+v\n", cCompl, compliant) + } + + compliant = "no [2] [3] [6]" + expFormat = ` %-16s | %-26s | %-15s | %-11s | %-14s | %2s +` + cFormat, cCompl = colorPrint(format, compliant, colorScheme) + if cFormat != expFormat { + t.Errorf("got: %+v, expected: %+v\n", cFormat, expFormat) + } + if cCompl != compliant { + t.Errorf("got: %+v, expected: %+v\n", cCompl, compliant) + } + + compliant = " - [1]" + cFormat, cCompl = colorPrint(format, compliant, colorScheme) + if cFormat != format { + t.Errorf("got: %+v, expected: %+v\n", cFormat, format) + } + if cCompl != compliant { + t.Errorf("got: %+v, expected: %+v\n", cCompl, compliant) + } + + colorScheme = "yellow-noncmpl" + compliant = "yes" + cFormat, cCompl = colorPrint(format, compliant, colorScheme) + if cFormat != format { + t.Errorf("got: %+v, expected: %+v\n", cFormat, format) + } + if cCompl != compliant { + t.Errorf("got: %+v, expected: %+v\n", cCompl, compliant) + } + + compliant = "no [2] [3] [6]" + expCompl = `no [2] [3] [6]` + cFormat, cCompl = colorPrint(format, compliant, colorScheme) + if cFormat != format { + t.Errorf("got: %+v, expected: %+v\n", cFormat, format) + } + if cCompl != expCompl { + t.Errorf("got: %+v, expected: %+v\n", cCompl, expCompl) + } + + compliant = " - [1]" + cFormat, cCompl = colorPrint(format, compliant, colorScheme) + if cFormat != format { + t.Errorf("got: %+v, expected: %+v\n", cFormat, format) + } + if cCompl != compliant { + t.Errorf("got: %+v, expected: %+v\n", cCompl, compliant) + } + + colorScheme = "black" + cFormat, cCompl = colorPrint(format, compliant, colorScheme) + if cFormat != format { + t.Errorf("got: %+v, expected: %+v\n", cFormat, format) + } + if cCompl != compliant { + t.Errorf("got: %+v, expected: %+v\n", cCompl, compliant) + } } diff --git a/app/app_test.go b/app/app_test.go index d089c236..129ee8f6 100644 --- a/app/app_test.go +++ b/app/app_test.go @@ -6,10 +6,12 @@ import ( "github.com/SUSE/saptune/sap/note" "github.com/SUSE/saptune/sap/param" "github.com/SUSE/saptune/sap/solution" + "github.com/SUSE/saptune/system" "io/ioutil" "os" "path" "reflect" + "strings" "testing" "time" ) @@ -139,6 +141,7 @@ func TestReadConfig(t *testing.T) { time.Sleep(5 * time.Second) // Read from testdata config 'testdata/etc/sysconfig/saptune' + _ = system.CopyFile(path.Join(TstFilesInGOPATH, "etc/sysconfig/saptune_tstorg"), path.Join(TstFilesInGOPATH, "etc/sysconfig/saptune")) tApp := InitialiseApp(TstFilesInGOPATH, "", AllTestNotes, AllTestSolutions) matchTxt := ` current order of enabled notes is: 2205917 2684254 1680803 @@ -476,27 +479,90 @@ func TestGetNoteByID(t *testing.T) { if _, err := tuneApp.GetNoteByID("8932147"); err == nil { t.Errorf("Note ID '8932147' should NOT be available, but is reported as available. AllNote is '%+v'\n", tuneApp.AllNotes) } - tuneApp = InitialiseApp(path.Join(SampleNoteDataDir, "conf"), path.Join(SampleNoteDataDir, "data"), AllTestNotes, AllTestSolutions) } func TestNoteSanityCheck(t *testing.T) { os.RemoveAll(SampleNoteDataDir) defer os.RemoveAll(SampleNoteDataDir) tuneApp := InitialiseApp(path.Join(SampleNoteDataDir, "conf"), path.Join(SampleNoteDataDir, "data"), AllTestNotes, AllTestSolutions) + + sectPath := "/run/saptune/sections" + sectFile := "/run/saptune/sections/8932147.sections" + // copy empty file + src := path.Join(os.Getenv("GOPATH"), "/src/github.com/SUSE/saptune/testdata/saptune_NOEXIT") + dest := tuneApp.State.GetPathToNote("8932147") + os.MkdirAll(path.Join(SampleNoteDataDir, "/data/run/saptune/saved_state"), 0755) + err := system.CopyFile(src, dest) + if err != nil { + t.Error(err) + } + defer os.RemoveAll(dest) + os.MkdirAll(sectPath, 0755) + err = system.CopyFile(src, sectFile) + if err != nil { + t.Error(err) + } + defer os.RemoveAll(sectFile) + if err := tuneApp.TuneNote("1002"); err != nil { t.Error(err) } if err := tuneApp.TuneNote("1001"); err != nil { t.Error(err) } + if err := tuneApp.NoteSanityCheck(); err != nil { + t.Errorf("Error during NoteSanityCheck - '%v'\n", err) + } + tuneApp.NoteApplyOrder = append(tuneApp.NoteApplyOrder, "8932147") if err := tuneApp.NoteSanityCheck(); err != nil { t.Errorf("Error during NoteSanityCheck - '%v'\n", err) } + os.Remove(dest) + os.Remove(sectFile) + // existing, but empty section file + err = system.CopyFile(src, sectFile) + if err != nil { + t.Error(err) + } + // copy NON empty file + src = path.Join(os.Getenv("GOPATH"), "/src/github.com/SUSE/saptune/testdata/product_name") + err = system.CopyFile(src, dest) + if err != nil { + t.Error(err) + } + tuneApp.NoteApplyOrder = append(tuneApp.NoteApplyOrder, "8932147") + if err := tuneApp.NoteSanityCheck(); err != nil { + t.Errorf("Error during NoteSanityCheck - '%v'\n", err) + } + + os.Remove(dest) + os.Remove(sectFile) + // no section file + src = path.Join(os.Getenv("GOPATH"), "/src/github.com/SUSE/saptune/testdata/product_name") + err = system.CopyFile(src, dest) + if err != nil { + t.Error(err) + } tuneApp.NoteApplyOrder = append(tuneApp.NoteApplyOrder, "8932147") - err := tuneApp.NoteSanityCheck() - t.Logf("NoteSanityCheck - '%v'\n", err) + if err := tuneApp.NoteSanityCheck(); err != nil { + t.Errorf("Error during NoteSanityCheck - '%v'\n", err) + } + + os.Remove(dest) + os.Remove(sectFile) + // section file, but no state file + src = path.Join(os.Getenv("GOPATH"), "/src/github.com/SUSE/saptune/testdata/saptune_NOEXIT") + err = system.CopyFile(src, sectFile) + if err != nil { + t.Error(err) + } + tuneApp.NoteApplyOrder = append(tuneApp.NoteApplyOrder, "8932147") + if err := tuneApp.NoteSanityCheck(); err != nil { + t.Errorf("Error during NoteSanityCheck - '%v'\n", err) + } + tuneApp = InitialiseApp(path.Join(SampleNoteDataDir, "conf"), path.Join(SampleNoteDataDir, "data"), AllTestNotes, AllTestSolutions) } @@ -517,6 +583,151 @@ func TestTuneAll(t *testing.T) { tuneApp = InitialiseApp(path.Join(SampleNoteDataDir, "conf"), path.Join(SampleNoteDataDir, "data"), AllTestNotes, AllTestSolutions) } +func TestAppliedNotes(t *testing.T) { + os.RemoveAll(SampleNoteDataDir) + defer os.RemoveAll(SampleNoteDataDir) + tuneApp := InitialiseApp(path.Join(SampleNoteDataDir, "conf"), path.Join(SampleNoteDataDir, "data"), AllTestNotes, AllTestSolutions) + tuneApp.NoteApplyOrder = append(tuneApp.NoteApplyOrder, "1001") + tuneApp.NoteApplyOrder = append(tuneApp.NoteApplyOrder, "1002") + + expNotes := "" + applNotes := tuneApp.AppliedNotes() + if expNotes != applNotes { + t.Errorf("got: %+v, expected: %+v\n", applNotes, expNotes) + } + + src := path.Join(os.Getenv("GOPATH"), "/src/github.com/SUSE/saptune/testdata/saptune_NOEXIT") + dest := tuneApp.State.GetPathToNote("1001") + os.MkdirAll(path.Join(SampleNoteDataDir, "/data/run/saptune/saved_state"), 0755) + err := system.CopyFile(src, dest) + if err != nil { + t.Error(err) + } + defer os.RemoveAll(dest) + + expNotes = "1001" + applNotes = tuneApp.AppliedNotes() + if expNotes != applNotes { + t.Errorf("got: %+v, expected: %+v\n", applNotes, expNotes) + } +} + +func TestIsNoteApplied(t *testing.T) { + // test fix for bsc#1167618 + os.RemoveAll(SampleNoteDataDir) + defer os.RemoveAll(SampleNoteDataDir) + tuneApp := InitialiseApp(path.Join(SampleNoteDataDir, "conf"), path.Join(SampleNoteDataDir, "data"), AllTestNotes, AllTestSolutions) + tuneApp.NoteApplyOrder = append(tuneApp.NoteApplyOrder, "1001") + tuneApp.NoteApplyOrder = append(tuneApp.NoteApplyOrder, "1002") + + // copy empty file + src := path.Join(os.Getenv("GOPATH"), "/src/github.com/SUSE/saptune/testdata/saptune_NOEXIT") + dest := tuneApp.State.GetPathToNote("8932147") + os.MkdirAll(path.Join(SampleNoteDataDir, "/data/run/saptune/saved_state"), 0755) + err := system.CopyFile(src, dest) + if err != nil { + t.Error(err) + } + defer os.RemoveAll(dest) + + expmmatch := "" + mmatch, applied := tuneApp.IsNoteApplied("8932147") + if applied { + t.Errorf("expected 'false' but got '%+v'\n", applied) + } + if expmmatch != mmatch { + t.Errorf("got: %+v, expected: %+v\n", mmatch, expmmatch) + } + + os.Remove(dest) + // copy NON empty file + src = path.Join(os.Getenv("GOPATH"), "/src/github.com/SUSE/saptune/testdata/product_name") + err = system.CopyFile(src, dest) + if err != nil { + t.Error(err) + } + + expmmatch = "mismatch" + mmatch, applied = tuneApp.IsNoteApplied("8932147") + if !applied { + t.Errorf("expected 'false' but got '%+v'\n", applied) + } + if expmmatch != mmatch { + t.Errorf("got: %+v, expected: %+v\n", mmatch, expmmatch) + } +} + +func TestGetSortedAllNotes(t *testing.T) { + os.RemoveAll(SampleNoteDataDir) + defer os.RemoveAll(SampleNoteDataDir) + tuneApp := InitialiseApp(path.Join(SampleNoteDataDir, "conf"), path.Join(SampleNoteDataDir, "data"), AllTestNotes, AllTestSolutions) + expNotes := "1001 1002" + allNotes := strings.Join(tuneApp.GetSortedAllNotes(), " ") + if expNotes != allNotes { + t.Errorf("got: %+v, expected: %+v\n", allNotes, expNotes) + } +} + +func TestIsSolutionEnabled(t *testing.T) { + tuneApp := InitialiseApp(path.Join(SampleNoteDataDir, "conf"), path.Join(SampleNoteDataDir, "data"), AllTestNotes, AllTestSolutions) + tuneApp.TuneForSolutions = []string{"sol1"} + if tuneApp.IsSolutionEnabled("sol15") { + t.Error("expected 'false' but got 'true'") + } + if !tuneApp.IsSolutionEnabled("sol1") { + t.Error("expected 'true' but got 'false'") + } +} + +func TestAppliedSolution(t *testing.T) { + os.RemoveAll(SampleNoteDataDir) + defer os.RemoveAll(SampleNoteDataDir) + tuneApp := InitialiseApp(path.Join(SampleNoteDataDir, "conf"), path.Join(SampleNoteDataDir, "data"), AllTestNotes, AllTestSolutions) + tuneApp.NoteApplyOrder = append(tuneApp.NoteApplyOrder, "1001") + tuneApp.NoteApplyOrder = append(tuneApp.NoteApplyOrder, "1002") + tuneApp.TuneForSolutions = []string{"sol1"} + + expSol := "" + expState := "" + applSol, state := tuneApp.AppliedSolution() + if expSol != applSol { + t.Errorf("got: %+v, expected: %+v\n", applSol, expSol) + } + if expState != state { + t.Errorf("got: %+v, expected: %+v\n", state, expState) + } + + src := path.Join(os.Getenv("GOPATH"), "/src/github.com/SUSE/saptune/testdata/saptune_NOEXIT") + dest := tuneApp.State.GetPathToNote("1001") + os.MkdirAll(path.Join(SampleNoteDataDir, "/data/run/saptune/saved_state"), 0755) + err := system.CopyFile(src, dest) + if err != nil { + t.Error(err) + } + defer os.RemoveAll(dest) + + expSol = "sol1" + expState = "fully" + applSol, state = tuneApp.AppliedSolution() + if expSol != applSol { + t.Errorf("got: %+v, expected: %+v\n", applSol, expSol) + } + if expState != state { + t.Errorf("got: %+v, expected: %+v\n", state, expState) + } + + tuneApp.TuneForSolutions = []string{"sol12"} + expSol = "sol12" + expState = "partial" + applSol, state = tuneApp.AppliedSolution() + if expSol != applSol { + t.Errorf("got: %+v, expected: %+v\n", applSol, expSol) + } + if expState != state { + t.Errorf("got: %+v, expected: %+v\n", state, expState) + } +} + func TestInitialiseApp(t *testing.T) { tstApp := InitialiseApp("/sys/", "", AllTestNotes, AllTestSolutions) if len(tstApp.TuneForSolutions) != 0 && len(tstApp.TuneForNotes) != 0 && len(tstApp.NoteApplyOrder) != 0 { diff --git a/main.go b/main.go index e5b4fe79..b2a7f62e 100644 --- a/main.go +++ b/main.go @@ -122,7 +122,7 @@ func main() { system.ErrorExit("Error during NoteSanityCheck - '%v'\n", err) } checkForTuned() - actions.SelectAction(tuneApp, SaptuneVersion) + actions.SelectAction(os.Stdout, tuneApp, SaptuneVersion) system.ErrorExit("", 0) } diff --git a/main_test.go b/main_test.go index 292ee423..2e7e1c19 100644 --- a/main_test.go +++ b/main_test.go @@ -35,7 +35,91 @@ var checkOut = func(t *testing.T, got, want string) { } } -// func TestcheckForTuned() +func TestCheckWorkingArea(t *testing.T) { + os.Remove("/usr/share/saptune") + src := path.Join(os.Getenv("GOPATH"), "/src/github.com/SUSE/saptune/ospackage/usr/share/saptune") + target := "/usr/share/saptune" + if err := os.Symlink(src, target); err != nil { + t.Errorf("linking '%s' to '%s' failed - '%v'", src, target, err) + } + defer os.Remove("/usr/share/saptune") + + // setup ErrorExit handling + oldOSExit := system.OSExit + defer func() { system.OSExit = oldOSExit }() + system.OSExit = tstosExit + oldErrorExitOut := system.ErrorExitOut + defer func() { system.ErrorExitOut = oldErrorExitOut }() + system.ErrorExitOut = tstErrorExitOut + + errExitbuffer := bytes.Buffer{} + tstwriter = &errExitbuffer + + checkWorkingArea() + if tstRetErrorExit != -1 { + t.Errorf("error exit should be '-1' and NOT '%v'\n", tstRetErrorExit) + } + errExOut := errExitbuffer.String() + if errExOut != "" { + t.Errorf("wrong text returned by ErrorExit: '%v' instead of ''\n", errExOut) + } + + // cleanup - remove link and create directory + os.Remove("/usr/share/saptune") + os.MkdirAll("/usr/share/saptune", 0755) + os.RemoveAll("/var/lib/saptune/working/notes") +} + +func TestCallSaptuneCheckScript(t *testing.T) { + // setup ErrorExit handling + oldOSExit := system.OSExit + defer func() { system.OSExit = oldOSExit }() + system.OSExit = tstosExit + oldErrorExitOut := system.ErrorExitOut + defer func() { system.ErrorExitOut = oldErrorExitOut }() + system.ErrorExitOut = tstErrorExitOut + + errExitbuffer := bytes.Buffer{} + tstwriter = &errExitbuffer + txt2chk := `ERROR: command '/usr/sbin/saptune_check' failed with error 'fork/exec /usr/sbin/saptune_check: no such file or directory' + +` + + callSaptuneCheckScript("check") + if tstRetErrorExit != 1 { + t.Errorf("error exit should be '1' and NOT '%v'\n", tstRetErrorExit) + } + errExOut := errExitbuffer.String() + if errExOut != txt2chk { + t.Errorf("wrong text returned by ErrorExit: '%v' instead of '%v'\n", errExOut, txt2chk) + } + + src := path.Join(os.Getenv("GOPATH"), "/src/github.com/SUSE/saptune/testdata/saptune_check") + dest := "/usr/sbin/saptune_check" + err := system.CopyFile(src, dest) + if err != nil { + t.Error(err) + } + err = os.Chmod(dest, 0755) + if err != nil { + t.Error(err) + } + defer os.Remove(dest) + errExitbuffer.Reset() + + callSaptuneCheckScript("check") + if tstRetErrorExit != 0 { + t.Errorf("error exit should be '0' and NOT '%v'\n", tstRetErrorExit) + } + errExOut = errExitbuffer.String() + if errExOut != "" { + t.Errorf("wrong text returned by ErrorExit: '%v' instead of ''\n", errExOut) + } +} + +func TestCheckForTuned(t *testing.T) { + checkForTuned() +} func TestCheckUpdateLeftOvers(t *testing.T) { checkUpdateLeftOvers() @@ -75,7 +159,7 @@ func TestCheckSaptuneConfigFile(t *testing.T) { saptuneConf = fmt.Sprintf("%s/saptune_NoVersion", TstFilesInGOPATH) matchTxt := fmt.Sprintf("Error: File '%s' is broken. Missing variables 'SAPTUNE_VERSION'\n", saptuneConf) lSwitch = logSwitch - saptuneVers = checkSaptuneConfigFile(&buffer, saptuneConf, lSwitch) + _ = checkSaptuneConfigFile(&buffer, saptuneConf, lSwitch) txt := buffer.String() checkOut(t, txt, matchTxt) @@ -96,7 +180,7 @@ func TestCheckSaptuneConfigFile(t *testing.T) { saptuneVers = "" matchTxt = fmt.Sprintf("Error: Variable 'STAGING' from file '%s' contains a wrong value 'hugo'. Needs to be 'true' or 'false'\n", saptuneConf) lSwitch = logSwitch - saptuneVers = checkSaptuneConfigFile(&buffer, saptuneConf, lSwitch) + _ = checkSaptuneConfigFile(&buffer, saptuneConf, lSwitch) txt = buffer.String() checkOut(t, txt, matchTxt) diff --git a/ospackage/usr/share/saptune/notes/1680803 b/ospackage/usr/share/saptune/notes/1680803 index 413a9f55..3a995f63 100644 --- a/ospackage/usr/share/saptune/notes/1680803 +++ b/ospackage/usr/share/saptune/notes/1680803 @@ -85,9 +85,9 @@ net.ipv4.tcp_rmem = 4096 87380 16777216 net.ipv4.tcp_wmem = 4096 65536 16777216 # Set the keepalive interval to a value higher than 1200 seconds. -# The ABAP dispatcher initiates an empty network request to the database connection -# every 1200 seconds. If the keepalive interval is lower, the operating system might -# close the database connection. +# The ABAP dispatcher initiates an empty network request to the database +# connection every 1200 seconds. If the keepalive interval is lower, the +# operating system might close the database connection. net.ipv4.tcp_keepalive_time = 1250 # Increase the max packet backlog diff --git a/run_saptune_ci_tst.sh b/run_saptune_ci_tst.sh index 6776fa55..87e6be49 100755 --- a/run_saptune_ci_tst.sh +++ b/run_saptune_ci_tst.sh @@ -3,9 +3,6 @@ #echo "zypper in ..." #zypper -n --gpg-auto-import-keys ref && zypper -n --gpg-auto-import-keys in go1.10 go rpcbind cpupower uuidd polkit tuned sysstat -/bin/systemctl start tuned -tuned-adm profile balanced - systemctl --no-pager status # try to resolve systemd status 'degraded' systemctl reset-failed @@ -43,6 +40,18 @@ cd saptune pwd ls -al +echo "start tuned and set profile 'balanced'" +/bin/systemctl start tuned +tuned-adm profile balanced +tuned-adm list + +/bin/systemctl status saptune + +systemctl --no-pager status +# try to resolve systemd status 'degraded' +systemctl reset-failed +systemctl --no-pager status + # to get TasksMax settings work, needs a user login session echo "start nobody login session in background" su --login nobody -c "sleep 4m" & diff --git a/sap/note/db.go b/sap/note/db.go index 4ea3a391..46d8ba9c 100644 --- a/sap/note/db.go +++ b/sap/note/db.go @@ -60,7 +60,7 @@ func (paging LinuxPagingImprovements) Optimise() (Note, error) { // Apply sets the new values in the system func (paging LinuxPagingImprovements) Apply() error { - errs := make([]error, 0, 0) + errs := make([]error, 0) errs = append(errs, system.SetSysctlUint64(system.SysctlPagecacheLimitMB, paging.VMPagecacheLimitMB)) errs = append(errs, system.SetSysctlInt(system.SysctlPagecacheLimitIgnoreDirty, paging.VMPagecacheLimitIgnoreDirty)) diff --git a/sap/note/db_test.go b/sap/note/db_test.go index b5276676..948f37af 100644 --- a/sap/note/db_test.go +++ b/sap/note/db_test.go @@ -29,7 +29,7 @@ func TestLinuxPagingImprovements(t *testing.T) { if o.VMPagecacheLimitMB != 0 || o.VMPagecacheLimitIgnoreDirty != 1 { t.Fatal(o) } - err = optimised.Apply() + _ = optimised.Apply() } func TestLinuxPagingImprovementsError(t *testing.T) { diff --git a/sap/note/ini.go b/sap/note/ini.go index 090e415a..8389e4c7 100644 --- a/sap/note/ini.go +++ b/sap/note/ini.go @@ -259,7 +259,7 @@ func (vend INISettings) Optimise() (Note, error) { // revert the system to the former parameter values func (vend INISettings) Apply() error { var err error - errs := make([]error, 0, 0) + errs := make([]error, 0) revertValues := false pvendID := vend.ID diff --git a/sap/note/ini_test.go b/sap/note/ini_test.go index 6100b356..b638f79b 100644 --- a/sap/note/ini_test.go +++ b/sap/note/ini_test.go @@ -28,7 +28,7 @@ func TestVendorSettings(t *testing.T) { if ini.Name() == "" { t.Error(ini.Name()) } - if ini.Name() != fmt.Sprintf("ini_test: SAP Note file for ini_test\n\t\t\tVersion 2 from 02.11.2017") { + if ini.Name() != "ini_test: SAP Note file for ini_test\n\t\t\tVersion 2 from 02.11.2017" { t.Error(ini.Name()) } @@ -100,7 +100,7 @@ func TestAllSettings(t *testing.T) { if ini.Name() == "" { t.Error(ini.Name()) } - if ini.Name() != fmt.Sprintf("ini_all_test: SAP Note file for ini_all_test\n\t\t\tVersion 3 from 02.01.2019") { + if ini.Name() != "ini_all_test: SAP Note file for ini_all_test\n\t\t\tVersion 3 from 02.01.2019" { t.Error(ini.Name()) } @@ -301,7 +301,7 @@ func TestOverrideAllSettings(t *testing.T) { if ini.Name() == "" { t.Error(ini.Name()) } - if ini.Name() != fmt.Sprintf("ini_all_test: SAP Note file for ini_all_test\n\t\t\tVersion 3 from 02.01.2019") { + if ini.Name() != "ini_all_test: SAP Note file for ini_all_test\n\t\t\tVersion 3 from 02.01.2019" { t.Error(ini.Name()) } @@ -424,7 +424,7 @@ func TestPageCacheSettings(t *testing.T) { if ini.Name() == "" { t.Error(ini.Name()) } - if ini.Name() != fmt.Sprintf("Linux paging improvements\n\t\t\tVersion 14 from 10.08.2015") { + if ini.Name() != "Linux paging improvements\n\t\t\tVersion 14 from 10.08.2015" { t.Error(ini.Name()) } diff --git a/sap/note/note_test.go b/sap/note/note_test.go index add545d5..e6c48bb2 100644 --- a/sap/note/note_test.go +++ b/sap/note/note_test.go @@ -48,6 +48,13 @@ func TestNoteSerialisation(t *testing.T) { if eq, diff, valapply := CompareNoteFields(sysctl, newSysctl); !eq { t.Fatal(diff, valapply) } + + sysctl = INISettings{ConfFilePath: path.Join(os.Getenv("GOPATH"), "/src/github.com/SUSE/saptune/testdata/grub_test.ini"), SysctlParams: map[string]string{"grub:transparent_hugepage": "never", "grub:quiet": "", "systemd:uuidd.socket": "start", "systemd:sysstat.service": "start", "reminder": ""}, ValuesToApply: map[string]string{"": ""}} + newSysctl = INISettings{} + jsonMarshalAndBack(sysctl, &newSysctl, t) + if eq, diff, valapply := CompareNoteFields(sysctl, newSysctl); !eq { + t.Fatal(diff, valapply) + } } func TestCmpMapValue(t *testing.T) { @@ -278,6 +285,34 @@ func TestGetTuningOptions(t *testing.T) { if sorted := allOpts.GetSortedIDs(); len(allOpts) != len(sorted) { t.Fatal(sorted, allOpts) } + allOpts = GetTuningOptions(OSNotesInGOPATH, TstFilesInGOPATH) + if sorted := allOpts.GetSortedIDs(); len(allOpts) != len(sorted) { + t.Fatal(sorted, allOpts) + } +} + +func TestGetNoteHeadData(t *testing.T) { + allOpts := GetTuningOptions("", TstFilesInGOPATH) + tstNote := allOpts["900929"] + tstDesc := "Linux: STORAGE_PARAMETERS_WRONG_SET and 'mmap() failed'" + tstVers := "7" + tstDate := "31.07.2017" + tstRef := "https://launchpad.support.sap.com/#/notes/900929" + noteDesc, noteVers, noteRdate, noteRefs := GetNoteHeadData(tstNote) + if noteDesc != tstDesc { + t.Errorf("got: %+v, expected: %+v\n", noteDesc, tstDesc) + } + if noteVers != tstVers { + t.Errorf("got: %+v, expected: %+v\n", noteVers, tstVers) + } + if noteRdate != tstDate { + t.Errorf("got: %+v, expected: %+v\n", noteRdate, tstDate) + } + for _, ref := range noteRefs { + if ref != tstRef { + t.Errorf("got: %+v, expected: %+v\n", noteRefs, tstRef) + } + } } func TestCompareJSValu(t *testing.T) { diff --git a/sap/note/parameter.go b/sap/note/parameter.go index 7cd803f3..1ef01aaf 100644 --- a/sap/note/parameter.go +++ b/sap/note/parameter.go @@ -111,7 +111,7 @@ func GetSavedParameterNotes(param string) ParameterNotes { return pEntries } if len(content) != 0 { - err = json.Unmarshal(content, &pEntries) + _ = json.Unmarshal(content, &pEntries) } return pEntries } diff --git a/sap/note/sectBlock.go b/sap/note/sectBlock.go index c0a3a1e7..f7941606 100644 --- a/sap/note/sectBlock.go +++ b/sap/note/sectBlock.go @@ -14,10 +14,6 @@ import ( // GetBlkVal initialise the block device structure with the current // system settings func GetBlkVal(key string, cur *param.BlockDeviceQueue) (string, string, error) { - newQueue := make(map[string]string) - newReq := make(map[string]int) - newRah := make(map[string]int) - newMse := make(map[string]int) retVal := "" info := "" @@ -27,7 +23,7 @@ func GetBlkVal(key string, cur *param.BlockDeviceQueue) (string, string, error) if err != nil { return "", info, err } - newQueue = newIOQ.(param.BlockDeviceSchedulers).SchedulerChoice + newQueue := newIOQ.(param.BlockDeviceSchedulers).SchedulerChoice retVal = newQueue[strings.TrimPrefix(key, "IO_SCHEDULER_")] cur.BlockDeviceSchedulers = newIOQ.(param.BlockDeviceSchedulers) case system.IsNrreq.MatchString(key): @@ -35,7 +31,7 @@ func GetBlkVal(key string, cur *param.BlockDeviceQueue) (string, string, error) if err != nil { return "", info, err } - newReq = newNrR.(param.BlockDeviceNrRequests).NrRequests + newReq := newNrR.(param.BlockDeviceNrRequests).NrRequests retVal = strconv.Itoa(newReq[strings.TrimPrefix(key, "NRREQ_")]) cur.BlockDeviceNrRequests = newNrR.(param.BlockDeviceNrRequests) case system.IsRahead.MatchString(key): @@ -43,7 +39,7 @@ func GetBlkVal(key string, cur *param.BlockDeviceQueue) (string, string, error) if err != nil { return "", info, err } - newRah = newRahead.(param.BlockDeviceReadAheadKB).ReadAheadKB + newRah := newRahead.(param.BlockDeviceReadAheadKB).ReadAheadKB retVal = strconv.Itoa(newRah[strings.TrimPrefix(key, "READ_AHEAD_KB_")]) cur.BlockDeviceReadAheadKB = newRahead.(param.BlockDeviceReadAheadKB) case system.IsMsect.MatchString(key): @@ -51,7 +47,7 @@ func GetBlkVal(key string, cur *param.BlockDeviceQueue) (string, string, error) if err != nil { return "", info, err } - newMse = newMsect.(param.BlockDeviceMaxSectorsKB).MaxSectorsKB + newMse := newMsect.(param.BlockDeviceMaxSectorsKB).MaxSectorsKB retVal = strconv.Itoa(newMse[strings.TrimPrefix(key, "MAX_SECTORS_KB_")]) cur.BlockDeviceMaxSectorsKB = newMsect.(param.BlockDeviceMaxSectorsKB) } @@ -108,7 +104,7 @@ func OptBlkVal(key, cfgval string, cur *param.BlockDeviceQueue, bOK map[string][ opt, _ := cur.BlockDeviceReadAheadKB.Optimise(ival) cur.BlockDeviceReadAheadKB = opt.(param.BlockDeviceReadAheadKB) case system.IsMsect.MatchString(key): - ival, _ := strconv.Atoi(sval) + var ival int ival, sval, info = chkMaxHWsector(key, sval) opt, _ := cur.BlockDeviceMaxSectorsKB.Optimise(ival) cur.BlockDeviceMaxSectorsKB = opt.(param.BlockDeviceMaxSectorsKB) diff --git a/sap/note/sectBlock_test.go b/sap/note/sectBlock_test.go index 8af5c9b0..bde0b54d 100644 --- a/sap/note/sectBlock_test.go +++ b/sap/note/sectBlock_test.go @@ -81,12 +81,16 @@ func TestOptBlkVal(t *testing.T) { if info == "NA" { t.Logf("scheduler '%s' is not supported\n", val) } + val, info = OptBlkVal("IO_SCHEDULER_sda", "", &tblck, blckOK) + if val != "" || info != "" { + t.Error(val, info) + } - val, info = OptBlkVal("NRREQ_sda", "512", &tblck, blckOK) + val, _ = OptBlkVal("NRREQ_sda", "512", &tblck, blckOK) if val != "512" { t.Error(val) } - val, info = OptBlkVal("NRREQ_sdc", "128", &tblck, blckOK) + val, _ = OptBlkVal("NRREQ_sdc", "128", &tblck, blckOK) if val != "128" { t.Error(val) } @@ -98,14 +102,23 @@ func TestSetBlkVal(t *testing.T) { val, info, err := GetBlkVal("IO_SCHEDULER_sda", &tblck) oval := val if err != nil { - t.Error(err) + t.Error(err, info) } val, info = OptBlkVal("IO_SCHEDULER_sda", "noop, none", &tblck, blckOK) if val != "noop" && val != "none" { t.Error(val, info) } // apply - value not used, but map changed above in optimise - err = SetBlkVal("IO_SCHEDULER_sda", "notUsed", &tblck, false) + _ = SetBlkVal("IO_SCHEDULER_sda", "notUsed", &tblck, false) // revert - value will be used to change map before applying - err = SetBlkVal("IO_SCHEDULER_sda", oval, &tblck, true) + _ = SetBlkVal("IO_SCHEDULER_sda", oval, &tblck, true) +} + +func TestChkMaxHWsector(t *testing.T) { + key := "MAX_SECTORS_KB_sda" + val := "18446744073709551615" + ival, sval, info := chkMaxHWsector(key, val) + if info != "limited" { + t.Errorf("expected info as 'limited', but got '%s' - '%+v' - '%+v'\n", info, ival, sval) + } } diff --git a/sap/note/sectLimits.go b/sap/note/sectLimits.go index ef7c9ebf..fd51f3f5 100644 --- a/sap/note/sectLimits.go +++ b/sap/note/sectLimits.go @@ -25,6 +25,8 @@ func GetLimitsVal(value string) (string, string, error) { // /etc/security/limits.d/saptune---.conf if len(lim) < 3 { return "", info, fmt.Errorf("Wrong format of limit entry in Note definition file. Please check") + } else if len(lim) == 3 { + lim = append(lim, "") } else if len(lim) > 3 { info = getLimitInfo(lim[3]) } @@ -54,6 +56,7 @@ func OptLimitsVal(actval, cfgval string) string { // SetLimitsVal applies the settings to the system func SetLimitsVal(key, noteID, value string, revert bool) error { var err error + var secLimits *system.SecLimits limit := value if limit != "" && limit != "NA" { lim := strings.Fields(limit) @@ -68,7 +71,7 @@ func SetLimitsVal(key, noteID, value string, revert bool) error { return nil } - secLimits, err := system.ParseSecLimitsFile(dropInFile) + secLimits, err = system.ParseSecLimitsFile(dropInFile) if err != nil { return err } diff --git a/sap/note/sectLimits_test.go b/sap/note/sectLimits_test.go index 839d08e9..cb95131c 100644 --- a/sap/note/sectLimits_test.go +++ b/sap/note/sectLimits_test.go @@ -5,6 +5,17 @@ import ( ) //GetLimitsVal +func TestGetLimitsVal(t *testing.T) { + val, info, err := GetLimitsVal("@sdba soft nofile") + if val != "@sdba soft nofile NA" || info != "" || err != nil { + t.Error(val, info, err) + } + val, info, err = GetLimitsVal("@sdba soft") + if val != "" || info != "" || err == nil { + t.Error(val, info, err) + } +} + func TestOptLimitsVal(t *testing.T) { val := OptLimitsVal("@sdba soft nofile NA", "@sdba soft nofile 32800") if val != "@sdba soft nofile 32800" { diff --git a/sap/note/sectMem.go b/sap/note/sectMem.go index f165c861..83682ce5 100644 --- a/sap/note/sectMem.go +++ b/sap/note/sectMem.go @@ -65,9 +65,10 @@ func OptMemVal(key, actval, cfgval, tmpfspercent string) string { // SetMemVal applies the settings to the system func SetMemVal(key, value string) error { var err error + var val uint64 switch key { case "ShmFileSystemSizeMB": - val, err := strconv.ParseUint(value, 10, 64) + val, err = strconv.ParseUint(value, 10, 64) if val > 0 { if err = system.RemountSHM(uint64(val)); err != nil { return err diff --git a/sap/note/sectSysctl_test.go b/sap/note/sectSysctl_test.go index 4aed835b..acb8cd11 100644 --- a/sap/note/sectSysctl_test.go +++ b/sap/note/sectSysctl_test.go @@ -46,6 +46,14 @@ func TestOptSysctlVal(t *testing.T) { if val != "120" { t.Error(val) } + val = OptSysctlVal(op, "TestParam", "120 300", "100 330 180") + if val != "" { + t.Error(val) + } + val = OptSysctlVal(op, "TestParam", "120 300", "100") + if val != "100 300" { + t.Error(val) + } op = txtparser.Operator(">") val = OptSysctlVal(op, "TestParam", "120", "100") if val != "100" { diff --git a/sap/param/io_test.go b/sap/param/io_test.go index 0811ccaf..caa58008 100644 --- a/sap/param/io_test.go +++ b/sap/param/io_test.go @@ -166,6 +166,43 @@ func TestNrRequests(t *testing.T) { } } + // trigger errors + optimised, err = applied.Optimise(104632) + if err != nil { + t.Error(err) + } + err = optimised.Apply("sda") + if err != nil { + t.Error(err) + } + + schedInspected, err := BlockDeviceSchedulers{}.Inspect() + if err != nil { + t.Error(err, schedInspected) + } + optVal := "sda none" + schedOptimised, err := schedInspected.Optimise(optVal) + if err != nil { + t.Error(err) + } + err = schedOptimised.Apply("sda") + if err != nil { + t.Error(err) + } + + applied, err = BlockDeviceNrRequests{}.Inspect() + if err != nil { + t.Error(err, applied) + } + optimised, err = applied.Optimise(1098776544632) + if err != nil { + t.Error(err) + } + err = optimised.Apply("sda") + if err != nil { + t.Error(err) + } + // reset original values for _, bdev := range blockDev { err = oldvals.Apply(bdev) @@ -248,6 +285,16 @@ func TestReadAheadKB(t *testing.T) { } } + // trigger errors + optimised, err = applied.Optimise(2147483647) + if err != nil { + t.Error(err) + } + err = optimised.Apply("sda") + if err != nil { + t.Error(err) + } + // reset original values for _, bdev := range blockDev { err = oldvals.Apply(bdev) diff --git a/sap/solution/solution.go b/sap/solution/solution.go index 4af102e3..ee9afbcb 100644 --- a/sap/solution/solution.go +++ b/sap/solution/solution.go @@ -145,7 +145,7 @@ func GetOtherSolution(solsDir, noteFiles, extraFiles string) map[string]map[stri // as the function most of the time is called // before the logging is initialized use // Fprintf instead to give customers a hint. - fmt.Fprintf(os.Stderr, "Warning: extra solution '%s' will not override built-in solution implementation\n",param.Key) + fmt.Fprintf(os.Stderr, "Warning: extra solution '%s' will not override built-in solution implementation\n", param.Key) continue } sol[param.Key] = strings.Split(param.Value, "\t") diff --git a/sap/solution/solution_test.go b/sap/solution/solution_test.go index c9e9fa42..d78f87d9 100644 --- a/sap/solution/solution_test.go +++ b/sap/solution/solution_test.go @@ -4,6 +4,7 @@ import ( "github.com/SUSE/saptune/system" "os" "path" + "reflect" "runtime" "strings" "testing" @@ -16,6 +17,15 @@ var DeprecFilesInGOPATH = path.Join(os.Getenv("GOPATH"), "/src/github.com/SUSE/s var TstFilesInGOPATH = path.Join(os.Getenv("GOPATH"), "/src/github.com/SUSE/saptune/testdata/") func TestGetSolutionDefintion(t *testing.T) { + src := path.Join(os.Getenv("GOPATH"), "/src/github.com/SUSE/saptune/testdata/sol/sols/BWA.sol") + if err := os.MkdirAll(ShippedSolSheets, 0755); err != nil { + t.Error(err) + } + dst := path.Join(ShippedSolSheets, "BWA.sol") + err := system.CopyFile(src, dst) + if err != nil { + t.Error(err) + } // prepare custom solution and override noteFiles := TstFilesInGOPATH + "/" extraNoteFiles := TstFilesInGOPATH + "/extra/" @@ -36,11 +46,14 @@ func TestGetSolutionDefintion(t *testing.T) { if strings.Join(solutions[runtime.GOARCH]["NETW"], " ") != nwsols { t.Error(solutions) } + t.Logf("CustomSolutions is '%+v', OverrideSolutions is '%+v', solutions is '%+v'}n", CustomSolutions, OverrideSolutions, solutions) sols := GetSolutionDefintion("/saptune_file_not_avail", "", "") if len(sols) != 0 { t.Error(sols) } + os.Remove(dst) + os.RemoveAll(ShippedSolSheets) } func TestAvailableShippedSolution(t *testing.T) { @@ -164,3 +177,26 @@ func TestGetSortedSolutionIDs(t *testing.T) { t.Error(GetSortedSolutionNames(runtime.GOARCH)) } } + +func TestRefresh(t *testing.T) { + custSols := map[string]map[string]Solution{} + ovSols := map[string]map[string]Solution{} + sol1 := Solution{"SAP_BWA"} + sol2 := Solution{"941735", "1771258", "1980196", "1984787", "2205917", "2382421", "2534844"} + sol3 := Solution{"941735", "1771258", "1984787"} + sol4 := Solution{"941735", "1771258", "1980196", "1984787", "2534844"} + amdSols := map[string]Solution{"BWA": sol1, "HANA": sol2, "MAXDB": sol3, "NETW": sol4} + ppcSols := map[string]Solution{"BWA": sol1, "HANA": sol2, "MAXDB": sol3, "NETW": sol4} + allSols := map[string]map[string]Solution{"amd64": amdSols, "ppc64le": ppcSols} + + Refresh() + if !reflect.DeepEqual(custSols, CustomSolutions) { + t.Errorf("got: %+v, expected: %+v\n", CustomSolutions, custSols) + } + if !reflect.DeepEqual(ovSols, OverrideSolutions) { + t.Errorf("got: %+v, expected: %+v\n", OverrideSolutions, ovSols) + } + if !reflect.DeepEqual(allSols, AllSolutions) { + t.Errorf("got: %+v, expected: %+v\n", AllSolutions, allSols) + } +} diff --git a/staticcheck.conf b/staticcheck.conf new file mode 100644 index 00000000..e22c73a3 --- /dev/null +++ b/staticcheck.conf @@ -0,0 +1 @@ +checks = ["inherit", "-ST1005"] diff --git a/system/argsAndFlags_test.go b/system/argsAndFlags_test.go index d838f39e..272328e4 100644 --- a/system/argsAndFlags_test.go +++ b/system/argsAndFlags_test.go @@ -70,7 +70,7 @@ func TestCliArg(t *testing.T) { } func TestCliFlags(t *testing.T) { - os.Args = []string{"saptune", "note", "list", "--format=json", "--force", "--dryrun", "--help", "--version"} + os.Args = []string{"saptune", "note", "list", "--format=json", "--force", "--dryrun", "--help", "--version", "--colorscheme=zebra", "--show-non-compliant", "--wrongflag", "--unknownflag=none"} // parse command line, to get the test parameters saptArgs, saptFlags = ParseCliArgs() @@ -89,12 +89,28 @@ func TestCliFlags(t *testing.T) { if !IsFlagSet("format") { t.Errorf("Test failed, expected 'format' flag as 'true', but got 'false'") } + if !IsFlagSet("colorscheme") { + t.Errorf("Test failed, expected 'colorscheme' flag as 'true', but got 'false'") + } + if !IsFlagSet("show-non-compliant") { + t.Errorf("Test failed, expected 'show-non-compliant' flag as 'true', but got 'false'") + } + if !IsFlagSet("notSupported") { + t.Errorf("Test failed, expected 'notSupported' flag as 'true', but got 'false'") + } + expected := "json" actual := GetFlagVal("format") if actual != expected { t.Errorf("Test failed, expected: '%s', got: '%s'", expected, actual) } + expected = "" + actual = GetFlagVal("unknownflag") + if actual != expected { + t.Errorf("Test failed, expected: '%s', got: '%s'", expected, actual) + } + // reset CLI flags and args saptArgs = []string{} saptFlags = map[string]string{} @@ -149,3 +165,267 @@ func TestCliFlags(t *testing.T) { saptArgs = []string{} saptFlags = map[string]string{} } + +func TestRereadArgs(t *testing.T) { + os.Args = []string{"saptune", "note", "list"} + // parse command line, to get the test parameters + saptArgs, saptFlags = ParseCliArgs() + os.Args = []string{"saptune", "--format=json", "solution", "enabled"} + RereadArgs() + + expected := "solution" + actual := CliArg(1) + if actual != expected { + t.Errorf("Test failed, expected: '%s', got: '%s'", expected, actual) + } + expected = "enabled" + actual = CliArg(2) + if actual != expected { + t.Errorf("Test failed, expected: '%s', got: '%s'", expected, actual) + } + expected = "" + actual = CliArg(4) + if actual != expected { + t.Errorf("Test failed, expected: '%s', got: '%s'", expected, actual) + } + expectedSlice := []string{"solution", "enabled"} + actualSlice := CliArgs(1) + for i, arg := range actualSlice { + if arg != expectedSlice[i] { + t.Errorf("Test failed, expected: '%s', got: '%s'", expectedSlice[i], arg) + } + } + expectedSlice = []string{} + actualSlice = CliArgs(4) + if len(actualSlice) != 0 { + t.Errorf("Test failed, expected: '%v', got: '%v'", expectedSlice, actualSlice) + } + + if IsFlagSet("version") { + t.Errorf("Test failed, expected 'version' flag as 'false', but got 'true'") + } + if !IsFlagSet("format") { + t.Errorf("Test failed, expected 'format' flag as 'true', but got 'false'") + } + + // reset CLI flags and args + saptArgs = []string{} + saptFlags = map[string]string{} +} + +func TestChkCliSyntax(t *testing.T) { + // {"saptune", "note", "list", "--format=json"} -> wrong + os.Args = []string{"saptune", "note", "list", "--format=json"} + // parse command line, to get the test parameters + saptArgs, saptFlags = ParseCliArgs() + if ChkCliSyntax() { + t.Errorf("Test failed, expected wrong syntax, but got 'good'") + } + + // to few arguments + // {"saptune", "staging"} -> ok + // the check of this situation will be postponed to our 'old' default + // checks (in 'main' and/or 'actions' + os.Args = []string{"saptune", "staging"} + saptArgs, saptFlags = ParseCliArgs() + if !ChkCliSyntax() { + t.Errorf("Test failed, expected good syntax, but got 'wrong'") + } + + // line with unknown flag + // {"saptune", "--unknown", "note", "list"} -> wrong + os.Args = []string{"saptune", "--unknown", "note", "list"} + saptArgs, saptFlags = ParseCliArgs() + if ChkCliSyntax() { + t.Errorf("Test failed, expected wrong syntax, but got 'good'") + } + + // {"saptune", "note", "list", "--unknown"} -> wrong + os.Args = []string{"saptune", "note", "list", "--unknown"} + saptArgs, saptFlags = ParseCliArgs() + if ChkCliSyntax() { + t.Errorf("Test failed, expected wrong syntax, but got 'good'") + } + + // {"saptune", "--out=json", "note", "list"} -> wrong + os.Args = []string{"saptune", "--out=json", "note", "list"} + saptArgs, saptFlags = ParseCliArgs() + if ChkCliSyntax() { + t.Errorf("Test failed, expected wrong syntax, but got 'good'") + } + + // {"saptune", "--format=json", "note", "list"} -> ok + os.Args = []string{"saptune", "--format=json", "note", "list"} + saptArgs, saptFlags = ParseCliArgs() + if !ChkCliSyntax() { + t.Errorf("Test failed, expected good syntax, but got 'wrong'") + } + + // {"saptune", "note", "list", "--dry-run"} -> wrong + os.Args = []string{"saptune", "note", "list", "--dry-run"} + saptArgs, saptFlags = ParseCliArgs() + if ChkCliSyntax() { + t.Errorf("Test failed, expected wrong syntax, but got 'good'") + } + + // {"saptune", "note", "list", "--force"} -> wrong + os.Args = []string{"saptune", "note", "list", "--force"} + saptArgs, saptFlags = ParseCliArgs() + if ChkCliSyntax() { + t.Errorf("Test failed, expected wrong syntax, but got 'good'") + } + + // saptune staging release [--force|--dry-run] [NOTE...|SOLUTION...|all] + // {"saptune", "staging", "list", "--force"} -> wrong + os.Args = []string{"saptune", "staging", "list", "--force"} + saptArgs, saptFlags = ParseCliArgs() + if ChkCliSyntax() { + t.Errorf("Test failed, expected wrong syntax, but got 'good'") + } + + // {"saptune", "staging", "release"} -> ok + os.Args = []string{"saptune", "staging", "release"} + saptArgs, saptFlags = ParseCliArgs() + if !ChkCliSyntax() { + t.Errorf("Test failed, expected good syntax, but got 'wrong'") + } + + // {"saptune", "staging", "release", "--force"} -> ok + os.Args = []string{"saptune", "staging", "release", "--force"} + saptArgs, saptFlags = ParseCliArgs() + if !ChkCliSyntax() { + t.Errorf("Test failed, expected good syntax, but got 'wrong'") + } + + // {"saptune", "staging", "release", "--dry-run"} -> ok + os.Args = []string{"saptune", "staging", "release", "--dry-run"} + saptArgs, saptFlags = ParseCliArgs() + if !ChkCliSyntax() { + t.Errorf("Test failed, expected good syntax, but got 'wrong'") + } + + // line with force AND dry-run + // {"saptune", "staging", "release", "--force", "--dry-run"} -> wrong + os.Args = []string{"saptune", "staging", "release", "--force", "--dry-run"} + saptArgs, saptFlags = ParseCliArgs() + if ChkCliSyntax() { + t.Errorf("Test failed, expected wrong syntax, but got 'good'") + } + + // {"saptune", "staging", "release", "--show-non-compliant"} -> wrong + os.Args = []string{"saptune", "staging", "release", "--show-non-compliant"} + saptArgs, saptFlags = ParseCliArgs() + if ChkCliSyntax() { + t.Errorf("Test failed, expected wrong syntax, but got 'good'") + } + + // {"saptune", "staging", "release", "--force", "--show-non-compliant"} -> wrong + os.Args = []string{"saptune", "staging", "release", "--force", "--show-non-compliant"} + saptArgs, saptFlags = ParseCliArgs() + if ChkCliSyntax() { + t.Errorf("Test failed, expected wrong syntax, but got 'good'") + } + + // {"saptune", "--force", "staging", "release"} -> wrong + os.Args = []string{"saptune", "--force", "staging", "release"} + saptArgs, saptFlags = ParseCliArgs() + if ChkCliSyntax() { + t.Errorf("Test failed, expected wrong syntax, but got 'good'") + } + + // {"saptune", "--force", "staging", "release", "--dry-run"} -> wrong + os.Args = []string{"saptune", "--force", "staging", "release", "--dry-run"} + saptArgs, saptFlags = ParseCliArgs() + if ChkCliSyntax() { + t.Errorf("Test failed, expected wrong syntax, but got 'good'") + } + + // {"saptune", "staging", "release", "--hugo", "--dry-run"} -> wrong + os.Args = []string{"saptune", "--force", "staging", "release", "--hugo", "--dry-run"} + saptArgs, saptFlags = ParseCliArgs() + if ChkCliSyntax() { + t.Errorf("Test failed, expected wrong syntax, but got 'good'") + } + + // saptune note verify [--colorscheme=] [--show-non-compliant] [NOTEID] + // {"saptune", "note", "list", "--colorscheme=zebra"} -> wrong + os.Args = []string{"saptune", "note", "list", "--colorscheme=zebra"} + saptArgs, saptFlags = ParseCliArgs() + if ChkCliSyntax() { + t.Errorf("Test failed, expected wrong syntax, but got 'good'") + } + + // {"saptune", "note", "list", "--show-non-compliant"} -> wrong + os.Args = []string{"saptune", "note", "list", "--show-non-compliant"} + saptArgs, saptFlags = ParseCliArgs() + if ChkCliSyntax() { + t.Errorf("Test failed, expected wrong syntax, but got 'good'") + } + + // {"saptune", "note", "list", "--colorscheme=zebra", "--show-non-compliant"} -> wrong + os.Args = []string{"saptune", "note", "list", "--colorscheme=zebra", "--show-non-compliant"} + saptArgs, saptFlags = ParseCliArgs() + if ChkCliSyntax() { + t.Errorf("Test failed, expected wrong syntax, but got 'good'") + } + + // {"saptune", "staging", "list", "--colorscheme=zebra"} -> wrong + os.Args = []string{"saptune", "staging", "list", "--colorscheme=zebra"} + saptArgs, saptFlags = ParseCliArgs() + if ChkCliSyntax() { + t.Errorf("Test failed, expected wrong syntax, but got 'good'") + } + + // {"saptune", "note", "verify"} -> ok + os.Args = []string{"saptune", "note", "verify"} + saptArgs, saptFlags = ParseCliArgs() + if !ChkCliSyntax() { + t.Errorf("Test failed, expected good syntax, but got 'wrong'") + } + + // {"saptune", "note", "verify", "--colorscheme=zebra"} -> ok + os.Args = []string{"saptune", "note", "verify", "--colorscheme=zebra"} + saptArgs, saptFlags = ParseCliArgs() + if !ChkCliSyntax() { + t.Errorf("Test failed, expected good syntax, but got 'wrong'") + } + + // {"saptune", "note", "verify", "--colorscheme=zebra", "--show-non-compliant"} -> ok + os.Args = []string{"saptune", "note", "verify", "--colorscheme=zebra", "--show-non-compliant"} + saptArgs, saptFlags = ParseCliArgs() + if !ChkCliSyntax() { + t.Errorf("Test failed, expected good syntax, but got 'wrong'") + } + + // {"saptune", "note", "verify", "--show-non-compliant"} -> ok + os.Args = []string{"saptune", "note", "verify", "--show-non-compliant"} + saptArgs, saptFlags = ParseCliArgs() + if !ChkCliSyntax() { + t.Errorf("Test failed, expected good syntax, but got 'wrong'") + } + + // {"saptune", "note", "verify", "--show-non-compliant", "--colorscheme=zebra"} -> wrong + os.Args = []string{"saptune", "note", "verify", "--show-non-compliant", "--colorscheme=zebra"} + saptArgs, saptFlags = ParseCliArgs() + if ChkCliSyntax() { + t.Errorf("Test failed, expected wrong syntax, but got 'good'") + } + + // {"saptune", "--show-non-compliant", "note", "verify"} -> wrong + os.Args = []string{"saptune", "--show-non-compliant", "note", "verify"} + saptArgs, saptFlags = ParseCliArgs() + if ChkCliSyntax() { + t.Errorf("Test failed, expected wrong syntax, but got 'good'") + } + + // {"saptune", "note", "--colorscheme=zebra", "verify"} -> wrong + os.Args = []string{"saptune", "note", "--colorscheme=zebra", "verify"} + saptArgs, saptFlags = ParseCliArgs() + if ChkCliSyntax() { + t.Errorf("Test failed, expected wrong syntax, but got 'good'") + } + + // reset CLI flags and args + saptArgs = []string{} + saptFlags = map[string]string{} +} diff --git a/system/blockdev.go b/system/blockdev.go index b55cf33d..4f2f2abd 100644 --- a/system/blockdev.go +++ b/system/blockdev.go @@ -136,11 +136,10 @@ func CollectBlockDeviceInfo() []string { AllBlockDevs: make([]string, 0, 64), BlockAttributes: make(map[string]map[string]string), } - blockMap := make(map[string]string) for _, bdev := range getValidBlockDevices() { // add new block device - blockMap = make(map[string]string) + blockMap := make(map[string]string) // Remember, GetSysChoice does not accept the leading /sys/ elev, _ := GetSysChoice(path.Join("block", bdev, "queue", "scheduler")) diff --git a/system/csp_test.go b/system/csp_test.go index d4357f84..771f6fea 100644 --- a/system/csp_test.go +++ b/system/csp_test.go @@ -41,6 +41,12 @@ func TestGetCSP(t *testing.T) { t.Errorf("Test failed, expected 'aws', but got '%s'", val) } dmiBoardVendor = noCloud + dmiSysVendor = path.Join(os.Getenv("GOPATH"), "/src/github.com/SUSE/saptune/testdata/csp_aws") + val = GetCSP() + if val != "aws" { + t.Errorf("Test failed, expected 'aws', but got '%s'", val) + } + dmiSysVendor = noCloud // GoogleCloud dmiBiosVendor = path.Join(os.Getenv("GOPATH"), "/src/github.com/SUSE/saptune/testdata/csp_google") @@ -97,11 +103,20 @@ func TestGetCSP(t *testing.T) { } dmiSystemManufacturer = noCloud + // dmiDir does not exist + dmiDir = "/path/does/not/exist" + val = GetCSP() + if val != "" { + t.Errorf("Test failed, expected empty, but got '%s'", val) + } + // restore original environment + dmiDir = "/sys/class/dmi" dmiChassisAssetTag = "/sys/class/dmi/id/chassis_asset_tag" dmiBoardVendor = "/sys/class/dmi/id/board_vendor" dmiBiosVendor = "/sys/class/dmi/id/bios_vendor" dmiBiosVersion = "/sys/class/dmi/id/bios_version" dmiSystemVersion = "/sys/class/dmi/id/system_version" dmiSystemManufacturer = "/sys/class/dmi/id/system-manufacturer" + dmiSysVendor = "/sys/class/dmi/id/sys_vendor" } diff --git a/system/daemon_test.go b/system/daemon_test.go index 452aae12..261ccb0e 100644 --- a/system/daemon_test.go +++ b/system/daemon_test.go @@ -22,87 +22,107 @@ func TestSystemctl(t *testing.T) { testService := "rpcbind.service" if !IsServiceAvailable("rpcbind") { - t.Fatalf("service 'rpcbind' not available on the system\n") + t.Errorf("service 'rpcbind' not available on the system\n") } if !IsServiceAvailable(testService) { - t.Fatalf("service '%s' not available on the system\n", testService) + t.Errorf("service '%s' not available on the system\n", testService) } if err := SystemctlEnable(testService); err != nil { - t.Fatal(err) + t.Error(err) } if err := SystemctlDisable(testService); err != nil { - t.Fatal(err) + t.Error(err) } if err := SystemctlStart(testService); err != nil { - t.Fatal(err) + t.Error(err) } if err := SystemctlStatus(testService); err != nil { - t.Fatal(err) + t.Error(err) } active, _ := SystemctlIsRunning(testService) if !active { - t.Fatalf("service '%s' not running\n", testService) + t.Errorf("service '%s' not running\n", testService) } if err := SystemctlRestart(testService); err != nil { - t.Fatal(err) + t.Error(err) } active, _ = SystemctlIsRunning(testService) if !active { - t.Fatalf("service '%s' not running\n", testService) + t.Errorf("service '%s' not running\n", testService) } if err := SystemctlReloadTryRestart(testService); err != nil { - t.Fatal(err) + t.Error(err) } active, _ = SystemctlIsRunning(testService) if !active { - t.Fatalf("service '%s' not running\n", testService) + t.Errorf("service '%s' not running\n", testService) } if err := SystemctlStop(testService); err != nil { - t.Fatal(err) + t.Error(err) } if err := SystemctlStatus(testService); err == nil { - t.Fatal(err) + t.Error(err) } active, _ = SystemctlIsRunning(testService) if active { - t.Fatalf("service '%s' still running\n", testService) + t.Errorf("service '%s' still running\n", testService) } if err := SystemctlEnableStart(testService); err != nil { - t.Fatal(err) + t.Error(err) } active, _ = SystemctlIsRunning(testService) if !active { - t.Fatalf("service '%s' not running\n", testService) + t.Errorf("service '%s' not running\n", testService) } if err := SystemctlDisableStop(testService); err != nil { - t.Fatal(err) + t.Error(err) } active, _ = SystemctlIsRunning(testService) if active { - t.Fatalf("service '%s' still running\n", testService) + t.Errorf("service '%s' still running\n", testService) + } + if err := SystemctlEnableStart(testService); err != nil { + t.Error(err) + } + isactive, err := SystemctlIsActive(testService) + if isactive == "" { + t.Errorf("problems getting state of service '%s' - '%+v'\n", testService, err) + } + if isactive != "active" { + t.Errorf("service '%s' not running, state is '%s' - '%+v'\n", testService, isactive, err) + } + if err := SystemctlDisableStop(testService); err != nil { + t.Error(err) + } + isactive, err = SystemctlIsActive(testService) + if isactive == "" { + t.Errorf("problems getting state of service '%s' - '%+v'\n", testService, err) + } + if isactive != "inactive" { + t.Errorf("service '%s' still running, state is '%s' - '%+v'\n", testService, isactive, err) } if IsServiceAvailable("UnkownService") { - t.Fatalf("service '%s' should not, but is available on the system\n", testService) + t.Errorf("service '%s' should not, but is available on the system\n", testService) } if err := SystemctlEnable("UnkownService"); err == nil { - t.Fatal(err) + t.Error(err) } if err := SystemctlDisable("UnkownService"); err == nil { - t.Fatal(err) + t.Error(err) } if err := SystemctlEnableStart("UnkownService"); err == nil { - t.Fatal(err) + t.Error(err) } if err := SystemctlDisableStop("UnkownService"); err == nil { - t.Fatal(err) + t.Error(err) } if err := SystemctlStatus("UnkownService"); err == nil { - t.Fatal(err) + t.Error(err) } if SystemctlIsStarting() { - t.Fatal("systemctl reports system is in state 'starting'") + t.Error("systemctl reports system is in state 'starting'") } sysState, err := GetSystemState() if sysState == "degraded" { @@ -113,18 +133,28 @@ func TestSystemctl(t *testing.T) { } sysState, err = GetSystemState() if err != nil { - t.Fatal(err, sysState) + t.Error(err, sysState) } if sysState != "running" { - t.Fatalf("'%s'\n", sysState) + t.Errorf("'%s'\n", sysState) + } + + err = SystemctlResetFailed() + if err != nil { + t.Error(err) } } func TestIsSapconfActive(t *testing.T) { sapconf := "sapconf.service" if IsSapconfActive(sapconf) { - t.Fatalf("sapconf service active") + t.Errorf("sapconf service active") + } + _, _ = ReadConfigFile("/run/sapconf/active", true) + if !IsSapconfActive(sapconf) { + t.Errorf("sapconf service NOT active") } + os.RemoveAll("/run/sapconf") } func TestSystemctlIsEnabled(t *testing.T) { @@ -157,7 +187,7 @@ func TestSystemctlIsRunning(t *testing.T) { } active, _ := SystemctlIsRunning("dbus.service") if !active { - t.Fatal("'dbus.service' not running") + t.Error("'dbus.service' not running") } active, _ = SystemctlIsRunning("tuned.service") if !active { @@ -374,6 +404,21 @@ func TestDaemonErrorCases(t *testing.T) { if IsServiceAvailable("tstserv") { t.Error("service 'tstserv' should not, but is available on the system") } + if _, err := SystemctlIsEnabled("tstserv"); err == nil { + t.Error("should return an error and not 'nil'") + } + if _, err := SystemctlIsRunning("tstserv"); err == nil { + t.Error("should return an error and not 'nil'") + } + if _, err := SystemctlIsActive("tstserv"); err == nil { + t.Error("should return an error and not 'nil'") + } + if err := SystemctlResetFailed(); err == nil { + t.Error("should return an error and not 'nil'") + } + if err := TunedAdmOff(); err == nil { + t.Log("should return an error and not 'nil'") + } systemctlCmd = oldSystemctlCmd oldActTunedProfile := actTunedProfile diff --git a/system/file_test.go b/system/file_test.go index 4fa45d96..9305e489 100644 --- a/system/file_test.go +++ b/system/file_test.go @@ -1,6 +1,7 @@ package system import ( + "bytes" "io/ioutil" "os" "path" @@ -96,6 +97,28 @@ func TestEditFile(t *testing.T) { os.Remove(dst) } +func TestEditAndCheckFile(t *testing.T) { + oldEditor := os.Getenv("EDITOR") + defer func() { os.Setenv("EDITOR", oldEditor) }() + os.Setenv("EDITOR", "/usr/bin/cat") + //src := "/app/testdata/tstfile" + src := path.Join(os.Getenv("GOPATH"), "/src/github.com/SUSE/saptune/testdata/tstfile") + dst := "/tmp/saptune_tstfile" + changed, err := EditAndCheckFile(src, dst, "ANGI", "note") + if err != nil { + t.Error(err) + } + if changed { + t.Error("got 'true', but expected 'false'") + } + + changed, err = EditAndCheckFile("/file_does_not_exist", dst, "ANGI", "note") + if err == nil { + t.Errorf("copied from non existing file") + } + os.Remove(dst) +} + func TestMD5(t *testing.T) { //src := "/app/testdata/tstfile" match1 := "1fd006c2c4a9c3bebb749b43889339f6" @@ -111,7 +134,13 @@ func TestMD5(t *testing.T) { t.Error("checksum should be equal, but is not.") } sum1, err := GetMD5Hash(src) + if err != nil { + t.Error(sum1, err) + } dum1, err := GetMD5Hash(dst) + if err != nil { + t.Error(dum1, err) + } if sum1 != dum1 { t.Errorf("checksum should be equal, but is not. %s - %s.\n", sum1, dum1) } @@ -177,4 +206,38 @@ func TestBackupValue(t *testing.T) { if val != start { t.Errorf("got: %s, expected: %s\n", val, start) } + start = "" + file = "/tmp/tst_backup" + WriteBackupValue(start, file) + val = GetBackupValue(file) + if val != "NA" { + t.Errorf("got: %s, expected: NA\n", val) + } +} + +func TestAddGap(t *testing.T) { + os.Args = []string{"saptune", "--format=json"} + RereadArgs() + buffer := bytes.Buffer{} + AddGap(&buffer) + txt := buffer.String() + if txt != "" { + t.Errorf("got: %s, expected: empty\n", txt) + } + os.Args = []string{"saptune", "status"} + RereadArgs() + buffer2 := bytes.Buffer{} + AddGap(&buffer2) + txt2 := buffer2.String() + if txt2 != "\n" { + t.Errorf("got: %s, expected: '\n'\n", txt2) + } + os.Args = []string{"saptune", "status", "--format="} + RereadArgs() + buffer3 := bytes.Buffer{} + AddGap(&buffer3) + txt3 := buffer3.String() + if txt3 != "\n" { + t.Errorf("got: %s, expected: '\n'\n", txt3) + } } diff --git a/system/fs.go b/system/fs.go index 4a8d8def..186099ba 100644 --- a/system/fs.go +++ b/system/fs.go @@ -95,7 +95,7 @@ func (mounts MountPoints) GetByMountOption(fstype, mountOption, chkDflt string) // ParseMounts return all mount points defined in the input text. // Skipping malformed entry. func ParseMounts(txt string) (mounts MountPoints) { - mounts = make([]MountPoint, 0, 0) + mounts = make([]MountPoint, 0) for _, line := range strings.Split(txt, "\n") { fields := consecutiveSpaces.Split(strings.TrimSpace(line), -1) if len(fields) == 0 || len(fields[0]) == 0 || fields[0][0] == '#' { @@ -171,13 +171,11 @@ func RemountSHM(newSizeMB uint64) error { return nil } -// GetMountOpts checks if oint points with the given type exists and contain +// GetMountOpts checks if mount points with the given type exists and contain // the needed/not needed option. -// Returns a list of mount point containing the option and alist of mount +// Returns a list of mount point containing the option and a list of mount // point NOT containing the option func GetMountOpts(mustExist bool, fstype, fsopt string) ([]string, []string) { - mntOk := []string{} - mntNok := []string{} // Find out mount options chkdflt := "noChk" // check the mounted FS @@ -189,8 +187,8 @@ func GetMountOpts(mustExist bool, fstype, fsopt string) ([]string, []string) { } // check /etc/fstab to get the not mounted FS as well mountFSTOk, mountFSTNok := ParseFstab().GetByMountOption(fstype, fsopt, chkdflt) - mntOk = getMounts(mountProcOk, mountFSTOk) - mntNok = getMounts(mountProcNok, mountFSTNok) + mntOk := getMounts(mountProcOk, mountFSTOk) + mntNok := getMounts(mountProcNok, mountFSTNok) return mntOk, mntNok } diff --git a/system/fs_test.go b/system/fs_test.go index 1da11b85..d1cd2c04 100644 --- a/system/fs_test.go +++ b/system/fs_test.go @@ -1,6 +1,9 @@ package system import ( + "os" + "path" + "reflect" "testing" ) @@ -137,6 +140,10 @@ UUID=12931751-bad1-4b49-992c-4eee57dda0a1 / ext4 acl,us UUID=069ea3e6-573e-48e4-87ff-00b15ab6f2db swap swap defaults 0 0 UUID=c32aa786-b9c2-4212-8f5b-c4ab01f1ad91 / ext4 acl,user_xattr 1 1 UUID=92595693-aa49-45d6-9770-a767c498d40d /mass ext4 defaults 1 2 + +/dev/sdg /homeg xfs defaults +/dev/sde /homee xfs +/dev/sdf /homef xfs defaults X Y ` func TestParseMounts(t *testing.T) { @@ -167,7 +174,7 @@ func TestParseMounts(t *testing.T) { // source from /etc/fstab mountPoints = ParseMounts(fstabSample) - if len(mountPoints) != 14 { + if len(mountPoints) != 16 { t.Fatal(len(mountPoints)) } for _, mount := range mountPoints { @@ -202,3 +209,67 @@ func TestMountPointGetFileSystemSizeMB(t *testing.T) { t.Fatal(size) } } + +func TestGetMountOpts(t *testing.T) { + fstab = path.Join(os.Getenv("GOPATH"), "/src/github.com/SUSE/saptune/testdata/fstest/fstab") + procMounts = path.Join(os.Getenv("GOPATH"), "/src/github.com/SUSE/saptune/testdata/fstest/procMounts") + resOk := []string{"/home1", "/homeB", "/homeC", "/homeE"} + resNok := []string{"/homeE", "/app", "/homeD"} + mountOk, mountNok := GetMountOpts(true, "xfs", "nobarrier") + if !reflect.DeepEqual(mountOk, resOk) { + t.Errorf("got: %+v, expected: %+v\n", mountOk, resOk) + } + if !reflect.DeepEqual(mountNok, resNok) { + t.Errorf("got: %+v, expected: %+v\n", mountNok, resNok) + } + + resOk = []string{"/homeE", "/app", "/home1", "/homeB", "/homeC"} + resNok = []string{"/homeD"} + mountOk, mountNok = GetMountOpts(true, "xfs", "relatime") + if !reflect.DeepEqual(mountOk, resOk) { + t.Errorf("got: %+v, expected: %+v\n", mountOk, resOk) + } + if !reflect.DeepEqual(mountNok, resNok) { + t.Errorf("got: %+v, expected: %+v\n", mountNok, resNok) + } + + resOk = []string{"/homeB"} + resNok = []string{"/homeE", "/app", "/home1", "/homeC", "/homeD"} + mountOk, mountNok = GetMountOpts(false, "xfs", "nobarrier") + if !reflect.DeepEqual(mountOk, resOk) { + t.Errorf("got: %+v, expected: %+v\n", mountOk, resOk) + } + if !reflect.DeepEqual(mountNok, resNok) { + t.Errorf("got: %+v, expected: %+v\n", mountNok, resNok) + } + + resOk = []string{"/homeE", "/app", "/homeB"} + resNok = []string{"/home1", "/homeC", "/homeD", "/homeE"} + mountOk, mountNok = GetMountOpts(false, "xfs", "relatime") + if !reflect.DeepEqual(mountOk, resOk) { + t.Errorf("got: %+v, expected: %+v\n", mountOk, resOk) + } + if !reflect.DeepEqual(mountNok, resNok) { + t.Errorf("got: %+v, expected: %+v\n", mountNok, resNok) + } + + resOk = []string{"/home1", "/homeC", "/homeE"} + resNok = []string{"/homeE", "/app", "/homeB", "/homeD"} + mountOk, mountNok = GetMountOpts(true, "xfs", "") + if !reflect.DeepEqual(mountOk, resOk) { + t.Errorf("got: %+v, expected: %+v\n", mountOk, resOk) + } + if !reflect.DeepEqual(mountNok, resNok) { + t.Errorf("got: %+v, expected: %+v\n", mountNok, resNok) + } + + resOk = []string{} + resNok = []string{"/homeE", "/app", "/home1", "/homeB", "/homeC", "/homeD"} + mountOk, mountNok = GetMountOpts(false, "xfs", "") + if !reflect.DeepEqual(mountOk, resOk) { + t.Errorf("got: %+v, expected: %+v\n", mountOk, resOk) + } + if !reflect.DeepEqual(mountNok, resNok) { + t.Errorf("got: %+v, expected: %+v\n", mountNok, resNok) + } +} diff --git a/system/limits.go b/system/limits.go index 03345283..3f7570fe 100644 --- a/system/limits.go +++ b/system/limits.go @@ -72,8 +72,8 @@ func ParseSecLimitsFile(fileName string) (*SecLimits, error) { // ParseSecLimits read limits.conf text and parse the text into memory structures. func ParseSecLimits(input string) *SecLimits { - limits := &SecLimits{Entries: make([]*SecLimitsEntry, 0, 0)} - leadingComments := make([]string, 0, 0) + limits := &SecLimits{Entries: make([]*SecLimitsEntry, 0)} + leadingComments := make([]string, 0) splitInput := strings.Split(input, "\n") noOfLines := len(splitInput) for lineNo, line := range splitInput { @@ -96,7 +96,7 @@ func ParseSecLimits(input string) *SecLimits { } limits.Entries = append(limits.Entries, entry) // Get ready for the next entry by clearing comments - leadingComments = make([]string, 0, 0) + leadingComments = make([]string, 0) } else { // Consider other lines (such as blank lines) as comments // seems that strings.Split(input, "\n") adds an additional new line to the split result, which should not end up in the resulting SecLimits structure diff --git a/system/lock.go b/system/lock.go index 6781530f..1249fb6a 100644 --- a/system/lock.go +++ b/system/lock.go @@ -27,10 +27,7 @@ func isOwnLock() bool { // file exists, check if empty or if pid inside is from a dead process // if yes, remove file and return false pid, _ := strconv.Atoi(string(p)) - if pid == os.Getpid() { - return true - } - return false + return pid == os.Getpid() } // SaptuneLock creates the saptune lock file diff --git a/system/logging_test.go b/system/logging_test.go index 62b82ef4..6410744f 100644 --- a/system/logging_test.go +++ b/system/logging_test.go @@ -7,7 +7,7 @@ import ( func TestLog(t *testing.T) { logFile := "/tmp/saptune_tst.log" - logSwitch := map[string]string{"verbose": "on", "debug": "1"} + logSwitch := map[string]string{"verbose": "on", "debug": "1", "error": "on"} LogInit(logFile, logSwitch) DebugLog("TestMessage%s_%s", "1", "Debug") @@ -30,6 +30,10 @@ func TestLog(t *testing.T) { if !CheckForPattern(logFile, "TestMessage5_Notice") { t.Error("Error message not found in log file") } + ErrLog("TestMessage%s_%s", "6", "Error") + if !CheckForPattern(logFile, "TestMessage6_Error") { + t.Error("Error message not found in log file") + } SwitchOffLogging() os.Remove(logFile) } diff --git a/system/rpm.go b/system/rpm.go index 9d5180f7..6fdd1366 100644 --- a/system/rpm.go +++ b/system/rpm.go @@ -68,6 +68,7 @@ func CmpRpmVers(vers1, vers2 string) bool { } // rpm version is equal, so check rpm release ret = CheckRpmVers(actV[1], expV[1]) + //lint:ignore S1008 we want to keep the comments if ret < 0 { // installed package release is less than expected return false diff --git a/system/service.go b/system/service.go index e286e892..15c12985 100644 --- a/system/service.go +++ b/system/service.go @@ -29,7 +29,7 @@ func GetAvailServices() map[string]string { // GetServiceName returns the systemd service name for supported services func GetServiceName(service string) string { serviceName := "" - if services == nil || len(services) == 0 { + if len(services) == 0 { services = GetAvailServices() } if _, ok := services[service]; ok { diff --git a/system/service_test.go b/system/service_test.go index ee19b431..31ed8343 100644 --- a/system/service_test.go +++ b/system/service_test.go @@ -29,7 +29,7 @@ func TestGetAvailServices(t *testing.T) { t.Error(err) } value := GetAvailServices() - if value != nil && len(value) != 0 { + if len(value) != 0 { t.Error("found services") } service := GetServiceName("sysstat") diff --git a/system/sys_test.go b/system/sys_test.go index 92d69a24..c71591a4 100644 --- a/system/sys_test.go +++ b/system/sys_test.go @@ -76,6 +76,9 @@ func TestWriteSys(t *testing.T) { if err := SetSysInt("kernel/not_avail", 1); err == nil { t.Error("writing to an non existent sys key") } + if err := SetSysString("kernel/not_avail", "PNA"); err != nil { + t.Error(err) + } } func TestTestSysString(t *testing.T) { diff --git a/system/sysctl.go b/system/sysctl.go index d667a435..0f2ee177 100644 --- a/system/sysctl.go +++ b/system/sysctl.go @@ -256,8 +256,5 @@ func SetSysctlUint64Field(param string, field int, value uint64) error { // IsPagecacheAvailable check, if system supports pagecache limit func IsPagecacheAvailable() bool { _, err := ioutil.ReadFile(path.Join("/proc/sys", strings.Replace(SysctlPagecacheLimitMB, ".", "/", -1))) - if err == nil { - return true - } - return false + return err == nil } diff --git a/system/sysctl_test.go b/system/sysctl_test.go index 3bf049aa..eb0d590c 100644 --- a/system/sysctl_test.go +++ b/system/sysctl_test.go @@ -4,13 +4,13 @@ import "testing" func TestReadSysctl(t *testing.T) { if value, err := GetSysctlInt("vm.max_map_count"); err != nil { - t.Fatal(value) + t.Error(value) } if value, err := GetSysctlUint64("vm.max_map_count"); err != nil { - t.Fatal(value) + t.Error(value) } if value, _ := GetSysctlString("vm.max_map_count"); len(value) < 2 { // indeed testing string length - t.Fatal(value) + t.Error(value) } if value, err := GetSysctlUint64Field("net.ipv4.ip_local_port_range", 0); err != nil { t.Error(value, err) @@ -19,13 +19,13 @@ func TestReadSysctl(t *testing.T) { } if value, err := GetSysctlInt("does not exist"); err == nil { - t.Fatal(value) + t.Error(value) } if value, err := GetSysctlUint64("does not exist"); err == nil { - t.Fatal(value) + t.Error(value) } if value, err := GetSysctlString("does not exist"); err == nil { - t.Fatal(value) + t.Error(value) } if value, err := GetSysctlUint64Field("does not exist", 0); err == nil { @@ -36,47 +36,62 @@ func TestReadSysctl(t *testing.T) { func TestWriteSysctl(t *testing.T) { oldval, err := GetSysctlInt("vm.max_map_count") if err != nil { - t.Fatal(err) + t.Error(err) } if err := SetSysctlInt("vm.max_map_count", 65630); err != nil { - t.Fatal(err) + t.Error(err) } intval, err := GetSysctlInt("vm.max_map_count") + if err != nil { + t.Error(err) + } if intval != 65630 { - t.Fatal(intval) + t.Error(intval) } if err := SetSysctlUint64("vm.max_map_count", 65635); err != nil { - t.Fatal(err) + t.Error(err) } uintval, err := GetSysctlUint64("vm.max_map_count") + if err != nil { + t.Error(err) + } if uintval != 65635 { - t.Fatal(uintval) + t.Error(uintval) } if err := SetSysctlString("vm.max_map_count", "65640"); err != nil { - t.Fatal(err) + t.Error(err) } sval, err := GetSysctlString("vm.max_map_count") + if err != nil { + t.Error(err) + } if sval != "65640" { - t.Fatal(sval) + t.Error(sval) } oldfield, err := GetSysctlUint64Field("net.ipv4.ip_local_port_range", 0) if err != nil { - t.Fatal(err) + t.Error(err) } if err := SetSysctlUint64Field("net.ipv4.ip_local_port_range", 0, 31768); err != nil { - t.Fatal(err) + t.Error(err) } uintval, err = GetSysctlUint64Field("net.ipv4.ip_local_port_range", 0) + if err != nil { + t.Error(err) + } if uintval != 31768 { - t.Fatal(uintval) + t.Error(uintval) } if err := SetSysctlString("vm.dirty_bytes", "100"); err == nil { t.Error("should return an error and not 'nil'") } + if err := SetSysctlString("net.ipv4.ip_local_port_range", "PNA"); err != nil { + t.Error(err) + } if err := SetSysctlString("UnknownKey", "100"); err != nil { - t.Fatal(err) + t.Error(err) } // net.ipv4.ip_local_port_range has only 2 fields if err := SetSysctlUint64Field("net.ipv4.ip_local_port_range", 3, 4711); err == nil { @@ -87,10 +102,10 @@ func TestWriteSysctl(t *testing.T) { } // set test value back if err := SetSysctlInt("vm.max_map_count", oldval); err != nil { - t.Fatal(err) + t.Error(err) } if err := SetSysctlUint64Field("net.ipv4.ip_local_port_range", 0, oldfield); err != nil { - t.Fatal(err) + t.Error(err) } } diff --git a/system/system.go b/system/system.go index 0bbd4753..9d4b6c38 100644 --- a/system/system.go +++ b/system/system.go @@ -2,6 +2,7 @@ package system import ( "fmt" + "io" "io/ioutil" "os" "os/exec" @@ -30,6 +31,10 @@ var OSExit = os.Exit // ErrorExitOut defines, which exit output function should be used var ErrorExitOut = ErrorLog +// ErrExitOut defines the output function, which should be used in case +// of colored output +var ErrExitOut = errExitOut + // InfoOut defines, which log output function should be used var InfoOut = InfoLog @@ -133,6 +138,16 @@ func CalledFrom() string { return ret } +func errExitOut(writer io.Writer, template string, stuff ...interface{}) { + // stuff is: color, bold, text/template, reset bold, reset color + stuff = stuff[1:] + fmt.Fprintf(writer, "%s%sERROR: "+template+"%s%s\n", stuff...) + if len(stuff) >= 4 { + stuff = stuff[2 : len(stuff)-2] + } + ErrLog(template+"\n", stuff...) +} + // ErrorExit prints the message to stderr and exit 1. func ErrorExit(template string, stuff ...interface{}) { exState := 1 @@ -153,13 +168,7 @@ func ErrorExit(template string, stuff ...interface{}) { } if len(template) != 0 { if len(stuff) > 0 && stuff[0] == "colorPrint" { - // color, bold, text/template, reset bold, reset color - stuff = stuff[1:] - fmt.Fprintf(os.Stderr, "%s%sERROR: "+template+"%s%s\n", stuff...) - if len(stuff) >= 4 { - stuff = stuff[2 : len(stuff)-2] - } - ErrLog(template+"\n", stuff...) + ErrExitOut(os.Stderr, template, stuff...) } else { ErrorExitOut(template+"\n", stuff...) } diff --git a/system/system_test.go b/system/system_test.go index e679c1b5..33c4998c 100644 --- a/system/system_test.go +++ b/system/system_test.go @@ -17,10 +17,20 @@ var tstosExit = func(val int) { tstRetErrorExit = val } var tstwriter io.Writer +var errwriter io.Writer var tstErrorExitOut = func(str string, out ...interface{}) error { fmt.Fprintf(tstwriter, "ERROR: "+str, out...) return fmt.Errorf(str+"\n", out...) } +var tstErrExitOut = func(errw io.Writer, str string, out ...interface{}) { + out = out[1:] + fmt.Printf("%v\n", errw) + fmt.Fprintf(errwriter, "%s%sERROR: "+str+"%s%s\n", out...) + if len(out) >= 4 { + out = out[2 : len(out)-2] + } + fmt.Fprintf(tstwriter, "ERROR: "+str, out...) +} var checkOut = func(t *testing.T, got, want string) { t.Helper() @@ -156,12 +166,20 @@ func TestCalledFrom(t *testing.T) { } func TestErrorExit(t *testing.T) { + var setRedText = "\033[31m" + var setBoldText = "\033[1m" + var resetBoldText = "\033[22m" + var resetTextColor = "\033[0m" + oldOSExit := OSExit defer func() { OSExit = oldOSExit }() OSExit = tstosExit oldErrorExitOut := ErrorExitOut defer func() { ErrorExitOut = oldErrorExitOut }() ErrorExitOut = tstErrorExitOut + oldErrExitOut := ErrExitOut + defer func() { ErrExitOut = oldErrExitOut }() + ErrExitOut = tstErrExitOut buffer := bytes.Buffer{} tstwriter = &buffer @@ -171,7 +189,23 @@ func TestErrorExit(t *testing.T) { } txt := buffer.String() checkOut(t, txt, "ERROR: Hallo\n") - //buffer.Reset() - if we plan to check more test cases + + buffer.Reset() + errbuf := bytes.Buffer{} + errwriter = &errbuf + ErrorExit("Colored Hallo", "colorPrint", setRedText, setBoldText, resetBoldText, resetTextColor) + txt = buffer.String() + checkOut(t, txt, "ERROR: Colored Hallo") + errtxt := errbuf.String() + //lint:ignore ST1018 Unicode control characters are expected here + checkOut(t, errtxt, "ERROR: Colored Hallo\n") + + // check errExitOut function + outbuf := bytes.Buffer{} + errExitOut(&outbuf, "Colored Hallo direct", "colorPrint", setRedText, setBoldText, resetBoldText, resetTextColor) + txt = outbuf.String() + //lint:ignore ST1018 Unicode control characters are expected here + checkOut(t, txt, "ERROR: Colored Hallo direct\n") SaptuneLock() // to reach ErrorExit("saptune currently in use, try later ...", 11) @@ -309,7 +343,7 @@ func TestGetDmiID(t *testing.T) { t.Errorf("Test failed, expected: '%s', got: '%s'", expected, dmi) } file = "no_dmi_file_found" - dmi, err := GetDmiID(file) + _, err := GetDmiID(file) if err == nil { t.Errorf("file '%s' exists, but shouldn't", file) } diff --git a/testdata/SAP_BWA b/testdata/SAP_BWA new file mode 100644 index 00000000..e69de29b diff --git a/testdata/etc/saptune/extra/900929.conf b/testdata/etc/saptune/extra/900929.conf new file mode 100644 index 00000000..babdbc5b --- /dev/null +++ b/testdata/etc/saptune/extra/900929.conf @@ -0,0 +1,19 @@ +# 900929 - Linux: STORAGE_PARAMETERS_WRONG_SET and "mmap() failed" +# Version 7 from 31.07.2017 in English + +[version] +VERSION=7 +DATE=31.07.2017 +DESCRIPTION=Linux: STORAGE_PARAMETERS_WRONG_SET and 'mmap() failed' +REFERENCES=https://launchpad.support.sap.com/#/notes/900929 + +[sysctl] +# vm.max_map_count +# The value is the maximum number of memory map areas a process may have. +# Memory map areas are used as a side-effect of calling malloc, directly by +# mmap and mprotect, and also when loading shared libraries. +# vm.max_map_count should be set to MAX_INT (2147483647) +# +# SAP Note 1980196, 900929, HANA Administration Guide +# +vm.max_map_count=2147483647 diff --git a/testdata/etc/saptune/extra/extraTest2Note.conf b/testdata/etc/saptune/extra/extraTest2Note.conf new file mode 100644 index 00000000..fbbf9ade --- /dev/null +++ b/testdata/etc/saptune/extra/extraTest2Note.conf @@ -0,0 +1 @@ +extraTest2Note diff --git a/testdata/etc/saptune/extra/extraTestNote.conf b/testdata/etc/saptune/extra/extraTestNote.conf new file mode 100644 index 00000000..1fb3c4a9 --- /dev/null +++ b/testdata/etc/saptune/extra/extraTestNote.conf @@ -0,0 +1 @@ +extraTestNote diff --git a/testdata/etc/saptune/override/1234567 b/testdata/etc/saptune/override/1234567 new file mode 100644 index 00000000..c7d53664 --- /dev/null +++ b/testdata/etc/saptune/override/1234567 @@ -0,0 +1,9 @@ +# 1234567 - ini_test +# Description: SAP Note file for ini_test +# Version 2 from 02.11.2017 in English + +[version] +# SAP-NOTE=1234567 CATEGORY=Linux VERSION=2 DATE=02.11.2017 NAME="ini_test: SAP Note file for ini_test" + +[sysctl] +vm.swappiness = 30 diff --git a/testdata/etc/saptune/override/extraTestNote b/testdata/etc/saptune/override/extraTestNote new file mode 100644 index 00000000..df52c2e2 --- /dev/null +++ b/testdata/etc/saptune/override/extraTestNote @@ -0,0 +1 @@ +extraTestNote override diff --git a/testdata/etc/saptune/override/testNote b/testdata/etc/saptune/override/testNote new file mode 100644 index 00000000..f655b7e8 --- /dev/null +++ b/testdata/etc/saptune/override/testNote @@ -0,0 +1 @@ +ANGI diff --git a/testdata/etc/sysconfig/saptune b/testdata/etc/sysconfig/saptune index 6d347968..4a50b2bc 100644 --- a/testdata/etc/sysconfig/saptune +++ b/testdata/etc/sysconfig/saptune @@ -32,3 +32,14 @@ NOTE_APPLY_ORDER="2205917 2684254 1680803" # # Version of saptune SAPTUNE_VERSION="3" + +## Type: string +## Default: "" +# +# Color scheme used for 'saptune verify' +# Possible values: 'full-green-zebra', 'cmpl-green-zebra', 'full-blue-zebra', +# 'cmpl-blue-zebra', 'full-red-noncmpl', 'red-noncmpl', 'full-yellow-noncmpl' +# 'yellow-noncmpl' +# Refer to the man page for a desciprion of the color schemes. +COLOR_SCHEME="full-green-zebra" + diff --git a/testdata/etc/sysconfig/saptune_tstorg b/testdata/etc/sysconfig/saptune_tstorg new file mode 100644 index 00000000..4a50b2bc --- /dev/null +++ b/testdata/etc/sysconfig/saptune_tstorg @@ -0,0 +1,45 @@ +## Path: SAP/System Tuning/General +## Description: Global settings for saptune - the comprehensive optimisation management utility for SAP solutions +## ServiceRestart: saptune + +## Type: string +## Default: "" +# +# When saptune is activated, apply optimisations for these SAP solutions. +# The value is a list of solution names, separated by spaces. +# Run "saptune solution list" to get a comprehensive list of solution names. +TUNE_FOR_SOLUTIONS="" + +## Type: string +## Default: "" +# +# When saptune is activated, apply tuning for these SAP notes in addition to those +# already recommended by the above list of SAP solutions. +# The value is a list of note numbers, separated by spaces. +# Run "saptune note list" to get a comprehensive list of note numbers. +TUNE_FOR_NOTES="1680803 2205917 2684254" + +## Type: string +## Default: "" +# +# When saptune is activated, apply tuning for the notes in exactly the below +# order +# The value is a list of note numbers, separated by spaces. +NOTE_APPLY_ORDER="2205917 2684254 1680803" + +## Type: string +## Default: "3" +# +# Version of saptune +SAPTUNE_VERSION="3" + +## Type: string +## Default: "" +# +# Color scheme used for 'saptune verify' +# Possible values: 'full-green-zebra', 'cmpl-green-zebra', 'full-blue-zebra', +# 'cmpl-blue-zebra', 'full-red-noncmpl', 'red-noncmpl', 'full-yellow-noncmpl' +# 'yellow-noncmpl' +# Refer to the man page for a desciprion of the color schemes. +COLOR_SCHEME="full-green-zebra" + diff --git a/testdata/extra/900929.conf b/testdata/extra/900929.conf new file mode 100644 index 00000000..babdbc5b --- /dev/null +++ b/testdata/extra/900929.conf @@ -0,0 +1,19 @@ +# 900929 - Linux: STORAGE_PARAMETERS_WRONG_SET and "mmap() failed" +# Version 7 from 31.07.2017 in English + +[version] +VERSION=7 +DATE=31.07.2017 +DESCRIPTION=Linux: STORAGE_PARAMETERS_WRONG_SET and 'mmap() failed' +REFERENCES=https://launchpad.support.sap.com/#/notes/900929 + +[sysctl] +# vm.max_map_count +# The value is the maximum number of memory map areas a process may have. +# Memory map areas are used as a side-effect of calling malloc, directly by +# mmap and mprotect, and also when loading shared libraries. +# vm.max_map_count should be set to MAX_INT (2147483647) +# +# SAP Note 1980196, 900929, HANA Administration Guide +# +vm.max_map_count=2147483647 diff --git a/testdata/extra/BWA.sol b/testdata/extra/BWA.sol new file mode 100644 index 00000000..c8508d6f --- /dev/null +++ b/testdata/extra/BWA.sol @@ -0,0 +1,8 @@ +[version] +# SAP-NOTE=BWA CATEGORY=SOLUTION VERSION=3 DATE=15.12.2020 NAME="BWA solution definition for testing" + +[ArchX86] +SAP_BWA + +[ArchPPC64LE] +SAP_BWA diff --git a/testdata/fstest/fstab b/testdata/fstest/fstab new file mode 100644 index 00000000..f5bdb64a --- /dev/null +++ b/testdata/fstest/fstab @@ -0,0 +1,5 @@ +/dev/sda /home1 xfs defaults 1 2 +/dev/sdb /homeB xfs noauto,nobarrier,rw,relatime,attr2,inode64,logbufs=8,logbsize=32k,noquota 0 0 +/dev/sdc /homeC xfs noauto,defaults 0 0 +/dev/sdd /homeD xfs noauto,rw,attr2,inode64,logbufs=8,logbsize=32k,noquota 0 0 +/dev/sde /homeE xfs defaults 0 0 diff --git a/testdata/fstest/procMounts b/testdata/fstest/procMounts new file mode 100644 index 00000000..97dafaff --- /dev/null +++ b/testdata/fstest/procMounts @@ -0,0 +1,35 @@ +/dev/mapper/cr_system-root / btrfs rw,relatime,ssd,space_cache,subvolid=443,subvol=/@/var/lib/docker/btrfs/subvolumes/61c0a9855bc8e748affe3fe33d1604307cabb682d158975071ab9a24a2224db9 0 0 +proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0 +tmpfs /dev tmpfs rw,nosuid,size=65536k,mode=755 0 0 +devpts /dev/pts devpts rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=666 0 0 +sysfs /sys sysfs rw,nosuid,nodev,noexec,relatime 0 0 +mqueue /dev/mqueue mqueue rw,nosuid,nodev,noexec,relatime 0 0 +shm /dev/shm tmpfs rw,nosuid,nodev,noexec,relatime,size=65536k 0 0 +/dev/sde /homeE xfs rw,relatime,attr2,inode64,logbufs=8,logbsize=32k,noquota 0 0 +/dev/mapper/system-home /app xfs rw,relatime,attr2,inode64,logbufs=8,logbsize=32k,noquota 0 0 +/dev/mapper/cr_system-root /run btrfs rw,relatime,ssd,space_cache,subvolid=258,subvol=/@/var/lib/docker/volumes/045349d48d950991b55030d01ee5d193a9a3d20ecdf7d22dfc8fb2732e79573d/_data 0 0 +/dev/mapper/cr_system-root /etc/resolv.conf btrfs rw,relatime,ssd,space_cache,subvolid=258,subvol=/@/var/lib/docker/containers/c449c6c90f8dd31286c1d95ec90af28c86781cd2cbd85281c54f6e6e9e299e66/resolv.conf 0 0 +/dev/mapper/cr_system-root /etc/hostname btrfs rw,relatime,ssd,space_cache,subvolid=258,subvol=/@/var/lib/docker/containers/c449c6c90f8dd31286c1d95ec90af28c86781cd2cbd85281c54f6e6e9e299e66/hostname 0 0 +/dev/mapper/cr_system-root /etc/hosts btrfs rw,relatime,ssd,space_cache,subvolid=258,subvol=/@/var/lib/docker/containers/c449c6c90f8dd31286c1d95ec90af28c86781cd2cbd85281c54f6e6e9e299e66/hosts 0 0 +tmpfs /sys/fs/cgroup tmpfs ro,nosuid,nodev,noexec,size=4096k,nr_inodes=1024,mode=755 0 0 +cgroup2 /sys/fs/cgroup/unified cgroup2 rw,nosuid,nodev,noexec,relatime,nsdelegate 0 0 +cgroup /sys/fs/cgroup/systemd cgroup rw,nosuid,nodev,noexec,relatime,xattr,name=systemd 0 0 +cgroup /sys/fs/cgroup/net_cls,net_prio cgroup rw,nosuid,nodev,noexec,relatime,net_cls,net_prio 0 0 +cgroup /sys/fs/cgroup/hugetlb cgroup rw,nosuid,nodev,noexec,relatime,hugetlb 0 0 +cgroup /sys/fs/cgroup/blkio cgroup rw,nosuid,nodev,noexec,relatime,blkio 0 0 +cgroup /sys/fs/cgroup/pids cgroup rw,nosuid,nodev,noexec,relatime,pids 0 0 +cgroup /sys/fs/cgroup/rdma cgroup rw,nosuid,nodev,noexec,relatime,rdma 0 0 +cgroup /sys/fs/cgroup/devices cgroup rw,nosuid,nodev,noexec,relatime,devices 0 0 +cgroup /sys/fs/cgroup/cpu,cpuacct cgroup rw,nosuid,nodev,noexec,relatime,cpu,cpuacct 0 0 +cgroup /sys/fs/cgroup/freezer cgroup rw,nosuid,nodev,noexec,relatime,freezer 0 0 +cgroup /sys/fs/cgroup/cpuset cgroup rw,nosuid,nodev,noexec,relatime,cpuset 0 0 +cgroup /sys/fs/cgroup/memory cgroup rw,nosuid,nodev,noexec,relatime,memory 0 0 +cgroup /sys/fs/cgroup/perf_event cgroup rw,nosuid,nodev,noexec,relatime,perf_event 0 0 +tmpfs /run/secrets/credentials.d tmpfs ro,relatime 0 0 +devpts /dev/console devpts rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=666 0 0 +systemd-1 /proc/sys/fs/binfmt_misc autofs rw,relatime,fd=27,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=82082 0 0 +debugfs /sys/kernel/debug debugfs rw,nosuid,nodev,noexec,relatime 0 0 +hugetlbfs /dev/hugepages hugetlbfs rw,relatime,pagesize=2M 0 0 +tracefs /sys/kernel/tracing tracefs rw,nosuid,nodev,noexec,relatime 0 0 +fusectl /sys/fs/fuse/connections fusectl rw,nosuid,nodev,noexec,relatime 0 0 +configfs /sys/kernel/config configfs rw,nosuid,nodev,noexec,relatime 0 0 diff --git a/testdata/grub_and_service_test.ini b/testdata/grub_and_service_test.ini new file mode 100644 index 00000000..c70b7c95 --- /dev/null +++ b/testdata/grub_and_service_test.ini @@ -0,0 +1,15 @@ +# 4711081 - grub_and_service_test +# Description: SAP Note file for grub_and_service_test +# Version 3 from 18.07.2022 in English + +[version] +# SAP-NOTE=4711081 VERSION=3 DATE=18.07.2022 NAME="grub_and_service_test: SAP Note file for grub_and_service_test" + +[grub] +transparent_hugepage=never +quiet + +[service] +# start the related services +uuidd.socket=start +sysstat.service=start diff --git a/testdata/ini_all_test.ini b/testdata/ini_all_test.ini index 6d0efc7c..c37f699d 100644 --- a/testdata/ini_all_test.ini +++ b/testdata/ini_all_test.ini @@ -61,6 +61,7 @@ unknown vm.nr_hugepages=128 vm.dirty_ratio=10 vm.dirty_background_ratio=5 +net.ipv4.ip_local_reserved_ports=32768 61000 [vm] THP=always @@ -68,12 +69,17 @@ KSM=1 [sys] module.watchdog.parameters.open_timeout=1 +kernel.debug.sync.sw_sync= +kernel.debug.not_avail=9 wrong/parameter/syntax/for/sys/section=0 [filesystem] xfs_options= -nobarrier, +relatime btrfs_options=notSupportedForNow +[filesystem] +xfs_options= + [unknownsection] unknown_parameter=unknownvalue diff --git a/testdata/ini_missing_test.ini b/testdata/ini_missing_test.ini new file mode 100644 index 00000000..8fc04908 --- /dev/null +++ b/testdata/ini_missing_test.ini @@ -0,0 +1,40 @@ +# 1557506 - Linux paging improvements +# Description: Tune page cache limit to prevent eviction of SAP applications memory into swap +# Version 16 from 06.02.2020 in English + +[pagecache] +## Type: yesno +## Default: no +# +# Consider to enable pagecache limit feature if your SAP workloads cause +# frequent and excessive swapping activities. +# It is recommended to leave pagecache limit disabled if the system has low +# or no swap space. +ENABLE_PAGECACHE_LIMIT=no + +## Type: integer +## Default: 1 +# +# Whether or not to ignore dirty memory when enforcing the pagecache limit. +# If set to 0, dirty (unmapped) memory will be considered freeable and the Linux +# kernel will try to write the pages out when enforcing the page cache limit. +# If set to 1 (ignore all dirty memory in the page cache when enforcing the limit) +# the page cache can actually grow well beyond the configured limit if lots of writes +# happen to local filesystems. +# If set to 2 - a middle ground, some dirty memory will be freed when enforcing +# the limit. +vm.pagecache_limit_ignore_dirty=1 + +## Type: integer +## Default: "" +# +# When pagecache limit feature is enabled, this parameter defines how to set +# the pagecache limit +# The limit value can be calculated automatically or the value can be overridden +# if you set this parameter to the desired limit value. +# +# To calculate the limit, set the parameter to 0. +# Or set the parameter to the desired limit value. +# An empty string will be treated as 0. To disable pagecache limit use the +# variable ENABLE_PAGECACHE_LIMIT above +OVERRIDE_PAGECACHE_LIMIT_MB=0 diff --git a/testdata/ini_new_test.ini b/testdata/ini_new_test.ini new file mode 100644 index 00000000..c2f71657 --- /dev/null +++ b/testdata/ini_new_test.ini @@ -0,0 +1,46 @@ +# 1557506 - Linux paging improvements +# Description: Tune page cache limit to prevent eviction of SAP applications memory into swap +# Version 16 from 06.02.2020 in English + +[version] +VERSION=16 +DATE=06.02.2020 +DESCRIPTION=Linux paging improvements +CATEGORY=Linux + +[pagecache] +## Type: yesno +## Default: no +# +# Consider to enable pagecache limit feature if your SAP workloads cause +# frequent and excessive swapping activities. +# It is recommended to leave pagecache limit disabled if the system has low +# or no swap space. +ENABLE_PAGECACHE_LIMIT=no + +## Type: integer +## Default: 1 +# +# Whether or not to ignore dirty memory when enforcing the pagecache limit. +# If set to 0, dirty (unmapped) memory will be considered freeable and the Linux +# kernel will try to write the pages out when enforcing the page cache limit. +# If set to 1 (ignore all dirty memory in the page cache when enforcing the limit) +# the page cache can actually grow well beyond the configured limit if lots of writes +# happen to local filesystems. +# If set to 2 - a middle ground, some dirty memory will be freed when enforcing +# the limit. +vm.pagecache_limit_ignore_dirty=1 + +## Type: integer +## Default: "" +# +# When pagecache limit feature is enabled, this parameter defines how to set +# the pagecache limit +# The limit value can be calculated automatically or the value can be overridden +# if you set this parameter to the desired limit value. +# +# To calculate the limit, set the parameter to 0. +# Or set the parameter to the desired limit value. +# An empty string will be treated as 0. To disable pagecache limit use the +# variable ENABLE_PAGECACHE_LIMIT above +OVERRIDE_PAGECACHE_LIMIT_MB=0 diff --git a/testdata/ini_test.ini b/testdata/ini_test.ini index 95a6304f..4747adde 100644 --- a/testdata/ini_test.ini +++ b/testdata/ini_test.ini @@ -3,7 +3,7 @@ # Version 2 from 02.11.2017 in English [version] -# SAP-NOTE=1234567 VERSION=2 DATE=02.11.2017 NAME="ini_test: SAP Note file for ini_test" +# SAP-NOTE=1234567 CATEGORY=Linux VERSION=2 DATE=02.11.2017 NAME="ini_test: SAP Note file for ini_test" [sysctl] vm.dirty_ratio > 10 diff --git a/testdata/ini_wrong_test.ini b/testdata/ini_wrong_test.ini new file mode 100644 index 00000000..7876dad9 --- /dev/null +++ b/testdata/ini_wrong_test.ini @@ -0,0 +1,46 @@ +# 1557506 - Linux paging improvements +# Description: Tune page cache limit to prevent eviction of SAP applications memory into swap +# Version 16 from 06.02.2020 in English + +[version] +# SAP-NOTE=1234567 CATEGORY=Linux VERSION=2 DATE=02.11.2017 NAME="ini_test: SAP Note file for ini_test" + +VERSION=16 +DATE=06.02.2020 + +[pagecache] +## Type: yesno +## Default: no +# +# Consider to enable pagecache limit feature if your SAP workloads cause +# frequent and excessive swapping activities. +# It is recommended to leave pagecache limit disabled if the system has low +# or no swap space. +ENABLE_PAGECACHE_LIMIT=no + +## Type: integer +## Default: 1 +# +# Whether or not to ignore dirty memory when enforcing the pagecache limit. +# If set to 0, dirty (unmapped) memory will be considered freeable and the Linux +# kernel will try to write the pages out when enforcing the page cache limit. +# If set to 1 (ignore all dirty memory in the page cache when enforcing the limit) +# the page cache can actually grow well beyond the configured limit if lots of writes +# happen to local filesystems. +# If set to 2 - a middle ground, some dirty memory will be freed when enforcing +# the limit. +vm.pagecache_limit_ignore_dirty=1 + +## Type: integer +## Default: "" +# +# When pagecache limit feature is enabled, this parameter defines how to set +# the pagecache limit +# The limit value can be calculated automatically or the value can be overridden +# if you set this parameter to the desired limit value. +# +# To calculate the limit, set the parameter to 0. +# Or set the parameter to the desired limit value. +# An empty string will be treated as 0. To disable pagecache limit use the +# variable ENABLE_PAGECACHE_LIMIT above +OVERRIDE_PAGECACHE_LIMIT_MB=0 diff --git a/testdata/saptune_check b/testdata/saptune_check new file mode 100755 index 00000000..a5ef099c --- /dev/null +++ b/testdata/saptune_check @@ -0,0 +1,3 @@ +#!/bin/bash +echo "saptune check" +exit 0 diff --git a/testdata/sol/sols/BWA.sol b/testdata/sol/sols/BWA.sol index 69530635..c8508d6f 100644 --- a/testdata/sol/sols/BWA.sol +++ b/testdata/sol/sols/BWA.sol @@ -2,7 +2,7 @@ # SAP-NOTE=BWA CATEGORY=SOLUTION VERSION=3 DATE=15.12.2020 NAME="BWA solution definition for testing" [ArchX86] -941735 2534844 SAP_BWA +SAP_BWA [ArchPPC64LE] -941735 2534844 SAP_BWA +SAP_BWA diff --git a/testdata/test2Note b/testdata/test2Note new file mode 100644 index 00000000..2421d2c0 --- /dev/null +++ b/testdata/test2Note @@ -0,0 +1 @@ +HUGO diff --git a/testdata/testNote b/testdata/testNote new file mode 100644 index 00000000..2421d2c0 --- /dev/null +++ b/testdata/testNote @@ -0,0 +1 @@ +HUGO diff --git a/testdata/tstedit b/testdata/tstedit new file mode 100755 index 00000000..6d3499d0 --- /dev/null +++ b/testdata/tstedit @@ -0,0 +1,2 @@ +#!/bin/bash +/usr/bin/echo 'Hello from test editor' > $1 diff --git a/txtparser/ini_test.go b/txtparser/ini_test.go index 72e396fb..67873a5c 100644 --- a/txtparser/ini_test.go +++ b/txtparser/ini_test.go @@ -14,12 +14,22 @@ import ( var fileNotExist = "/file_does_not_exist" var tstFile = path.Join(os.Getenv("GOPATH"), "/src/github.com/SUSE/saptune/testdata/ini_all_test.ini") var tst2File = path.Join(os.Getenv("GOPATH"), "/src/github.com/SUSE/saptune/testdata/wrong_limit_test.ini") +var fileNameOld = path.Join(os.Getenv("GOPATH"), "/src/github.com/SUSE/saptune/testdata/ini_test.ini") +var fileNameNew = path.Join(os.Getenv("GOPATH"), "/src/github.com/SUSE/saptune/testdata/ini_new_test.ini") +var fileNameWrong = path.Join(os.Getenv("GOPATH"), "/src/github.com/SUSE/saptune/testdata/ini_wrong_test.ini") +var fileNameMissing = path.Join(os.Getenv("GOPATH"), "/src/github.com/SUSE/saptune/testdata/ini_missing_test.ini") var fileName = path.Join(os.Getenv("GOPATH"), "/src/github.com/SUSE/saptune/ospackage/usr/share/saptune/notes/1557506") var descName = fmt.Sprintf("%s\n\t\t\t%sVersion %s from %s\n\t\t\t%s", "Linux paging improvements", "", "16", "06.02.2020", "https://launchpad.support.sap.com/#/notes/1557506") +var descNameNew = fmt.Sprintf("%s\n\t\t\t%sVersion %s from %s", "Linux paging improvements", "", "16", "06.02.2020") var noteVersion = "16" var noteDate = "06.02.2020" var noteTitle = "Linux paging improvements" var noteRefs = "https://launchpad.support.sap.com/#/notes/1557506" +var oldDescName = fmt.Sprintf("%s\n\t\t\t%sVersion %s from %s", "ini_test: SAP Note file for ini_test", "", "2", "02.11.2017") +var oldNoteVersion = "2" +var oldNoteDate = "02.11.2017" +var oldNoteTitle = "ini_test: SAP Note file for ini_test" +var oldNoteCategory = "Linux" var iniExample = ` # comment @@ -197,13 +207,13 @@ func TestParseINI(t *testing.T) { if err != nil { t.Error(err) } - newINI := ParseINI(string(content)) + _ = ParseINI(string(content)) content, err = ioutil.ReadFile(tst2File) if err != nil { t.Error(err) } - newINI = ParseINI(string(content)) + newINI := ParseINI(string(content)) var wrongINI INIFile if err := json.Unmarshal([]byte(iniWrongJSON), &wrongINI); err != nil { t.Error(err) @@ -219,12 +229,33 @@ func TestGetINIFileDescriptiveName(t *testing.T) { if str != descName { t.Errorf("\n'%+v'\nis not\n'%+v'\n", str, descName) } + str = GetINIFileDescriptiveName(fileNameNew) + if str != descNameNew { + t.Errorf("\n'%+v'\nis not\n'%+v'\n", str, descNameNew) + } + str = GetINIFileDescriptiveName(fileNameOld) + if str != oldDescName { + t.Errorf("\n'%+v'\nis not\n'%+v'\n", str, oldDescName) + } str = GetINIFileDescriptiveName(fileNotExist) if str != "" { t.Errorf(str) } } +func TestGetINIFileVersionSectionRefs(t *testing.T) { + refs := GetINIFileVersionSectionRefs(fileName) + for _, ref := range refs { + if ref != noteRefs { + t.Errorf("\n'%+v'\nis not\n'%+v'\n", ref, noteRefs) + } + } + refs = GetINIFileVersionSectionRefs(fileNameNew) + if len(refs) > 0 { + t.Errorf("refs contain '%+v'\n", refs) + } +} + func TestGetINIFileVersionSectionEntry(t *testing.T) { str := GetINIFileVersionSectionEntry(fileName, "reference") if str != noteRefs { @@ -238,6 +269,10 @@ func TestGetINIFileVersionSectionEntry(t *testing.T) { if str != noteVersion { t.Errorf("\n'%+v'\nis not\n'%+v'\n", str, noteVersion) } + str = GetINIFileVersionSectionEntry(fileNameOld, "version") + if str != oldNoteVersion { + t.Errorf("\n'%+v'\nis not\n'%+v'\n", str, oldNoteVersion) + } str = GetINIFileVersionSectionEntry(fileNotExist, "version") if str != "" { t.Errorf(str) @@ -246,6 +281,10 @@ func TestGetINIFileVersionSectionEntry(t *testing.T) { if str != noteDate { t.Errorf("\n'%+v'\nis not\n'%+v'\n", str, noteDate) } + str = GetINIFileVersionSectionEntry(fileNameOld, "date") + if str != oldNoteDate { + t.Errorf("\n'%+v'\nis not\n'%+v'\n", str, oldNoteDate) + } str = GetINIFileVersionSectionEntry(fileNotExist, "date") if str != "" { t.Errorf(str) @@ -254,12 +293,43 @@ func TestGetINIFileVersionSectionEntry(t *testing.T) { if str != noteTitle { t.Errorf("\n'%+v'\nis not\n'%+v'\n", str, noteTitle) } + str = GetINIFileVersionSectionEntry(fileNameOld, "name") + if str != oldNoteTitle { + t.Errorf("\n'%+v'\nis not\n'%+v'\n", str, oldNoteTitle) + } str = GetINIFileVersionSectionEntry(fileNotExist, "name") if str != "" { t.Errorf(str) } + str = GetINIFileVersionSectionEntry(fileNameNew, "category") + if str != oldNoteCategory { + t.Errorf("\n'%+v'\nis not\n'%+v'\n", str, oldNoteCategory) + } + str = GetINIFileVersionSectionEntry(fileNameOld, "category") + if str != oldNoteCategory { + t.Errorf("\n'%+v'\nis not\n'%+v'\n", str, oldNoteCategory) + } str = GetINIFileVersionSectionEntry(fileName, "not_avail") if str != "" { t.Errorf("\n'%+v'\nis not\n'%+v'\n", str, "") } + str = GetINIFileVersionSectionEntry(fileNameWrong, "name") + if str != "" { + t.Errorf("\n'%+v'\nis not\n'%+v'\n", str, "") + } + str = GetINIFileVersionSectionEntry(fileNameMissing, "name") + if str != "" { + t.Errorf("\n'%+v'\nis not\n'%+v'\n", str, "") + } +} + +func TestBlkInfoNeeded(t *testing.T) { + sectFields := []string{"hugo", "blkvendor=HUGO", "blkmodel=EGON"} + if !blkInfoNeeded(sectFields) { + t.Error("should be 'true', but returns 'false'") + } + sectFields = []string{"hugo", "HUGO", "EGON"} + if blkInfoNeeded(sectFields) { + t.Error("should be 'false', but returns 'true'") + } } diff --git a/txtparser/section_test.go b/txtparser/section_test.go new file mode 100644 index 00000000..5acf09c0 --- /dev/null +++ b/txtparser/section_test.go @@ -0,0 +1,65 @@ +package txtparser + +import ( + "github.com/SUSE/saptune/system" + "os" + "path" + "reflect" + "testing" +) + +func TestStoreSectionInfo(t *testing.T) { + saptuneSectionDir = "/tmp/saptune_sections" + runFile := path.Join(saptuneSectionDir, "1234567.run") + sectionFile := path.Join(saptuneSectionDir, "1234567.sections") + if err := os.Mkdir(saptuneSectionDir, 0755); err != nil { + t.Error(err) + } + // parse configuration file + iniPath := path.Join(os.Getenv("GOPATH"), "/src/github.com/SUSE/saptune/testdata/ini_test.ini") + ini, err := ParseINIFile(iniPath, false) + if err != nil { + t.Error(err) + } + // write section data + err = StoreSectionInfo(ini, "run", "1234567", true) + if err != nil { + t.Error(err) + } + if _, err = os.Stat(runFile); err != nil { + t.Error(err) + } + + readIni, err := GetSectionInfo("sns", "1234567", false) + if !reflect.DeepEqual(ini, readIni) { + t.Errorf("got: %+v, expected: %+v\n", readIni, ini) + } + + // looking for override file + override, ow := GetOverrides("ovw", "1234567") + if override { + t.Errorf("override file found, but should not exist - '%+v'\n", ow) + } + + _ = system.CopyFile(path.Join(os.Getenv("GOPATH"), "/src/github.com/SUSE/saptune/testdata/etc/saptune/override/1234567"), "/etc/saptune/override/1234567") + override, ow = GetOverrides("ovw", "1234567") + if !override { + t.Errorf("override file not found, but should exist - '%+v'\n", ow) + } + os.Remove("/etc/saptune/override/1234567") + + // write section data + err = StoreSectionInfo(ini, "section", "1234567", true) + if err != nil { + t.Error(err) + } + if _, err = os.Stat(sectionFile); err != nil { + t.Error(err) + } + readIni, err = GetSectionInfo("sns", "1234567", true) + if !reflect.DeepEqual(ini, readIni) { + t.Errorf("got: %+v, expected: %+v\n", readIni, ini) + } + + defer os.RemoveAll(saptuneSectionDir) +} diff --git a/txtparser/sysconfig.go b/txtparser/sysconfig.go index eea20a21..7aedd0b0 100644 --- a/txtparser/sysconfig.go +++ b/txtparser/sysconfig.go @@ -40,10 +40,10 @@ func ParseSysconfigFile(fileName string, autoCreate bool) (*Sysconfig, error) { // ParseSysconfig read sysconfig text and parse the text into memory structures. func ParseSysconfig(input string) (*Sysconfig, error) { conf := &Sysconfig{ - AllValues: make([]*SysconfigEntry, 0, 0), + AllValues: make([]*SysconfigEntry, 0), KeyValue: make(map[string]*SysconfigEntry), } - leadingComments := make([]string, 0, 0) + leadingComments := make([]string, 0) for _, line := range strings.Split(input, "\n") { line = strings.TrimSpace(line) if strings.HasPrefix(line, "#") { @@ -61,7 +61,7 @@ func ParseSysconfig(input string) (*Sysconfig, error) { conf.AllValues = append(conf.AllValues, kv) conf.KeyValue[key] = kv // Clear comments to be ready for the next key-value pair - leadingComments = make([]string, 0, 0) + leadingComments = make([]string, 0) } else { // Consider other lines (such as blank lines) as comments leadingComments = append(leadingComments, line) @@ -206,8 +206,5 @@ func (conf *Sysconfig) ToText() string { // false, if the key does not exist. func (conf *Sysconfig) IsKeyAvail(key string) bool { _, exists := conf.KeyValue[key] - if !exists { - return false - } - return true + return exists } diff --git a/txtparser/tags.go b/txtparser/tags.go index 43cd7d2c..461b0c64 100644 --- a/txtparser/tags.go +++ b/txtparser/tags.go @@ -61,7 +61,7 @@ func chkSecTags(secFields, blkDev []string) (bool, []string) { default: ret = chkOtherTags(tagField[0], tagField[1], secFields) } - if ret == false { + if !ret { break } } diff --git a/txtparser/tags_test.go b/txtparser/tags_test.go index 136cbaac..cd74cc1b 100644 --- a/txtparser/tags_test.go +++ b/txtparser/tags_test.go @@ -94,3 +94,40 @@ func TestChkOtherTags(t *testing.T) { } system.DmiID = "/sys/class/dmi/id" } + +func TestIsTagAvail(t *testing.T) { + secFields := []string{"block", "blkvendor=HUGO", "blkmodel=EGON"} + tag := "blkvendor" + if !isTagAvail(tag, secFields) { + t.Errorf("tag '%s' is expected to be available, but is repoted as not available\n", tag) + } + tag = "blkmodel" + if !isTagAvail(tag, secFields) { + t.Errorf("tag '%s' is expected to be available, but is repoted as not available\n", tag) + } + tag = "blkpat" + if isTagAvail(tag, secFields) { + t.Errorf("tag '%s' is expected to be NOT available, but is repoted as available\n", tag) + } + tag = "blkvendor" + secFields = []string{"block", "blkvendor="} + if !isTagAvail(tag, secFields) { + t.Error("expected 'true', because of correct syntax, but got 'false'") + } + secFields = []string{"block", "blkvendor"} + if isTagAvail(tag, secFields) { + t.Error("expected 'false', because of wrong syntax, but got 'true'") + } + secFields = []string{"block", "=EGON"} + if isTagAvail(tag, secFields) { + t.Error("expected 'false', because of wrong syntax, but got 'true'") + } + secFields = []string{"block", "="} + if isTagAvail(tag, secFields) { + t.Error("expected 'false', because of wrong syntax, but got 'true'") + } + secFields = []string{"block", ""} + if isTagAvail(tag, secFields) { + t.Error("expected 'false', because of wrong syntax, but got 'true'") + } +} From 40e0dcb7b385ae6e8e981c616efe3f84488b2229 Mon Sep 17 00:00:00 2001 From: AngelaBriel Date: Mon, 8 Aug 2022 12:16:00 +0200 Subject: [PATCH 12/12] adapt the change of the version section syntax to the staging commands --- README.md | 22 ++++++++++++++----- actions/stagingacts.go | 4 ++-- .../usr/share/saptune/scripts/upd_helper | 8 ++++++- txtparser/section.go | 13 +++++++++-- 4 files changed, 37 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index c8c3897f..34d7a704 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -[![Build Status](https://travis-ci.org/SUSE/saptune.svg?branch=master)](https://travis-ci.org/SUSE/saptune) +[![Build Status](https://github.com/SUSE/saptune/actions/workflows/saptune-ut.yml/badge.svg)](https://github.com/SUSE/saptune/actions/workflows/saptune-ut.yml/badge.svg) [![Test Coverage](https://api.codeclimate.com/v1/badges/5375e2ca293dd0e8b322/test_coverage)](https://codeclimate.com/github/SUSE/saptune/test_coverage) [![Maintainability](https://api.codeclimate.com/v1/badges/5375e2ca293dd0e8b322/maintainability)](https://codeclimate.com/github/SUSE/saptune/maintainability) @@ -47,7 +47,7 @@ They give our administrators and our partners a simple way to implement their ow # Migration: -Guide to help you migrate to the new saptune\ +Guide to help you migrate from saptune running version 1 to the saptune version 2 or 3\ Migration? Isn’t a simple package update enough? Not in every case. @@ -57,8 +57,18 @@ If the update discovers applied saptune SAP notes or solutions, saptune will con The switch to version 2 has to be done deliberately.\ To help you, we will provide a step-by-step guide. Just plan your switch when you are ready, no rush! -We will support saptune version 1 until end of the lifetime of SLES 12 / SLES 15 SP1, which should give enough time to move. Although please bear in mind that since saptune version 1 will be deprecated, we will only do bug fixing. New features, new SAP notes or new parameters will only be done for version 2! +We will support saptune version 1 until end of the lifetime of SLES 12 / SLES 15 SP1, which should give enough time to move. Although please bear in mind that since saptune version 1 will be deprecated, we will only do bug fixing. New features, new SAP notes or new parameters will only be done for future versions of saptune! +With saptune version 3 the support for version 1 is abandoned. +If the migration from version 1 to version 2 or 3 was not done before the package update, saptune will not work and that's expected. +But all needed files for the migration are still available, so following the procedure described in the man page saptune-migrate(7) will bring you back to a working saptune. + +For the update from version 2 to version 3 NO migration is needed. + +But as we discontinued the use of tuned to get the tuning started during boot and instead use our own systemd service saptune.service we will need to stop the tuning of the system during the package update for a short timeframe to avoid tuned error messages. + +If you are used to start saptune using tuned directly in the past, please move to 'saptune service start' instead. +If you use 'saptune daemon start', you will now get a 'deprecated' warning, but the command will still continue to work. # Where to find documentation? @@ -68,12 +78,14 @@ When the technical blog series about the details of saptune and how to do a migr For now:\ \ \ - +\ +\ + # Feedback -Supporters, contributors, colleagues, customers and partners are welcome to approach us with ideas and suggestions. If you miss something or you think something should be done better, then don’t hesitate to contact us. You are welcome to give further feedback via email at SapAlliance@suse.com, create an issue in this repository, carrier pigeon etc. and tell us your needs.\ +Supporters, contributors, colleagues, customers and partners are welcome to approach us with ideas and suggestions. If you miss something or you think something should be done better, then don’t hesitate to contact us. You are welcome to give further feedback via email at sapalliance@suse.com, create an issue in this repository, carrier pigeon etc. and tell us your needs.\ With each new version of saptune we implement many of them, but the journey will continue and you can expect further enhancements in the future. diff --git a/actions/stagingacts.go b/actions/stagingacts.go index 28d8036e..749b2f9d 100644 --- a/actions/stagingacts.go +++ b/actions/stagingacts.go @@ -703,7 +703,7 @@ func diffStageObj(writer io.Writer, sName string) { return } for _, param := range stagingNote.AllValues { - if solName != "" && param.Section != solSelect { + if (solName != "" && param.Section != solSelect) || param.Section == "version" { continue } stgNote[param.Key] = param.Value @@ -717,7 +717,7 @@ func diffStageObj(writer io.Writer, sName string) { return } for _, param := range workingNote.AllValues { - if solName != "" && param.Section != solSelect { + if solName != "" && param.Section != solSelect || param.Section == "version" { continue } wrkNote[param.Key] = param.Value diff --git a/ospackage/usr/share/saptune/scripts/upd_helper b/ospackage/usr/share/saptune/scripts/upd_helper index e6b5a546..ca5c7070 100644 --- a/ospackage/usr/share/saptune/scripts/upd_helper +++ b/ospackage/usr/share/saptune/scripts/upd_helper @@ -525,7 +525,13 @@ copy_packednotes2staging() { # create a note file in staging area, which only contains the # version information of the 'old' note # as a flag for a later removal from the working area - grep '# .*VERSION=.*DATE=.*NAME=".*"' "${wnote}" > ${STAGINGAREA}/latest/"${note}" + awk '/\[version\]/ { + print $0 + while (getline > 0) { + if ($1 ~ /\[/) { exit } + print $0 + } + }' "${wnote}" > ${STAGINGAREA}/latest/"${note}" fi done # check for changed or new solutions diff --git a/txtparser/section.go b/txtparser/section.go index 1f014d5c..ebee80c5 100644 --- a/txtparser/section.go +++ b/txtparser/section.go @@ -99,11 +99,17 @@ func GetOverrides(filetype, ID string) (bool, *INIFile) { // readVersionSection read content of [version] section from config file func readVersionSection(fileName string) ([]string, bool, error) { skipSection := false + staging := false chkVersEntries := map[string]bool{"missing": false, "found": false, "isNew": false, "isOld": false, "skip": false, "mandVers": false, "mandDate": false, "mandDesc": false, "mandRefs": false} vsection := []string{} fName := filepath.Base(fileName) + if strings.Contains(filepath.Dir(fileName), "/staging/") { + staging = true + } versRun := fmt.Sprintf("%s/version_%s.run", saptuneSectionDir, fName) - if _, err := os.Stat(versRun); err == nil { + // if processing a note from the staging area, read from staging file + // and NOT from the stored 'run' file + if _, err := os.Stat(versRun); err == nil && !staging { return getVersionRunInfo(versRun) } content, err := ioutil.ReadFile(fileName) @@ -147,7 +153,10 @@ func readVersionSection(fileName string) ([]string, bool, error) { } err = chkVersEntriesResult(fileName, chkVersEntries) - if !chkVersEntries["missing"] { + // if processing a note from the staging area do NOT store the version + // info in the 'run' file to not override the section info from the + // working area + if !chkVersEntries["missing"] && !staging { err = storeVersionRunInfo(versRun, vsection, chkVersEntries["isNew"]) } return vsection, chkVersEntries["isNew"], err