SUSE/saptune

View on GitHub
actions/stagingacts.go

Summary

Maintainability
D
3 days
Test Coverage
F
1%
package actions

import (
    "fmt"
    "github.com/SUSE/saptune/app"
    "github.com/SUSE/saptune/sap/note"
    "github.com/SUSE/saptune/sap/solution"
    "github.com/SUSE/saptune/system"
    "github.com/SUSE/saptune/txtparser"
    "io"
    "os"
    "sort"
    "strconv"
    "strings"
)

type stageFiles struct {
    AllStageFiles   []string
    StageAttributes map[string]map[string]string
}

type stageComparison struct {
    FieldName        string
    stgVal, wrkVal   string
    MatchExpectation bool
}

type writerDescriptor struct {
    writer         io.Writer
    textApplied    string
    textEnabled    string
    textNotEnabled string
}

var stagingSwitch = false
var stagingOptions = note.GetTuningOptions(StagingSheets, "")
var stgFiles stageFiles
var stagingSolutions = solution.GetOtherSolution(StagingSheets, "", "")

// StagingAction  Staging actions like apply, revert, verify asm.
func StagingAction(actionName string, stageName []string, tuneApp *app.App) {
    txtparser.ResetVersionSectCnts("/staging/")
    stagingSwitch = getStagingFromConf()
    if len(stgFiles.AllStageFiles) == 0 && len(stgFiles.StageAttributes) == 0 {
        stgFiles = collectStageFileInfo(tuneApp)
    }

    switch actionName {
    case "status":
        stagingActionStatus(os.Stdout)
    case "is-enabled":
        // Returns the status of staging as exit code
        // 0 == enabled (STAGING=true), 1 == disabled (STAGING=false)
        if stagingSwitch {
            system.ErrorExit("", 0)
        } else {
            system.ErrorExit("", 1)
        }
    case "enable":
        stagingActionEnable()
    case "disable":
        stagingActionDisable()
    case "list":
        chkStageExit(os.Stdout)
        stagingActionList(os.Stdout)
    case "diff":
        if len(stageName) == 0 {
            stageName = []string{"all"}
        }
        chkStageExit(os.Stdout)
        stagingActionDiff(os.Stdout, stageName)
    case "analysis":
        if len(stageName) == 0 {
            stageName = []string{"all"}
        }
        chkStageExit(os.Stdout)
        stagingActionAnalysis(os.Stdout, stageName)
    case "release":
        if len(stageName) == 0 {
            stageName = []string{"all"}
        }
        chkStageExit(os.Stdout)
        stagingActionRelease(os.Stdin, os.Stdout, stageName)
    default:
        PrintHelpAndExit(os.Stdout, 1)
    }
}

// stagingActionStatus shows the status of staging,
// basically the content of STAGING in /etc/sysconfig/saptune.
func stagingActionStatus(writer io.Writer) {
    if stagingSwitch {
        fmt.Fprintf(writer, "Staging is enabled\n")
    } else {
        fmt.Fprintf(writer, "Staging is disabled\n")
    }
}

// stagingActionEnable enables staging by setting STAGING in /etc/sysconfig/saptune.
func stagingActionEnable() {
    system.NoticeLog("Enable staging")
    stagingSwitch = true
    if err := writeStagingToConf("true"); err != nil {
        system.ErrorExit("Staging could NOT be enabled. - '%v'\n", err)
    }
    system.NoticeLog("Staging has been enabled.")
}

// stagingActionDisable disables staging by setting STAGING in /etc/sysconfig/saptune.
func stagingActionDisable() {
    system.NoticeLog("Disable staging")
    stagingSwitch = false
    if err := writeStagingToConf("false"); err != nil {
        system.ErrorExit("Staging could NOT be disabled. - '%v'\n", err)
    }
    system.NoticeLog("Staging has been disabled.")
}

// stagingActionList lists all Notes and solution definition which can be
// released from the staging area.
// If a Note or the solution definition is part of the working area, but not
// in the package area, it will be listed as deleted.
func stagingActionList(writer io.Writer) {
    fmt.Fprintf(writer, "\n")
    for _, stageName := range stgFiles.AllStageFiles {
        desc := stgFiles.StageAttributes[stageName]["desc"]
        flag := ""
        flags := []string{"deleted", "updated", "new"}
        for _, f := range flags {
            if stgFiles.StageAttributes[stageName][f] == "true" {
                flag = fmt.Sprintf("(%s)", f)
                break
            }
        }
        format := "\t%s\t\t%s\n\t\t\t%s\n"
        if len(stageName) >= 8 {
            format = "\t%s\t%s\n\t\t\t%s\n"
        }
        fmt.Fprintf(writer, format, stageName, desc, flag)
    }
    fmt.Fprintf(writer, "\nRemember: To release from staging use the command 'saptune staging release ...'.\n          You can check the differences with 'saptune staging diff ...'.\n")
}

// stagingActionDiff shows the differences between the Note, the solution definition
// or all objects in the staging area and the working area.
// For each Note in the staging area the output contains the values of all
// parameter which differ.
// This includes new or removed parameters as well as changes in the reminder
// section.
// For the Solution, all changed solutions are displayed with their differences.
func stagingActionDiff(writer io.Writer, sObject []string) {
    for _, sName := range sObject {
        switch sName {
        case "all":
            for _, stageName := range stgFiles.AllStageFiles {
                diffStageObj(writer, stageName)
            }
        default:
            diffStageObj(writer, sName)
        }
    }
    fmt.Fprintf(writer, "\nRemember: To release from staging use the command 'saptune staging release ...'.\n")
}

// stagingActionAnalysis does an analysis of the requested Notes, the solution
// definition or everything in the staging area to warn the user about possible
// issues or additional steps to perform.
func stagingActionAnalysis(writer io.Writer, stageObject []string) {
    releaseable := true
    ret := 0
    breakingObj := []string{}
    fmt.Fprintf(writer, "\n")
    for _, sObj := range stageObject {
        switch sObj {
        case "all":
            for _, stageName := range stgFiles.AllStageFiles {
                rel, err := showAnalysis(writer, stageName)
                if !rel {
                    releaseable = false
                    breakingObj = append(breakingObj, stageName)
                }
                ret = system.MaxI(ret, err)
            }
        default:
            rel, err := showAnalysis(writer, sObj)
            if !rel {
                releaseable = false
                breakingObj = append(breakingObj, sObj)
            }
            ret = system.MaxI(ret, err)
        }
    }
    if !releaseable {
        system.ErrorExit("Releasing '%s' will break the functionality of saptune. Please fix.", strings.Join(breakingObj, ", "), ret)
    }
    fmt.Fprintf(writer, "\nRemember: To release from staging use the command 'saptune staging release ...'. Check the differences first with 'saptune staging diff...'.\n")
    system.ErrorExit("", ret)
}

// StagingActionRelease releases the requested Notes, the solution definition or
// everything in the stages area.
// This means the Notes or the solution definition gets moved from the staging
// area to the working area.
// In case of a deleted Note, it will be removed from the working area.
// First the command will show an analysis of the objects going to be released
// to make the user aware of further needed actions or potential problems
// (for details see saptune staging analysis).
// The customer has to confirm this, because the action is irreversible.
func stagingActionRelease(reader io.Reader, writer io.Writer, sObject []string) {
    for _, sName := range sObject {
        stagingFile := stgFiles.StageAttributes[sName]["sfilename"]
        stageVers := stgFiles.StageAttributes[sName]["version"]
        stageDate := stgFiles.StageAttributes[sName]["date"]

        switch sName {
        case "all":
            if len(stgFiles.AllStageFiles) == 0 {
                system.ErrorExit("No staging files available, so nothig to do.", 0)
            }
            for _, stageName := range stgFiles.AllStageFiles {
                rel, _ := showAnalysis(writer, stageName)
                if !rel {
                    system.ErrorExit("Releasing '%s' will break the functionality of saptune. Please fix", stageName, 2)
                }
            }
            if system.IsFlagSet("dryrun") {
                system.ErrorExit("Flag 'dryrun' set, so staging action 'release' finished now without releasing anything", 0)
            }
            if !system.IsFlagSet("force") {
                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)
            for _, stageName := range stgFiles.AllStageFiles {
                stagingFile = stgFiles.StageAttributes[stageName]["sfilename"]
                if _, err := os.Stat(stagingFile); err != nil {
                    system.ErrorLog("file '%s' not found in staging area, nothing to do, skipping ...", stagingFile)
                    errs = append(errs, err)
                    continue
                }
                stageVers = stgFiles.StageAttributes[stageName]["version"]
                stageDate = stgFiles.StageAttributes[stageName]["date"]
                err := mvStageToWork(stageName)
                if err != nil {
                    errs = append(errs, err)
                } else {
                    system.NoticeLog("%s Version %s (%s) released", stageName, stageVers, stageDate)
                }
            }
            if len(errs) != 0 {
                system.ErrorExit("", 1)
            }
        default:
            if stagingFile == "" {
                system.ErrorExit("'%s' not found in staging area, nothing to do.", sName, 1)
            }
            rel, _ := showAnalysis(writer, sName)
            if !rel {
                system.ErrorExit("Releasing '%s' will break the functionality of saptune. Please fix", sName, 2)
            }
            if system.IsFlagSet("dryrun") {
                system.ErrorExit("Flag 'dryrun' set, so staging action 'release' finished now without releasing anything", 0)
            }
            if !system.IsFlagSet("force") {
                txtConfirm := "Releasing is irreversible! Are you sure"
                if !readYesNo(txtConfirm, reader, writer) {
                    system.ErrorExit("Staging action 'release' aborted by user interaction", 0)
                }
            }
            if err := mvStageToWork(sName); err != nil {
                system.ErrorExit("", 1)
            }
            system.NoticeLog("%s Version %s (%s) released", sName, stageVers, stageDate)
        }
    }
}

// showAnalysis does an analysis of the requested object in the staging area
// to warn the user about possible issues or additional steps to perform.
func showAnalysis(writer io.Writer, stageName string) (bool, int) {
    release := true
    ret := 0
    if stageName == "" {
        PrintHelpAndExit(writer, 0)
    }

    txtPrefix := "    --> "
    txtReleaseNote := "Release of %s Version %s (%s)\n"
    vers := stgFiles.StageAttributes[stageName]["version"]
    date := stgFiles.StageAttributes[stageName]["date"]
    flag := ""
    flags := []string{"deleted", "updated", "new"}
    for _, f := range flags {
        if stgFiles.StageAttributes[stageName][f] == "true" {
            flag = f
            break
        }
    }
    if flag != "deleted" {
        fmt.Fprintf(writer, txtReleaseNote, stageName, vers, date)
    }
    if strings.HasSuffix(stageName, ".sol") {
        // print solution analysis
        release, ret = printSolAnalysis(writer, stageName, txtPrefix, flag)
    } else {
        // print note analysis
        release, ret = printNoteAnalysis(writer, stageName, txtPrefix, flag)
    }
    return release, ret
}

// printSolAnalysis handles the solution related analysis
func printSolAnalysis(writer io.Writer, stageName, txtPrefix, flag string) (bool, int) {
    releaseable := true
    retVal := 0
    txtDeleteSol := "Deletion of %s\n"
    txtSolNew := txtPrefix + "Solution is new, no action required.\n"
    txtOverrideExists := txtPrefix + "Override file exists and might need adjustments.\n"
    txtSolEnabled := txtPrefix + "Solution is enabled, but not applied, no action required.\n"
    txtSolApplied := txtPrefix + "Solution is applied and must be re-applied.\n"
    txtSolNotEnabled := txtPrefix + "Solution is not enabled, no action required.\n"
    txtUpdatedNote := txtPrefix + "Solution benefits from releasing '%s' because this note will be updated too!\n"
    txtRequiredNote := txtPrefix + "Solution requires releasing of the new note '%s' or it breaks!\n"
    txtMissingNote := txtPrefix + "Because of missing note '%s' the Solution will break after release!\n"
    txtDeletedNote := txtPrefix + "Solution will break, if note '%s' will be released because this note will be deleted!\n"

    if flag == "deleted" {
        txtOverrideExists = txtPrefix + "Override file exists and can be deleted.\n"
        txtSolEnabled = txtPrefix + "Solution is enabled and must be reverted.\n"
        txtSolApplied = txtPrefix + "Solution is applied and must be reverted.\n"

        fmt.Fprintf(writer, txtDeleteSol, stageName)
    }

    if stgFiles.StageAttributes[stageName]["override"] == "true" {
        fmt.Fprint(writer, txtOverrideExists)
    }

    retVal = writeIfNotNew(flag, stageName, writerDescriptor{writer, txtSolApplied, txtSolEnabled, txtSolNotEnabled}, retVal)

    for _, note := range strings.Split(stgFiles.StageAttributes[stageName]["notes"], " ") {
        if note == "" {
            continue
        }
        for _, n := range strings.Split(stgFiles.StageAttributes[stageName]["missingNotes"], " ") {
            if n == note {
                fmt.Fprintf(writer, txtMissingNote, note)
                releaseable = false
                retVal = system.MaxI(retVal, 2)
                break
            }
        }
        for _, stgName := range strings.Split(stgFiles.StageAttributes[stageName]["notesInStaging"], " ") {
            if stgName == note {
                if stgFiles.StageAttributes[note]["deleted"] == "true" {
                    fmt.Fprintf(writer, txtDeletedNote, note)
                    releaseable = false
                    retVal = system.MaxI(retVal, 2)
                }
                if stgFiles.StageAttributes[note]["new"] == "true" {
                    fmt.Fprintf(writer, txtRequiredNote, note)
                    releaseable = false
                    retVal = system.MaxI(retVal, 2)
                }
                if stgFiles.StageAttributes[note]["updated"] == "true" {
                    fmt.Fprintf(writer, txtUpdatedNote, note)
                    retVal = system.MaxI(retVal, 1)
                }
            }
        }
    }
    if flag == "new" {
        fmt.Fprint(writer, txtSolNew)
    }
    return releaseable, retVal
}

func writeIfNotNew(flag string, stageName string, toWrite writerDescriptor, retVal int) int {
    writer := toWrite.writer
    textApplied := toWrite.textApplied
    textEnabled := toWrite.textEnabled
    textNotEnabled := toWrite.textNotEnabled

    if flag != "new" {
        if stgFiles.StageAttributes[stageName]["applied"] == "true" {
            fmt.Fprint(writer, textApplied)
            retVal = system.MaxI(retVal, 1)
        } else if stgFiles.StageAttributes[stageName]["enabled"] == "true" {
            fmt.Fprint(writer, textEnabled)
            if flag == "deleted" {
                retVal = system.MaxI(retVal, 1)
            }
        } else {
            fmt.Fprint(writer, textNotEnabled)
        }
    }

    return retVal
}

// printNoteAnalysis handles the solution related analysis
func printNoteAnalysis(writer io.Writer, stageName, txtPrefix, flag string) (bool, int) {
    releaseable := true
    retVal := 0
    txtDeleteNote := "Deletion of %s\n"
    txtOverrideExists := txtPrefix + "Override file exists and might need adjustments.\n"
    txtNoteNew := txtPrefix + "Note is new, no action required.\n"
    txtNoteEnabled := txtPrefix + "Note is enabled, but not applied, no action required.\n"
    txtNoteApplied := txtPrefix + "Note is applied and must be re-applied.\n"
    txtNoteNotEnabled := txtPrefix + "Note is not enabled, no action required.\n"
    txtSolEnabled := txtPrefix + "Note is part of the currently enabled solution '%s'.\n"
    txtSolNotEnabled := txtPrefix + "Note is part of the not-enabled solution(s) '%s'\n"
    txtCustomSolEnabled := txtPrefix + "Note is part of the currently enabled custom solution '%s'.\n"
    txtCustomSolNotEnabled := txtPrefix + "Note is part of the not-enabled custom solution '%s'.\n"

    if flag == "deleted" {
        txtOverrideExists = txtPrefix + "Override file exists and can be deleted.\n"
        txtNoteEnabled = txtPrefix + "Note is enabled and must be reverted.\n"
        txtNoteApplied = txtPrefix + "Note is applied and must be reverted.\n"
        txtSolEnabled = txtPrefix + "Note is part of the currently enabled solution '%s'. Release would break the solution!\n"
        txtSolNotEnabled = txtPrefix + "Note is part of the not-enabled solution '%s'. Release would break the solution!\n"
        txtCustomSolEnabled = txtPrefix + "Note is part of the currently enabled custom solution '%s'. Release would break the solution!\n"
        txtCustomSolNotEnabled = txtPrefix + "Note is part of the not-enabled custom solution '%s'. Release would break the solution!\n"

        fmt.Fprintf(writer, txtDeleteNote, stageName)
    }
    if stgFiles.StageAttributes[stageName]["override"] == "true" {
        fmt.Fprint(writer, txtOverrideExists)
    }

    retVal = writeIfNotNew(flag, stageName, writerDescriptor{writer, txtNoteApplied, txtNoteEnabled, txtNoteNotEnabled}, retVal)

    if stgFiles.StageAttributes[stageName]["inSolution"] != "" {
        for _, sol := range strings.Split(stgFiles.StageAttributes[stageName]["inSolution"], ",") {
            txtSEnabled := txtSolEnabled
            txtSNotEnabled := txtSolNotEnabled
            // check for custom solution
            for _, csol := range strings.Split(stgFiles.StageAttributes[stageName]["inCustomSolution"], ",") {
                if csol == sol {
                    //sol is a custom solution
                    txtSEnabled = txtCustomSolEnabled
                    txtSNotEnabled = txtCustomSolNotEnabled
                    break
                }
            }
            sol = strings.TrimSpace(sol)
            if sol == stgFiles.StageAttributes[stageName]["enabledSol"] {
                fmt.Fprintf(writer, txtSEnabled, sol)
            } else {
                fmt.Fprintf(writer, txtSNotEnabled, sol)
            }
            if flag == "deleted" {
                retVal = system.MaxI(retVal, 2)
                releaseable = false
            }
        }
    }
    if flag == "new" {
        fmt.Fprint(writer, txtNoteNew)
    }
    return releaseable, retVal
}

// 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)
    stagingFile := stgFiles.StageAttributes[stageName]["sfilename"]
    workingFile := stgFiles.StageAttributes[stageName]["wfilename"]
    packageFile := stgFiles.StageAttributes[stageName]["pfilename"]
    // check, if note should be deleted
    if _, err := os.Stat(workingFile); err == nil {
        if _, perr := os.Stat(packageFile); os.IsNotExist(perr) {
            // in working, but not in packaging, delete from working and staging
            if rerr := os.Remove(workingFile); rerr != nil {
                system.ErrorLog("Problems during removal of '%s' from working area: %v", stageName, rerr)
                errs = append(errs, rerr)
            }
            if rerr := os.Remove(stagingFile); rerr != nil {
                system.ErrorLog("Problems during removal of '%s' from staging area: %v", stageName, rerr)
                errs = append(errs, rerr)
            }
            if len(errs) == 0 {
                system.NoticeLog("'%s' successfully removed from working and staging area", stageName)
                return nil
            }
        }
    }
    // move new or changed/updated note/solution from staging to working area
    if err := os.Rename(stagingFile, workingFile); err != nil {
        system.ErrorLog("Problems during move of '%s' from staging to working area: %v", stageName, err)
        errs = append(errs, err)
    } else {
        system.NoticeLog("'%s' successfully moved from staging to working area", stageName)
    }
    if len(errs) != 0 {
        return fmt.Errorf("Problems during releasing '%s' from staging to working area", stageName)
    }
    return nil
}

// getStagingFromConf reads STAGING setting from /etc/sysconfig/saptune
func getStagingFromConf() bool {
    sconf, err := txtparser.ParseSysconfigFile(saptuneSysconfig, true)
    if err != nil {
        system.ErrorExit("Unable to read file '/etc/sysconfig/saptune': '%v'\n", err, 2)
    }
    if sconf.GetString("STAGING", "false") == "true" {
        stagingSwitch = true
    }
    return stagingSwitch
}

// writeStagingToConf writes STAGING setting to /etc/sysconfig/saptune
func writeStagingToConf(staging string) error {
    sconf, err := txtparser.ParseSysconfigFile(saptuneSysconfig, true)
    if err != nil {
        return err
    }
    sconf.Set("STAGING", staging)
    return os.WriteFile(saptuneSysconfig, []byte(sconf.ToText()), 0644)
}

// collectStageFileInfo is collecting all needed info about the file
// available in the staging area for later processing
func collectStageFileInfo(tuneApp *app.App) stageFiles {
    stageConf := stageFiles{
        AllStageFiles:   make([]string, 0, 64),
        StageAttributes: make(map[string]map[string]string),
    }
    for _, stageName := range stagingOptions.GetSortedIDs() {
        // add new stage file
        stageMap := make(map[string]string)

        // get Note/Solution Description and setup absolute filenames
        solStageName, name, workingFile, packageFile := getSolOrNoteEnv(stageName)
        stagingFile := fmt.Sprintf("%s%s", StagingSheets, stageName)
        // get flags
        stageMap["new"], stageMap["deleted"], stageMap["updated"] = collectStageFlags(workingFile, packageFile)

        if stageMap["deleted"] != "true" {
            // Description
            stageMap["desc"] = name
            // Version
            stageMap["version"] = txtparser.GetINIFileVersionSectionEntry(stagingFile, "version")
            // Date
            stageMap["date"] = txtparser.GetINIFileVersionSectionEntry(stagingFile, "date")
        }
        // filenames
        stageMap["wfilename"] = workingFile
        stageMap["pfilename"] = packageFile
        stageMap["sfilename"] = stagingFile
        // enabled solution
        if len(tuneApp.TuneForSolutions) > 0 {
            stageMap["enabledSol"] = tuneApp.TuneForSolutions[0]
        }

        // check for override file
        stageMap["override"] = "false"
        if _, override := getovFile(stageName, OverrideTuningSheets); override {
            stageMap["override"] = "true"
        }
        // check if applied
        stageMap["applied"] = getStageAppliedState(tuneApp, solStageName, stageName)
        // check if enabled
        stageMap["enabled"] = getStageEnabledState(tuneApp, solStageName, stageName)
        if solStageName == "" {
            // note - check if in a solution
            stageMap["inSolution"], stageMap["inCustomSolution"] = getNoteInSol(tuneApp, stageName)
        } else {
            // solution - check for notes
            stageMap["notes"], stageMap["notesInStaging"], stageMap["missingNotes"] = getStageSolRequiredNotes(tuneApp, solStageName)
        }

        stageConf.StageAttributes[stageName] = stageMap
        stageConf.AllStageFiles = append(stageConf.AllStageFiles, stageName)
    }
    return stageConf
}

// getSolOrNoteEnv returns Note/Solution name, description and absolute filenames
func getSolOrNoteEnv(stgName string) (string, string, string, string) {
    sName := ""
    dName := stagingOptions[stgName].Name()
    wFile := fmt.Sprintf("%s%s", NoteTuningSheets, stgName)
    pFile := fmt.Sprintf("%snotes/%s", PackageArea, stgName)
    if strings.HasSuffix(stgName, ".sol") {
        // stage file is a solution file
        sName = strings.TrimSuffix(stgName, ".sol")
        if dName == "" {
            dName = "Definition of saptune solutions\n\t\t\tVersion 1"
        }
        wFile = fmt.Sprintf("%s%s", SolutionSheets, stgName)
        pFile = fmt.Sprintf("%ssols/%s", PackageArea, stgName)
    }
    return sName, dName, wFile, pFile
}

// collectStageFlags collect the state of a file in staging into the flags
func collectStageFlags(work, pack string) (string, string, string) {
    // default is updated Note/Solution
    uflag := "true"
    nflag := "false"
    dflag := "false"

    if _, err := os.Stat(work); os.IsNotExist(err) {
        // not in working, but in staging
        // new Note/Solution
        nflag = "true"
        uflag = "false"
    } else if err == nil {
        if _, perr := os.Stat(pack); os.IsNotExist(perr) {
            // in working, but not in packaging
            // deleted Note/Solution
            dflag = "true"
            uflag = "false"
        }
    }
    return nflag, dflag, uflag
}

// getStageAppliedState returns, if a stage object is applied or not
func getStageAppliedState(tApp *app.App, sol, note string) string {
    applied := "false"
    if sol != "" {
        // solution
        if _, ok := tApp.IsSolutionApplied(sol); ok {
            applied = "true"
        }
    } else {
        // note
        if _, ok := tApp.IsNoteApplied(note); ok {
            applied = "true"
        }
    }
    return applied
}

// getStageEnabledState returns, if a stage object is enabled or not
func getStageEnabledState(tApp *app.App, sol, note string) string {
    enabled := "true"
    if sol != "" {
        // solution
        enabled = "false"
        if len(tApp.TuneForSolutions) != 0 {
            if tApp.TuneForSolutions[0] == sol {
                enabled = "true"
            }
        }
    } else {
        // note
        if tApp.PositionInNoteApplyOrder(note) < 0 { // noteID not yet available
            enabled = "false"
        }
    }
    return enabled
}

// getStageSolRequiredNotes checks, which Notes are related to a Solution from
// the staging area and if there are missing Notes or Notes available in the
// staging area
// returns strings with Note names (notes, notesInStaging, missingNotes)
func getStageSolRequiredNotes(tApp *app.App, solName string) (string, string, string) {
    notesInStaging := ""
    missingNotes := ""
    solNotes := stagingSolutions[solutionSelector][solName]
    sNotes := strings.Join(solNotes, " ")
    for _, note := range solNotes {
        if note == "" {
            continue
        }
        inStaging := false
        for _, snote := range stagingOptions.GetSortedIDs() {
            if snote == note {
                if notesInStaging == "" {
                    notesInStaging = note
                } else {
                    notesInStaging = notesInStaging + " " + note
                }
                inStaging = true
                break
            }
        }
        if _, exists := tApp.AllNotes[note]; !exists && !inStaging {
            // note definition file for NoteID does not exist
            if missingNotes == "" {
                missingNotes = note
            } else {
                missingNotes = missingNotes + " " + note
            }
        }
    }
    return sNotes, notesInStaging, missingNotes
}

// diffStageObj diffs a note from the staging area with a note from the working area
func diffStageObj(writer io.Writer, sName string) {
    var workingNote *txtparser.INIFile
    stgNote := map[string]string{}
    wrkNote := map[string]string{}
    solName := ""
    solSelect := "ArchX86"
    if solutionSelector == "ppc64le" {
        solSelect = "ArchPPC64LE"
    }
    if strings.HasSuffix(sName, ".sol") {
        solName = strings.TrimSuffix(sName, ".sol")
    }
    // parse staging file
    stagingNote, err := txtparser.ParseINIFile(stgFiles.StageAttributes[sName]["sfilename"], false)
    if err != nil {
        system.ErrorLog("Problems while parsing the staging definition file. Check the name")
        return
    }
    for _, param := range stagingNote.AllValues {
        if (solName != "" && param.Section != solSelect) || param.Section == "version" {
            continue
        }
        stgNote[param.Key] = param.Value
    }

    if stgFiles.StageAttributes[sName]["new"] != "true" {
        // parse working file
        workingNote, err = txtparser.ParseINIFile(stgFiles.StageAttributes[sName]["wfilename"], false)
        if err != nil {
            system.ErrorLog("Problems while parsing the working definition file. Check the name")
            return
        }
        for _, param := range workingNote.AllValues {
            if solName != "" && param.Section != solSelect || param.Section == "version" {
                continue
            }
            wrkNote[param.Key] = param.Value
        }
    }

    conforming, comparisons := compareStageFields(sName, stgNote, wrkNote)
    if !conforming {
        PrintStageFields(writer, sName, comparisons)
    } else {
        // paranoia log, should not be the case, because the saptune rpm takes care of this
        system.NoticeLog("'%s' - no diffs in staging", sName)
    }
}

// compareStageFields compares a note from the staging area with a note from the working area
func compareStageFields(sName string, stage, work map[string]string) (allMatch bool, comparisons map[string]stageComparison) {
    comparisons = make(map[string]stageComparison)
    allMatch = true
    // check for deleted Notes
    if stgFiles.StageAttributes[sName]["deleted"] == "true" {
        for Key, workValue := range work {
            comparisons[Key] = stageComparison{
                FieldName:        Key,
                stgVal:           "-",
                wrkVal:           workValue,
                MatchExpectation: false,
            }
            allMatch = false
        }
        return
    }
    // check for new Notes
    if stgFiles.StageAttributes[sName]["new"] == "true" {
        for Key, stageValue := range stage {
            comparisons[Key] = stageComparison{
                FieldName:        Key,
                stgVal:           stageValue,
                wrkVal:           "-",
                MatchExpectation: false,
            }
            allMatch = false
        }
        return
    }

    // changed Notes
    // check for deleted parameter in staging Note
    for Key, workValue := range work {
        _, ok := stage[Key]
        if ok {
            continue
        }
        comparisons[Key] = stageComparison{
            FieldName:        Key,
            stgVal:           "-",
            wrkVal:           workValue,
            MatchExpectation: false,
        }
        allMatch = false
    }
    for Key, stageValue := range stage {
        // new/additional parameter settings in staging Note - Key will not be available in map
        workValue, ok := work[Key]
        if !ok {
            workValue = "-"
        }

        stageValueJS, workValueJS, match := note.CompareJSValue(stageValue, workValue, "")
        if !match {
            comparisons[Key] = stageComparison{
                FieldName:        Key,
                stgVal:           stageValueJS,
                wrkVal:           workValueJS,
                MatchExpectation: match,
            }
            allMatch = match
        }
    }
    return
}

// PrintStageFields prints mismatching parameters between Notes in staging
// and working area
func PrintStageFields(writer io.Writer, stageName string, comparison map[string]stageComparison) {

    workFile := stgFiles.StageAttributes[stageName]["wfilename"]
    headWork := fmt.Sprintf("Version %s (%s) ", txtparser.GetINIFileVersionSectionEntry(workFile, "version"), txtparser.GetINIFileVersionSectionEntry(workFile, "date"))
    headStage := fmt.Sprintf("Version %s (%s) ", stgFiles.StageAttributes[stageName]["version"], stgFiles.StageAttributes[stageName]["date"])

    // sort output
    sortkeys := sortStageComparisonsOutput(comparison)

    // setup table format values
    fmtdash, fmtplus, format, colwidth := setupStageTableFormat(comparison)

    // print table header
    fmt.Fprint(writer, fmtdash)
    fmt.Fprintf(writer, format, stageName, headWork, headStage)
    fmt.Fprintf(writer, format, "", "(working area)", "(staging area)")
    fmt.Fprint(writer, fmtplus)
    for _, skey := range sortkeys {
        // print table body
        if skey == "reminder" {
            // reminder handling - split text into lines so that
            // they fit the column width, more than one line for
            // this parameter possible
            wrappedWrkVal := system.WrapTxt(comparison[skey].wrkVal, colwidth)
            wrappedStgVal := system.WrapTxt(comparison[skey].stgVal, colwidth)
            wrkLines := len(wrappedWrkVal)
            stgLines := len(wrappedStgVal)
            if wrkLines > stgLines {
                stgLine := ""
                for w, wrkLine := range wrappedWrkVal {
                    if w < stgLines {
                        stgLine = wrappedStgVal[w]
                    } else {
                        stgLine = "-"
                    }
                    fmt.Fprintf(writer, format, skey, wrkLine, stgLine)
                }
            } else {
                wrkLine := ""
                for s, stgLine := range wrappedStgVal {
                    if s < wrkLines {
                        wrkLine = wrappedWrkVal[s]
                    } else {
                        wrkLine = "-"
                    }
                    fmt.Fprintf(writer, format, skey, wrkLine, stgLine)
                }
            }
            //fmt.Fprintf(writer, format, skey, wrappedWrkVal, wrappedStgVal)
        } else {
            fmt.Fprintf(writer, format, skey, strings.Replace(comparison[skey].wrkVal, "\t", " ", -1), strings.Replace(comparison[skey].stgVal, "\t", " ", -1))
        }
    }
    // print footer
    fmt.Fprint(writer, fmtdash)
    fmt.Fprintf(writer, "\n")
}

// sortStageComparisonsOutput sorts the output of the stage comparison
// the reminder section should be the last one
func sortStageComparisonsOutput(noteCompare map[string]stageComparison) []string {
    skeys := make([]string, 0, len(noteCompare))
    rkeys := make([]string, 0, len(noteCompare))
    // sort output
    for key := range noteCompare {
        if key != "reminder" {
            skeys = append(skeys, key)
        } else {
            rkeys = append(rkeys, key)
        }
    }
    sort.Strings(skeys)
    skeys = append(skeys, rkeys...)
    return skeys
}

// setupStageTableFormat sets the format of the table columns dependent on the content
func setupStageTableFormat(stageCompare map[string]stageComparison) (string, string, string, int) {
    var fmtdash string
    var fmtplus string
    var format string
    // define start values for the column width
    fmtlen1 := 12
    fmtlen2 := 26
    fmtlen3 := 26

    for skey, comparison := range stageCompare {
        // 1:parameter, 2:working, 3:staging
        if skey == "reminder" {
            // reminder section should not influence the
            // column size
            continue
        }
        if len(skey) > fmtlen1 {
            fmtlen1 = len(skey)
        }
        if len(comparison.wrkVal) > fmtlen2 {
            fmtlen2 = len(comparison.wrkVal)
        }
        if len(comparison.stgVal) > fmtlen3 {
            fmtlen3 = len(comparison.stgVal)
        }
    }
    colwidth := fmtlen2
    if fmtlen2 > fmtlen3 {
        colwidth = fmtlen3
    }

    format = " %-" + strconv.Itoa(fmtlen1) + "s | %-" + strconv.Itoa(fmtlen2) + "s | %-" + strconv.Itoa(fmtlen3) + "s \n"

    tableLen := fmtlen1 + fmtlen2 + fmtlen3 + 8
    // line with dashes, used as borders for the table
    for i := 0; i < tableLen; i++ {
        fmtdash = fmtdash + "-"
    }
    fmtdash = fmtdash + "\n"
    // line with dashes and plus, used as separator between head and body
    for i := 0; i < tableLen; i++ {
        if i == 1+fmtlen1+1 || i == 1+fmtlen1+3+fmtlen2+1 || i == 1+fmtlen1+3+fmtlen2+4+fmtlen3+1 {
            fmtplus = fmtplus + "+"
        } else {
            fmtplus = fmtplus + "-"
        }
    }
    fmtplus = fmtplus + "\n"
    return fmtdash, fmtplus, format, colwidth
}

// chkStageExit checks, if a staging action should be executed or not
func chkStageExit(writer io.Writer) {
    if !stagingSwitch {
        fmt.Fprintf(writer, "ATTENTION: Staging is currently disabled. Please enable staging first and try again.\n")
        system.ErrorExit("", 0)
    }
    if len(stagingOptions.GetSortedIDs()) == 0 {
        fmt.Fprintf(writer, "Empty staging area, no Notes or solutions available. So nothing to do\n")
        system.ErrorExit("", 0)
    }
}

// listStageNotesAndSols gives the list of Notes in the staging area and
// the list of Solutions in the staging area
func listStageNotesAndSols() (notes, sols []string) {
    notes = make([]string, 0)
    sols = make([]string, 0)
    for _, sName := range stagingOptions.GetSortedIDs() {
        if strings.HasSuffix(sName, ".sol") {
            // stage file is a solution file
            sols = append(sols, sName)
        } else {
            notes = append(notes, sName)
        }
    }
    sort.Strings(notes)
    sort.Strings(sols)
    return notes, sols
}