diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..2f38ceaaebb24237ab1dfc22532c44fd510fec1c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +.git +tools/ +doc/ +webui/node_modules/ \ No newline at end of file diff --git a/Rakefile b/Rakefile index 58b7fc524abba7283e942d3de43ad09941e46416..934fd077c309687f9f27acad92bfc504ded04197 100644 --- a/Rakefile +++ b/Rakefile @@ -7,7 +7,8 @@ SWAGGER_CODEGEN_VER = '2.4.12' GOSWAGGER_VER = 'v0.21.0' GOLANGCILINT_VER = '1.21.0' GO_VER = '1.13.5' -PROTOC_VER = '3.11.1' +PROTOC_VER = '3.11.2' +PROTOC_GEN_GO_VER = 'v1.3.3' # Check host OS UNAME=`uname -s` @@ -71,7 +72,7 @@ GO = "#{GO_DIR}/go/bin/go" GOLANGCILINT = "#{TOOLS_DIR}/golangci-lint-#{GOLANGCILINT_VER}-#{GOLANGCILINT_SUFFIX}/golangci-lint" PROTOC_DIR = "#{TOOLS_DIR}/#{PROTOC_VER}" PROTOC = "#{PROTOC_DIR}/bin/protoc" -PROTOC_GEN_GO = "#{GOBIN}/protoc-gen-go" +PROTOC_GEN_GO = "#{GOBIN}/protoc-gen-go-#{PROTOC_GEN_GO_VER}" MOCKERY = "#{GOBIN}/mockery" MOCKGEN = "#{GOBIN}/mockgen" RICHGO = "#{GOBIN}/richgo" @@ -155,7 +156,10 @@ file PROTOC do end file PROTOC_GEN_GO do - sh "#{GO} get -u #{PROTOC_GEN_GO_URL}" + sh "#{GO} get -d -u #{PROTOC_GEN_GO_URL}" + sh "git -C \"$(#{GO} env GOPATH)\"/src/github.com/golang/protobuf checkout #{PROTOC_GEN_GO_VER}" + sh "#{GO} install github.com/golang/protobuf/protoc-gen-go" + sh "cp #{GOBIN}/protoc-gen-go #{PROTOC_GEN_GO}" end file MOCKERY do @@ -225,7 +229,7 @@ task :run_server_db do |t, args| ENV['STORK_DATABASE_HOST'] = "localhost" ENV['STORK_DATABASE_PORT'] = "5678" at_exit { - sh "docker rm -f stork-app-pgsql" + sh "docker rm -f -v stork-app-pgsql" } sh 'docker run --name stork-app-pgsql -d -p 5678:5432 -e POSTGRES_DB=storkapp -e POSTGRES_USER=storkapp -e POSTGRES_PASSWORD=storkapp postgres:11 && sleep 5' Rake::Task["run_server"].invoke() @@ -312,7 +316,7 @@ task :unittest_backend => [GO, RICHGO, MOCKERY, MOCKGEN, :build_server, :build_a 'Password', 'loggingMiddleware', 'GlobalMiddleware', 'Authorizer', 'CreateSession', 'DeleteSession', 'Listen', 'Shutdown', 'NewRestUser', 'CreateUser', 'UpdateUser', 'SetupLogging', 'UTCNow', 'detectApps', - 'updateMachineFieldsKea', 'updateMachineFieldsBind9', 'prepareTLS'] + 'prepareTLS'] if cov < 35 and not ignore_list.include? func puts "FAIL: %-80s %5s%% < 35%%" % ["#{file} #{func}", "#{cov}"] problem = true @@ -327,7 +331,7 @@ end desc 'Run backend unit tests with local postgres docker container' task :unittest_backend_db do at_exit { - sh "docker rm -f stork-ut-pgsql" + sh "docker rm -f -v stork-ut-pgsql" } sh "docker run --name stork-ut-pgsql -d -p 5678:5432 -e POSTGRES_DB=storktest -e POSTGRES_USER=storktest -e POSTGRES_PASSWORD=storktest postgres:11" ENV['POSTGRES_ADDR'] = "localhost:5678" diff --git a/api/services-defs.yaml b/api/services-defs.yaml index 6e22341195d450773bb18c66bb57f44155d91b8e..138893d75e880e90e37aeec0a983247a58e5beae 100644 --- a/api/services-defs.yaml +++ b/api/services-defs.yaml @@ -128,6 +128,11 @@ type: string extendedVersion: type: string + uptime: + type: integer + reloadedAt: + type: string + format: date-time hooks: type: array items: diff --git a/backend/agent/agent.go b/backend/agent/agent.go index f7fee92bad292e5281b6bf2f0ca862be81b720b4..057fcd6ed72b601e40ee17bc6defef3fe043128c 100644 --- a/backend/agent/agent.go +++ b/backend/agent/agent.go @@ -37,7 +37,7 @@ type StorkAgent struct { func NewStorkAgent() *StorkAgent { caClient := NewCAClient() sa := &StorkAgent{ - AppMonitor: NewAppMonitor(caClient), + AppMonitor: NewAppMonitor(), CAClient: caClient, } return sa @@ -51,51 +51,12 @@ func (sa *StorkAgent) GetState(ctx context.Context, in *agentapi.GetStateReq) (* loadStr := fmt.Sprintf("%.2f %.2f %.2f", load.Load1, load.Load5, load.Load15) var apps []*agentapi.App - for _, srv := range sa.AppMonitor.GetApps() { - switch s := srv.(type) { - case AppKea: - var daemons []*agentapi.KeaDaemon - for _, d := range s.Daemons { - daemons = append(daemons, &agentapi.KeaDaemon{ - Pid: d.Pid, - Name: d.Name, - Active: d.Active, - Version: d.Version, - ExtendedVersion: d.ExtendedVersion, - }) - } - apps = append(apps, &agentapi.App{ - Version: s.Version, - CtrlAddress: s.CtrlAddress, - CtrlPort: s.CtrlPort, - Active: s.Active, - App: &agentapi.App_Kea{ - Kea: &agentapi.AppKea{ - ExtendedVersion: s.ExtendedVersion, - Daemons: daemons, - }, - }, - }) - case AppBind9: - var daemon = &agentapi.Bind9Daemon{ - Pid: s.Daemon.Pid, - Name: s.Daemon.Name, - Active: s.Daemon.Active, - Version: s.Daemon.Version, - } - apps = append(apps, &agentapi.App{ - Version: s.Version, - CtrlPort: s.CtrlPort, - Active: s.Active, - App: &agentapi.App_Bind9{ - Bind9: &agentapi.AppBind9{ - Daemon: daemon, - }, - }, - }) - default: - panic(fmt.Sprint("Unknown app type")) - } + for _, app := range sa.AppMonitor.GetApps() { + apps = append(apps, &agentapi.App{ + Type: app.Type, + CtrlAddress: app.CtrlAddress, + CtrlPort: app.CtrlPort, + }) } state := agentapi.GetStateRsp{ @@ -122,44 +83,85 @@ func (sa *StorkAgent) GetState(ctx context.Context, in *agentapi.GetStateReq) (* return &state, nil } -// Restart Kea app. -func (sa *StorkAgent) RestartKea(ctx context.Context, in *agentapi.RestartKeaReq) (*agentapi.RestartKeaRsp, error) { - log.Printf("Received: RestartKea %v", in) - return &agentapi.RestartKeaRsp{Xyz: "321"}, nil +func (sa *StorkAgent) GetBind9State(ctx context.Context, in *agentapi.GetBind9StateReq) (*agentapi.GetBind9StateRsp, error) { + app := &App{ + CtrlAddress: in.CtrlAddress, + CtrlPort: in.CtrlPort, + } + state, err := getBind9State(app) + + status := &agentapi.Status{ + Code: agentapi.Status_OK, // all ok + } + if err != nil { + status.Code = agentapi.Status_ERROR + status.Message = err.Error() + } + + rsp := &agentapi.GetBind9StateRsp{ + Status: status, + Version: state.Version, + Active: state.Active, + Daemon: &agentapi.Bind9Daemon{ + Pid: state.Daemon.Pid, + Name: state.Daemon.Name, + Active: state.Daemon.Active, + Version: state.Daemon.Version, + }, + } + return rsp, nil } -// Forwards Kea command sent by the Stork server to the appropriate Kea instance over +// Forwards one or more Kea commands sent by the Stork server to the appropriate Kea instance over // HTTP (via Control Agent). func (sa *StorkAgent) ForwardToKeaOverHTTP(ctx context.Context, in *agentapi.ForwardToKeaOverHTTPReq) (*agentapi.ForwardToKeaOverHTTPRsp, error) { reqURL := in.GetUrl() - payload := in.GetKeaRequest() - - rsp := &agentapi.ForwardToKeaOverHTTPRsp{} + requests := in.GetKeaRequests() - // Try to forward the command to Kea Control Agent. - keaRsp, err := sa.CAClient.Call(reqURL, bytes.NewBuffer([]byte(payload))) - if err != nil { - log.WithFields(log.Fields{ - "URL": reqURL, - }).Errorf("Failed to forward command to Kea: %+v", err) - return rsp, err + response := &agentapi.ForwardToKeaOverHTTPRsp{ + Status: &agentapi.Status{ + Code: agentapi.Status_OK, // all ok + }, } - defer keaRsp.Body.Close() - // Read the response body. - body, err := ioutil.ReadAll(keaRsp.Body) - if err != nil { - log.WithFields(log.Fields{ - "URL": reqURL, - }).Errorf("Failed to read the body of the Kea response to forwarded command: %+v", err) - return rsp, err - } + // forward requests to kea one by one + for _, req := range requests { + rsp := &agentapi.KeaResponse{ + Status: &agentapi.Status{}, + } + // Try to forward the command to Kea Control Agent. + keaRsp, err := sa.CAClient.Call(reqURL, bytes.NewBuffer([]byte(req.Request))) + if err != nil { + log.WithFields(log.Fields{ + "URL": reqURL, + }).Errorf("Failed to forward commands to Kea: %+v", err) + rsp.Status.Code = agentapi.Status_ERROR + rsp.Status.Message = "Failed to forward commands to Kea" + response.KeaResponses = append(response.KeaResponses, rsp) + continue + } + + // Read the response body. + body, err := ioutil.ReadAll(keaRsp.Body) + keaRsp.Body.Close() + if err != nil { + log.WithFields(log.Fields{ + "URL": reqURL, + }).Errorf("Failed to read the body of the Kea response to forwarded commands: %+v", err) + rsp.Status.Code = agentapi.Status_ERROR + rsp.Status.Message = "Failed to read the body of the Kea response" + response.KeaResponses = append(response.KeaResponses, rsp) + continue + } - // Everything looks good, so include the body in the response. - rsp.KeaResponse = string(body) + // Everything looks good, so include the body in the response. + rsp.Response = string(body) + rsp.Status.Code = agentapi.Status_OK + response.KeaResponses = append(response.KeaResponses, rsp) + } - return rsp, err + return response, nil } func (sa *StorkAgent) Serve() { diff --git a/backend/agent/agent_test.go b/backend/agent/agent_test.go index dcdc6a00bdb150b8edcc5bd0ab737a88242eba6d..94a306bf7bcc51b30b77a8192770122c54966616 100644 --- a/backend/agent/agent_test.go +++ b/backend/agent/agent_test.go @@ -12,7 +12,7 @@ import ( ) type FakeAppMonitor struct { - Apps []interface{} + Apps []*App } // Initializes StorkAgent instance and context used by the tests. @@ -29,7 +29,7 @@ func setupAgentTest() (*StorkAgent, context.Context) { return sa, ctx } -func (fsm *FakeAppMonitor) GetApps() []interface{} { +func (fsm *FakeAppMonitor) GetApps() []*App { return fsm.Apps } @@ -52,18 +52,16 @@ func TestGetState(t *testing.T) { require.Empty(t, rsp.Apps) // add some apps to app monitor so GetState should return something - var apps []interface{} - apps = append(apps, AppKea{ - AppCommon: AppCommon{ - Version: "1.2.3", - Active: true, - }, + var apps []*App + apps = append(apps, &App{ + Type: "kea", + CtrlAddress: "1.2.3.1", + CtrlPort: 1234, }) - apps = append(apps, AppBind9{ - AppCommon: AppCommon{ - Version: "9.16.0", - Active: false, - }, + apps = append(apps, &App{ + Type: "bind9", + CtrlAddress: "2.3.4.4", + CtrlPort: 2345, }) fsm, _ := sa.AppMonitor.(*FakeAppMonitor) fsm.Apps = apps @@ -75,10 +73,10 @@ func TestGetState(t *testing.T) { keaApp := rsp.Apps[0] bind9App := rsp.Apps[1] - require.Equal(t, "1.2.3", keaApp.Version) - require.Equal(t, true, keaApp.Active) - require.Equal(t, "9.16.0", bind9App.Version) - require.False(t, bind9App.Active) + require.Equal(t, "1.2.3.1", keaApp.CtrlAddress) + require.Equal(t, int64(1234), keaApp.CtrlPort) + require.Equal(t, "2.3.4.4", bind9App.CtrlAddress) + require.Equal(t, int64(2345), bind9App.CtrlPort) } // Test forwarding command to Kea when HTTP 200 status code @@ -98,8 +96,8 @@ func TestForwardToKeaOverHTTPSuccess(t *testing.T) { // Forward the request with the expected body. req := &agentapi.ForwardToKeaOverHTTPReq{ - Url: "http://localhost:45634/", - KeaRequest: "{ \"command\": \"list-commands\"}", + Url: "http://localhost:45634/", + KeaRequests: []*agentapi.KeaRequest{{Request: "{ \"command\": \"list-commands\"}"}}, } // Kea should respond with non-empty body and the status code 200. @@ -108,7 +106,8 @@ func TestForwardToKeaOverHTTPSuccess(t *testing.T) { rsp, err := sa.ForwardToKeaOverHTTP(ctx, req) require.NotNil(t, rsp) require.NoError(t, err) - require.JSONEq(t, "[{\"result\":0}]", rsp.KeaResponse) + require.Len(t, rsp.KeaResponses, 1) + require.JSONEq(t, "[{\"result\":0}]", rsp.KeaResponses[0].Response) } // Test forwarding command to Kea when HTTP 400 (Bad Request) status @@ -124,8 +123,8 @@ func TestForwardToKeaOverHTTPBadRequest(t *testing.T) { JSON([]map[string]string{{"HttpCode": "Bad Request"}}) req := &agentapi.ForwardToKeaOverHTTPReq{ - Url: "http://localhost:45634/", - KeaRequest: "{ \"command\": \"list-commands\"}", + Url: "http://localhost:45634/", + KeaRequests: []*agentapi.KeaRequest{{Request: "{ \"command\": \"list-commands\"}"}}, } // The response to the forwarded command should contain HTTP @@ -134,7 +133,8 @@ func TestForwardToKeaOverHTTPBadRequest(t *testing.T) { rsp, err := sa.ForwardToKeaOverHTTP(ctx, req) require.NotNil(t, rsp) require.NoError(t, err) - require.JSONEq(t, "[{\"HttpCode\":\"Bad Request\"}]", rsp.KeaResponse) + require.Len(t, rsp.KeaResponses, 1) + require.JSONEq(t, "[{\"HttpCode\":\"Bad Request\"}]", rsp.KeaResponses[0].Response) } // Test forwarding command to Kea when no body is returned. @@ -148,8 +148,8 @@ func TestForwardToKeaOverHTTPEmptyBody(t *testing.T) { Reply(200) req := &agentapi.ForwardToKeaOverHTTPReq{ - Url: "http://localhost:45634/", - KeaRequest: "{ \"command\": \"list-commands\"}", + Url: "http://localhost:45634/", + KeaRequests: []*agentapi.KeaRequest{{Request: "{ \"command\": \"list-commands\"}"}}, } // Forward the command to Kea. The response contains no body, but @@ -159,7 +159,8 @@ func TestForwardToKeaOverHTTPEmptyBody(t *testing.T) { rsp, err := sa.ForwardToKeaOverHTTP(ctx, req) require.NotNil(t, rsp) require.NoError(t, err) - require.Equal(t, 0, len(rsp.KeaResponse)) + require.Len(t, rsp.KeaResponses, 1) + require.Equal(t, 0, len(rsp.KeaResponses[0].Response)) } // Test forwarding command when Kea is unavailable. @@ -167,14 +168,31 @@ func TestForwardToKeaOverHTTPNoKea(t *testing.T) { sa, ctx := setupAgentTest() req := &agentapi.ForwardToKeaOverHTTPReq{ - Url: "http://localhost:45634/", - KeaRequest: "{ \"command\": \"list-commands\"}", + Url: "http://localhost:45634/", + KeaRequests: []*agentapi.KeaRequest{{Request: "{ \"command\": \"list-commands\"}"}}, } // Kea is unreachable, so we'll have to signal an error to the sender. // The response should be empty. rsp, err := sa.ForwardToKeaOverHTTP(ctx, req) require.NotNil(t, rsp) - require.Error(t, err) - require.Equal(t, 0, len(rsp.KeaResponse)) + require.NoError(t, err) + require.Len(t, rsp.KeaResponses, 1) + require.NotEqual(t, 0, rsp.KeaResponses[0].Status.Code) + require.Equal(t, 0, len(rsp.KeaResponses[0].Response)) +} + +func TestGetBind9StateError(t *testing.T) { + sa, ctx := setupAgentTest() + + req := &agentapi.GetBind9StateReq{ + CtrlAddress: "127.0.0.1", + CtrlPort: 1234, + } + + rsp, err := sa.GetBind9State(ctx, req) + require.NotNil(t, rsp) + require.NoError(t, err) + require.NotEqual(t, int32(0), rsp.Status.Code) + require.NotEmpty(t, rsp.Status.Message) } diff --git a/backend/agent/bind9.go b/backend/agent/bind9.go new file mode 100644 index 0000000000000000000000000000000000000000..4cdaaa3d28afa9b1ba9706f4b071639175e6c22b --- /dev/null +++ b/backend/agent/bind9.go @@ -0,0 +1,66 @@ +package agent + +import ( + "os/exec" + "regexp" + + log "github.com/sirupsen/logrus" +) + +type Bind9Daemon struct { + Pid int32 + Name string + Active bool + Version string +} + +type Bind9State struct { + Version string + Active bool + Daemon Bind9Daemon +} + +func detectBind9App() *App { + // TODO: address, control port + + bind9App := &App{ + Type: "bind9", + } + + return bind9App +} + +func getBind9State(app *App) (*Bind9State, error) { //nolint:unparam + state := &Bind9State{ + Active: false, + } + + // version + cmd := exec.Command("rndc", "-k", "/etc/bind/rndc.key", "status") + out, err := cmd.Output() + if err != nil { + log.Warnf("cannot get BIND 9 status: %+v", err) + } else { + versionPtrn := regexp.MustCompile(`version:\s(.+)\n`) + match := versionPtrn.FindStringSubmatch(string(out)) + if match != nil { + state.Version = match[1] + } else { + log.Warnf("cannot get BIND 9 version: unable to find version in rndc output") + } + + state.Active = true + } + + // TODO: pid + + namedDaemon := Bind9Daemon{ + Name: "named", + Active: state.Active, + Version: state.Version, + } + + state.Daemon = namedDaemon + + return state, err +} diff --git a/backend/agent/caclient.go b/backend/agent/caclient.go index 681e11b6872ba463521e7c5752efc75ad36168af..bcb2e275095cf7a2f442e8a9ba515891068c07e1 100644 --- a/backend/agent/caclient.go +++ b/backend/agent/caclient.go @@ -4,6 +4,8 @@ import ( "bytes" "crypto/tls" "net/http" + + "github.com/pkg/errors" ) type CAClient struct { @@ -32,5 +34,5 @@ func NewCAClient() *CAClient { func (c *CAClient) Call(caURL string, payload *bytes.Buffer) (*http.Response, error) { caRsp, err := c.client.Post(caURL, "application/json", payload) - return caRsp, err + return caRsp, errors.Wrapf(err, "problem with sending POST to Kea Control Agent %s", caURL) } diff --git a/backend/agent/kea.go b/backend/agent/kea.go new file mode 100644 index 0000000000000000000000000000000000000000..1fa21d50e095fb1cffd5778578005dab8189d4d1 --- /dev/null +++ b/backend/agent/kea.go @@ -0,0 +1,63 @@ +package agent + +import ( + "io/ioutil" + "regexp" + "strconv" + + log "github.com/sirupsen/logrus" +) + +func getCtrlAddressFromKeaConfig(path string) (string, int64) { + text, err := ioutil.ReadFile(path) + if err != nil { + log.Warnf("cannot read kea config file: %+v", err) + return "", 0 + } + + ptrn := regexp.MustCompile(`"http-port"\s*:\s*([0-9]+)`) + m := ptrn.FindStringSubmatch(string(text)) + if len(m) == 0 { + log.Warnf("cannot parse http-port: %+v", err) + return "", 0 + } + + port, err := strconv.Atoi(m[1]) + if err != nil { + log.Warnf("cannot parse http-port: %+v", err) + return "", 0 + } + + ptrn = regexp.MustCompile(`"http-host"\s*:\s*\"(\S+)\"\s*,`) + m = ptrn.FindStringSubmatch(string(text)) + address := "localhost" + if len(m) == 0 { + log.Warnf("cannot parse http-host: %+v", err) + } else { + address = m[1] + if address == "0.0.0.0" { + address = "127.0.0.1" + } else if address == "::" { + address = "::1" + } + } + + return address, int64(port) +} + +func detectKeaApp(match []string) *App { + keaConfPath := match[1] + + ctrlAddress, ctrlPort := getCtrlAddressFromKeaConfig(keaConfPath) + if ctrlPort == 0 || len(ctrlAddress) == 0 { + return nil + } + + keaApp := &App{ + Type: "kea", + CtrlAddress: ctrlAddress, + CtrlPort: ctrlPort, + } + + return keaApp +} diff --git a/backend/agent/monitor.go b/backend/agent/monitor.go index 701b07155ea15e7c99f3acfe603af80199c82acb..9b88c8c59a483ebdcffe14fcfefd56bbb6ccb9d0 100644 --- a/backend/agent/monitor.go +++ b/backend/agent/monitor.go @@ -1,72 +1,35 @@ package agent import ( - "bytes" - "encoding/json" - "io/ioutil" - "os/exec" "regexp" - "strconv" "time" - "github.com/pkg/errors" "github.com/shirou/gopsutil/process" log "github.com/sirupsen/logrus" - - storkutil "isc.org/stork/util" ) -type KeaDaemon struct { - Pid int32 - Name string - Active bool - Version string - ExtendedVersion string -} - -type Bind9Daemon struct { - Pid int32 - Name string - Active bool - Version string -} - -type AppCommon struct { - Version string +type App struct { + Type string // currently supported types are: "kea" and "bind9" CtrlAddress string CtrlPort int64 - Active bool -} - -type AppKea struct { - AppCommon - ExtendedVersion string - Daemons []KeaDaemon -} - -type AppBind9 struct { - AppCommon - Daemon Bind9Daemon } type AppMonitor interface { - GetApps() []interface{} + GetApps() []*App Shutdown() } type appMonitor struct { - requests chan chan []interface{} // input to app monitor, ie. channel for receiving requests - quit chan bool // channel for stopping app monitor + requests chan chan []*App // input to app monitor, ie. channel for receiving requests + quit chan bool // channel for stopping app monitor - apps []interface{} // list of detected apps on the host - CAClient *CAClient + apps []*App // list of detected apps on the host } -func NewAppMonitor(caClient *CAClient) AppMonitor { +func NewAppMonitor() AppMonitor { sm := &appMonitor{ - requests: make(chan chan []interface{}), + requests: make(chan chan []*App), quit: make(chan bool), - CAClient: caClient, } go sm.run() return sm @@ -92,236 +55,17 @@ func (sm *appMonitor) run() { } } -func getCtrlAddressFromKeaConfig(path string) (string, int64) { - text, err := ioutil.ReadFile(path) - if err != nil { - log.Warnf("cannot read kea config file: %+v", err) - return "", 0 - } - - ptrn := regexp.MustCompile(`"http-port"\s*:\s*([0-9]+)`) - m := ptrn.FindStringSubmatch(string(text)) - if len(m) == 0 { - log.Warnf("cannot parse http-port: %+v", err) - return "", 0 - } - - port, err := strconv.Atoi(m[1]) - if err != nil { - log.Warnf("cannot parse http-port: %+v", err) - return "", 0 - } - - ptrn = regexp.MustCompile(`"http-host"\s*:\s*\"(\S+)\"\s*,`) - m = ptrn.FindStringSubmatch(string(text)) - address := "localhost" - if len(m) == 0 { - log.Warnf("cannot parse http-host: %+v", err) - } else { - address = m[1] - if address == "0.0.0.0" { - address = "127.0.0.1" - } - } - - return address, int64(port) -} - -func keaDaemonVersionGet(caClient *CAClient, caURL string, daemon string) (map[string]interface{}, error) { - var jsonCmd = []byte(`{"command": "version-get"}`) - if daemon != "" { - jsonCmd = []byte(`{"command": "version-get", "service": ["` + daemon + `"]}`) - } - - resp, err := caClient.Call(caURL, bytes.NewBuffer(jsonCmd)) - if err != nil { - return nil, err - } - defer resp.Body.Close() - body, _ := ioutil.ReadAll(resp.Body) - - var data interface{} - err = json.Unmarshal(body, &data) - if err != nil { - return nil, err - } - - data1, ok := data.([]interface{}) - if !ok || len(data1) == 0 { - return nil, errors.New("bad data") - } - data2, ok := data1[0].(map[string]interface{}) - if !ok { - return nil, errors.New("bad data") - } - return data2, nil -} - -func detectBind9App() (bind9App *AppBind9) { - bind9App = &AppBind9{ - AppCommon: AppCommon{ - Active: false, - }, - } - - // version - cmd := exec.Command("rndc", "-k", "/etc/bind/rndc.key", "status") - out, err := cmd.Output() - if err != nil { - log.Warnf("cannot get BIND 9 status: %+v", err) - } else { - versionPtrn := regexp.MustCompile(`version:\s(.+)\n`) - match := versionPtrn.FindStringSubmatch(string(out)) - if match != nil { - bind9App.Version = match[1] - } else { - log.Warnf("cannot get BIND 9 version: unable to find version in rndc output") - } - - bind9App.Active = true - } - - // TODO: control port, pid - - namedDaemon := Bind9Daemon{ - Name: "named", - Active: bind9App.Active, - Version: bind9App.Version, - } - - bind9App.Daemon = namedDaemon - - return bind9App -} - -func detectKeaApp(caClient *CAClient, match []string) *AppKea { - var keaApp *AppKea - - keaConfPath := match[1] - - ctrlAddress, ctrlPort := getCtrlAddressFromKeaConfig(keaConfPath) - keaApp = &AppKea{ - AppCommon: AppCommon{ - Active: false, - CtrlAddress: ctrlAddress, - CtrlPort: ctrlPort, - }, - Daemons: []KeaDaemon{}, - } - if ctrlPort == 0 || len(ctrlAddress) == 0 { - return nil - } - - caURL := storkutil.HostWithPortURL(ctrlAddress, ctrlPort) - - // retrieve ctrl-agent information, it is also used as a general app information - info, err := keaDaemonVersionGet(caClient, caURL, "") - if err == nil { - if int(info["result"].(float64)) == 0 { - keaApp.Active = true - keaApp.Version = info["text"].(string) - info2 := info["arguments"].(map[string]interface{}) - keaApp.ExtendedVersion = info2["extended"].(string) - } else { - log.Warnf("ctrl-agent returned negative response: %+v", info) - } - } else { - log.Warnf("cannot get daemon version: %+v", err) - } - - // add info about ctrl-agent daemon - caDaemon := KeaDaemon{ - Name: "ca", - Active: keaApp.Active, - Version: keaApp.Version, - ExtendedVersion: keaApp.ExtendedVersion, - } - keaApp.Daemons = append(keaApp.Daemons, caDaemon) - - // get list of daemons configured in ctrl-agent - var jsonCmd = []byte(`{"command": "config-get"}`) - resp, err := caClient.Call(caURL, bytes.NewBuffer(jsonCmd)) - if err != nil { - log.Warnf("problem with request to kea-ctrl-agent: %+v", err) - return nil - } - defer resp.Body.Close() - body, _ := ioutil.ReadAll(resp.Body) - var data interface{} - err = json.Unmarshal(body, &data) - if err != nil { - log.Warnf("cannot parse response from kea-ctrl-agent: %+v", err) - return nil - } - - // unpack the data in the JSON structure until we reach the daemons list. - m, ok := data.([]interface{}) - if !ok || len(m) == 0 { - return nil - } - m2, ok := m[0].(map[string]interface{}) - if !ok { - return nil - } - m3, ok := m2["arguments"].(map[string]interface{}) - if !ok { - return nil - } - m4, ok := m3["Control-agent"].(map[string]interface{}) - if !ok { - return nil - } - daemonsListInCA, ok := m4["control-sockets"].(map[string]interface{}) - if !ok { - return nil - } - for daemonName := range daemonsListInCA { - daemon := KeaDaemon{ - Name: daemonName, - Active: false, - } - - // retrieve info about daemon - info, err := keaDaemonVersionGet(caClient, caURL, daemonName) - if err == nil { - if int(info["result"].(float64)) == 0 { - daemon.Active = true - daemon.Version = info["text"].(string) - info2 := info["arguments"].(map[string]interface{}) - daemon.ExtendedVersion = info2["extended"].(string) - } else { - log.Warnf("ctrl-agent returned negative response: %+v", info) - } - } else { - log.Warnf("cannot get daemon version: %+v", err) - } - // if any daemon is inactive, then whole kea app is treated as inactive - if !daemon.Active { - keaApp.Active = false - } - - // if any daemon is inactive, then whole kea app is treated as inactive - if !daemon.Active { - keaApp.Active = false - } - - keaApp.Daemons = append(keaApp.Daemons, daemon) - } - - return keaApp -} - func (sm *appMonitor) detectApps() { // Kea app is being detected by browsing list of processes in the systam // where cmdline of the process contains given pattern with kea-ctrl-agent // substring. Such found processes are being processed further and all other // Kea daemons are discovered and queried for their versions, etc. keaPtrn := regexp.MustCompile(`kea-ctrl-agent.*-c\s+(\S+)`) - // Bind9 app is being detecting by browsing list of processes in the system + // BIND 9 app is being detecting by browsing list of processes in the system // where cmdline of the process contains given pattern with named substring. bind9Ptrn := regexp.MustCompile(`named.*-c\s+(\S+)`) - var apps []interface{} + var apps []*App procs, _ := process.Processes() for _, p := range procs { @@ -335,9 +79,9 @@ func (sm *appMonitor) detectApps() { // detect kea m := keaPtrn.FindStringSubmatch(cmdline) if m != nil { - keaApp := detectKeaApp(sm.CAClient, m) + keaApp := detectKeaApp(m) if keaApp != nil { - apps = append(apps, *keaApp) + apps = append(apps, keaApp) } } continue @@ -354,7 +98,7 @@ func (sm *appMonitor) detectApps() { if m != nil { bind9App := detectBind9App() if bind9App != nil { - apps = append(apps, *bind9App) + apps = append(apps, bind9App) } } continue @@ -364,8 +108,8 @@ func (sm *appMonitor) detectApps() { sm.apps = apps } -func (sm *appMonitor) GetApps() []interface{} { - ret := make(chan []interface{}) +func (sm *appMonitor) GetApps() []*App { + ret := make(chan []*App) sm.requests <- ret srvs := <-ret return srvs diff --git a/backend/agent/monitor_test.go b/backend/agent/monitor_test.go index 580ea06414cc0efca4393e23f6da413a807a7843..0611a13c7064cdcf29940d8371e41588a4d5f7a6 100644 --- a/backend/agent/monitor_test.go +++ b/backend/agent/monitor_test.go @@ -7,43 +7,13 @@ import ( "testing" "github.com/stretchr/testify/require" - "gopkg.in/h2non/gock.v1" ) func TestGetApps(t *testing.T) { - // Forces gock to intercept the HTTP/1.1 client. Otherwise it would - // use the HTTP/2. - caClient := NewCAClient() - gock.InterceptClient(caClient.client) - sm := NewAppMonitor(caClient) - - apps := sm.GetApps() + am := NewAppMonitor() + apps := am.GetApps() require.Len(t, apps, 0) - sm.Shutdown() -} - -func TestKeaDaemonVersionGetBadUrl(t *testing.T) { - caClient := NewCAClient() - gock.InterceptClient(caClient.client) - _, err := keaDaemonVersionGet(caClient, "aaa", "") - require.Contains(t, err.Error(), "unsupported protocol ") -} - -func TestKeaDaemonVersionGetDataOk(t *testing.T) { - defer gock.Off() - - gock.New("http://localhost:45634"). - Post("/"). - Reply(200). - JSON([]map[string]string{{"arguments": "bar"}}) - - caClient := NewCAClient() - gock.InterceptClient(caClient.client) - - data, err := keaDaemonVersionGet(caClient, "http://localhost:45634/", "") - require.NoError(t, err) - require.Equal(t, true, gock.IsDone()) - require.Equal(t, map[string]interface{}{"arguments": "bar"}, data) + am.Shutdown() } func TestGetCtrlAddressFromKeaConfigNonExisting(t *testing.T) { @@ -123,23 +93,43 @@ func TestGetCtrlAddressFromKeaConfigAddress0000(t *testing.T) { require.Equal(t, "127.0.0.1", address) } +func TestGetCtrlAddressFromKeaConfigAddressColons(t *testing.T) { + // prepare kea conf file + tmpFile, err := ioutil.TempFile(os.TempDir(), "prefix-") + if err != nil { + log.Fatal("Cannot create temporary file", err) + } + defer os.Remove(tmpFile.Name()) + + text := []byte(string("\"http-host\": \"::\", \"http-port\": 1234")) + if _, err = tmpFile.Write(text); err != nil { + log.Fatal("Failed to write to temporary file", err) + } + if err := tmpFile.Close(); err != nil { + log.Fatal(err) + } + + // check reading from proper file; + // if CA is listening on :: then ::1 should be returned + // as it is not possible to connect to :: + address, port := getCtrlAddressFromKeaConfig(tmpFile.Name()) + require.Equal(t, int64(1234), port) + require.Equal(t, "::1", address) +} + func TestDetectApps(t *testing.T) { - caClient := NewCAClient() - gock.InterceptClient(caClient.client) - sm := NewAppMonitor(caClient) - sm.(*appMonitor).detectApps() - sm.Shutdown() + am := NewAppMonitor() + am.(*appMonitor).detectApps() + am.Shutdown() } func TestDetectBind9App(t *testing.T) { - // check bind9 app detection - srv := detectBind9App() - require.NotNil(t, srv) - require.Empty(t, srv.Version) - require.False(t, srv.Active) - require.Equal(t, "named", srv.Daemon.Name) - require.Empty(t, srv.Daemon.Version) - require.False(t, srv.Daemon.Active) + // check BIND 9 app detection + app := detectBind9App() + require.NotNil(t, app) + require.Equal(t, "bind9", app.Type) + require.Equal(t, "", app.CtrlAddress) + require.Equal(t, int64(0), app.CtrlPort) } func TestDetectKeaApp(t *testing.T) { @@ -158,29 +148,10 @@ func TestDetectKeaApp(t *testing.T) { log.Fatal(err) } - // prepare response for ctrl-agent - defer gock.Off() - // first request to the kea ctrl-agent - gock.New("http://localhost:45634"). - Post("/"). - Reply(200). - JSON([]map[string]interface{}{{ - "arguments": map[string]interface{}{"extended": "bla bla"}, - "result": 0, "text": "1.2.3", - }}) - // - second request to kea daemon - gock.New("http://localhost:45634"). - Post("/"). - Reply(200). - JSON([]map[string]interface{}{{ - "arguments": map[string]interface{}{"extended": "bla bla"}, - "result": 0, "text": "1.2.3", - }}) - - caClient := NewCAClient() - gock.InterceptClient(caClient.client) - // check kea app detection - srv := detectKeaApp(caClient, []string{"", tmpFile.Name()}) - require.Nil(t, srv) + app := detectKeaApp([]string{"", tmpFile.Name()}) + require.NotNil(t, app) + require.Equal(t, "kea", app.Type) + require.Equal(t, "localhost", app.CtrlAddress) + require.Equal(t, int64(45634), app.CtrlPort) } diff --git a/backend/api/agent.proto b/backend/api/agent.proto index d50e8f21b37ceae0efe8c215c3c32c5b6084d3f9..2f04b783df56015b5fb2df80077a7fac474f3939 100644 --- a/backend/api/agent.proto +++ b/backend/api/agent.proto @@ -2,15 +2,39 @@ syntax = "proto3"; package agentapi; +// This package defines API exposed by Stork Agent to Stork Server. + service Agent { - rpc getState(GetStateReq) returns (GetStateRsp) {} - rpc restartKea(RestartKeaReq) returns (RestartKeaRsp) {} + // Get state of machine where agent is running. It gathers information about operations system, + // its version, CPU and available memory and runtime information like memory usage. + rpc GetState(GetStateReq) returns (GetStateRsp) {} + + // Get state of BIND 9 application. It returns information about named version. + rpc GetBind9State(GetBind9StateReq) returns (GetBind9StateRsp) {} + + // Forward commands (one or more) to Kea Control Agent and return results. rpc ForwardToKeaOverHTTP(ForwardToKeaOverHTTPReq) returns (ForwardToKeaOverHTTPRsp) {} } + +message Status { + enum StatusCode { + OK = 0; + ERROR = 1; + } + + // A simple error code that can be easily handled by the client. + StatusCode code = 1; + + // An error message in English. + string message = 2; +} + + message GetStateReq { } +// State of machine and its system message GetStateRsp { string agentVersion = 1; repeated App apps = 2; @@ -32,54 +56,62 @@ message GetStateRsp { string hostID = 18; } +// Basic information about application. message App { - string version = 1; + string type = 1; // currently supported types are: "kea" and "bind9" string ctrlAddress = 2; int64 ctrlPort = 3; - bool active = 4; - oneof app { - AppKea kea = 5; - AppBind9 bind9 = 6; - } } -message AppKea { - string extendedVersion = 1; - repeated KeaDaemon daemons = 2; +message GetBind9StateReq { + string ctrlAddress = 1; + int64 ctrlPort = 2; } -message KeaDaemon { +// Detailed description of BIND 9 daemon. +message Bind9Daemon { int32 pid = 1; string name = 2; bool active = 3; string version = 4; - string extendedVersion = 5; } -message Bind9Daemon { - int32 pid = 1; - string name = 2; +// Detailed description of BIND 9 application. +message GetBind9StateRsp { + // Status of call execution. + Status status = 1; + string version = 2; bool active = 3; - string version = 4; + Bind9Daemon daemon = 4; } -message AppBind9 { - Bind9Daemon daemon = 1; +// Request to Kea CA. +message KeaRequest { + // Request to Kea CA, JSON encoded as string. + string request = 1; } -message RestartKeaReq { - string xyz = 1; -} +message ForwardToKeaOverHTTPReq { + // URL to Kea CA + string url = 1; -message RestartKeaRsp { - string xyz = 1; + // List of requests to CA. + repeated KeaRequest keaRequests = 2; } -message ForwardToKeaOverHTTPReq { - string url = 1; - string keaRequest = 2; +// Response from Kea CA. +message KeaResponse { + // Response from CA, JSON encoded as string. + string response = 1; + + // Status of request execution. + Status status = 2; } message ForwardToKeaOverHTTPRsp { - string keaResponse = 1; + // Status of call execution. + Status status = 1; + + // List of responses from CA. + repeated KeaResponse keaResponses = 2; } diff --git a/backend/go.mod b/backend/go.mod index b3ed258ab3ea2ec21c337268f585603823d75ceb..b19c14f72f0196a9fb71a4b93a52a3c1d45b917b 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -27,6 +27,6 @@ require ( github.com/stretchr/testify v1.4.0 golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 golang.org/x/net v0.0.0-20190923162816-aa69164e4478 - google.golang.org/grpc v1.24.0 + google.golang.org/grpc v1.27.0 gopkg.in/h2non/gock.v1 v1.0.15 ) diff --git a/backend/go.sum b/backend/go.sum index 4e9efee2f1742d1705edab1717cb289f70948a3b..1989d35a29a16f8d4fd7ea8ead8730618b81745b 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -15,6 +15,7 @@ github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4 github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/brianvoe/gofakeit v3.18.0+incompatible h1:wDOmHc9DLG4nRjUVVaxA+CEglKOW72Y5+4WNxUIkjM8= github.com/brianvoe/gofakeit v3.18.0+incompatible/go.mod h1:kfwdRA90vvNhPutZWfH7WPaDzUjz+CZFqG+rPkOjGOc= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/codemodus/kace v0.5.1 h1:4OCsBlE2c/rSJo375ggfnucv9eRzge/U5LrrOZd47HA= github.com/codemodus/kace v0.5.1/go.mod h1:coddaHoX1ku1YFSe4Ip0mL9kQjJvKkzb9CfIdG1YR04= @@ -24,6 +25,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= @@ -152,6 +155,7 @@ github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/shirou/gopsutil v2.19.9+incompatible h1:IrPVlK4nfwW10DF7pW+7YJKws9NkgNzWozwwWv9FsgY= github.com/shirou/gopsutil v2.19.9+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= @@ -186,9 +190,15 @@ golang.org/x/crypto v0.0.0-20191029031824-8986dd9e96cf/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g= golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -200,7 +210,9 @@ golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7 golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -215,6 +227,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= @@ -222,10 +236,17 @@ golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20190617190820-da514acc4774 h1:CQVOmarCBFzTx0kbOU0ru54Cvot8SdSrNYjZPhQl+gk= golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.24.0 h1:vb/1TCsVn3DcJlQ0Gs1yB1pKI6Do2/QNwxdKqmc/b0s= google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= +google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -241,6 +262,7 @@ gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= mellium.im/sasl v0.2.1 h1:nspKSRg7/SyO0cRGY71OkfHab8tf9kCts6a6oTDut0w= mellium.im/sasl v0.2.1/go.mod h1:ROaEDLQNuf9vjKqE1SrAfnsobm2YKXT1gnN1uDp1PjQ= diff --git a/backend/server/agentcomm/agentcomm.go b/backend/server/agentcomm/agentcomm.go index c1572228fd63d1442bf693700bfe92d38cb596d3..9f953d2a39ccc787e1a80f7324d85e38a31cb1ec 100644 --- a/backend/server/agentcomm/agentcomm.go +++ b/backend/server/agentcomm/agentcomm.go @@ -50,7 +50,8 @@ type ConnectedAgents interface { Shutdown() GetConnectedAgent(address string) (*Agent, error) GetState(ctx context.Context, address string, agentPort int64) (*State, error) - ForwardToKeaOverHTTP(ctx context.Context, caURL string, agentAddress string, agentPort int64, command *KeaCommand, response interface{}) error + GetBind9State(ctx context.Context, agentAddress string, agentPort int64) (*Bind9State, error) + ForwardToKeaOverHTTP(ctx context.Context, agentAddress string, agentPort int64, caURL string, commands []*KeaCommand, cmdResponses ...interface{}) (*KeaCmdsResult, error) } // Agents management map. It tracks Agents currently connected to the Server. diff --git a/backend/server/agentcomm/grpcli.go b/backend/server/agentcomm/grpcli.go index 4a6c30732ff791766ac735782cc41d4217ef576f..43eb55b73a24794be60d586e754e0365db72ceb0 100644 --- a/backend/server/agentcomm/grpcli.go +++ b/backend/server/agentcomm/grpcli.go @@ -13,37 +13,10 @@ import ( storkutil "isc.org/stork/util" ) -type Bind9Daemon struct { - Pid int32 - Name string - Active bool - Version string -} - -type KeaDaemon struct { - Pid int32 - Name string - Active bool - Version string - ExtendedVersion string -} - -type AppCommon struct { - Version string +type App struct { + Type string // currently supported types are: "kea" and "bind9" CtrlAddress string CtrlPort int64 - Active bool -} - -type AppKea struct { - AppCommon - ExtendedVersion string - Daemons []KeaDaemon -} - -type AppBind9 struct { - AppCommon - Daemon Bind9Daemon } // State of the machine. It describes multiple properties of the machine like number of CPUs @@ -68,7 +41,7 @@ type State struct { HostID string LastVisited time.Time Error string - Apps []interface{} + Apps []*App } // Get version from agent. @@ -95,49 +68,13 @@ func (agents *connectedAgentsData) GetState(ctx context.Context, address string, } } - var apps []interface{} - for _, srv := range grpcState.Apps { - switch s := srv.App.(type) { - case *agentapi.App_Kea: - log.Printf("s.Kea.Daemons %+v", s.Kea.Daemons) - var daemons []KeaDaemon - for _, d := range s.Kea.Daemons { - daemons = append(daemons, KeaDaemon{ - Pid: d.Pid, - Name: d.Name, - Active: d.Active, - Version: d.Version, - ExtendedVersion: d.ExtendedVersion, - }) - } - apps = append(apps, &AppKea{ - AppCommon: AppCommon{ - Version: srv.Version, - CtrlAddress: srv.CtrlAddress, - CtrlPort: srv.CtrlPort, - Active: srv.Active, - }, - ExtendedVersion: s.Kea.ExtendedVersion, - Daemons: daemons, - }) - case *agentapi.App_Bind9: - var daemon = Bind9Daemon{ - Pid: s.Bind9.Daemon.Pid, - Name: s.Bind9.Daemon.Name, - Active: s.Bind9.Daemon.Active, - Version: s.Bind9.Daemon.Version, - } - apps = append(apps, &AppBind9{ - AppCommon: AppCommon{ - Version: srv.Version, - CtrlPort: srv.CtrlPort, - Active: srv.Active, - }, - Daemon: daemon, - }) - default: - log.Println("unsupported app type") - } + var apps []*App + for _, app := range grpcState.Apps { + apps = append(apps, &App{ + Type: app.Type, + CtrlAddress: app.CtrlAddress, + CtrlPort: app.CtrlPort, + }) } state := State{ @@ -166,39 +103,115 @@ func (agents *connectedAgentsData) GetState(ctx context.Context, address string, return &state, nil } -// Forwards a Kea command via the Stork Agent and Kea Control Agent and then -// parses the response. caURL is URL to Kea Control Agent. -func (agents *connectedAgentsData) ForwardToKeaOverHTTP(ctx context.Context, caURL string, agentAddress string, agentPort int64, command *KeaCommand, response interface{}) error { +type Bind9Daemon struct { + Pid int32 + Name string + Active bool + Version string +} + +type Bind9State struct { + Version string + Active bool + Daemon Bind9Daemon +} + +func (agents *connectedAgentsData) GetBind9State(ctx context.Context, agentAddress string, agentPort int64) (*Bind9State, error) { // Find the agent by address and port. addrPort := net.JoinHostPort(agentAddress, strconv.FormatInt(agentPort, 10)) agent, err := agents.GetConnectedAgent(addrPort) if err != nil { err = errors.Wrapf(err, "there is no agent available at address %s:%d", agentAddress, agentPort) - return err + return nil, err } - // Prepare the on-wire representation of the command. - c := command.Marshal() + req := &agentapi.GetBind9StateReq{ + CtrlAddress: agentAddress, + CtrlPort: agentPort, + } - req := &agentapi.ForwardToKeaOverHTTPReq{ - Url: caURL, - KeaRequest: c, + // call agent to get BIND 9 state + rsp, err := agent.Client.GetBind9State(ctx, req) + if err != nil { + err = errors.Wrapf(err, "failed to get BIND 9 state: %+v", req) + return nil, err + } + if rsp.Status.Code != agentapi.Status_OK { + err = errors.New(rsp.Status.Message) + return nil, err + } + + state := &Bind9State{ + Version: rsp.Version, + Active: rsp.Active, + Daemon: Bind9Daemon{ + Pid: rsp.Daemon.Pid, + Name: rsp.Daemon.Name, + Active: rsp.Daemon.Active, + Version: rsp.Daemon.Version, + }, } - // Send the command to the Stork agent. - rsp, err := agent.Client.ForwardToKeaOverHTTP(ctx, req) + return state, nil +} + +type KeaCmdsResult struct { + Error error + CmdsErrors []error +} + +// Forwards a Kea command via the Stork Agent and Kea Control Agent and then +// parses the response. caURL is URL to Kea Control Agent. +func (agents *connectedAgentsData) ForwardToKeaOverHTTP(ctx context.Context, agentAddress string, agentPort int64, caURL string, commands []*KeaCommand, cmdResponses ...interface{}) (*KeaCmdsResult, error) { + // Find the agent by address and port. + addrPort := net.JoinHostPort(agentAddress, strconv.FormatInt(agentPort, 10)) + agent, err := agents.GetConnectedAgent(addrPort) if err != nil { - err = errors.Wrapf(err, "failed to forward Kea command to %s, command was: %s", caURL, c) - return err + err = errors.Wrapf(err, "there is no agent available at address %s:%d", agentAddress, agentPort) + return nil, err + } + + // Prepare the on-wire representation of the commands. + fdReq := &agentapi.ForwardToKeaOverHTTPReq{ + Url: caURL, + } + for _, cmd := range commands { + fdReq.KeaRequests = append(fdReq.KeaRequests, &agentapi.KeaRequest{ + Request: cmd.Marshal(), + }) } - // Try to parse the response from the on-wire format. - err = UnmarshalKeaResponseList(command, rsp.GetKeaResponse(), response) + // Send the commands to the Stork agent. + fdRsp, err := agent.Client.ForwardToKeaOverHTTP(ctx, fdReq) if err != nil { - err = errors.Wrapf(err, "failed to parse Kea response from %s, response was: %s", caURL, rsp.GetKeaResponse()) - return err + err = errors.Wrapf(err, "failed to forward Kea commands to %s, commands were: %+v", caURL, fdReq.KeaRequests) + return nil, err + } + + result := &KeaCmdsResult{} + result.Error = nil + if fdRsp.Status.Code != agentapi.Status_OK { + result.Error = errors.New(fdRsp.Status.Message) + } + + for idx, rsp := range fdRsp.GetKeaResponses() { + cmdResp := cmdResponses[idx] + if rsp.Status.Code != agentapi.Status_OK { + result.CmdsErrors = append(result.CmdsErrors, errors.New(rsp.Status.Message)) + continue + } + + // Try to parse the response from the on-wire format. + err = UnmarshalKeaResponseList(commands[idx], rsp.Response, cmdResp) + if err != nil { + err = errors.Wrapf(err, "failed to parse Kea response from %s, response was: %s", caURL, rsp) + result.CmdsErrors = append(result.CmdsErrors, err) + continue + } + + result.CmdsErrors = append(result.CmdsErrors, nil) } // Everything was fine, so return no error. - return nil + return result, nil } diff --git a/backend/server/agentcomm/grpcli_test.go b/backend/server/agentcomm/grpcli_test.go index 67efa6f1df9ed18d0b49c46e0dfa9d16a8f3cbe7..06d891c6e74a25655f0eab64842923f38e2fe440 100644 --- a/backend/server/agentcomm/grpcli_test.go +++ b/backend/server/agentcomm/grpcli_test.go @@ -44,10 +44,9 @@ func TestGetState(t *testing.T) { AgentVersion: expVer, Apps: []*agentapi.App{ { - Version: "1.2.3", - App: &agentapi.App_Kea{ - Kea: &agentapi.AppKea{}, - }, + Type: "kea", + CtrlAddress: "1.2.3.4", + CtrlPort: 1234, }, }, } @@ -58,7 +57,8 @@ func TestGetState(t *testing.T) { ctx := context.Background() state, err := agents.GetState(ctx, "127.0.0.1", 8080) require.NoError(t, err) - require.Equal(t, state.AgentVersion, expVer) + require.Equal(t, expVer, state.AgentVersion) + require.Equal(t, "kea", state.Apps[0].Type) } // Test that a command can be successfully forwarded to Kea and the response @@ -68,7 +68,14 @@ func TestForwardToKeaOverHTTP(t *testing.T) { defer teardown() rsp := agentapi.ForwardToKeaOverHTTPRsp{ - KeaResponse: `[ + Status: &agentapi.Status{ + Code: 0, + }, + KeaResponses: []*agentapi.KeaResponse{{ + Status: &agentapi.Status{ + Code: 0, + }, + Response: `[ { "result": 1, "text": "operation failed" @@ -80,17 +87,21 @@ func TestForwardToKeaOverHTTP(t *testing.T) { "success": true } } - ]`, + ]`}}, } + mockAgentClient.EXPECT().ForwardToKeaOverHTTP(gomock.Any(), gomock.Any()). Return(&rsp, nil) ctx := context.Background() command, _ := NewKeaCommand("test-command", nil, nil) actualResponse := KeaResponseList{} - err := agents.ForwardToKeaOverHTTP(ctx, "http://localhost:8000/", "127.0.0.1", 8080, command, &actualResponse) + cmdsResult, err := agents.ForwardToKeaOverHTTP(ctx, "127.0.0.1", 8080, "http://localhost:8000/", []*KeaCommand{command}, &actualResponse) require.NoError(t, err) require.NotNil(t, actualResponse) + require.NoError(t, cmdsResult.Error) + require.Len(t, cmdsResult.CmdsErrors, 1) + require.NoError(t, cmdsResult.CmdsErrors[0]) responseList := actualResponse require.Equal(t, 2, len(responseList)) @@ -106,6 +117,82 @@ func TestForwardToKeaOverHTTP(t *testing.T) { require.Contains(t, *responseList[1].Arguments, "success") } +// Test that two commands can be successfully forwarded to Kea and the response +// can be parsed. +func TestForwardToKeaOverHTTPWith2Cmds(t *testing.T) { + mockAgentClient, agents, teardown := setupGrpcliTestCase(t) + defer teardown() + + rsp := agentapi.ForwardToKeaOverHTTPRsp{ + Status: &agentapi.Status{ + Code: 0, + }, + KeaResponses: []*agentapi.KeaResponse{{ + Status: &agentapi.Status{ + Code: 0, + }, + Response: `[ + { + "result": 1, + "text": "operation failed" + }, + { + "result": 0, + "text": "operation succeeded", + "arguments": { + "success": true + } + } + ]`}, { + Status: &agentapi.Status{ + Code: 0, + }, + Response: `[ + { + "result": 1, + "text": "operation failed" + } + ]`}}, + } + + mockAgentClient.EXPECT().ForwardToKeaOverHTTP(gomock.Any(), gomock.Any()). + Return(&rsp, nil) + + ctx := context.Background() + command1, _ := NewKeaCommand("test-command", nil, nil) + command2, _ := NewKeaCommand("test-command", nil, nil) + actualResponse1 := KeaResponseList{} + actualResponse2 := KeaResponseList{} + cmdsResult, err := agents.ForwardToKeaOverHTTP(ctx, "127.0.0.1", 8080, "http://localhost:8000/", []*KeaCommand{command1, command2}, &actualResponse1, &actualResponse2) + require.NoError(t, err) + require.NotNil(t, actualResponse1) + require.NotNil(t, actualResponse2) + require.NoError(t, cmdsResult.Error) + require.Len(t, cmdsResult.CmdsErrors, 2) + require.NoError(t, cmdsResult.CmdsErrors[0]) + require.NoError(t, cmdsResult.CmdsErrors[1]) + + responseList := actualResponse1 + require.Equal(t, 2, len(responseList)) + + require.Equal(t, 1, responseList[0].Result) + require.Equal(t, "operation failed", responseList[0].Text) + require.Nil(t, responseList[0].Arguments) + + require.Equal(t, 0, responseList[1].Result) + require.Equal(t, "operation succeeded", responseList[1].Text) + require.NotNil(t, responseList[1].Arguments) + require.Equal(t, 1, len(*responseList[1].Arguments)) + require.Contains(t, *responseList[1].Arguments, "success") + + responseList = actualResponse2 + require.Equal(t, 1, len(responseList)) + + require.Equal(t, 1, responseList[0].Result) + require.Equal(t, "operation failed", responseList[0].Text) + require.Nil(t, responseList[0].Arguments) +} + // Test that the error is returned when the response to the forwarded Kea command // is malformed. func TestForwardToKeaOverHTTPInvalidResponse(t *testing.T) { @@ -113,11 +200,18 @@ func TestForwardToKeaOverHTTPInvalidResponse(t *testing.T) { defer teardown() rsp := agentapi.ForwardToKeaOverHTTPRsp{ - KeaResponse: `[ + Status: &agentapi.Status{ + Code: 0, + }, + KeaResponses: []*agentapi.KeaResponse{{ + Status: &agentapi.Status{ + Code: 0, + }, + Response: `[ { "result": "a string" } - ]`, + ]`}}, } mockAgentClient.EXPECT().ForwardToKeaOverHTTP(gomock.Any(), gomock.Any()). Return(&rsp, nil) @@ -125,6 +219,56 @@ func TestForwardToKeaOverHTTPInvalidResponse(t *testing.T) { ctx := context.Background() command, _ := NewKeaCommand("test-command", nil, nil) actualResponse := KeaResponseList{} - err := agents.ForwardToKeaOverHTTP(ctx, "http://localhost:8080/", "127.0.0.1", 8080, command, &actualResponse) + cmdsResult, err := agents.ForwardToKeaOverHTTP(ctx, "127.0.0.1", 8080, "http://localhost:8080/", []*KeaCommand{command}, &actualResponse) + require.NoError(t, err) + require.NotNil(t, cmdsResult) + require.NoError(t, cmdsResult.Error) + require.Len(t, cmdsResult.CmdsErrors, 1) + // and now for our command we get an error + require.Error(t, cmdsResult.CmdsErrors[0]) +} + +func TestGetBind9StateAllOk(t *testing.T) { + mockAgentClient, agents, teardown := setupGrpcliTestCase(t) + defer teardown() + + rsp := agentapi.GetBind9StateRsp{ + Status: &agentapi.Status{ + Code: 0, // all ok + }, + Version: "1.2.3", + Daemon: &agentapi.Bind9Daemon{ + Name: "named", + }, + } + mockAgentClient.EXPECT().GetBind9State(gomock.Any(), gomock.Any()). + Return(&rsp, nil) + + ctx := context.Background() + + state, err := agents.GetBind9State(ctx, "127.0.0.1", 8080) + require.NoError(t, err) + require.NotNil(t, state) + require.Equal(t, "1.2.3", state.Version) + require.Equal(t, "named", state.Daemon.Name) +} + +func TestGetBind9StateError(t *testing.T) { + mockAgentClient, agents, teardown := setupGrpcliTestCase(t) + defer teardown() + + rsp := agentapi.GetBind9StateRsp{ + Status: &agentapi.Status{ + Code: 1, // all ok + Message: "some problems", + }, + } + mockAgentClient.EXPECT().GetBind9State(gomock.Any(), gomock.Any()). + Return(&rsp, nil) + + ctx := context.Background() + + state, err := agents.GetBind9State(ctx, "127.0.0.1", 8080) require.Error(t, err) + require.Nil(t, state) } diff --git a/backend/server/agentcomm/kea.go b/backend/server/agentcomm/kea.go index d92aaf5ba28adda3c019641cae907adcf78638cf..f8495de65c8a5f1a0cb9ec418c7ff20f42c9e4d4 100644 --- a/backend/server/agentcomm/kea.go +++ b/backend/server/agentcomm/kea.go @@ -136,5 +136,5 @@ func UnmarshalKeaResponseList(request *KeaCommand, response string, parsed inter } } - return err + return nil } diff --git a/backend/server/apps/bind9/appbind9.go b/backend/server/apps/bind9/appbind9.go new file mode 100644 index 0000000000000000000000000000000000000000..ad07eaa0b58cedbfbc112bb4ab2031c0c0c10bbb --- /dev/null +++ b/backend/server/apps/bind9/appbind9.go @@ -0,0 +1,34 @@ +package bind9 + +import ( + "context" + "time" + + log "github.com/sirupsen/logrus" + + "isc.org/stork/server/agentcomm" + dbmodel "isc.org/stork/server/database/model" +) + +func GetAppState(ctx context.Context, agents agentcomm.ConnectedAgents, dbApp *dbmodel.App) { + ctx2, cancel := context.WithTimeout(ctx, 2*time.Second) + defer cancel() + + state, err := agents.GetBind9State(ctx2, dbApp.Machine.Address, dbApp.Machine.AgentPort) + if err != nil { + log.Warnf("problem with getting BIND 9 state: %s", err) + return + } + + // store all collected details in app db record + dbApp.Active = state.Active + dbApp.Meta.Version = state.Version + dbApp.Details = dbmodel.AppBind9{ + Daemon: dbmodel.Bind9Daemon{ + Pid: state.Daemon.Pid, + Name: state.Daemon.Name, + Active: state.Daemon.Active, + Version: state.Daemon.Version, + }, + } +} diff --git a/backend/server/apps/kea/appkea.go b/backend/server/apps/kea/appkea.go index 8827806ce21b405f194bbeb994f4583df2ef0ddd..5b86916b79ff1e1a3e022ee179fc95c09871c1c1 100644 --- a/backend/server/apps/kea/appkea.go +++ b/backend/server/apps/kea/appkea.go @@ -12,55 +12,17 @@ import ( storkutil "isc.org/stork/util" ) -// Retrieve configuration of the selected Kea deamons using the config-get -// command. -func GetConfig(ctx context.Context, agents agentcomm.ConnectedAgents, dbApp *dbmodel.App, daemons *agentcomm.KeaDaemons) (agentcomm.KeaResponseList, error) { - // Stork Agent will figure out the URL. - caURL := storkutil.HostWithPortURL(dbApp.CtrlAddress, dbApp.CtrlPort) - - // prepare the command - cmd, _ := agentcomm.NewKeaCommand("config-get", daemons, nil) - - ctx2, cancel := context.WithTimeout(ctx, 2*time.Second) - defer cancel() - - // send the command to daemons through agent and return response list - responseList := agentcomm.KeaResponseList{} - err := agents.ForwardToKeaOverHTTP(ctx2, caURL, dbApp.Machine.Address, dbApp.Machine.AgentPort, cmd, &responseList) - if err != nil { - return nil, err - } - return responseList, nil -} - // Get list of hooks for all DHCP daemons of the given Kea application. -// It uses GetConfig function. -func GetDaemonHooks(ctx context.Context, agents agentcomm.ConnectedAgents, dbApp *dbmodel.App) (map[string][]string, error) { +func GetDaemonHooks(dbApp *dbmodel.App) map[string][]string { hooksByDaemon := make(map[string][]string) - // find out which daemons are active - daemons := make(agentcomm.KeaDaemons) - for _, d := range dbApp.Details.(dbmodel.AppKea).Daemons { - if d.Active && (d.Name == "dhcp4" || d.Name == "dhcp6") { - daemons[d.Name] = true - } - } - - // get configs from daemons - rspList, err := GetConfig(ctx, agents, dbApp, &daemons) - if err != nil { - return nil, err - } - // go through response list with configs from each daemon and retrieve their hooks lists - for _, rsp := range rspList { - if rsp.Result != 0 { - log.Warnf("getting installed hooks from daemon %s failed with error code: %d, text: %s", - rsp.Daemon, rsp.Result, rsp.Text) + for _, dmn := range dbApp.Details.(dbmodel.AppKea).Daemons { + if dmn.Config == nil { continue } - rootNodeName := strings.Title(rsp.Daemon) - dhcpNode, ok := (*rsp.Arguments)[rootNodeName].(map[string]interface{}) + rootNodeName := strings.Title(dmn.Name) + dhcpNode, ok := (*dmn.Config)[rootNodeName].(map[string]interface{}) if !ok { log.Warnf("missing root node %s", rootNodeName) continue @@ -80,8 +42,293 @@ func GetDaemonHooks(ctx context.Context, agents agentcomm.ConnectedAgents, dbApp hooks = append(hooks, library) } } - hooksByDaemon[rsp.Daemon] = hooks + hooksByDaemon[dmn.Name] = hooks + } + + return hooksByDaemon +} + +// === CA config-get response structs ================================================ + +type SocketData struct { + SocketName string `json:"socket-name"` + SocketType string `json:"socket-type"` +} + +type ControlSocketsData struct { + D2 *SocketData + Dhcp4 *SocketData + Dhcp6 *SocketData + NetConf *SocketData +} + +type ControlAgentData struct { + ControlSockets *ControlSocketsData `json:"control-sockets"` +} + +type CAConfigGetRespArgs struct { + ControlAgent *ControlAgentData `json:"Control-agent"` +} + +type CAConfigGetResponse struct { + agentcomm.KeaResponseHeader + Arguments *CAConfigGetRespArgs +} + +// === version-get response structs =============================================== + +type VersionGetRespArgs struct { + Extended string +} + +type VersionGetResponse struct { + agentcomm.KeaResponseHeader + Arguments *VersionGetRespArgs `json:"arguments,omitempty"` +} + +// === status-get response structs ================================================ + +// Represents the status of the local server (the one that +// responded to the command). +type HALocalStatus struct { + Role string + Scopes []string + State string +} + +// Represents the status of the remote server. +type HARemoteStatus struct { + Age int64 + InTouch bool `json:"in-touch"` + Role string + LastScopes []string `json:"last-scopes"` + LastState string `json:"last-state"` +} + +// Represents the status of the HA enabled Kea servers. +type HAServersStatus struct { + Local HALocalStatus + Remote HARemoteStatus +} + +// Represents a response from the single Kea server to the status-get +// command. The HAServers value is nil if it is not present in the +// response (i.e. the Kea server has HA disabled). +type StatusGetRespArgs struct { + Pid int64 + Uptime int64 + Reload int64 + HAServers *HAServersStatus `json:"ha-servers"` +} + +type StatusGetResponse struct { + agentcomm.KeaResponseHeader + Arguments *StatusGetRespArgs `json:"arguments,omitempty"` +} + +// Get state of Kea application Control Agent using ForwardToKeaOverHTTP function. +// The state, that is stored into dbApp, includes: version and config of CA. +// It also returns: +// - list of all Kea daemons +// - list of DHCP daemons (dhcpv4 and/or dhcpv6) +func getStateFromCA(ctx context.Context, agents agentcomm.ConnectedAgents, caURL string, dbApp *dbmodel.App, daemonsMap map[string]*dbmodel.KeaDaemon) (agentcomm.KeaDaemons, agentcomm.KeaDaemons, error) { + // prepare the command to get config and version from CA + cmds := []*agentcomm.KeaCommand{ + { + Command: "version-get", + }, + { + Command: "config-get", + }, + } + + // get version and config from CA + versionGetResp := []VersionGetResponse{} + caConfigGetResp := []CAConfigGetResponse{} + + cmdsResult, err := agents.ForwardToKeaOverHTTP(ctx, dbApp.Machine.Address, dbApp.Machine.AgentPort, caURL, cmds, &versionGetResp, &caConfigGetResp) + if err != nil { + return nil, nil, err + } + if cmdsResult.Error != nil { + return nil, nil, cmdsResult.Error + } + + // process the response from CA + daemonsMap["ca"] = &dbmodel.KeaDaemon{ + Name: "ca", + Active: true, + } + + if cmdsResult.CmdsErrors[0] == nil { + vRsp := versionGetResp[0] + dmn := daemonsMap["ca"] + if vRsp.Result != 0 { + dmn.Active = false + log.Warnf("problem with version-get from CA: %s", vRsp.Text) + } else { + dmn.Version = vRsp.Text + dbApp.Meta.Version = vRsp.Text + if vRsp.Arguments != nil { + dmn.ExtendedVersion = vRsp.Arguments.Extended + } + } + } else { + log.Warnf("problem with version-get response from CA: %s", cmdsResult.CmdsErrors[0]) + } + + allDaemons := make(agentcomm.KeaDaemons) + dhcpDaemons := make(agentcomm.KeaDaemons) + if caConfigGetResp[0].Arguments.ControlAgent.ControlSockets != nil { + if caConfigGetResp[0].Arguments.ControlAgent.ControlSockets.Dhcp4 != nil { + allDaemons["dhcp4"] = true + dhcpDaemons["dhcp4"] = true + } + if caConfigGetResp[0].Arguments.ControlAgent.ControlSockets.Dhcp6 != nil { + allDaemons["dhcp6"] = true + dhcpDaemons["dhcp6"] = true + } + if caConfigGetResp[0].Arguments.ControlAgent.ControlSockets.D2 != nil { + allDaemons["d2"] = true + } + } + + return allDaemons, dhcpDaemons, nil +} + +// Get state of Kea application daemons (beside Control Agent) using ForwardToKeaOverHTTP function. +// The state, that is stored into dbApp, includes: version, config and runtime state of indicated Kea daemons. +func getStateFromDaemons(ctx context.Context, agents agentcomm.ConnectedAgents, caURL string, dbApp *dbmodel.App, daemonsMap map[string]*dbmodel.KeaDaemon, allDaemons agentcomm.KeaDaemons, dhcpDaemons agentcomm.KeaDaemons) error { + now := storkutil.UTCNow() + + // issue 3 commands to Kea daemons at once to get their state + cmds := []*agentcomm.KeaCommand{ + { + Command: "version-get", + Daemons: &allDaemons, + }, + { + Command: "status-get", + Daemons: &dhcpDaemons, + }, + { + Command: "config-get", + Daemons: &allDaemons, + }, + } + + versionGetResp := []VersionGetResponse{} + statusGetResp := []StatusGetResponse{} + configGetResp := []agentcomm.KeaResponse{} + + cmdsResult, err := agents.ForwardToKeaOverHTTP(ctx, dbApp.Machine.Address, dbApp.Machine.AgentPort, caURL, cmds, &versionGetResp, &statusGetResp, &configGetResp) + if err != nil { + return err + } + if cmdsResult.Error != nil { + return cmdsResult.Error + } + + for name := range allDaemons { + daemonsMap[name] = &dbmodel.KeaDaemon{ + Name: name, + Active: true, + } + } + + // process version-get responses + err = cmdsResult.CmdsErrors[0] + if err != nil { + log.Warnf("problem with version-get response: %s", err) + } else { + for _, vRsp := range versionGetResp { + dmn := daemonsMap[vRsp.Daemon] + if vRsp.Result != 0 { + dmn.Active = false + log.Warnf("problem with version-get and kea daemon %s: %s", vRsp.Daemon, vRsp.Text) + continue + } + + dmn.Version = vRsp.Text + if vRsp.Arguments != nil { + dmn.ExtendedVersion = vRsp.Arguments.Extended + } + } } - return hooksByDaemon, nil + // process status-get responses + err = cmdsResult.CmdsErrors[1] + if err != nil { + log.Warnf("problem with status-get response: %s", err) + } else { + for _, sRsp := range statusGetResp { + dmn := daemonsMap[sRsp.Daemon] + if sRsp.Result != 0 { + dmn.Active = false + log.Warnf("problem with status-get and kea daemon %s: %s", sRsp.Daemon, sRsp.Text) + continue + } + + if sRsp.Arguments != nil { + dmn.Uptime = sRsp.Arguments.Uptime + dmn.ReloadedAt = now.Add(time.Second * time.Duration(-sRsp.Arguments.Reload)) + // TODO: HA status + } + } + } + + // process config-get responses + err = cmdsResult.CmdsErrors[2] + if err != nil { + log.Warnf("problem with config-get response: %s", err) + } else { + for _, cRsp := range configGetResp { + dmn := daemonsMap[cRsp.Daemon] + if cRsp.Result != 0 { + dmn.Active = false + log.Warnf("problem with config-get and kea daemon %s: %s", cRsp.Daemon, cRsp.Text) + continue + } + + dmn.Config = cRsp.Arguments + } + } + + return nil +} + +// Get state of Kea application daemons using ForwardToKeaOverHTTP function. +// The state, that is stored into dbApp, includes: version, config and runtime state of indicated Kea daemons. +func GetAppState(ctx context.Context, agents agentcomm.ConnectedAgents, dbApp *dbmodel.App) { + // prepare URL to CA + caURL := storkutil.HostWithPortURL(dbApp.CtrlAddress, dbApp.CtrlPort) + + ctx2, cancel := context.WithTimeout(ctx, 2*time.Second) + defer cancel() + + // get state from CA + daemonsMap := map[string]*dbmodel.KeaDaemon{} + allDaemons, dhcpDaemons, err := getStateFromCA(ctx2, agents, caURL, dbApp, daemonsMap) + + // if not problems then now get state from the rest of Kea daemons + if err == nil { + err = getStateFromDaemons(ctx2, agents, caURL, dbApp, daemonsMap, allDaemons, dhcpDaemons) + if err != nil { + log.Warnf("problem with getting state from kea daemons: %s", err) + } + } else { + log.Warnf("problem with getting state from kea CA: %s", err) + } + + // store all collected details in app db record + keaApp := dbmodel.AppKea{} + dbApp.Active = true + for name := range daemonsMap { + dmn := daemonsMap[name] + // if all daemons are active then whole app is active + dbApp.Active = dbApp.Active && dmn.Active + + keaApp.Daemons = append(keaApp.Daemons, dmn) + } + dbApp.Details = keaApp } diff --git a/backend/server/apps/kea/appkea_test.go b/backend/server/apps/kea/appkea_test.go index b0658bc7818b8a10029d6384188eec9a2c8ee102..15a32454052cfe0f43bc5374a3f4ded607205bcf 100644 --- a/backend/server/apps/kea/appkea_test.go +++ b/backend/server/apps/kea/appkea_test.go @@ -12,12 +12,102 @@ import ( storktest "isc.org/stork/server/test" ) -// Kea servers' response to config-get command. The argument indicates if +// Kea servers' response to config-get command from CA. The argument indicates if // it is a response from a single server or two servers. -func mockGetConfigResponse(daemons int, response interface{}) { - list := response.(*agentcomm.KeaResponseList) +func mockGetConfigFromCAResponse(daemons int, cmdResponses []interface{}) { + list1 := cmdResponses[0].(*[]VersionGetResponse) + *list1 = []VersionGetResponse{ + { + KeaResponseHeader: agentcomm.KeaResponseHeader{ + Result: 0, + Daemon: "ca", + }, + Arguments: &VersionGetRespArgs{ + Extended: "Extended version", + }, + }, + } + list2 := cmdResponses[1].(*[]CAConfigGetResponse) + *list2 = []CAConfigGetResponse{ + { + KeaResponseHeader: agentcomm.KeaResponseHeader{ + Result: 0, + Daemon: "ca", + }, + Arguments: &CAConfigGetRespArgs{ + ControlAgent: &ControlAgentData{ + ControlSockets: &ControlSocketsData{ + Dhcp4: &SocketData{ + SocketName: "aaaa", + SocketType: "unix", + }, + }, + }, + }, + }, + } + if daemons > 1 { + (*list2)[0].Arguments.ControlAgent.ControlSockets.Dhcp6 = &SocketData{ + SocketName: "bbbb", + SocketType: "unix", + } + } +} - *list = agentcomm.KeaResponseList{ +// Kea servers' response to config-get command from other Kea daemons. The argument indicates if +// it is a response from a single server or two servers. +func mockGetConfigFromOtherDaemonsResponse(daemons int, cmdResponses []interface{}) { + // version-get response + list1 := cmdResponses[0].(*[]VersionGetResponse) + *list1 = []VersionGetResponse{ + { + KeaResponseHeader: agentcomm.KeaResponseHeader{ + Result: 0, + Daemon: "dhcp4", + }, + Arguments: &VersionGetRespArgs{ + Extended: "Extended version", + }, + }, + } + if daemons > 1 { + *list1 = append(*list1, VersionGetResponse{ + KeaResponseHeader: agentcomm.KeaResponseHeader{ + Result: 0, + Daemon: "dhcp6", + }, + Arguments: &VersionGetRespArgs{ + Extended: "Extended version", + }, + }) + } + // status-get response + list2 := cmdResponses[1].(*[]StatusGetResponse) + *list2 = []StatusGetResponse{ + { + KeaResponseHeader: agentcomm.KeaResponseHeader{ + Result: 0, + Daemon: "dhcp4", + }, + Arguments: &StatusGetRespArgs{ + Pid: 123, + }, + }, + } + if daemons > 1 { + *list2 = append(*list2, StatusGetResponse{ + KeaResponseHeader: agentcomm.KeaResponseHeader{ + Result: 0, + Daemon: "dhcp6", + }, + Arguments: &StatusGetRespArgs{ + Pid: 123, + }, + }) + } + // config-get response + list3 := cmdResponses[2].(*[]agentcomm.KeaResponse) + *list3 = []agentcomm.KeaResponse{ { KeaResponseHeader: agentcomm.KeaResponseHeader{ Result: 0, @@ -29,13 +119,16 @@ func mockGetConfigResponse(daemons int, response interface{}) { map[string]interface{}{ "library": "hook_abc.so", }, + map[string]interface{}{ + "library": "hook_def.so", + }, }, }, }, }, } if daemons > 1 { - *list = append(*list, agentcomm.KeaResponse{ + *list3 = append(*list3, agentcomm.KeaResponse{ KeaResponseHeader: agentcomm.KeaResponseHeader{ Result: 0, Daemon: "dhcp6", @@ -56,109 +149,131 @@ func mockGetConfigResponse(daemons int, response interface{}) { } } -// Check if GetConfig returns response to the forwarded command. -func TestGetConfig(t *testing.T) { +// Check if GetAppState returns response to the forwarded command. +func TestGetAppStateWith1Daemon(t *testing.T) { ctx := context.Background() // check getting config of 1 daemon - fa := storktest.NewFakeAgents(func(response interface{}) { - mockGetConfigResponse(1, response) + fa := storktest.NewFakeAgents(func(callNo int, cmdResponses []interface{}) { + if callNo == 0 { + mockGetConfigFromCAResponse(1, cmdResponses) + } else if callNo == 1 { + mockGetConfigFromOtherDaemonsResponse(1, cmdResponses) + } }) dbApp := dbmodel.App{ CtrlAddress: "192.0.2.0", CtrlPort: 1234, - Details: dbmodel.AppKea{ - Daemons: []dbmodel.KeaDaemon{ - { - Name: "dhcp4", - }, - }, + Machine: &dbmodel.Machine{ + Address: "192.0.2.0", + AgentPort: 1111, }, } - daemons := make(agentcomm.KeaDaemons) - daemons["dhcp4"] = true - - list, err := GetConfig(ctx, fa, &dbApp, &daemons) - require.NoError(t, err) - require.NotNil(t, list) - require.Len(t, list, 1) + GetAppState(ctx, fa, &dbApp) require.Equal(t, "http://192.0.2.0:1234/", fa.RecordedURL) - require.Equal(t, "config-get", fa.RecordedCommand) + require.Equal(t, "version-get", fa.RecordedCommands[0]) + require.Equal(t, "config-get", fa.RecordedCommands[1]) +} + +func TestGetAppStateWith2Daemons(t *testing.T) { + ctx := context.Background() // check getting configs of 2 daemons - fa = storktest.NewFakeAgents(func(response interface{}) { - mockGetConfigResponse(2, response) + fa := storktest.NewFakeAgents(func(callNo int, cmdResponses []interface{}) { + if callNo == 0 { + mockGetConfigFromCAResponse(2, cmdResponses) + } else if callNo == 1 { + mockGetConfigFromOtherDaemonsResponse(2, cmdResponses) + } }) - dbApp = dbmodel.App{ - Details: dbmodel.AppKea{ - Daemons: []dbmodel.KeaDaemon{ - { - Name: "dhcp4", - }, - { - Name: "dhcp6", - }, - }, + + dbApp := dbmodel.App{ + CtrlAddress: "192.0.2.0", + CtrlPort: 1234, + Machine: &dbmodel.Machine{ + Address: "192.0.2.0", + AgentPort: 1111, }, } - daemons["dhcp6"] = true + GetAppState(ctx, fa, &dbApp) - list, err = GetConfig(ctx, fa, &dbApp, &daemons) - require.NoError(t, err) - require.NotNil(t, list) - require.Len(t, list, 2) + require.Equal(t, "http://192.0.2.0:1234/", fa.RecordedURL) + require.Equal(t, "version-get", fa.RecordedCommands[0]) + require.Equal(t, "config-get", fa.RecordedCommands[1]) } // Check if GetDaemonHooks returns hooks for given daemon. -func TestGetDaemonHooks(t *testing.T) { - ctx := context.Background() - // check getting config of 1 daemon - fa := storktest.NewFakeAgents(func(response interface{}) { - mockGetConfigResponse(1, response) - }) +func TestGetDaemonHooksFrom1Daemon(t *testing.T) { dbApp := dbmodel.App{ Details: dbmodel.AppKea{ - Daemons: []dbmodel.KeaDaemon{ + Daemons: []*dbmodel.KeaDaemon{ { Name: "dhcp4", + Config: &map[string]interface{}{ + "Dhcp4": map[string]interface{}{ + "hooks-libraries": []interface{}{ + map[string]interface{}{ + "library": "hook_abc.so", + }, + }, + }, + }, }, }, }, } - hooksMap, err := GetDaemonHooks(ctx, fa, &dbApp) - require.NoError(t, err) + hooksMap := GetDaemonHooks(&dbApp) require.NotNil(t, hooksMap) hooks, ok := hooksMap["dhcp4"] require.True(t, ok) require.Len(t, hooks, 1) require.Equal(t, "hook_abc.so", hooks[0]) +} - // check getting configs of 2 daemons - fa = storktest.NewFakeAgents(func(response interface{}) { - mockGetConfigResponse(2, response) - }) - dbApp = dbmodel.App{ +// Check getting hooks of 2 daemons +func TestGetDaemonHooksFrom2Daemons(t *testing.T) { + dbApp := dbmodel.App{ Details: dbmodel.AppKea{ - Daemons: []dbmodel.KeaDaemon{ + Daemons: []*dbmodel.KeaDaemon{ { Name: "dhcp6", + Config: &map[string]interface{}{ + "Dhcp6": map[string]interface{}{ + "hooks-libraries": []interface{}{ + map[string]interface{}{ + "library": "hook_abc.so", + }, + map[string]interface{}{ + "library": "hook_def.so", + }, + }, + }, + }, }, { Name: "dhcp4", + Config: &map[string]interface{}{ + "Dhcp4": map[string]interface{}{ + "hooks-libraries": []interface{}{ + map[string]interface{}{ + "library": "hook_abc.so", + }, + }, + }, + }, }, }, }, } - hooksMap, err = GetDaemonHooks(ctx, fa, &dbApp) - require.NoError(t, err) + hooksMap := GetDaemonHooks(&dbApp) require.NotNil(t, hooksMap) - hooks, ok = hooksMap["dhcp4"] + hooks, ok := hooksMap["dhcp4"] require.True(t, ok) require.Len(t, hooks, 1) require.Equal(t, "hook_abc.so", hooks[0]) diff --git a/backend/server/apps/kea/status.go b/backend/server/apps/kea/status.go index 5168970128761e2038871259cde0b14cfbb774d1..54945063a53e057bc0899592bb576e30730f793a 100644 --- a/backend/server/apps/kea/status.go +++ b/backend/server/apps/kea/status.go @@ -12,29 +12,6 @@ import ( storkutil "isc.org/stork/util" ) -// Represents the status of the local server (the one that -// responded to the command). -type HALocalStatus struct { - Role string - Scopes []string - State string -} - -// Represents the status of the remote server. -type HARemoteStatus struct { - Age int64 - InTouch bool `json:"in-touch"` - Role string - LastScopes []string `json:"last-scopes"` - LastState string `json:"last-state"` -} - -// Represents the status of the HA enabled Kea servers. -type HAServersStatus struct { - Local HALocalStatus - Remote HARemoteStatus -} - // Represents a response from the single Kea server to the status-get // command. The HAServers value is nil if it is not present in the // response. @@ -61,7 +38,7 @@ func GetDHCPStatus(ctx context.Context, agents agentcomm.ConnectedAgents, dbApp ctx, cancel := context.WithTimeout(ctx, 2*time.Second) defer cancel() - url := storkutil.HostWithPortURL(dbApp.CtrlAddress, dbApp.CtrlPort) + caURL := storkutil.HostWithPortURL(dbApp.CtrlAddress, dbApp.CtrlPort) // The Kea response will be stored in this slice of structures. response := []struct { @@ -70,10 +47,16 @@ func GetDHCPStatus(ctx context.Context, agents agentcomm.ConnectedAgents, dbApp }{} // Send the command and receive the response. - err := agents.ForwardToKeaOverHTTP(ctx, url, dbApp.Machine.Address, dbApp.Machine.AgentPort, cmd, &response) + cmdsResult, err := agents.ForwardToKeaOverHTTP(ctx, dbApp.Machine.Address, dbApp.Machine.AgentPort, caURL, []*agentcomm.KeaCommand{cmd}, &response) if err != nil { return nil, err } + if cmdsResult.Error != nil && len(cmdsResult.CmdsErrors) == 0 { + return nil, cmdsResult.Error + } + if cmdsResult.CmdsErrors[0] != nil { + return nil, cmdsResult.CmdsErrors[0] + } // Extract the status value. appStatus := AppStatus{} diff --git a/backend/server/apps/kea/status_test.go b/backend/server/apps/kea/status_test.go index c6e5fd48eb538d064aa63ea2b0f70d374c6c65b8..9dac8547a37d659d2af52bbccb5beb7451127da9 100644 --- a/backend/server/apps/kea/status_test.go +++ b/backend/server/apps/kea/status_test.go @@ -13,7 +13,7 @@ import ( // Generate test response to status-get command including status of the // HA pair doing load balancing. -func mockGetStatusLoadBalancing(response interface{}) { +func mockGetStatusLoadBalancing(callNo int, cmdResponses []interface{}) { daemons, _ := agentcomm.NewKeaDaemons("dhcp4") command, _ := agentcomm.NewKeaCommand("status-get", daemons, nil) json := `[ @@ -42,12 +42,12 @@ func mockGetStatusLoadBalancing(response interface{}) { } } ]` - _ = agentcomm.UnmarshalKeaResponseList(command, json, response) + _ = agentcomm.UnmarshalKeaResponseList(command, json, cmdResponses[0]) } // Generates test response to status-get command lacking a status of the // HA pair. -func mockGetStatusNoHA(response interface{}) { +func mockGetStatusNoHA(callNo int, cmdResponses []interface{}) { daemons, _ := agentcomm.NewKeaDaemons("dhcp4") command, _ := agentcomm.NewKeaCommand("status-get", daemons, nil) json := `[ @@ -61,12 +61,12 @@ func mockGetStatusNoHA(response interface{}) { } } ]` - _ = agentcomm.UnmarshalKeaResponseList(command, json, response) + _ = agentcomm.UnmarshalKeaResponseList(command, json, cmdResponses[0]) } // Generates test response to status-get command indicating an error and // lacking argument.s -func mockGetStatusError(response interface{}) { +func mockGetStatusError(callNo int, cmdResponses []interface{}) { daemons, _ := agentcomm.NewKeaDaemons("dhcp4") command, _ := agentcomm.NewKeaCommand("status-get", daemons, nil) json := `[ @@ -75,7 +75,7 @@ func mockGetStatusError(response interface{}) { "text": "unable to communicate with the deamon" } ]` - _ = agentcomm.UnmarshalKeaResponseList(command, json, response) + _ = agentcomm.UnmarshalKeaResponseList(command, json, cmdResponses[0]) } // Test status-get command when HA status is returned. @@ -84,6 +84,10 @@ func TestGetDHCPStatus(t *testing.T) { app := dbmodel.App{ CtrlPort: 1234, + Machine: &dbmodel.Machine{ + Address: "192.0.2.0", + AgentPort: 1111, + }, } appStatus, err := GetDHCPStatus(context.Background(), fa, &app) @@ -126,6 +130,10 @@ func TestGetDHCPStatusNoHA(t *testing.T) { app := dbmodel.App{ CtrlPort: 1234, + Machine: &dbmodel.Machine{ + Address: "192.0.2.0", + AgentPort: 1111, + }, } appStatus, err := GetDHCPStatus(context.Background(), fa, &app) @@ -152,6 +160,10 @@ func TestGetDHCPStatusError(t *testing.T) { app := dbmodel.App{ CtrlPort: 1234, + Machine: &dbmodel.Machine{ + Address: "192.0.2.0", + AgentPort: 1111, + }, } appStatus, err := GetDHCPStatus(context.Background(), fa, &app) diff --git a/backend/server/database/model/app.go b/backend/server/database/model/app.go index ba94e75514e091731ca233a0581f89dd6e020c80..63582cbb93cf25a6cd535be1ca140ec35388b45d 100644 --- a/backend/server/database/model/app.go +++ b/backend/server/database/model/app.go @@ -27,13 +27,16 @@ type KeaDaemon struct { Active bool Version string ExtendedVersion string + Config *map[string]interface{} + Uptime int64 + ReloadedAt time.Time } const KeaAppType = "kea" type AppKea struct { ExtendedVersion string - Daemons []KeaDaemon + Daemons []*KeaDaemon } const Bind9AppType = "bind9" @@ -53,8 +56,8 @@ type App struct { Created time.Time Deleted time.Time MachineID int64 - Machine Machine - Type string + Machine *Machine + Type string // currently supported types are: "kea" and "bind9" CtrlAddress string CtrlPort int64 Active bool @@ -87,7 +90,7 @@ func (app *App) AfterScan(ctx context.Context) error { var bind9Details AppBind9 err = json.Unmarshal(bytes, &bind9Details) if err != nil { - return errors.Wrapf(err, "problem with unmarshaling bind9 app details") + return errors.Wrapf(err, "problem with unmarshaling BIND 9 app details") } app.Details = bind9Details } diff --git a/backend/server/database/model/app_test.go b/backend/server/database/model/app_test.go index c519cfcb6abbf8fee7447c0d568a461e992470da..b5402e95b3a41ff73a56f9d452b41edc89bc87a4 100644 --- a/backend/server/database/model/app_test.go +++ b/backend/server/database/model/app_test.go @@ -259,7 +259,7 @@ func TestGetActiveDHCPMultiple(t *testing.T) { a := &App{ Type: KeaAppType, Details: AppKea{ - Daemons: []KeaDaemon{ + Daemons: []*KeaDaemon{ { Active: true, Name: "dhcp4", @@ -283,7 +283,7 @@ func TestGetActiveDHCPSingle(t *testing.T) { a := &App{ Type: KeaAppType, Details: AppKea{ - Daemons: []KeaDaemon{ + Daemons: []*KeaDaemon{ { Active: false, Name: "dhcp4", diff --git a/backend/server/database/model/machine.go b/backend/server/database/model/machine.go index 8f6eb6a9644ba738a82bc6b0ae69b65e0b33aa1f..241961e5cbe0de24d32d96d7f32a30e8a160adac 100644 --- a/backend/server/database/model/machine.go +++ b/backend/server/database/model/machine.go @@ -41,7 +41,7 @@ type Machine struct { LastVisited time.Time Error string State MachineState - Apps []App + Apps []*App } func AddMachine(db *pg.DB, machine *Machine) error { @@ -84,7 +84,7 @@ func GetMachineByID(db *pg.DB, id int64) (*Machine, error) { } func RefreshMachineFromDb(db *pg.DB, machine *Machine) error { - machine.Apps = []App{} + machine.Apps = []*App{} q := db.Model(machine).Where("id = ?", machine.ID) q = q.Relation("Apps") err := q.Select() @@ -144,7 +144,7 @@ func DeleteMachine(db *pg.DB, machine *Machine) error { // first mark as deleted all apps of the machine for _, app := range machine.Apps { dbApp := app - err := DeleteApp(db, &dbApp) + err := DeleteApp(db, dbApp) if err != nil { log.Warnf("problem with deleting app %d: %s", app.ID, err) } diff --git a/backend/server/restservice/restimpl.go b/backend/server/restservice/restimpl.go index dab54c2bc07505837f77ee61d2c7dc782e8affe3..1bf268f49689623219a7b82b6b11437690382c07 100644 --- a/backend/server/restservice/restimpl.go +++ b/backend/server/restservice/restimpl.go @@ -13,6 +13,7 @@ import ( "isc.org/stork" "isc.org/stork/server/agentcomm" + "isc.org/stork/server/apps/bind9" "isc.org/stork/server/apps/kea" dbops "isc.org/stork/server/database" dbmodel "isc.org/stork/server/database/model" @@ -84,6 +85,106 @@ func machineToRestAPI(dbMachine dbmodel.Machine) *models.Machine { return &m } +func getMachineAndAppsState(ctx context.Context, db *dbops.PgDB, dbMachine *dbmodel.Machine, agents agentcomm.ConnectedAgents) string { + ctx2, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + + // get state of machine from agent + state, err := agents.GetState(ctx2, dbMachine.Address, dbMachine.AgentPort) + if err != nil { + log.Warn(err) + dbMachine.Error = "cannot get state of machine" + err = db.Update(dbMachine) + if err != nil { + log.Error(err) + return "problem with updating record in database" + } + return "" + } + + // store machine's state in db + err = updateMachineFields(db, dbMachine, state) + if err != nil { + log.Error(err) + return "cannot update machine in db" + } + + // If there are any new apps then get their state and add to db. + // Old ones are just updated. + oldAppsList := dbMachine.Apps + dbMachine.Apps = []*dbmodel.App{} + for _, app := range state.Apps { + // look for old app + var dbApp *dbmodel.App = nil + for _, dbApp2 := range oldAppsList { + if dbApp2.Type == app.Type && dbApp2.CtrlPort == app.CtrlPort { + dbApp = dbApp2 + break + } + } + // if no old app in db then prepare new record + if dbApp == nil { + dbApp = &dbmodel.App{ + ID: 0, + MachineID: dbMachine.ID, + Machine: dbMachine, + Type: app.Type, + CtrlAddress: app.CtrlAddress, + CtrlPort: app.CtrlPort, + } + } else { + dbApp.Machine = dbMachine + } + + switch app.Type { + case dbmodel.KeaAppType: + kea.GetAppState(ctx2, agents, dbApp) + case dbmodel.Bind9AppType: + bind9.GetAppState(ctx2, agents, dbApp) + } + + // either add new app record to db or update old one + if dbApp.ID == 0 { + err = dbmodel.AddApp(db, dbApp) + if err != nil { + log.Error(err) + return "problem with storing application state in database" + } + log.Printf("added %s app on %s", dbApp.Type, dbMachine.Address) + } else { + err = db.Update(dbApp) + if err != nil { + log.Error(err) + return "problem with storing application state in database" + } + log.Printf("updated %s app on %s", dbApp.Type, dbMachine.Address) + } + + // add app to machine's apps list + dbMachine.Apps = append(dbMachine.Apps, dbApp) + } + + // delete missing apps + for _, dbApp := range oldAppsList { + found := false + for _, app := range state.Apps { + if dbApp.Type == app.Type && dbApp.CtrlPort == app.CtrlPort { + found = true + break + } + } + if !found { + err = dbmodel.DeleteApp(db, dbApp) + if err != nil { + log.Error(err) + } + log.Printf("deleted %s app on %s", dbApp.Type, dbMachine.Address) + } + } + + return "" +} + // Get runtime state of indicated machine. func (r *RestAPI) GetMachineState(ctx context.Context, params services.GetMachineStateParams) middleware.Responder { dbMachine, err := dbmodel.GetMachineByID(r.Db, params.ID) @@ -103,32 +204,10 @@ func (r *RestAPI) GetMachineState(ctx context.Context, params services.GetMachin return rsp } - ctx2, cancel := context.WithTimeout(ctx, 2*time.Second) - defer cancel() - state, err := r.Agents.GetState(ctx2, dbMachine.Address, dbMachine.AgentPort) - if err != nil { - log.Warn(err) - dbMachine.Error = "cannot get state of machine" - err = r.Db.Update(dbMachine) - if err != nil { - log.Error(err) - msg := "problem with updating record in database" - rsp := services.NewGetMachineStateDefault(500).WithPayload(&models.APIError{ - Message: &msg, - }) - return rsp - } - m := machineToRestAPI(*dbMachine) - rsp := services.NewGetMachineStateOK().WithPayload(m) - return rsp - } - - err = updateMachineFields(r.Db, dbMachine, state) - if err != nil { - log.Error(err) - rsp := services.NewGetMachineStateOK().WithPayload(&models.Machine{ - ID: dbMachine.ID, - Error: "cannot update machine in db", + errStr := getMachineAndAppsState(ctx, r.Db, dbMachine, r.Agents) + if errStr != "" { + rsp := services.NewGetMachineStateDefault(500).WithPayload(&models.APIError{ + Message: &errStr, }) return rsp } @@ -261,38 +340,16 @@ func (r *RestAPI) CreateMachine(ctx context.Context, params services.CreateMachi dbMachine.Deleted = time.Time{} } - ctx2, cancel := context.WithTimeout(ctx, 100*time.Second) - defer cancel() - state, err := r.Agents.GetState(ctx2, addr, params.Machine.AgentPort) - if err != nil { - log.Warn(err) - dbMachine.Error = "cannot get state of machine" - err = r.Db.Update(dbMachine) - if err != nil { - log.Error(err) - msg := "problem with updating record in database" - rsp := services.NewGetMachineStateDefault(500).WithPayload(&models.APIError{ - Message: &msg, - }) - return rsp - } - m := machineToRestAPI(*dbMachine) - rsp := services.NewCreateMachineOK().WithPayload(m) - return rsp - } - - err = updateMachineFields(r.Db, dbMachine, state) - if err != nil { - log.Error(err) - rsp := services.NewCreateMachineOK().WithPayload(&models.Machine{ - ID: dbMachine.ID, - Address: &addr, - Error: "cannot update machine in db", + errStr := getMachineAndAppsState(ctx, r.Db, dbMachine, r.Agents) + if errStr != "" { + rsp := services.NewCreateMachineDefault(500).WithPayload(&models.APIError{ + Message: &errStr, }) return rsp } m := machineToRestAPI(*dbMachine) + log.Printf("machineToRestAPI %+v", m) rsp := services.NewCreateMachineOK().WithPayload(m) return rsp @@ -365,126 +422,6 @@ func (r *RestAPI) UpdateMachine(ctx context.Context, params services.UpdateMachi return rsp } -func updateMachineFieldsKea(db *dbops.PgDB, dbMachine *dbmodel.Machine, dbAppsMap map[string]dbmodel.App, keaApp *agentcomm.AppKea) (err error) { - var keaDaemons []dbmodel.KeaDaemon - if keaApp != nil { - for _, d := range keaApp.Daemons { - keaDaemons = append(keaDaemons, dbmodel.KeaDaemon{ - Pid: d.Pid, - Name: d.Name, - Active: d.Active, - Version: d.Version, - ExtendedVersion: d.ExtendedVersion, - }) - } - } - - dbKeaApp, dbOk := dbAppsMap[dbmodel.KeaAppType] - if dbOk { - if keaApp != nil { - // update app in db - meta := dbmodel.AppMeta{ - Version: keaApp.Version, - } - dbKeaApp.Deleted = time.Time{} // undelete if it was deleted - dbKeaApp.CtrlPort = keaApp.CtrlPort - dbKeaApp.Active = keaApp.Active - dbKeaApp.Meta = meta - dt := dbKeaApp.Details.(dbmodel.AppKea) - dt.ExtendedVersion = keaApp.ExtendedVersion - dt.Daemons = keaDaemons - err = db.Update(&dbKeaApp) - if err != nil { - return errors.Wrapf(err, "problem with updating app %v", dbKeaApp) - } - } else { - // delete app from db - err = dbmodel.DeleteApp(db, &dbKeaApp) - if err != nil { - return err - } - } - } else if keaApp != nil { - // add app to db - dbKeaApp = dbmodel.App{ - MachineID: dbMachine.ID, - Type: dbmodel.KeaAppType, - CtrlPort: keaApp.CtrlPort, - Active: keaApp.Active, - Meta: dbmodel.AppMeta{ - Version: keaApp.Version, - }, - Details: dbmodel.AppKea{ - ExtendedVersion: keaApp.ExtendedVersion, - Daemons: keaDaemons, - }, - } - err = dbmodel.AddApp(db, &dbKeaApp) - if err != nil { - return err - } - } - return nil -} - -func updateMachineFieldsBind9(db *dbops.PgDB, dbMachine *dbmodel.Machine, dbAppsMap map[string]dbmodel.App, bind9App *agentcomm.AppBind9) (err error) { - var bind9Daemon dbmodel.Bind9Daemon - - if bind9App != nil { - bind9Daemon = dbmodel.Bind9Daemon{ - Pid: bind9App.Daemon.Pid, - Name: bind9App.Daemon.Name, - Active: bind9App.Daemon.Active, - Version: bind9App.Daemon.Version, - } - } - - dbBind9App, dbOk := dbAppsMap[dbmodel.Bind9AppType] - if dbOk { - if bind9App != nil { - // update app in db - meta := dbmodel.AppMeta{ - Version: bind9App.Version, - } - dbBind9App.Deleted = time.Time{} // undelete if it was deleted - dbBind9App.CtrlPort = bind9App.CtrlPort - dbBind9App.Active = bind9App.Active - dbBind9App.Meta = meta - dt := dbBind9App.Details.(dbmodel.AppBind9) - dt.Daemon = bind9Daemon - err = db.Update(&dbBind9App) - if err != nil { - return errors.Wrapf(err, "problem with updating app %v", dbBind9App) - } - } else { - // delete app from db - err = dbmodel.DeleteApp(db, &dbBind9App) - if err != nil { - return err - } - } - } else if bind9App != nil { - // add app to db - dbBind9App = dbmodel.App{ - MachineID: dbMachine.ID, - Type: dbmodel.Bind9AppType, - CtrlPort: bind9App.CtrlPort, - Active: bind9App.Active, - Meta: dbmodel.AppMeta{ - Version: bind9App.Version, - }, - Details: dbmodel.AppBind9{ - Daemon: bind9Daemon, - }, - } - err = dbmodel.AddApp(db, &dbBind9App) - if err != nil { - return err - } - } - return nil -} - func updateMachineFields(db *dbops.PgDB, dbMachine *dbmodel.Machine, m *agentcomm.State) error { // update state fields in machine dbMachine.State.AgentVersion = m.AgentVersion @@ -509,48 +446,6 @@ func updateMachineFields(db *dbops.PgDB, dbMachine *dbmodel.Machine, m *agentcom if err != nil { return errors.Wrapf(err, "problem with updating machine %+v", dbMachine) } - - // update services associated with machine - - // get list of present services in db - dbApps, err := dbmodel.GetAppsByMachine(db, dbMachine.ID) - if err != nil { - return err - } - - dbAppsMap := make(map[string]dbmodel.App) - for _, dbSrv := range dbApps { - dbAppsMap[dbSrv.Type] = dbSrv - } - - var keaApp *agentcomm.AppKea = nil - var bind9App *agentcomm.AppBind9 = nil - for _, app := range m.Apps { - switch a := app.(type) { - case *agentcomm.AppKea: - keaApp = a - case *agentcomm.AppBind9: - bind9App = a - default: - log.Println("NOT IMPLEMENTED") - } - } - - err = updateMachineFieldsKea(db, dbMachine, dbAppsMap, keaApp) - if err != nil { - return err - } - - err = updateMachineFieldsBind9(db, dbMachine, dbAppsMap, bind9App) - if err != nil { - return err - } - - err = dbmodel.RefreshMachineFromDb(db, dbMachine) - if err != nil { - return err - } - return nil } @@ -584,7 +479,7 @@ func (r *RestAPI) DeleteMachine(ctx context.Context, params services.DeleteMachi return rsp } -func appToRestAPI(dbApp *dbmodel.App, hooks map[string][]string) *models.App { +func appToRestAPI(dbApp *dbmodel.App) *models.App { app := models.App{ ID: dbApp.ID, Type: dbApp.Type, @@ -611,10 +506,13 @@ func appToRestAPI(dbApp *dbmodel.App, hooks map[string][]string) *models.App { Active: d.Active, Version: d.Version, ExtendedVersion: d.ExtendedVersion, + Uptime: d.Uptime, + ReloadedAt: strfmt.DateTime(d.ReloadedAt), Hooks: []string{}, } - if hooks != nil { - hooksList, ok := hooks[d.Name] + hooksByDaemon := kea.GetDaemonHooks(dbApp) + if hooksByDaemon != nil { + hooksList, ok := hooksByDaemon[d.Name] if ok { dmn.Hooks = hooksList } @@ -698,7 +596,7 @@ func (r *RestAPI) GetApps(ctx context.Context, params services.GetAppsParams) mi for _, dbA := range dbApps { app := dbA - a := appToRestAPI(&app, nil) + a := appToRestAPI(&app) appsLst = append(appsLst, a) } @@ -730,13 +628,9 @@ func (r *RestAPI) GetApp(ctx context.Context, params services.GetAppParams) midd var a *models.App if dbApp.Type == dbmodel.Bind9AppType { - a = appToRestAPI(dbApp, nil) + a = appToRestAPI(dbApp) } else if dbApp.Type == dbmodel.KeaAppType { - hooksByDaemon, err := kea.GetDaemonHooks(ctx, r.Agents, dbApp) - if err != nil { - log.Warn(err) - } - a = appToRestAPI(dbApp, hooksByDaemon) + a = appToRestAPI(dbApp) } rsp := services.NewGetAppOK().WithPayload(a) return rsp diff --git a/backend/server/restservice/restimpl_test.go b/backend/server/restservice/restimpl_test.go index 8b84d01a3424f2c19d89857172393e069dfdf0f3..b0c41f2ed849824b68d82bb59e25897a9cc4be8a 100644 --- a/backend/server/restservice/restimpl_test.go +++ b/backend/server/restservice/restimpl_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/require" "isc.org/stork/server/agentcomm" + "isc.org/stork/server/apps/kea" dbmodel "isc.org/stork/server/database/model" dbtest "isc.org/stork/server/database/test" "isc.org/stork/server/gen/models" @@ -34,7 +35,7 @@ func TestGetVersion(t *testing.T) { require.Equal(t, "unset", *p.Date) } -func TestGetMachineState(t *testing.T) { +func TestGetMachineStateOnly(t *testing.T) { db, dbSettings, teardown := dbtest.SetupDatabaseTestCase(t) defer teardown() @@ -76,6 +77,164 @@ func TestGetMachineState(t *testing.T) { require.LessOrEqual(t, int64(0), okRsp.Payload.Uptime) } +func mockGetAppsState(callNo int, cmdResponses []interface{}) { + switch callNo { + case 0: + list1 := cmdResponses[0].(*[]kea.VersionGetResponse) + *list1 = []kea.VersionGetResponse{ + { + KeaResponseHeader: agentcomm.KeaResponseHeader{ + Result: 0, + Daemon: "ca", + }, + Arguments: &kea.VersionGetRespArgs{ + Extended: "Extended version", + }, + }, + } + list2 := cmdResponses[1].(*[]kea.CAConfigGetResponse) + *list2 = []kea.CAConfigGetResponse{ + { + KeaResponseHeader: agentcomm.KeaResponseHeader{ + Result: 0, + Daemon: "ca", + }, + Arguments: &kea.CAConfigGetRespArgs{ + ControlAgent: &kea.ControlAgentData{ + ControlSockets: &kea.ControlSocketsData{ + Dhcp4: &kea.SocketData{ + SocketName: "aaaa", + SocketType: "unix", + }, + }, + }, + }, + }, + } + case 1: + // version-get response + list1 := cmdResponses[0].(*[]kea.VersionGetResponse) + *list1 = []kea.VersionGetResponse{ + { + KeaResponseHeader: agentcomm.KeaResponseHeader{ + Result: 0, + Daemon: "dhcp4", + }, + Arguments: &kea.VersionGetRespArgs{ + Extended: "Extended version", + }, + }, + } + // status-get response + list2 := cmdResponses[1].(*[]kea.StatusGetResponse) + *list2 = []kea.StatusGetResponse{ + { + KeaResponseHeader: agentcomm.KeaResponseHeader{ + Result: 0, + Daemon: "dhcp4", + }, + Arguments: &kea.StatusGetRespArgs{ + Pid: 123, + }, + }, + } + // config-get response + list3 := cmdResponses[2].(*[]agentcomm.KeaResponse) + *list3 = []agentcomm.KeaResponse{ + { + KeaResponseHeader: agentcomm.KeaResponseHeader{ + Result: 0, + Daemon: "dhcp4", + }, + Arguments: &map[string]interface{}{ + "Dhcp4": map[string]interface{}{ + "hooks-libraries": []interface{}{ + map[string]interface{}{ + "library": "hook_abc.so", + }, + map[string]interface{}{ + "library": "hook_def.so", + }, + }, + }, + }, + }, + } + } +} + +func TestGetMachineAndAppsState(t *testing.T) { + db, dbSettings, teardown := dbtest.SetupDatabaseTestCase(t) + defer teardown() + + settings := RestAPISettings{} + fa := storktest.NewFakeAgents(mockGetAppsState) + rapi, err := NewRestAPI(&settings, dbSettings, db, fa) + require.NoError(t, err) + ctx := context.Background() + + // add machine + m := &dbmodel.Machine{ + Address: "localhost", + AgentPort: 8080, + } + err = dbmodel.AddMachine(db, m) + require.NoError(t, err) + + // add kea app + keaApp := &dbmodel.App{ + MachineID: m.ID, + Machine: m, + Type: "kea", + CtrlAddress: "1.2.3.4", + CtrlPort: 123, + } + err = dbmodel.AddApp(db, keaApp) + require.NoError(t, err) + m.Apps = append(m.Apps, keaApp) + + // add BIND 9 app + bind9App := &dbmodel.App{ + MachineID: m.ID, + Machine: m, + Type: "bind9", + CtrlAddress: "1.2.3.4", + CtrlPort: 124, + } + err = dbmodel.AddApp(db, bind9App) + require.NoError(t, err) + m.Apps = append(m.Apps, bind9App) + + fa.MachineState = &agentcomm.State{ + Apps: []*agentcomm.App{ + { + Type: "kea", + CtrlAddress: "1.2.3.4", + CtrlPort: 123, + }, + { + Type: "bind9", + CtrlAddress: "1.2.3.4", + CtrlPort: 124, + }, + }, + } + + fa.Bind9State = &agentcomm.Bind9State{ + Version: "1.2.3", + } + + // get added machine + params := services.GetMachineStateParams{ + ID: m.ID, + } + rsp := rapi.GetMachineState(ctx, params) + require.IsType(t, &services.GetMachineStateOK{}, rsp) + okRsp := rsp.(*services.GetMachineStateOK) + require.Len(t, okRsp.Payload.Apps, 2) + require.Equal(t, "kea", okRsp.Payload.Apps[0].Type) +} + func TestCreateMachine(t *testing.T) { db, dbSettings, teardown := dbtest.SetupDatabaseTestCase(t) defer teardown() @@ -207,7 +366,7 @@ func TestGetMachine(t *testing.T) { CtrlPort: 1234, Active: true, Details: dbmodel.AppKea{ - Daemons: []dbmodel.KeaDaemon{}, + Daemons: []*dbmodel.KeaDaemon{}, }, } err = dbmodel.AddApp(db, s) @@ -397,7 +556,7 @@ func TestGetApp(t *testing.T) { CtrlPort: 1234, Active: true, Details: dbmodel.AppKea{ - Daemons: []dbmodel.KeaDaemon{}, + Daemons: []*dbmodel.KeaDaemon{}, }, } err = dbmodel.AddApp(db, s) @@ -449,7 +608,7 @@ func TestRestGetApp(t *testing.T) { CtrlPort: 1234, Active: true, Details: dbmodel.AppKea{ - Daemons: []dbmodel.KeaDaemon{}, + Daemons: []*dbmodel.KeaDaemon{}, }, } err = dbmodel.AddApp(db, keaApp) @@ -464,7 +623,7 @@ func TestRestGetApp(t *testing.T) { okRsp := rsp.(*services.GetAppOK) require.Equal(t, keaApp.ID, okRsp.Payload.ID) - // add bind9 app to machine + // add BIND 9 app to machine bind9App := &dbmodel.App{ ID: 0, MachineID: m.ID, @@ -476,7 +635,7 @@ func TestRestGetApp(t *testing.T) { err = dbmodel.AddApp(db, bind9App) require.NoError(t, err) - // get added bind9 app + // get added BIND 9 app params = services.GetAppParams{ ID: bind9App.ID, } @@ -519,13 +678,13 @@ func TestRestGetApps(t *testing.T) { CtrlPort: 1234, Active: true, Details: dbmodel.AppKea{ - Daemons: []dbmodel.KeaDaemon{}, + Daemons: []*dbmodel.KeaDaemon{}, }, } err = dbmodel.AddApp(db, s1) require.NoError(t, err) - // add app bind9 to machine + // add app BIND 9 to machine s2 := &dbmodel.App{ ID: 0, MachineID: m.ID, @@ -548,7 +707,7 @@ func TestRestGetApps(t *testing.T) { // Generates a response to the status-get command including two status // structures, one for DHCPv4 and one for DHCPv6. Both contain HA // status information. -func mockGetStatusWithHA(response interface{}) { +func mockGetStatusWithHA(callNo int, cmdResponses []interface{}) { daemons, _ := agentcomm.NewKeaDaemons("dhcp4", "dhcp6") command, _ := agentcomm.NewKeaCommand("status-get", daemons, nil) json := `[ @@ -601,7 +760,7 @@ func mockGetStatusWithHA(response interface{}) { } } ]` - _ = agentcomm.UnmarshalKeaResponseList(command, json, response) + _ = agentcomm.UnmarshalKeaResponseList(command, json, cmdResponses[0]) } // Test that status of two HA services for a Kea application is parsed @@ -733,7 +892,7 @@ func TestRestGetAppsStats(t *testing.T) { CtrlPort: 1234, Active: true, Details: dbmodel.AppKea{ - Daemons: []dbmodel.KeaDaemon{}, + Daemons: []*dbmodel.KeaDaemon{}, }, } err = dbmodel.AddApp(db, s1) diff --git a/backend/server/test/fake_agents.go b/backend/server/test/fake_agents.go index fca8171a4fe19a638bcce9481364d575b0ed65bd..dd3e6ea6dd5e8745a473291fefa528e542b69681 100644 --- a/backend/server/test/fake_agents.go +++ b/backend/server/test/fake_agents.go @@ -8,9 +8,22 @@ import ( // Helper struct to mock Agents behavior. type FakeAgents struct { - RecordedURL string - RecordedCommand string - mockFunc func(interface{}) + RecordedURL string + RecordedCommands []string + mockFunc func(int, []interface{}) + callNo int + + MachineState *agentcomm.State + Bind9State *agentcomm.Bind9State +} + +// Creates new instance of the FakeAgents structure with the function returning +// a custom response set. +func NewFakeAgents(fn func(int, []interface{})) *FakeAgents { + fa := &FakeAgents{ + mockFunc: fn, + } + return fa } func (fa *FakeAgents) Shutdown() {} @@ -20,6 +33,10 @@ func (fa *FakeAgents) GetConnectedAgent(address string) (*agentcomm.Agent, error // FakeAgents specific implementation of the GetState. func (fa *FakeAgents) GetState(ctx context.Context, address string, agentPort int64) (*agentcomm.State, error) { + if fa.MachineState != nil { + return fa.MachineState, nil + } + state := agentcomm.State{ Cpus: 1, Memory: 4, @@ -27,26 +44,29 @@ func (fa *FakeAgents) GetState(ctx context.Context, address string, agentPort in return &state, nil } -// Creates new instance of the FakeAgents structure with the function returning -// a custom response set. -func NewFakeAgents(fn func(interface{})) *FakeAgents { - fa := &FakeAgents{ - mockFunc: fn, - } - return fa -} - // FakeAgents specific Implementation of the function to forward a command // to the Kea servers. It records some arguments used in the call to this // function so as they can be later validated. It also returns a custom // response to the command by calling the function specified in the // call to NewFakeAgents. -func (fa *FakeAgents) ForwardToKeaOverHTTP(ctx context.Context, caURL string, agentAddress string, agentPort int64, command *agentcomm.KeaCommand, response interface{}) error { +func (fa *FakeAgents) ForwardToKeaOverHTTP(ctx context.Context, agentAddress string, agentPort int64, caURL string, commands []*agentcomm.KeaCommand, cmdResponses ...interface{}) (*agentcomm.KeaCmdsResult, error) { fa.RecordedURL = caURL - fa.RecordedCommand = command.Command + result := &agentcomm.KeaCmdsResult{} + for _, cmd := range commands { + fa.RecordedCommands = append(fa.RecordedCommands, cmd.Command) + result.CmdsErrors = append(result.CmdsErrors, nil) + } // Generate response. if fa.mockFunc != nil { - fa.mockFunc(response) + fa.mockFunc(fa.callNo, cmdResponses) + } + fa.callNo++ + return result, nil +} + +func (fa *FakeAgents) GetBind9State(ctx context.Context, agentAddress string, agentPort int64) (*agentcomm.Bind9State, error) { + if fa.Bind9State != nil { + return fa.Bind9State, nil } - return nil + return nil, nil } diff --git a/webui/src/app/apps-page/apps-page.component.html b/webui/src/app/apps-page/apps-page.component.html index 287fb34edffb4325ac2faaa0e0119dc63b418529..249175e9c5e4aa02fde35e462cd0c0a0df11d336 100644 --- a/webui/src/app/apps-page/apps-page.component.html +++ b/webui/src/app/apps-page/apps-page.component.html @@ -116,13 +116,11 @@