diff --git a/README.md b/README.md index ff8d74c..7d9882d 100644 --- a/README.md +++ b/README.md @@ -244,6 +244,7 @@ m - show all status messages saved in the history s - auto-synchronize selected directories (local to DataPower) S - save a running DataPower configuration B - create and copy a secure backup of the appliance +e - run exec command on current or selected cfg file(s) 0 - cycle between different DataPower view modes filestore mode view / object mode view / status mode view - when using SOMA access changed objects are marked and object @@ -251,7 +252,7 @@ B - create and copy a secure backup of the appliance ? - show status information for the current DataPower object, local directory or local file P - show DataPower policy for the current DataPower object - (can be used only on service, policy, matches,rules & actions) + (can be used only on service, policy, matches, rules & actions) exports the current DataPower object, analyzes it and shows service, policy, matches, rules and actions for the object diff --git a/config/config.go b/config/config.go index 590eb16..ddaada4 100644 --- a/config/config.go +++ b/config/config.go @@ -417,7 +417,7 @@ func showVersion() { // help prints in-program help information to console. func showHelp(exitStatus int) { - fmt.Println(help.Help) + fmt.Print(help.Help) os.Exit(exitStatus) } diff --git a/help/help.go b/help/help.go index a685274..e50f64b 100644 --- a/help/help.go +++ b/help/help.go @@ -71,6 +71,7 @@ m - show all status messages saved in the history s - auto-synchronize selected directories (local to DataPower) S - save a running DataPower configuration B - create and copy a secure backup of the appliance +e - run exec command on current or selected cfg file(s) 0 - cycle between different DataPower view modes filestore mode view / object mode view / status mode view - when using SOMA access changed objects are marked and object @@ -78,7 +79,7 @@ B - create and copy a secure backup of the appliance ? - show status information for the current DataPower object, local directory or local file P - show DataPower policy for the current DataPower object - (can be used only on service, policy, matches,rules & actions) + (can be used only on service, policy, matches, rules & actions) exports the current DataPower object, analyzes it and shows service, policy, matches, rules and actions for the object diff --git a/repo/dp/dp.go b/repo/dp/dp.go index 4b554fa..1241779 100644 --- a/repo/dp/dp.go +++ b/repo/dp/dp.go @@ -2062,6 +2062,74 @@ func (r *dpRepo) FlushCache( } } +// ExecConfig run exec dommand for a DataPower configuration script. +func (r *dpRepo) ExecConfig(itemConfig *model.ItemConfig) error { + logging.LogDebugf("repo/dp/ExecConfig(%v)", itemConfig) + + switch r.dataPowerAppliance.DpManagmentInterface() { + case config.DpInterfaceRest: + execConfigRequestJSON := fmt.Sprintf(`{"ExecConfig":{"URL":"%s"}}`, itemConfig.Path) + resultText, _, err := r.restPostForResult( + "/mgmt/actionqueue/"+itemConfig.DpDomain, + execConfigRequestJSON, + "/ExecConfig", + "Operation completed.", + "/ExecConfig") + if err != nil { + return err + } + + logging.LogDebugf("repo/dp/ExecConfig(), resultText: '%s'", resultText) + + return nil + + case config.DpInterfaceSoma: + somaRequest := fmt.Sprintf(` + + + %s + + +`, itemConfig.DpDomain, itemConfig.Path) + response, err := r.soma(somaRequest) + if err != nil { + return err + } + result, err := parseSOMAFindOne(response, "//*[local-name()='response']/*[local-name()='result']") + logging.LogDebugf("repo/dp/ExecConfig(), result: '%v'", result) + if result != "OK" { + return errs.Errorf("DataPower exec config error: '%v'", result) + } + return nil + + default: + logging.LogDebug("repo/dp/ExecConfig(), using neither REST neither SOMA.") + return errs.Error("DataPower management interface not set.") + } + + // + // + // + // 2023-02-06T10:52:04-05:00 + // + // + // Unable to execute config:///default.cf - must be text file. + // + // + // + // + // + // + // + // + // 2023-02-06T11:06:01-05:00 + // OK + // + // + // +} + // GetManagementInterface returns current DataPower management interface used. func (r *dpRepo) GetManagementInterface() string { return r.dataPowerAppliance.DpManagmentInterface() diff --git a/repo/dp/dp_test.go b/repo/dp/dp_test.go index 29fc3fe..54bd592 100644 --- a/repo/dp/dp_test.go +++ b/repo/dp/dp_test.go @@ -2212,3 +2212,51 @@ func TestGetStatus(t *testing.T) { assert.Equals(t, "GetStatus", string(statusBytes), wantStatus) }) } + +func TestExecConfig(t *testing.T) { + itemToExec := model.ItemConfig{Type: model.ItemFile, + DpDomain: "MyExecDomain", DpFilestore: "local:", Path: "local:/config-ok.cfg"} + + t.Run("ExecConfig no REST/SOMA", func(t *testing.T) { + clearRepo() + + err := Repo.ExecConfig(&itemToExec) + assert.Equals(t, "ExecConfig", err, errs.Error("DataPower management interface not set.")) + }) + + itemToExec.Path = "local:/config-err.cfg" + t.Run("ExecConfig REST Error", func(t *testing.T) { + clearRepo() + Repo.dataPowerAppliance.RestUrl = testRestURL + + err := Repo.ExecConfig(&itemToExec) + assert.Equals(t, "ExecConfig", err, errs.Error("Unexpected response from server.")) + }) + + itemToExec.Path = "local:/config-ok.cfg" + t.Run("ExecConfig REST OK", func(t *testing.T) { + clearRepo() + Repo.dataPowerAppliance.RestUrl = testRestURL + + err := Repo.ExecConfig(&itemToExec) + assert.Nil(t, "ExecConfig", err) + }) + + itemToExec.Path = "local:/config-err.cfg" + t.Run("ExecConfig SOMA Error", func(t *testing.T) { + clearRepo() + Repo.dataPowerAppliance.SomaUrl = testSomaURL + + err := Repo.ExecConfig(&itemToExec) + assert.Equals(t, "ExecConfig", err, errs.Error("DataPower exec config error: 'Unable to execute local:/config-err.cfg - must be text file.'")) + }) + + itemToExec.Path = "local:/config-ok.cfg" + t.Run("ExecConfig SOMA OK", func(t *testing.T) { + clearRepo() + Repo.dataPowerAppliance.SomaUrl = testSomaURL + + err := Repo.ExecConfig(&itemToExec) + assert.Nil(t, "ExecConfig", err) + }) +} diff --git a/repo/dp/dpmock_test.go b/repo/dp/dpmock_test.go index 052ce53..944bbc3 100644 --- a/repo/dp/dpmock_test.go +++ b/repo/dp/dpmock_test.go @@ -4,6 +4,7 @@ import ( "fmt" "io/ioutil" "regexp" + "strings" "github.com/croz-ltd/dpcmder/utils/errs" ) @@ -68,6 +69,17 @@ func (nr mockRequester) httpRequest(dpa dpApplicance, urlFullPath, method, body default: return "", errs.Errorf("dpmock_test: Unrecognized method '%s'", method) } + case "https://my_dp_host:5554/mgmt/actionqueue/MyExecDomain": + switch method { + case "POST": + if strings.Contains(body, "config-ok") { + content, err = ioutil.ReadFile("testdata/ExecConfig_ok.json") + } else { + content, err = ioutil.ReadFile("testdata/ExecConfig_error.json") + } + default: + return "", errs.Errorf("dpmock_test: Unrecognized method '%s'", method) + } case "https://my_dp_host:5554/mgmt/actionqueue/tmp/pending/Export-20200228T061406Z-2": content, err = ioutil.ReadFile("testdata/export-svc-pending-get.json") case "https://my_dp_host:5554/mgmt/status/": @@ -130,6 +142,14 @@ func (nr mockRequester) httpRequest(dpa dpApplicance, urlFullPath, method, body if len(matches) == 1 { opAction = "SecureBackup" } + + if len(matches) == 0 { + r = regexp.MustCompile(`.*.*`) + matches = r.FindStringSubmatch(body) + if len(matches) == 1 { + opAction = "ExecConfig" + } + } } } @@ -180,6 +200,14 @@ func (nr mockRequester) httpRequest(dpa dpApplicance, urlFullPath, method, body } else { content, err = ioutil.ReadFile("testdata/SecureBackup_ok.xml") } + case opTag == "do-action" && opAction == "ExecConfig": + r = regexp.MustCompile(`.*config-err.*`) + matches = r.FindStringSubmatch(body) + if len(matches) == 1 { + content, err = ioutil.ReadFile("testdata/ExecConfig_error.xml") + } else { + content, err = ioutil.ReadFile("testdata/ExecConfig_ok.xml") + } default: fmt.Printf("dpmock_test: Unrecognized SOMA request opTag: '%s', "+ "opClass: '%s', opObjClass: '%s', opLayoutOnly: '%s', opFilePath: '%s'.\n", diff --git a/repo/dp/testdata/ExecConfig_error.json b/repo/dp/testdata/ExecConfig_error.json new file mode 100644 index 0000000..d71331e --- /dev/null +++ b/repo/dp/testdata/ExecConfig_error.json @@ -0,0 +1,12 @@ +{ + "_links": { + "self": { + "href": "/mgmt/actionqueue/tmp" + } + }, + "error": [ + "Bad request.", + "Unable to execute local:/config-err.cfg - must be text file.", + " exec config:/matko23.cfg" + ] +} diff --git a/repo/dp/testdata/ExecConfig_error.xml b/repo/dp/testdata/ExecConfig_error.xml new file mode 100644 index 0000000..89fdc26 --- /dev/null +++ b/repo/dp/testdata/ExecConfig_error.xml @@ -0,0 +1,12 @@ + + + + 2023-05-17T05:07:12-04:00 + + + Unable to execute local:/config-err.cfg - must be text file. + + + + + \ No newline at end of file diff --git a/repo/dp/testdata/ExecConfig_ok.json b/repo/dp/testdata/ExecConfig_ok.json new file mode 100644 index 0000000..7cb52c8 --- /dev/null +++ b/repo/dp/testdata/ExecConfig_ok.json @@ -0,0 +1,12 @@ +{ + "_links": { + "self": { + "href": "/mgmt/actionqueue/tmp" + }, + "doc": { + "href": "/mgmt/docs/actionqueue" + } + }, + "ExecConfig": "Operation completed.", + "script-log": "" +} diff --git a/repo/dp/testdata/ExecConfig_ok.xml b/repo/dp/testdata/ExecConfig_ok.xml new file mode 100644 index 0000000..71a8b0f --- /dev/null +++ b/repo/dp/testdata/ExecConfig_ok.xml @@ -0,0 +1,8 @@ + + + + 2023-05-17T05:07:29-04:00 + OK + + + \ No newline at end of file diff --git a/repo/localfs/localfs.go b/repo/localfs/localfs.go index c64691b..ae8ece2 100644 --- a/repo/localfs/localfs.go +++ b/repo/localfs/localfs.go @@ -3,11 +3,6 @@ package localfs import ( "fmt" - "github.com/croz-ltd/dpcmder/config" - "github.com/croz-ltd/dpcmder/model" - "github.com/croz-ltd/dpcmder/utils/errs" - "github.com/croz-ltd/dpcmder/utils/logging" - "github.com/croz-ltd/dpcmder/utils/paths" "io/ioutil" "os" "path/filepath" @@ -15,6 +10,12 @@ import ( "strconv" "strings" "time" + + "github.com/croz-ltd/dpcmder/config" + "github.com/croz-ltd/dpcmder/model" + "github.com/croz-ltd/dpcmder/utils/errs" + "github.com/croz-ltd/dpcmder/utils/logging" + "github.com/croz-ltd/dpcmder/utils/paths" ) type localRepo struct { @@ -211,6 +212,10 @@ file mode: %s`, return []byte(result), nil } +func (r localRepo) ExecConfig(itemConfig *model.ItemConfig) error { + return errs.Error("Can't exec configuration on local machine.") +} + // Tree represents file/dir with all it's children and modification time. type Tree struct { Dir bool diff --git a/repo/repo.go b/repo/repo.go index 103d01f..7e94f48 100644 --- a/repo/repo.go +++ b/repo/repo.go @@ -21,4 +21,5 @@ type Repo interface { Delete(currentView *model.ItemConfig, itemType model.ItemType, parentPath, fileName string) (bool, error) GetViewConfigByPath(currentView *model.ItemConfig, dirPath string) (*model.ItemConfig, error) GetItemInfo(itemConfig *model.ItemConfig) ([]byte, error) + ExecConfig(itemConfig *model.ItemConfig) error } diff --git a/ui/worker.go b/ui/worker.go index 3949bdf..b0da49e 100644 --- a/ui/worker.go +++ b/ui/worker.go @@ -240,6 +240,8 @@ func ProcessInputEvent(event tcell.Event) error { err = saveDataPowerConfig(&workingModel) case c == 'm': err = showStatusMessages(workingModel.Statuses()) + case c == 'e': + err = execConfigFile(&workingModel) case c == '0': err = toggleObjectMode(&workingModel) case c == '?': @@ -2167,6 +2169,57 @@ func syncLocalToDpLater(tree, treeOld *localfs.Tree) bool { return changesMade } +// execConfigFile switches between (default) filestore mode, object mode and +// status mode for the DataPower view. +func execConfigFile(m *model.Model) error { + logging.LogDebugf("worker/execConfigFile(), dp.Repo.DpViewMode: %s", dp.Repo.DpViewMode) + + side := m.CurrSide() + // viewConfig := m.ViewConfig(side) + switch side { + case model.Left: + itemsToExec := getSelectedOrCurrent(m) + pathsToExec := make([]string, len(itemsToExec)) + for idx, item := range itemsToExec { + ci := item.Config + pathsToExec[idx] = ci.Path + switch item.Config.Type { + case model.ItemFile: + default: + return errs.Errorf("Can't exec item '%s' (%s)", + ci.Name, ci.Type.UserFriendlyString()) + } + } + + res := "n" + dialogResult := askUserInput( + fmt.Sprintf("Confirm exec files %v (y/n): ", + pathsToExec), "", false) + if dialogResult.dialogSubmitted { + res = dialogResult.inputAnswer + } + logging.LogDebugf("ui/execConfigFile(), confirm exec: '%s'", res) + if res == "y" { + for _, item := range itemsToExec { + showProgressDialogf("Running exec command '%s' file from DataPower...", item.Name) + err := repos[m.CurrSide()].ExecConfig(item.Config) + if err != nil { + hideProgressDialog() + return err + } + } + hideProgressDialog() + updateStatusf("Exec config files success: %v", pathsToExec) + } + + default: + logging.LogDebug("worker/execConfigFile(), To exec config file select config file in the DataPower view.") + return errs.Error("To exec config file select config file in the DataPower view.") + } + + return nil +} + // toggleObjectMode switches between (default) filestore mode, object mode and // status mode for the DataPower view. func toggleObjectMode(m *model.Model) error {