diff --git a/Dangerfile b/Dangerfile index a1805b2ab5b4b59c43dd3ffac6edff422f2b5700..09884bfca4d1feba90f2b857d55bda7f9011f1bd 100644 --- a/Dangerfile +++ b/Dangerfile @@ -15,13 +15,13 @@ has_milestone = gitlab.mr_json["milestone"] != nil warn("This MR does not refer to an existing milestone", sticky: true) unless has_milestone # check commits' comments -commit_lint.check +commit_lint.check warn: :all # check gitlab issue in commit message git.commits.each do |c| m = c.message.match(/^\[\#(\d+)\]\ (.*)/) if not m - failure "No GitLab issue in commit message: #{c}" + warn "No GitLab issue in commit message: #{c}" gl_issue_msg = nil else gl_issue_msg = m.captures[0] @@ -30,13 +30,13 @@ git.commits.each do |c| mr_branch = gitlab.branch_for_head m = mr_branch.match(/^(\d+).*/) if not m - failure "Branch name does not start with GitLab issue: #{mr_branch}" + fail "Branch name does not start with GitLab issue: #{mr_branch}" gl_issue_br = nil else gl_issue_br = m.captures[0] end if gl_issue_msg and gl_issue_br and gl_issue_msg != gl_issue_br - failure "GitLab issue ##{gl_issue_msg} in msg of commit #{c} and issue ##{gl_issue_br} from branch #{mr_branch} do not match" + warn "GitLab issue ##{gl_issue_msg} in msg of commit #{c} and issue ##{gl_issue_br} from branch #{mr_branch} do not match" end end diff --git a/Rakefile b/Rakefile index 60d934e448d718518a3c33ceb6846b8b0aa04914..839a59e03e0f2013052fe4460c17bc8c8d8ebd44 100644 --- a/Rakefile +++ b/Rakefile @@ -153,6 +153,17 @@ file AGENT_PB_GO_FILE => [GO, PROTOC, PROTOC_GEN_GO, AGENT_PROTO_FILE] do end end +# prepare args for dlv debugger +headless = '' +if ENV['headless'] == 'true' + headless = '--headless -l 0.0.0.0:45678' +end + +desc 'Connect gdlv GUI Go debugger to waiting dlv debugger' +task :connect_dbg do + sh 'gdlv connect 127.0.0.1:45678' +end + desc 'Generate API sources from agent.proto' task :gen_agent => [AGENT_PB_GO_FILE] @@ -163,24 +174,28 @@ end desc 'Run agent' task :run_agent => [:build_agent, GO] do - sh "backend/cmd/stork-agent/stork-agent --port 8888" + if ENV['debug'] == 'true' + sh "cd backend/cmd/stork-agent/ && dlv #{headless} debug" + else + sh "backend/cmd/stork-agent/stork-agent --port 8888" + end end - desc 'Run server' -task :run_server, [:dbg] => [:build_server, GO] do |t, args| - args.with_defaults(:dbg => false) - if args[:dbg] - sh "cd backend/cmd/stork-server/ && dlv debug" +task :run_server => [:build_server, GO] do |t, args| + if ENV['debug'] == 'true' + sh "cd backend/cmd/stork-server/ && dlv #{headless} debug" else - # sh "backend/cmd/stork-server/stork-server" - sh "backend/cmd/stork-server/stork-server --db-trace-queries" + cmd = 'backend/cmd/stork-server/stork-server' + if ENV['dbtrace'] == 'true' + cmd = "#{cmd} --db-trace-queries" + end + sh cmd end end desc 'Run server with local postgres docker container' -task :run_server_db, [:dbg] do |t, args| - args.with_defaults(:dbg => false) +task :run_server_db do |t, args| ENV['STORK_DATABASE_NAME'] = "storkapp" ENV['STORK_DATABASE_USER_NAME'] = "storkapp" ENV['STORK_DATABASE_PASSWORD'] = "storkapp" @@ -190,7 +205,7 @@ task :run_server_db, [:dbg] do |t, args| sh "docker rm -f 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(args[:dbg]) + Rake::Task["run_server"].invoke() end @@ -227,11 +242,22 @@ task :unittest_backend => [GO, RICHGO, MOCKERY, MOCKGEN, :build_server, :build_a sh 'rm -f backend/server/agentcomm/api_mock.go' } sh 'rm -f backend/server/agentcomm/api_mock.go' + if ENV['scope'] + scope = ENV['scope'] + else + scope = './...' + end Dir.chdir('backend') do sh "#{GO} generate -v ./..." - sh "#{RICHGO} test -race -v -count=1 -p 1 -coverprofile=coverage.out ./..." # count=1 disables caching results - #sh "#{RICHGO} test -race -v -count=1 -p 1 -coverprofile=coverage.out ./server/database/session" # count=1 disables caching results - #sh "dlv test ./server/database" # count=1 disables caching results + if ENV['debug'] == 'true' + sh "dlv #{headless} test #{scope}" + else + gotool = RICHGO + if ENV['richgo'] == 'false' + gotool = GO + end + sh "#{gotool} test -race -v -count=1 -p 1 -coverprofile=coverage.out #{scope}" # count=1 disables caching results + end # check coverage level out = `#{GO} tool cover -func=coverage.out` @@ -250,8 +276,8 @@ task :unittest_backend => [GO, RICHGO, MOCKERY, MOCKGEN, :build_server, :build_a 'Password', 'loggingMiddleware', 'GlobalMiddleware', 'Authorizer', 'CreateSession', 'DeleteSession', 'Listen', 'Shutdown', 'NewRestUser', 'GetUsers', 'GetUser', 'CreateUser', 'UpdateUser'] - if cov < 50 and not ignore_list.include? func - puts "FAIL: %-80s %5s%% < 50%%" % ["#{file} #{func}", "#{cov}"] + if cov < 35 and not ignore_list.include? func + puts "FAIL: %-80s %5s%% < 35%%" % ["#{file} #{func}", "#{cov}"] problem = true end end @@ -361,13 +387,24 @@ task :docker_up => [:build_backend, :build_ui] do at_exit { sh "docker-compose down" } - sh "docker-compose build" - sh "docker-compose up" + sh 'docker-compose build' + sh 'docker-compose up' end desc 'Shut down all containers' task :docker_down do - sh "docker-compose down" + sh 'docker-compose down' +end + +desc 'Build container with Stork Agent and Kea' +task :build_agent_container do + sh 'docker build -f docker/docker-agent-kea.txt -t agent-kea .' +end + +desc 'Run container with Stork Agent and Kea and mount current Agent binary' +task :run_agent_container do + # host[8888]->agent[8080], host[8787]->kea-ca[8000] + sh 'docker run --rm -ti -p 8888:8080 -p 8787:8000 -v `pwd`/backend/cmd/stork-agent:/agent agent-kea' end @@ -405,7 +442,10 @@ task :clean do end desc 'Download all dependencies' -task :prepare_env => [GO, GOSWAGGER, GOLANGCILINT, SWAGGER_CODEGEN, NPX] +task :prepare_env => [GO, GOSWAGGER, GOLANGCILINT, SWAGGER_CODEGEN, NPX] do + sh "#{GO} get -u github.com/go-delve/delve/cmd/dlv" + sh "#{GO} get -u github.com/aarzilli/gdlv" +end desc 'Generate ctags for Emacs' task :ctags do diff --git a/api/swagger.yaml b/api/swagger.yaml index c2bcffec3fdba10623dace8c6760d20bc9adf4c4..5c533ecd1387e63bcb07cd5e355bf8a943587927 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -187,9 +187,9 @@ paths: - $ref: '#/parameters/paginationStartParam' - $ref: '#/parameters/paginationLimitParam' - $ref: '#/parameters/filterTextParam' - - name: service + - name: app in: query - description: Limit returned list of machines to these which provide given service, possible values 'bind' or 'kea'. + description: Limit returned list of machines to these which provide given app, possible values 'bind' or 'kea'. type: string responses: 200: @@ -313,6 +313,59 @@ paths: schema: $ref: "#/definitions/ApiError" + /apps: + get: + summary: Get list of apps. + description: >- + It is possible to filter the list of apps by several fields. It is also always paged. + Default page size is 10. + A list of apps is returned in items field accompanied by total count + which indicates total available number of records for given filtering + parameters. + operationId: getApps + tags: + - Services + parameters: + - $ref: '#/parameters/paginationStartParam' + - $ref: '#/parameters/paginationLimitParam' + - $ref: '#/parameters/filterTextParam' + - name: app + in: query + description: Limit returned list of apps, possible values 'bind' or 'kea'. + type: string + responses: + 200: + description: List of apps + schema: + $ref: "#/definitions/Apps" + default: + description: generic error response + schema: + $ref: "#/definitions/ApiError" + + /apps/{id}: + get: + summary: Get app by ID. + description: Get app by the database specific ID. + operationId: getApp + tags: + - Services + parameters: + - in: path + name: id + type: integer + required: true + description: App ID. + responses: + 200: + description: A app + schema: + $ref: "#/definitions/App" + default: + description: generic error response + schema: + $ref: "#/definitions/ApiError" + parameters: paginationStartParam: name: start @@ -329,7 +382,9 @@ parameters: filterTextParam: name: text in: query - description: Filtering text, e.g. hostname for the machines. + description: >- + Filtering text, e.g. hostname for the machines + or version for the apps. type: string definitions: @@ -460,6 +515,23 @@ definitions: error: type: string readOnly: true + apps: + type: array + items: + $ref: '#/definitions/MachineApp' + + MachineApp: + type: object + properties: + id: + type: integer + type: + type: string + version: + type: string + active: + type: boolean + Machines: type: object properties: @@ -470,6 +542,78 @@ definitions: total: type: integer + App: + type: object + properties: + id: + type: integer + readOnly: true + type: + type: string + ctrlPort: + type: integer + active: + type: boolean + version: + type: string + machine: + $ref: '#/definitions/AppMachine' + details: + allOf: + - $ref: '#/definitions/AppKea' + - $ref: '#/definitions/AppBind' + + KeaDaemon: + type: object + properties: + pid: + type: integer + name: + type: string + active: + type: boolean + version: + type: string + extendedVersion: + type: string + + AppKea: + type: object + properties: + extendedVersion: + type: string + daemons: + type: array + items: + $ref: '#/definitions/KeaDaemon' + + AppBind: + type: object + properties: + todo: + type: string + + AppMachine: + type: object + properties: + id: + type: integer + readOnly: true + address: + type: string + hostname: + type: string + + Apps: + type: object + properties: + items: + type: array + items: + $ref: '#/definitions/App' + total: + type: integer + ApiError: type: object required: diff --git a/backend/agent/agent.go b/backend/agent/agent.go index 3e43f6e355aa855ce1590f1775ff384263d4ac3b..351c7e98c7d3337bf63b66aa65e6b04749858dd9 100644 --- a/backend/agent/agent.go +++ b/backend/agent/agent.go @@ -26,6 +26,7 @@ type AgentSettings struct { // Global Stork Agent state type StorkAgent struct { Settings AgentSettings + AppMonitor AppMonitor } @@ -33,16 +34,44 @@ type StorkAgent struct { // Get state of machine. func (s *StorkAgent) GetState(ctx context.Context, in *agentapi.GetStateReq) (*agentapi.GetStateRsp, error) { - log.Printf("Received: GetState %v", in) - vm, _ := mem.VirtualMemory() hostInfo, _ := host.Info() load, _ := load.Avg() loadStr := fmt.Sprintf("%.2f %.2f %.2f", load.Load1, load.Load5, load.Load15) + var apps []*agentapi.App + for _, srv := range s.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, + CtrlPort: s.CtrlPort, + Active: s.Active, + App: &agentapi.App_Kea{ + Kea: &agentapi.AppKea{ + ExtendedVersion: s.ExtendedVersion, + Daemons: daemons, + }, + }, + }) + default: + panic(fmt.Sprint("Unknown app type")) + } + } + state := agentapi.GetStateRsp{ AgentVersion: stork.Version, - //Services []*Service + Apps: apps, Hostname: hostInfo.Hostname, Cpus: int64(runtime.NumCPU()), CpusLoad: loadStr, @@ -60,16 +89,11 @@ func (s *StorkAgent) GetState(ctx context.Context, in *agentapi.GetStateReq) (*a HostID: hostInfo.HostID, Error: "", } - return &state, nil -} -// Detect services (Kea, Bind). -func (s *StorkAgent) DetectServices(ctx context.Context, in *agentapi.DetectServicesReq) (*agentapi.DetectServicesRsp, error) { - log.Printf("Received: DetectServices %v", in) - return &agentapi.DetectServicesRsp{Abc: "321"}, nil + return &state, nil } -// Restart Kea service. +// Restart Kea app. func (s *StorkAgent) RestartKea(ctx context.Context, in *agentapi.RestartKeaReq) (*agentapi.RestartKeaRsp, error) { log.Printf("Received: RestartKea %v", in) return &agentapi.RestartKeaRsp{Xyz: "321"}, nil diff --git a/backend/agent/agent_test.go b/backend/agent/agent_test.go index a49538f0f1fc3913d8fe03f9c9ff32dcde0313f5..0ab2a3459fca33c98c38b25408cb62a0635ca217 100644 --- a/backend/agent/agent_test.go +++ b/backend/agent/agent_test.go @@ -10,12 +10,40 @@ import ( "isc.org/stork" ) +type FakeAppMonitor struct { + Apps []interface{} +} + +func (fsm *FakeAppMonitor) GetApps() []interface{} { + return nil +} + +func (fsm *FakeAppMonitor) Shutdown() { +} + func TestGetState(t *testing.T) { - sa := StorkAgent{} + fsm := FakeAppMonitor{} + sa := StorkAgent{ + AppMonitor: &fsm, + } + // app monitor is empty, no apps should be returned by GetState ctx := context.Background() rsp, err := sa.GetState(ctx, &agentapi.GetStateReq{}) require.NoError(t, err) require.Equal(t, rsp.AgentVersion, stork.Version) + + // add some app to app monitor so GetState should return something + var apps []interface{} + apps = append(apps, AppKea{ + AppCommon: AppCommon{ + Version: "1.2.3", + Active: true, + }, + }) + fsm.Apps = apps + rsp, err = sa.GetState(ctx, &agentapi.GetStateReq{}) + require.NoError(t, err) + require.Equal(t, rsp.AgentVersion, stork.Version) } diff --git a/backend/agent/monitor.go b/backend/agent/monitor.go new file mode 100644 index 0000000000000000000000000000000000000000..37cc462cc7f873eff780bd9d66159f2e0a8f6ddb --- /dev/null +++ b/backend/agent/monitor.go @@ -0,0 +1,292 @@ +package agent + +import ( + "fmt" + "time" + "bytes" + "net/http" + "regexp" + "strconv" + "io/ioutil" + "encoding/json" + + log "github.com/sirupsen/logrus" + "github.com/pkg/errors" + "github.com/shirou/gopsutil/process" +) + +type KeaDaemon struct { + Pid int32 + Name string + Active bool + Version string + ExtendedVersion string +} + +type AppCommon struct { + Version string + CtrlPort int64 + Active bool +} + +type AppKea struct { + AppCommon + ExtendedVersion string + Daemons []KeaDaemon +} + +type AppBind struct { + AppCommon +} + +type AppMonitor interface { + GetApps() []interface{} + 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 + + apps []interface{} // list of detected apps on the host +} + +func NewAppMonitor() *appMonitor { + sm := &appMonitor{ + requests: make(chan chan []interface{}), + quit: make(chan bool), + } + go sm.run() + return sm +} + +func (sm *appMonitor) run() { + const DETECTION_INTERVAL = 10 * time.Second + + for { + select { + case ret := <- sm.requests: + // process user request + ret <- sm.apps + + case <- time.After(DETECTION_INTERVAL): + // periodic detection + sm.detectApps() + + case <- sm.quit: + // exit run + return + } + } +} + +func getCtrlPortFromKeaConfig(path string) int { + 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 port: %+v", err) + return 0 + } + + port, err := strconv.Atoi(m[1]) + if err != nil { + log.Warnf("cannot parse port: %+v", err) + return 0 + } + return port +} + + +func keaDaemonVersionGet(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 := http.Post(caUrl, "application/json", 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 detectKeaApp(match []string) *AppKea { + var keaApp *AppKea + + keaConfPath := match[1] + + ctrlPort := int64(getCtrlPortFromKeaConfig(keaConfPath)) + keaApp = &AppKea{ + AppCommon: AppCommon{ + CtrlPort: ctrlPort, + Active: false, + }, + Daemons: []KeaDaemon{}, + } + if ctrlPort == 0 { + return nil + } + + caUrl := fmt.Sprintf("http://localhost:%d", ctrlPort) + + // retrieve ctrl-agent information, it is also used as a general app information + info, err := keaDaemonVersionGet(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 := http.Post(caUrl, "application/json", 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(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-ctr-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+)`) + + // TODO: BIND app is not yet being detect. It should happen here as well. + + var apps []interface{} + + procs, _ := process.Processes() + for _, p := range procs { + cmdline, err := p.Cmdline() + if err != nil { + log.Warnf("cannot get process command line %+v", err) + } + + // detect kea + m := keaPtrn.FindStringSubmatch(cmdline) + if m != nil { + keaApp := detectKeaApp(m) + if keaApp != nil { + apps = append(apps, *keaApp) + } + } + } + + sm.apps = apps +} + +func (sm *appMonitor) GetApps() []interface{} { + ret := make(chan []interface{}) + sm.requests <- ret + srvs := <- ret + return srvs +} + +func (sm *appMonitor) Shutdown() { + sm.quit <- true +} diff --git a/backend/agent/monitor_test.go b/backend/agent/monitor_test.go new file mode 100644 index 0000000000000000000000000000000000000000..de0f82777580710dbad0b0e4df90ef8a0a5ae79b --- /dev/null +++ b/backend/agent/monitor_test.go @@ -0,0 +1,133 @@ +package agent + +import ( + "os" + "log" + "io/ioutil" + "testing" + "gopkg.in/h2non/gock.v1" + "github.com/stretchr/testify/require" +) + + +func TestGetApps(t *testing.T) { + sm := NewAppMonitor() + apps := sm.GetApps() + require.Len(t, apps, 0) + sm.Shutdown() +} + +func TestKeaDaemonVersionGetBadUrl(t *testing.T) { + _, err := keaDaemonVersionGet("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"}}) + + data, err := keaDaemonVersionGet("http://localhost:45634/", "") + require.NoError(t, err) + require.Equal(t, true, gock.IsDone()) + require.Equal(t, map[string]interface{}{"arguments":"bar"}, data) +} + +func TestGetCtrlPortFromKeaConfigNonExisting(t *testing.T) { + // check reading from non existing file + path := "/tmp/non-exisiting-path" + port := getCtrlPortFromKeaConfig(path) + require.Equal(t, 0, port) +} + +func TestGetCtrlPortFromKeaConfigBadContent(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("random content") + 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 prepared file with bad content + // so 0 should be returned as port + port := getCtrlPortFromKeaConfig(tmpFile.Name()) + require.Equal(t, 0, port) +} + +func TestGetCtrlPortFromKeaConfigOk(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("\"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 + port := getCtrlPortFromKeaConfig(tmpFile.Name()) + require.Equal(t, 1234, port) +} + +func TestDetectApps(t *testing.T) { + sm := NewAppMonitor() + sm.detectApps() + sm.Shutdown() +} + +func TestDetectKeaApp(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("\"http-port\": 45634") + 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) + } + + // 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", + }}) + + // check kea app detection + srv := detectKeaApp([]string{"", tmpFile.Name()}) + require.Nil(t, srv) +} diff --git a/backend/api/agent.proto b/backend/api/agent.proto index 25f8f56bc603e02561fafd8685fdae0e26aac2d0..8b51a37a19797551aca4fbbdf97a866d4fc9d0a6 100644 --- a/backend/api/agent.proto +++ b/backend/api/agent.proto @@ -4,7 +4,6 @@ package agentapi; service Agent { rpc getState(GetStateReq) returns (GetStateRsp) {} - rpc detectServices(DetectServicesReq) returns (DetectServicesRsp) {} rpc restartKea(RestartKeaReq) returns (RestartKeaRsp) {} } @@ -13,7 +12,7 @@ message GetStateReq { message GetStateRsp { string agentVersion = 1; - repeated Service services = 2; + repeated App apps = 2; string hostname = 3; int64 cpus = 4; string cpusLoad = 5; @@ -32,27 +31,30 @@ message GetStateRsp { string hostID = 18; } -message Service { - oneof serviceType { - ServiceKea kea = 1; - ServiceBind bind = 2; - } -} - -message ServiceKea { +message App { string version = 1; + int64 ctrlPort = 2; + bool active = 3; + oneof app { + AppKea kea = 4; + AppBind bind = 5; + } } -message ServiceBind { - string version = 1; +message AppKea { + string extendedVersion = 1; + repeated KeaDaemon daemons = 2; } -message DetectServicesReq { - string abc = 1; +message KeaDaemon { + int32 pid = 1; + string name = 2; + bool active = 3; + string version = 4; + string extendedVersion = 5; } -message DetectServicesRsp { - string abc = 1; +message AppBind { } message RestartKeaReq { diff --git a/backend/cmd/stork-agent/main.go b/backend/cmd/stork-agent/main.go index 1c696f716f01dcc427defa58acdf0e58d19e10cc..14ac4e332dd47acea7b9723082cb40458ee8d549 100644 --- a/backend/cmd/stork-agent/main.go +++ b/backend/cmd/stork-agent/main.go @@ -3,21 +3,22 @@ package main import ( "os" - log "github.com/sirupsen/logrus" flags "github.com/jessevdk/go-flags" + "isc.org/stork" "isc.org/stork/agent" ) func main() { - storkAgent := agent.StorkAgent{} - // Setup logging - log.SetOutput(os.Stdout) - log.SetFormatter(&log.TextFormatter{ - FullTimestamp: true, - }) + stork.SetupLogging() + + // Start app monitor + sm := agent.NewAppMonitor() + storkAgent := agent.StorkAgent{ + AppMonitor: sm, + } // Prepare parse for command line flags. parser := flags.NewParser(&storkAgent.Settings, flags.Default) diff --git a/backend/cmd/stork-server/main.go b/backend/cmd/stork-server/main.go index c68cb6c9566038317a040f23e35054cf774e34be..dc1caa3aaf6cbd06dd0a0efc29dd7ecc4f408b2c 100644 --- a/backend/cmd/stork-server/main.go +++ b/backend/cmd/stork-server/main.go @@ -1,37 +1,15 @@ package main import ( - "os" - "fmt" - "path" - "runtime" - log "github.com/sirupsen/logrus" + "isc.org/stork" "isc.org/stork/server" ) func main() { // Setup logging - log.SetLevel(log.DebugLevel) - log.SetOutput(os.Stdout) - log.SetReportCaller(true) - log.SetFormatter(&log.TextFormatter{ - FullTimestamp: true, - TimestampFormat: "2006-01-02 15:04:05", - //PadLevelText: true, - // FieldMap: log.FieldMap{ - // FieldKeyTime: "@timestamp", - // FieldKeyLevel: "@level", - // FieldKeyMsg: "@message", - // }, - CallerPrettyfier: func(f *runtime.Frame) (string, string) { - // Grab filename and line of current frame and add it to log entry - _, filename := path.Split(f.File) - return "", fmt.Sprintf("%20v:%-5d", filename, f.Line) - }, - }) - + stork.SetupLogging() // Initialize global state of Stork Server storkServer, err := server.NewStorkServer() diff --git a/backend/go.mod b/backend/go.mod index 32e12dbed284d2f2b09c4242e4716e195705bcba..3eb4ca6c4f838dc506ebdde880eae888220da282 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -28,4 +28,5 @@ require ( golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 google.golang.org/grpc v1.24.0 + gopkg.in/h2non/gock.v1 v1.0.15 ) diff --git a/backend/go.sum b/backend/go.sum index 756b2536a9ccead012c108c94d852ba69c163652..99ab1a82f0bf369c8ee16e736e3f72e692b69d8c 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -107,6 +107,8 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= @@ -131,6 +133,7 @@ github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e h1:hB2xlXdHp/pmPZq github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -210,6 +213,8 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/h2non/gock.v1 v1.0.15 h1:SzLqcIlb/fDfg7UvukMpNcWsu7sI5tWwL+KCATZqks0= +gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/backend/server/agentcomm/grpcli.go b/backend/server/agentcomm/grpcli.go index 223cad9a05dd4dd09a79afb99324fc928a583941..351caa02ac532d9d59230e6d5f35eee91de2a9ea 100644 --- a/backend/server/agentcomm/grpcli.go +++ b/backend/server/agentcomm/grpcli.go @@ -13,6 +13,30 @@ import ( "isc.org/stork/api" ) +type KeaDaemon struct { + Pid int32 + Name string + Active bool + Version string + ExtendedVersion string +} + +type AppCommon struct { + Version string + CtrlPort int64 + Active bool +} + +type AppKea struct { + AppCommon + ExtendedVersion string + Daemons []KeaDaemon +} + +type AppBind struct { + AppCommon +} + // State of the machine. It describes multiple properties of the machine like number of CPUs // or operating system name and version. type State struct { @@ -35,6 +59,7 @@ type State struct { HostID string LastVisited time.Time Error string + Apps []interface{} } // Get version from agent. @@ -49,10 +74,50 @@ func (agents *connectedAgentsData) GetState(ctx context.Context, address string, // Call agent for version. grpcState, err := agent.Client.GetState(ctx, &agentapi.GetStateReq{}) if err != nil { - return nil, errors.Wrap(err, "problem with connection to agent") + // reconnect and try again + err2 := agent.MakeGrpcConnection() + if err2 != nil { + log.Warn(err) + return nil, errors.Wrap(err2, "problem with connection to agent") + } + grpcState, err = agent.Client.GetState(ctx, &agentapi.GetStateReq{}) + if err != nil { + return nil, errors.Wrap(err, "problem with connection to agent") + } } - log.Printf("state returned is %+v", grpcState) + + 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, + CtrlPort: srv.CtrlPort, + Active: srv.Active, + }, + ExtendedVersion: s.Kea.ExtendedVersion, + Daemons: daemons, + }) + case *agentapi.App_Bind: + log.Println("NOT IMPLEMENTED") + default: + log.Println("unsupported app type") + } + } state := State{ Address: address, @@ -74,6 +139,7 @@ func (agents *connectedAgentsData) GetState(ctx context.Context, address string, HostID: grpcState.HostID, LastVisited: stork.UTCNow(), Error: grpcState.Error, + Apps: apps, } return &state, nil diff --git a/backend/server/agentcomm/grpcli_test.go b/backend/server/agentcomm/grpcli_test.go index baf8c89ff741605981cb21f25067b13e9fde3b4a..63c65b14a0cead80dd23efc11ea3429bacf3a022 100644 --- a/backend/server/agentcomm/grpcli_test.go +++ b/backend/server/agentcomm/grpcli_test.go @@ -12,7 +12,7 @@ import ( //go:generate mockgen -package=agentcomm -destination=api_mock.go isc.org/stork/api AgentClient -func TestGetVersion(t *testing.T) { +func TestGetState(t *testing.T) { settings := AgentsSettings{} agents := NewConnectedAgents(&settings) @@ -27,10 +27,22 @@ func TestGetVersion(t *testing.T) { mockAgentClient := NewMockAgentClient(ctrl) agent.Client = mockAgentClient - // Call GetVersion + // Call GetState expVer := "123" + rsp := agentapi.GetStateRsp{ + AgentVersion: expVer, + Apps: []*agentapi.App{ + { + Version: "1.2.3", + App: &agentapi.App_Kea{ + Kea: &agentapi.AppKea{ + }, + }, + }, + }, + } mockAgentClient.EXPECT().GetState(gomock.Any(), gomock.Any()). - Return(&agentapi.GetStateRsp{AgentVersion: expVer}, nil) + Return(&rsp, nil) // Check response ctx := context.Background() diff --git a/backend/server/database/migrations/4_add_service.go b/backend/server/database/migrations/4_add_service.go new file mode 100644 index 0000000000000000000000000000000000000000..32428f892b557b0a4be0e7d6925a5fb2b7d6ac49 --- /dev/null +++ b/backend/server/database/migrations/4_add_service.go @@ -0,0 +1,39 @@ +package dbmigs + +import ( + "github.com/go-pg/migrations/v7" +) + +func init() { + migrations.MustRegisterTx(func(db migrations.DB) error { + _, err := db.Exec(` + -- Apps table. + CREATE TABLE public.app ( + id SERIAL PRIMARY KEY, + created TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT (now() AT TIME ZONE 'utc'), + deleted TIMESTAMP WITHOUT TIME ZONE, + machine_id INTEGER REFERENCES public.machine(id) NOT NULL, + type VARCHAR(10) NOT NULL, + ctrl_port INTEGER DEFAULT 0, + active BOOLEAN DEFAULT FALSE, + meta JSONB, + details JSONB, + UNIQUE (machine_id, ctrl_port) + ); + + -- App should be deleted after creation. + ALTER TABLE public.app + ADD CONSTRAINT app_created_deleted_check CHECK ( + (deleted > created) + ); + `) + return err + + }, func(db migrations.DB) error { + _, err := db.Exec(` + -- Remove table with apps. + DROP TABLE public.app; + `) + return err + }) +} diff --git a/backend/server/database/migrations_test.go b/backend/server/database/migrations_test.go index 57142a568ea7e544eb8d67fe6b8b6e79c5903adc..f36c7675f307d331a1b03b7ae3be66bb25f89242 100644 --- a/backend/server/database/migrations_test.go +++ b/backend/server/database/migrations_test.go @@ -106,7 +106,7 @@ func TestInitMigrateToLatest(t *testing.T) { o, n, err := MigrateToLatest(db) require.NoError(t, err) require.Equal(t, int64(0), o) - require.GreaterOrEqual(t, int64(3), n) + require.GreaterOrEqual(t, int64(4), n) } // Test that available schema version is returned as expected. diff --git a/backend/server/database/model/app.go b/backend/server/database/model/app.go new file mode 100644 index 0000000000000000000000000000000000000000..343f44c079ceef350081bd161ffe17c894594de1 --- /dev/null +++ b/backend/server/database/model/app.go @@ -0,0 +1,157 @@ +package dbmodel + +import ( + "time" + "encoding/json" + "github.com/go-pg/pg/v9" + "github.com/go-pg/pg/v9/orm" + "github.com/pkg/errors" + "isc.org/stork" +) + +type KeaDaemon struct { + Pid int32 + Name string + Active bool + Version string + ExtendedVersion string +} + +type AppKea struct { + ExtendedVersion string + Daemons []KeaDaemon +} + +type AppBind struct { +} + +// Part of app table in database that describes metadata of app. In DB it is stored as JSONB. +type AppMeta struct { + Version string +} + +// Represents a app held in app table in the database. +type App struct { + Id int64 + Created time.Time + Deleted time.Time + MachineID int64 + Machine Machine + Type string + CtrlPort int64 + Active bool + Meta AppMeta + Details interface{} +} + +func AddApp(db *pg.DB, app *App) error { + err := db.Insert(app) + if err != nil { + return errors.Wrapf(err, "problem with inserting app %v", app) + } + return nil +} + +func ReconvertAppDetails(app *App) error { + bytes, err := json.Marshal(app.Details) + if err != nil { + return errors.Wrapf(err, "problem with getting app from db: %v ", app) + } + var s AppKea + err = json.Unmarshal(bytes, &s) + if err != nil { + return errors.Wrapf(err, "problem with getting app from db: %v ", app) + } + app.Details = s + return nil +} + +func GetAppById(db *pg.DB, id int64) (*App, error) { + app := App{} + q := db.Model(&app).Where("app.id = ?", id) + q = q.Relation("Machine") + err := q.Select() + if err == pg.ErrNoRows { + return nil, nil + } else if err != nil { + return nil, errors.Wrapf(err, "problem with getting app %v", id) + } + err = ReconvertAppDetails(&app) + if err != nil { + return nil, err + } + return &app, nil +} + +func GetAppsByMachine(db *pg.DB, machineId int64) ([]App, error) { + var apps []App + + q := db.Model(&apps) + q = q.Where("machine_id = ?", machineId) + err := q.Select() + if err != nil { + return nil, errors.Wrapf(err, "problem with getting apps") + } + + for idx := range apps { + err = ReconvertAppDetails(&apps[idx]) + if err != nil { + return nil, err + } + } + return apps, nil +} + +// Fetches a collection of apps from the database. The offset and limit specify the +// beginning of the page and the maximum size of the page. Limit has to be greater +// then 0, otherwise error is returned. +func GetAppsByPage(db *pg.DB, offset int64, limit int64, text string, appType string) ([]App, int64, error) { + if limit == 0 { + return nil, 0, errors.New("limit should be greater than 0") + } + var apps []App + + // prepare query + q := db.Model(&apps) + q = q.Where("app.deleted is NULL") + q = q.Relation("Machine") + if appType != "" { + q = q.Where("type = ?", appType) + } + if text != "" { + text = "%" + text + "%" + q = q.WhereGroup(func(qq *orm.Query) (*orm.Query, error) { + qq = qq.WhereOr("meta->>'Version' ILIKE ?", text) + return qq, nil + }) + } + + // and then, first get total count + total, err := q.Clone().Count() + if err != nil { + return nil, 0, errors.Wrapf(err, "problem with getting apps total") + } + + // then retrive given page of rows + q = q.Order("id ASC").Offset(int(offset)).Limit(int(limit)) + err = q.Select() + if err != nil { + return nil, 0, errors.Wrapf(err, "problem with getting apps") + } + for idx := range apps { + err = ReconvertAppDetails(&apps[idx]) + if err != nil { + return nil, 0, err + } + } + return apps, int64(total), nil +} + +func DeleteApp(db *pg.DB, app *App) error { + app.Deleted = stork.UTCNow() + err := db.Update(app) + if err != nil { + return errors.Wrapf(err, "problem with deleting app %v", app.Id) + } + return nil +} diff --git a/backend/server/database/model/app_test.go b/backend/server/database/model/app_test.go new file mode 100644 index 0000000000000000000000000000000000000000..0ec9e1faaab6763d8833a0a844d45a28a94ceea0 --- /dev/null +++ b/backend/server/database/model/app_test.go @@ -0,0 +1,236 @@ +package dbmodel + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "isc.org/stork/server/database/test" +) + +func TestAddApp(t *testing.T) { + db, _, teardown := dbtest.SetupDatabaseTestCase(t) + defer teardown() + + // add first machine, should be no error + m := &Machine{ + Id: 0, + Address: "localhost", + AgentPort: 8080, + } + err := AddMachine(db, m) + require.NoError(t, err) + require.NotEqual(t, 0, m.Id) + + // add app but without machine, error should be raised + s := &App{ + Id: 0, + Type: "kea", + } + err = AddApp(db, s) + require.NotNil(t, err) + + // add app but without type, error should be raised + s = &App{ + Id: 0, + MachineID: m.Id, + } + err = AddApp(db, s) + require.NotNil(t, err) + + // add app, no error expected + s = &App{ + Id: 0, + MachineID: m.Id, + Type: "kea", + CtrlPort: 1234, + Active: true, + } + err = AddApp(db, s) + require.NoError(t, err) + require.NotEqual(t, 0, s.Id) + + // add app for the same machine and ctrl port - error should be raised + s = &App{ + Id: 0, + MachineID: m.Id, + Type: "bind", + CtrlPort: 1234, + Active: true, + } + err = AddApp(db, s) + require.Contains(t, err.Error(), "duplicate") +} + +func TestDeleteApp(t *testing.T) { + db, _, teardown := dbtest.SetupDatabaseTestCase(t) + defer teardown() + + // delete non-existing app + s0 := &App{ + Id: 123, + } + err := DeleteApp(db, s0) + require.Contains(t, err.Error(), "no rows in result") + + // add first machine, should be no error + m := &Machine{ + Id: 0, + Address: "localhost", + AgentPort: 8080, + } + err = AddMachine(db, m) + require.NoError(t, err) + require.NotEqual(t, 0, m.Id) + + // add app, no error expected + s := &App{ + Id: 0, + MachineID: m.Id, + Type: "kea", + CtrlPort: 1234, + Active: true, + } + err = AddApp(db, s) + require.NoError(t, err) + require.NotEqual(t, 0, s.Id) + + // delete added app + err = DeleteApp(db, s) + require.NoError(t, err) +} + +func TestGetAppsByMachine(t *testing.T) { + db, _, teardown := dbtest.SetupDatabaseTestCase(t) + defer teardown() + + // add first machine, should be no error + m := &Machine{ + Id: 0, + Address: "localhost", + AgentPort: 8080, + } + err := AddMachine(db, m) + require.NoError(t, err) + require.NotEqual(t, 0, m.Id) + + // there should be no apps yet + apps, err := GetAppsByMachine(db, m.Id) + require.Len(t, apps, 0) + require.NoError(t, err) + + // add app, no error expected + s := &App{ + Id: 0, + MachineID: m.Id, + Type: "kea", + CtrlPort: 1234, + Active: true, + } + err = AddApp(db, s) + require.NoError(t, err) + require.NotEqual(t, 0, s.Id) + + // get apps of given machine + apps, err = GetAppsByMachine(db, m.Id) + require.Len(t, apps, 1) + require.NoError(t, err) +} + +func TestGetAppById(t *testing.T) { + db, _, teardown := dbtest.SetupDatabaseTestCase(t) + defer teardown() + + // get non-existing app + app, err := GetAppById(db, 321) + require.NoError(t, err) + require.Nil(t, app) + + // add first machine, should be no error + m := &Machine{ + Id: 0, + Address: "localhost", + AgentPort: 8080, + } + err = AddMachine(db, m) + require.NoError(t, err) + require.NotEqual(t, 0, m.Id) + + // add app, no error expected + s := &App{ + Id: 0, + MachineID: m.Id, + Type: "kea", + CtrlPort: 1234, + Active: true, + } + err = AddApp(db, s) + require.NoError(t, err) + require.NotEqual(t, 0, s.Id) + + // get app by id + app, err = GetAppById(db, s.Id) + require.NoError(t, err) + require.NotNil(t, app) + require.Equal(t, s.Id, app.Id) +} + + +func TestGetAppsByPage(t *testing.T) { + db, _, teardown := dbtest.SetupDatabaseTestCase(t) + defer teardown() + + // add first machine, should be no error + m := &Machine{ + Id: 0, + Address: "localhost", + AgentPort: 8080, + } + err := AddMachine(db, m) + require.NoError(t, err) + require.NotEqual(t, 0, m.Id) + + // add kea app, no error expected + sKea := &App{ + Id: 0, + MachineID: m.Id, + Type: "kea", + CtrlPort: 1234, + Active: true, + } + err = AddApp(db, sKea) + require.NoError(t, err) + require.NotEqual(t, 0, sKea.Id) + + // add bind app, no error expected + sBind := &App{ + Id: 0, + MachineID: m.Id, + Type: "bind", + CtrlPort: 4321, + Active: true, + } + err = AddApp(db, sBind) + require.NoError(t, err) + require.NotEqual(t, 0, sBind.Id) + + // get all apps + apps, total, err := GetAppsByPage(db, 0, 10, "", "") + require.NoError(t, err) + require.Len(t, apps, 2) + require.Equal(t, int64(2), total) + + // get kea apps + apps, total, err = GetAppsByPage(db, 0, 10, "", "kea") + require.NoError(t, err) + require.Len(t, apps, 1) + require.Equal(t, int64(1), total) + require.Equal(t, "kea", apps[0].Type) + + // get bind apps + apps, total, err = GetAppsByPage(db, 0, 10, "", "bind") + require.NoError(t, err) + require.Len(t, apps, 1) + require.Equal(t, int64(1), total) + require.Equal(t, "bind", apps[0].Type) +} diff --git a/backend/server/database/model/machine.go b/backend/server/database/model/machine.go index f4093132346ea796299aee7df37e8de12381096f..7a44946bc6aa7c992b83960847b7462e42234a57 100644 --- a/backend/server/database/model/machine.go +++ b/backend/server/database/model/machine.go @@ -1,7 +1,6 @@ package dbmodel import ( - // "fmt" "time" "github.com/go-pg/pg/v9" "github.com/go-pg/pg/v9/orm" @@ -41,6 +40,7 @@ type Machine struct { LastVisited time.Time Error string State MachineState + Apps []App } func AddMachine(db *pg.DB, machine *Machine) error { @@ -71,7 +71,8 @@ func GetMachineByAddressAndAgentPort(db *pg.DB, address string, agentPort int64, func GetMachineById(db *pg.DB, id int64) (*Machine, error) { machine := Machine{} - q := db.Model(&machine).Where("id = ?", id) + q := db.Model(&machine).Where("machine.id = ?", id) + q = q.Relation("Apps") err := q.Select() if err == pg.ErrNoRows { return nil, nil @@ -81,6 +82,20 @@ func GetMachineById(db *pg.DB, id int64) (*Machine, error) { return &machine, nil } +func RefreshMachineFromDb(db *pg.DB, machine *Machine) error { + machine.Apps = []App{} + q := db.Model(machine).Where("id = ?", machine.Id) + q = q.Relation("Apps") + err := q.Select() + if err != nil { + return errors.Wrapf(err, "problem with getting machine %v", machine.Id) + } + return nil +} + +// Fetches a collection of apps from the database. The offset and limit specify the +// beginning of the page and the maximum size of the page. Limit has to be greater +// then 0, otherwise error is returned. func GetMachinesByPage(db *pg.DB, offset int64, limit int64, text string) ([]Machine, int64, error) { if limit == 0 { return nil, 0, errors.New("limit should be greater than 0") @@ -89,6 +104,7 @@ func GetMachinesByPage(db *pg.DB, offset int64, limit int64, text string) ([]Mac // prepare query q := db.Model(&machines).Where("deleted is NULL") + q = q.Relation("Apps") if text != "" { text = "%" + text + "%" q = q.WhereGroup(func(qq *orm.Query) (*orm.Query, error) { diff --git a/backend/server/database/model/machine_test.go b/backend/server/database/model/machine_test.go index 189d54803b461ec0b94ddc10a7d8149295aee174..538591a880a6112f7748cc27fe2117ce39637dd0 100644 --- a/backend/server/database/model/machine_test.go +++ b/backend/server/database/model/machine_test.go @@ -200,3 +200,31 @@ func TestDeleteMachine(t *testing.T) { err = DeleteMachine(db, m2) require.Contains(t, err.Error(), "no rows in result") } + +func TestRefreshMachineFromDb(t *testing.T) { + db, _, teardown := dbtest.SetupDatabaseTestCase(t) + defer teardown() + + // add machine + m := &Machine{ + Address: "localhost", + AgentPort: 8080, + Error: "some error", + State: MachineState{ + Hostname: "aaaa", + Cpus: 4, + }, + } + err := AddMachine(db, m) + require.NoError(t, err) + + m.State.Hostname = "bbbb" + m.State.Cpus = 2 + m.Error = "" + + err = RefreshMachineFromDb(db, m) + require.Nil(t, err) + require.Equal(t, "aaaa", m.State.Hostname) + require.Equal(t, int64(4), m.State.Cpus) + require.Equal(t, "some error", m.Error) +} diff --git a/backend/server/database/model/user.go b/backend/server/database/model/user.go index 5db9db51827b89c30ef8248381436ad18cfec081..94a88ef47d39375c3f2e009bf6478b8483f51f4d 100644 --- a/backend/server/database/model/user.go +++ b/backend/server/database/model/user.go @@ -105,9 +105,13 @@ func Authenticate(db *pg.DB, user *SystemUser) (bool, error) { } // Fetches a collection of users from the database. The offset and limit specify the -// beginning of the page and the maximum size of the page. If these values are set -// to 0, all users are returned. -func GetUsers(db *dbops.PgDB, offset, limit int, order SystemUserOrderBy) (users SystemUsers, err error) { +// beginning of the page and the maximum size of the page. Limit has to be greater +// then 0, otherwise error is returned. +func GetUsersByPage(db *dbops.PgDB, offset, limit int, order SystemUserOrderBy) (users SystemUsers, total int64, err error) { + total = int64(0) + if limit == 0 { + return nil, total, errors.New("limit should be greater than 0") + } q := db.Model(&users) switch order { @@ -117,6 +121,14 @@ func GetUsers(db *dbops.PgDB, offset, limit int, order SystemUserOrderBy) (users q = q.OrderExpr("id ASC") } + // first get total count + totalInt, err := q.Clone().Count() + if err != nil { + return nil, total, errors.Wrapf(err, "problem with getting users total") + } + total = int64(totalInt) + + // then do actual query q = q.Offset(offset).Limit(limit) err = q.Select() @@ -124,7 +136,7 @@ func GetUsers(db *dbops.PgDB, offset, limit int, order SystemUserOrderBy) (users err = errors.Wrapf(err, "problem with fetching a list of users from the database") } - return users, err + return users, total, err } // Fetches a user with a given id from the database. If the user does not exist diff --git a/backend/server/database/model/user_test.go b/backend/server/database/model/user_test.go index 0f1f1ad7f7ce33fc9874491b1aa69f19afe63955..6c0cefe9aa975124e9c7214355d71572272cacac 100644 --- a/backend/server/database/model/user_test.go +++ b/backend/server/database/model/user_test.go @@ -152,15 +152,16 @@ func TestPersistConflict(t *testing.T) { // Tests that all system users can be fetched from the database. -func TestGetUsers(t *testing.T) { +func TestGetUsersByPage(t *testing.T) { db, _, teardown := dbtest.SetupDatabaseTestCase(t) defer teardown() generateTestUsers(t, db) - users, err := GetUsers(db, 0, 0, SystemUserOrderById) + users, total, err := GetUsersByPage(db, 0, 1000, SystemUserOrderById) require.NoError(t, err) require.Equal(t, 101, len(users)) + require.Equal(t, int64(101), total) var prevId int = 0 for _, u := range users { @@ -170,15 +171,16 @@ func TestGetUsers(t *testing.T) { } // Tests that users can be fetched and sorted by login or email. -func TestGetUsersSortByLoginEmail(t *testing.T) { +func TestGetUsersByPageSortByLoginEmail(t *testing.T) { db, _, teardown := dbtest.SetupDatabaseTestCase(t) defer teardown() generateTestUsers(t, db) - users, err := GetUsers(db, 0, 0, SystemUserOrderByLoginEmail) + users, total, err := GetUsersByPage(db, 0, 1000, SystemUserOrderByLoginEmail) require.NoError(t, err) require.Equal(t, 101, len(users)) + require.Equal(t, int64(101), total) prevLogin := "" for _, u := range users { @@ -188,16 +190,17 @@ func TestGetUsersSortByLoginEmail(t *testing.T) { } // Tests that a page of users can be fetched. -func TestGetUsersPage(t *testing.T) { +func TestGetUsersByPagePage(t *testing.T) { db, _, teardown := dbtest.SetupDatabaseTestCase(t) defer teardown() generateTestUsers(t, db) - users, err := GetUsers(db, 50, 10, SystemUserOrderById) + users, total, err := GetUsersByPage(db, 50, 10, SystemUserOrderById) require.NoError(t, err) require.Equal(t, 10, len(users)) require.Equal(t, 51, users[0].Id) + require.Equal(t, int64(101), total) var prevId int = 0 for _, u := range users { @@ -207,16 +210,17 @@ func TestGetUsersPage(t *testing.T) { } // Tests that last page of users can be fetched without issues. -func TestGetUsersLastPage(t *testing.T) { +func TestGetUsersByPageLastPage(t *testing.T) { db, _, teardown := dbtest.SetupDatabaseTestCase(t) defer teardown() generateTestUsers(t, db) - users, err := GetUsers(db, 90, 20, SystemUserOrderById) + users, total, err := GetUsersByPage(db, 90, 20, SystemUserOrderById) require.NoError(t, err) require.Equal(t, 11, len(users)) require.Equal(t, 91, users[0].Id) + require.Equal(t, int64(101), total) var prevId int = 0 for _, u := range users { @@ -232,8 +236,9 @@ func TestGetUserById(t *testing.T) { generateTestUsers(t, db) - users, err := GetUsers(db, 0, 0, SystemUserOrderById) + users, total, err := GetUsersByPage(db, 0, 1000, SystemUserOrderById) require.NoError(t, err) + require.Equal(t, int64(101), total) user, err := GetUserById(db, users[0].Id) require.NoError(t, err) diff --git a/backend/server/restservice/restimpl.go b/backend/server/restservice/restimpl.go index bd46efa6bc5d2d72f1746f27b1e50c7a6c0cb230..bf8d219ff544bc59fe38950f85ae446aed19e029 100644 --- a/backend/server/restservice/restimpl.go +++ b/backend/server/restservice/restimpl.go @@ -6,6 +6,7 @@ import ( "context" log "github.com/sirupsen/logrus" + "github.com/pkg/errors" "github.com/go-openapi/strfmt" "github.com/go-openapi/runtime/middleware" "github.com/asaskevich/govalidator" @@ -36,7 +37,35 @@ func (r *RestAPI) GetVersion(ctx context.Context, params general.GetVersionParam return general.NewGetVersionOK().WithPayload(&ver) } -func machineToRestApi(dbMachine dbmodel.Machine) *models.Machine { +func machineToRestApi(dbMachine dbmodel.Machine) (*models.Machine, error) { + var apps []*models.MachineApp + for _, srv := range dbMachine.Apps { + active := true + if srv.Type == "kea" { + if srv.Active { + err := dbmodel.ReconvertAppDetails(&srv) + if err != nil { + return nil, err + } + for _, d := range srv.Details.(dbmodel.AppKea).Daemons { + if !d.Active { + active = false + break + } + } + } else { + active = false + } + } + s := models.MachineApp{ + ID: srv.Id, + Type: srv.Type, + Version: srv.Meta.Version, + Active: active, + } + apps = append(apps, &s) + } + m := models.Machine{ ID: dbMachine.Id, Address: &dbMachine.Address, @@ -59,8 +88,9 @@ func machineToRestApi(dbMachine dbmodel.Machine) *models.Machine { HostID: dbMachine.State.HostID, LastVisited: strfmt.DateTime(dbMachine.LastVisited), Error: dbMachine.Error, + Apps: apps, } - return &m + return &m, nil } // Get runtime state of indicated machine. @@ -91,14 +121,29 @@ func (r *RestAPI) GetMachineState(ctx context.Context, params services.GetMachin 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, err := machineToRestApi(*dbMachine) + if err != nil { + log.Error(err) + msg := "problem with serializing data" + 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", @@ -106,7 +151,15 @@ func (r *RestAPI) GetMachineState(ctx context.Context, params services.GetMachin return rsp } - m := machineToRestApi(*dbMachine) + m, err := machineToRestApi(*dbMachine) + if err != nil { + log.Error(err) + msg := "problem with serializing data" + rsp := services.NewGetMachineStateDefault(500).WithPayload(&models.APIError{ + Message: &msg, + }) + return rsp + } rsp := services.NewGetMachineStateOK().WithPayload(m) return rsp @@ -131,23 +184,23 @@ func (r *RestAPI) GetMachines(ctx context.Context, params services.GetMachinesPa text = *params.Text } - service := "" - if params.Service != nil { - service = *params.Service + app := "" + if params.App != nil { + app = *params.App } log.WithFields(log.Fields{ "start": start, "limit": limit, "text": text, - "service": service, + "app": app, }).Info("query machines") dbMachines, total, err := dbmodel.GetMachinesByPage(r.Db, start, limit, text) if err != nil { log.Error(err) msg := "cannot get machines from db" - rsp := services.NewCreateMachineDefault(500).WithPayload(&models.APIError{ + rsp := services.NewGetMachinesDefault(500).WithPayload(&models.APIError{ Message: &msg, }) return rsp @@ -155,7 +208,15 @@ func (r *RestAPI) GetMachines(ctx context.Context, params services.GetMachinesPa for _, dbM := range dbMachines { - mm := machineToRestApi(dbM) + mm, err := machineToRestApi(dbM) + if err != nil { + log.Error(err) + msg := "problem with serializing data" + rsp := services.NewGetMachinesDefault(500).WithPayload(&models.APIError{ + Message: &msg, + }) + return rsp + } machines = append(machines, mm) } @@ -185,7 +246,15 @@ func (r *RestAPI) GetMachine(ctx context.Context, params services.GetMachinePara }) return rsp } - m := machineToRestApi(*dbMachine) + m, err := machineToRestApi(*dbMachine) + if err != nil { + log.Error(err) + msg := "problem with serializing data" + rsp := services.NewGetMachineDefault(500).WithPayload(&models.APIError{ + Message: &msg, + }) + return rsp + } rsp := services.NewGetMachineOK().WithPayload(m) return rsp } @@ -244,14 +313,28 @@ func (r *RestAPI) CreateMachine(ctx context.Context, params services.CreateMachi 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, err := machineToRestApi(*dbMachine) + if err != nil { + log.Error(err) + msg := "problem with serializing data" + rsp := services.NewGetMachineDefault(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, @@ -260,7 +343,15 @@ func (r *RestAPI) CreateMachine(ctx context.Context, params services.CreateMachi return rsp } - m := machineToRestApi(*dbMachine) + m, err := machineToRestApi(*dbMachine) + if err != nil { + log.Error(err) + msg := "problem with serializing data" + rsp := services.NewGetMachineDefault(500).WithPayload(&models.APIError{ + Message: &msg, + }) + return rsp + } rsp := services.NewCreateMachineOK().WithPayload(m) return rsp @@ -328,12 +419,21 @@ func (r *RestAPI) UpdateMachine(ctx context.Context, params services.UpdateMachi }) return rsp } - m := machineToRestApi(*dbMachine) + m, err := machineToRestApi(*dbMachine) + if err != nil { + log.Error(err) + msg := "problem with serializing data" + rsp := services.NewUpdateMachineDefault(500).WithPayload(&models.APIError{ + Message: &msg, + }) + return rsp + } rsp := services.NewUpdateMachineOK().WithPayload(m) return rsp } func updateMachineFields(db *dbops.PgDB, dbMachine *dbmodel.Machine, m *agentcomm.State) error { + // update state fields in machine dbMachine.State.AgentVersion = m.AgentVersion dbMachine.State.Cpus = m.Cpus dbMachine.State.CpusLoad = m.CpusLoad @@ -352,7 +452,100 @@ func updateMachineFields(db *dbops.PgDB, dbMachine *dbmodel.Machine, m *agentcom dbMachine.State.HostID = m.HostID dbMachine.LastVisited = m.LastVisited dbMachine.Error = m.Error - return db.Update(dbMachine) + err := db.Update(dbMachine) + 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 keaSrv *agentcomm.AppKea = nil + //var bindSrv *agentcomm.AppBind + for _, srv := range m.Apps { + switch s := srv.(type) { + case *agentcomm.AppKea: + keaSrv = s + // case agentcomm.AppBind: + // bindSrv = &s + default: + log.Println("NOT IMPLEMENTED") + } + } + + var keaDaemons []dbmodel.KeaDaemon + if keaSrv != nil { + for _, d := range keaSrv.Daemons { + keaDaemons = append(keaDaemons, dbmodel.KeaDaemon{ + Pid: d.Pid, + Name: d.Name, + Active: d.Active, + Version: d.Version, + ExtendedVersion: d.ExtendedVersion, + }) + } + } + + dbKeaSrv, dbOk := dbAppsMap["kea"] + if dbOk && keaSrv != nil { + // update app in db + meta := dbmodel.AppMeta{ + Version: keaSrv.Version, + } + dbKeaSrv.Deleted = time.Time{} // undelete if it was deleted + dbKeaSrv.CtrlPort = keaSrv.CtrlPort + dbKeaSrv.Active = keaSrv.Active + dbKeaSrv.Meta = meta + dt := dbKeaSrv.Details.(dbmodel.AppKea) + dt.ExtendedVersion = keaSrv.ExtendedVersion + dt.Daemons = keaDaemons + err = db.Update(&dbKeaSrv) + if err != nil { + return errors.Wrapf(err, "problem with updating app %v", dbKeaSrv) + } + } else if dbOk && keaSrv == nil { + // delete app from db + err = dbmodel.DeleteApp(db, &dbKeaSrv) + if err != nil { + return err + } + } else if !dbOk && keaSrv != nil { + // add app to db + dbKeaSrv = dbmodel.App{ + MachineID: dbMachine.Id, + Type: "kea", + CtrlPort: keaSrv.CtrlPort, + Active: keaSrv.Active, + Meta: dbmodel.AppMeta{ + Version: keaSrv.Version, + }, + Details: dbmodel.AppKea{ + ExtendedVersion: keaSrv.ExtendedVersion, + Daemons: keaDaemons, + }, + } + err = dbmodel.AddApp(db, &dbKeaSrv) + if err != nil { + return err + } + } + + err = dbmodel.RefreshMachineFromDb(db, dbMachine) + if err != nil { + return err + } + + return nil } // Add a machine where Stork Agent is running. @@ -384,3 +577,115 @@ func (r *RestAPI) DeleteMachine(ctx context.Context, params services.DeleteMachi return rsp } + +func appToRestApi(dbApp dbmodel.App) *models.App { + var daemons []*models.KeaDaemon + for _, d := range dbApp.Details.(dbmodel.AppKea).Daemons { + daemons = append(daemons, &models.KeaDaemon{ + Pid: int64(d.Pid), + Name: d.Name, + Active: d.Active, + Version: d.Version, + ExtendedVersion: d.ExtendedVersion, + }) + } + s := models.App{ + ID: dbApp.Id, + Type: dbApp.Type, + CtrlPort: dbApp.CtrlPort, + Active: dbApp.Active, + Version: dbApp.Meta.Version, + Details: struct { + models.AppKea + models.AppBind + }{ + models.AppKea{ + ExtendedVersion: dbApp.Details.(dbmodel.AppKea).ExtendedVersion, + Daemons: daemons, + }, + models.AppBind{}, + }, + Machine: &models.AppMachine{ + ID: dbApp.MachineID, + Address: dbApp.Machine.Address, + Hostname: dbApp.Machine.State.Hostname, + }, + } + return &s +} + +func (r *RestAPI) GetApps(ctx context.Context, params services.GetAppsParams) middleware.Responder { + appsLst := []*models.App{} + + var start int64 = 0 + if params.Start != nil { + start = *params.Start + } + + var limit int64 = 10 + if params.Limit != nil { + limit = *params.Limit + } + + text := "" + if params.Text != nil { + text = *params.Text + } + + app := "" + if params.App != nil { + app = *params.App + } + + log.WithFields(log.Fields{ + "start": start, + "limit": limit, + "text": text, + "app": app, + }).Info("query apps") + + dbApps, total, err := dbmodel.GetAppsByPage(r.Db, start, limit, text, app) + if err != nil { + log.Error(err) + msg := "cannot get apps from db" + rsp := services.NewGetAppsDefault(500).WithPayload(&models.APIError{ + Message: &msg, + }) + return rsp + } + + + for _, dbS := range dbApps { + ss := appToRestApi(dbS) + appsLst = append(appsLst, ss) + } + + s := models.Apps{ + Items: appsLst, + Total: total, + } + rsp := services.NewGetAppsOK().WithPayload(&s) + return rsp +} + +func (r *RestAPI) GetApp(ctx context.Context, params services.GetAppParams) middleware.Responder { + dbApp, err := dbmodel.GetAppById(r.Db, params.ID) + if err != nil { + msg := fmt.Sprintf("cannot get app with id %d from db", params.ID) + log.Error(err) + rsp := services.NewGetAppDefault(500).WithPayload(&models.APIError{ + Message: &msg, + }) + return rsp + } + if dbApp == nil { + msg := fmt.Sprintf("cannot find app with id %d", params.ID) + rsp := services.NewGetAppDefault(404).WithPayload(&models.APIError{ + Message: &msg, + }) + return rsp + } + s := appToRestApi(*dbApp) + rsp := services.NewGetAppOK().WithPayload(s) + return rsp +} diff --git a/backend/server/restservice/restimpl_test.go b/backend/server/restservice/restimpl_test.go index 2c31003272fea3824ecd2c113d9145632e3e3adc..1957b85fcd8fed5e7d8ef0e49e7ae7e5133dae0c 100644 --- a/backend/server/restservice/restimpl_test.go +++ b/backend/server/restservice/restimpl_test.go @@ -211,6 +211,38 @@ func TestGetMachine(t *testing.T) { require.IsType(t, &services.GetMachineOK{}, rsp) okRsp := rsp.(*services.GetMachineOK) require.Equal(t, m.Id, okRsp.Payload.ID) + + // add machine 2 + m2 := &dbmodel.Machine{ + Address: "localhost", + AgentPort: 8082, + } + err = dbmodel.AddMachine(db, m2) + require.NoError(t, err) + + // add app to machine 2 + s := &dbmodel.App{ + Id: 0, + MachineID: m2.Id, + Type: "kea", + CtrlPort: 1234, + Active: true, + } + err = dbmodel.AddApp(db, s) + require.NoError(t, err) + require.NotEqual(t, 0, s.Id) + + // get added machine 2 with kea app + params = services.GetMachineParams{ + ID: m2.Id, + } + rsp = rapi.GetMachine(ctx, params) + require.IsType(t, &services.GetMachineOK{}, rsp) + okRsp = rsp.(*services.GetMachineOK) + require.Equal(t, m2.Id, okRsp.Payload.ID) + require.Len(t, okRsp.Payload.Apps, 1) + require.Equal(t, s.Id, okRsp.Payload.Apps[0].ID) + } func TestUpdateMachine(t *testing.T) { @@ -347,3 +379,157 @@ func TestDeleteMachine(t *testing.T) { okRsp = rsp.(*services.GetMachineOK) require.Equal(t, m.Id, okRsp.Payload.ID) } + +func TestGetApp(t *testing.T) { + db, dbSettings, teardown := dbtest.SetupDatabaseTestCase(t) + defer teardown() + + settings := RestApiSettings{} + fa := FakeAgents{} + rapi, err := NewRestAPI(&settings, dbSettings, db, &fa) + require.NoError(t, err) + ctx := context.Background() + + // get non-existing app + params := services.GetAppParams{ + ID: 123, + } + rsp := rapi.GetApp(ctx, params) + require.IsType(t, &services.GetAppDefault{}, rsp) + defaultRsp := rsp.(*services.GetAppDefault) + require.Equal(t, 404, getStatusCode(*defaultRsp)) + require.Equal(t, "cannot find app with id 123", *defaultRsp.Payload.Message) + + // add machine + m := &dbmodel.Machine{ + Address: "localhost", + AgentPort: 8080, + } + err = dbmodel.AddMachine(db, m) + require.NoError(t, err) + + // add app to machine + s := &dbmodel.App{ + Id: 0, + MachineID: m.Id, + Type: "kea", + CtrlPort: 1234, + Active: true, + } + err = dbmodel.AddApp(db, s) + require.NoError(t, err) + + // get added app + params = services.GetAppParams{ + ID: s.Id, + } + rsp = rapi.GetApp(ctx, params) + require.IsType(t, &services.GetAppOK{}, rsp) + okRsp := rsp.(*services.GetAppOK) + require.Equal(t, s.Id, okRsp.Payload.ID) +} + +func TestRestGetApp(t *testing.T) { + db, dbSettings, teardown := dbtest.SetupDatabaseTestCase(t) + defer teardown() + + settings := RestApiSettings{} + fa := FakeAgents{} + rapi, err := NewRestAPI(&settings, dbSettings, db, &fa) + require.NoError(t, err) + ctx := context.Background() + + // get non-existing app + params := services.GetAppParams{ + ID: 123, + } + rsp := rapi.GetApp(ctx, params) + require.IsType(t, &services.GetAppDefault{}, rsp) + defaultRsp := rsp.(*services.GetAppDefault) + require.Equal(t, 404, getStatusCode(*defaultRsp)) + require.Equal(t, "cannot find app with id 123", *defaultRsp.Payload.Message) + + // add machine + m := &dbmodel.Machine{ + Address: "localhost", + AgentPort: 8080, + } + err = dbmodel.AddMachine(db, m) + require.NoError(t, err) + + // add app to machine + s := &dbmodel.App{ + Id: 0, + MachineID: m.Id, + Type: "kea", + CtrlPort: 1234, + Active: true, + } + err = dbmodel.AddApp(db, s) + require.NoError(t, err) + + // get added app + params = services.GetAppParams{ + ID: s.Id, + } + rsp = rapi.GetApp(ctx, params) + require.IsType(t, &services.GetAppOK{}, rsp) + okRsp := rsp.(*services.GetAppOK) + require.Equal(t, s.Id, okRsp.Payload.ID) +} + +func TestRestGetApps(t *testing.T) { + db, dbSettings, teardown := dbtest.SetupDatabaseTestCase(t) + defer teardown() + + settings := RestApiSettings{} + fa := FakeAgents{} + rapi, err := NewRestAPI(&settings, dbSettings, db, &fa) + require.NoError(t, err) + ctx := context.Background() + + // get empty list of app + params := services.GetAppsParams{} + rsp := rapi.GetApps(ctx, params) + require.IsType(t, &services.GetAppsOK{}, rsp) + okRsp := rsp.(*services.GetAppsOK) + require.Equal(t, int64(0), okRsp.Payload.Total) + + // add machine + m := &dbmodel.Machine{ + Address: "localhost", + AgentPort: 8080, + } + err = dbmodel.AddMachine(db, m) + require.NoError(t, err) + + // add app kea to machine + s1 := &dbmodel.App{ + Id: 0, + MachineID: m.Id, + Type: "kea", + CtrlPort: 1234, + Active: true, + } + err = dbmodel.AddApp(db, s1) + require.NoError(t, err) + + // add app bind to machine + s2 := &dbmodel.App{ + Id: 0, + MachineID: m.Id, + Type: "bind", + CtrlPort: 4321, + Active: true, + } + err = dbmodel.AddApp(db, s2) + require.NoError(t, err) + + // get added app + params = services.GetAppsParams{ + } + rsp = rapi.GetApps(ctx, params) + require.IsType(t, &services.GetAppsOK{}, rsp) + okRsp = rsp.(*services.GetAppsOK) + require.Equal(t, int64(2), okRsp.Payload.Total) +} diff --git a/backend/server/restservice/restusers.go b/backend/server/restservice/restusers.go index 92519771dc18d2c180840298d6d2c84156ca3cc2..7d79663685b07483b5c1dae127a1a64d5bb41fea 100644 --- a/backend/server/restservice/restusers.go +++ b/backend/server/restservice/restusers.go @@ -74,7 +74,7 @@ func (r *RestAPI) DeleteSession(ctx context.Context, params users.DeleteSessionP // Get users having an account in the system. func (r *RestAPI) GetUsers(ctx context.Context, params users.GetUsersParams) middleware.Responder { - systemUsers, err := dbmodel.GetUsers(r.Db, int(*params.Start), int(*params.Limit), dbmodel.SystemUserOrderById) + systemUsers, total, err := dbmodel.GetUsersByPage(r.Db, int(*params.Start), int(*params.Limit), dbmodel.SystemUserOrderById) if err != nil { log.WithFields(log.Fields{ "start": int(*params.Start), @@ -96,7 +96,7 @@ func (r *RestAPI) GetUsers(ctx context.Context, params users.GetUsersParams) mid u := models.Users{ Items: usersList, - Total: int64(len(usersList)), + Total: total, } rsp := users.NewGetUsersOK().WithPayload(&u) return rsp diff --git a/backend/util.go b/backend/util.go index c76744dd52d1fc939e5e4b04cc82e93fcc761516..620e76c17a51d11a761a85358225bab4745514cc 100644 --- a/backend/util.go +++ b/backend/util.go @@ -1,10 +1,39 @@ package stork import ( + "os" "time" + "fmt" + "path" + "runtime" + log "github.com/sirupsen/logrus" ) func UTCNow() time.Time { return time.Now().UTC() } + + +func SetupLogging() { + log.SetLevel(log.DebugLevel) + log.SetOutput(os.Stdout) + log.SetReportCaller(true) + log.SetFormatter(&log.TextFormatter{ + ForceColors: true, + FullTimestamp: true, + TimestampFormat: "2006-01-02 15:04:05", + // TODO: do more research and enable if it brings value + //PadLevelText: true, + // FieldMap: log.FieldMap{ + // FieldKeyTime: "@timestamp", + // FieldKeyLevel: "@level", + // FieldKeyMsg: "@message", + // }, + CallerPrettyfier: func(f *runtime.Frame) (string, string) { + // Grab filename and line of current frame and add it to log entry + _, filename := path.Split(f.File) + return "", fmt.Sprintf("%20v:%-5d", filename, f.Line) + }, + }) +} diff --git a/docker/docker-agent-kea.txt b/docker/docker-agent-kea.txt index 7444e1fc0ca6603c5da1aca6959c5285d89c66b6..0434253345ec33ee56cea92522399b8326aabdc2 100644 --- a/docker/docker-agent-kea.txt +++ b/docker/docker-agent-kea.txt @@ -3,6 +3,7 @@ WORKDIR /agent RUN apt-get update && apt-get install -y --no-install-recommends sudo curl ca-certificates gnupg apt-transport-https supervisor RUN curl -1sLf 'https://dl.cloudsmith.io/public/isc/kea-1-7/cfg/setup/bash.deb.sh' | bash RUN apt-get update && apt-get install -y --no-install-recommends isc-kea-dhcp4-server isc-kea-ctrl-agent && mkdir -p /var/run/kea/ +RUN perl -pi -e 's/8080/8000/g' /etc/kea/kea-ctrl-agent.conf && perl -pi -e 's/127\.0\.0\.1/0\.0\.0\.0/g' /etc/kea/kea-ctrl-agent.conf COPY backend/cmd/stork-agent/stork-agent /agent/ COPY docker/supervisor-agent-kea.conf /etc/supervisor.conf CMD ["supervisord", "-c", "/etc/supervisor.conf"] diff --git a/docker/supervisor-agent-kea.conf b/docker/supervisor-agent-kea.conf index 5c94a425f61df935e950e0ad9d5bfc43be4a64cd..038166bac0dacf2236237d6ba635a94c978cf32d 100644 --- a/docker/supervisor-agent-kea.conf +++ b/docker/supervisor-agent-kea.conf @@ -5,8 +5,25 @@ nodaemon=true command=/usr/sbin/kea-dhcp4 -c /etc/kea/kea-dhcp4.conf autostart = true autorestart = true +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 + +[program:kea-agent] +command=/usr/sbin/kea-ctrl-agent -c /etc/kea/kea-ctrl-agent.conf +autostart = true +autorestart = true +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 [program:stork-agent] command=/agent/stork-agent autostart = true autorestart = true +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 \ No newline at end of file diff --git a/webui/package-lock.json b/webui/package-lock.json index f1447feb75c3f2b5a107be7453b53645fbdca123..1b0eb9cdd820d2aa8fc82e658e2e01980e627bf4 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -2277,6 +2277,16 @@ "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", "dev": true }, + "angular-rename": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/angular-rename/-/angular-rename-1.0.7.tgz", + "integrity": "sha512-exQd1ASkjHENW+b3dfF90Vsik4GQdiaGxhUKx8/EHPcWOeGDDSCHslWxcehYgQb7YfOe47Z4f22PBIIvldesxg==", + "dev": true, + "requires": { + "fs": "0.0.1-security", + "path": "^0.12.7" + } + }, "ansi-colors": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", @@ -5173,6 +5183,12 @@ "readable-stream": "^2.0.0" } }, + "fs": { + "version": "0.0.1-security", + "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz", + "integrity": "sha1-invTcYa23d84E/I4WLV+yq9eQdQ=", + "dev": true + }, "fs-access": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/fs-access/-/fs-access-1.0.1.tgz", diff --git a/webui/package.json b/webui/package.json index e0af84c60ff89f51a7ed594c382d3bd8ec1abba0..ab071e6552d6be7b78bdc0b21c1a7a2deb984ee4 100644 --- a/webui/package.json +++ b/webui/package.json @@ -43,6 +43,7 @@ "@types/jasmine": "~3.3.8", "@types/jasminewd2": "~2.0.3", "@types/node": "~8.9.4", + "angular-rename": "^1.0.7", "codelyzer": "^5.0.0", "jasmine-core": "~3.4.0", "jasmine-spec-reporter": "~4.2.1", diff --git a/webui/src/app/app-routing.module.ts b/webui/src/app/app-routing.module.ts index 977eb648362cb1e1914e37cf89283c8bc2d0036f..d5a92585e293410ef9c7b6fac046070a6978fed6 100644 --- a/webui/src/app/app-routing.module.ts +++ b/webui/src/app/app-routing.module.ts @@ -7,6 +7,7 @@ import { LoginScreenComponent } from './login-screen/login-screen.component' import { SwaggerUiComponent } from './swagger-ui/swagger-ui.component' import { MachinesPageComponent } from './machines-page/machines-page.component' import { UsersPageComponent } from './users-page/users-page.component' +import { AppsPageComponent } from './apps-page/apps-page.component' const routes: Routes = [ { @@ -30,6 +31,21 @@ const routes: Routes = [ component: MachinesPageComponent, canActivate: [AuthGuard], }, + { + path: 'apps/:srv', + pathMatch: 'full', + redirectTo: 'apps/:srv/all', + }, + { + path: 'apps/:srv/:id', + component: AppsPageComponent, + canActivate: [AuthGuard], + }, + { + path: 'swagger-ui', + component: SwaggerUiComponent, + canActivate: [AuthGuard], + }, { path: 'users', redirectTo: 'users/', @@ -45,11 +61,6 @@ const routes: Routes = [ component: UsersPageComponent, canActivate: [AuthGuard], }, - { - path: 'swagger-ui', - component: SwaggerUiComponent, - canActivate: [AuthGuard], - }, // otherwise redirect to home { path: '**', redirectTo: '' }, diff --git a/webui/src/app/app.component.ts b/webui/src/app/app.component.ts index b1fb0d15c03b5a798bbd2c971625156603fb2bd3..a21da8d0b433c13693bcb98759aef5a21ae9b1c3 100644 --- a/webui/src/app/app.component.ts +++ b/webui/src/app/app.component.ts @@ -2,10 +2,9 @@ import { Component, OnInit } from '@angular/core' import { Router } from '@angular/router' import { Observable } from 'rxjs' -import { MenubarModule } from 'primeng/menubar' import { MenuItem } from 'primeng/api' -import { AuthService, User } from './auth.service' +import { AuthService } from './auth.service' import { LoadingService } from './loading.service' @Component({ @@ -28,22 +27,33 @@ export class AppComponent implements OnInit { ngOnInit() { this.menuItems = [ { - label: 'Configuration', + label: 'Services', items: [ { - label: 'Users', - icon: 'fa fa-user', - routerLink: '/users', + label: 'Kea DHCP', + icon: 'fa fa-server', + routerLink: '/apps/kea/all', + }, + // TODO: add support for BIND apps + // { + // label: 'BIND DNS', + // icon: 'fa fa-server', + // routerLink: '/apps/bind/all', + // }, + { + label: 'Machines', + icon: 'fa fa-server', + routerLink: '/machines/all', }, ], }, { - label: 'Services', + label: 'Configuration', items: [ { - label: 'Machines', - icon: 'fa fa-server', - routerLink: '/machines/all', + label: 'Users', + icon: 'fa fa-user', + routerLink: '/users', }, ], }, diff --git a/webui/src/app/app.module.ts b/webui/src/app/app.module.ts index 9a2e0f135288d48e58cbc98c587bfe614cc1393d..73196bead83c13a2aa3f2107a3bdaea7e57847c1 100644 --- a/webui/src/app/app.module.ts +++ b/webui/src/app/app.module.ts @@ -42,6 +42,8 @@ import { SwaggerUiComponent } from './swagger-ui/swagger-ui.component' import { MachinesPageComponent } from './machines-page/machines-page.component' import { LocaltimePipe } from './localtime.pipe' import { UsersPageComponent } from './users-page/users-page.component' +import { AppsPageComponent } from './apps-page/apps-page.component' +import { KeaAppTabComponent } from './kea-app-tab/kea-app-tab.component' export function cfgFactory() { const params: ConfigurationParameters = { @@ -61,6 +63,8 @@ export function cfgFactory() { MachinesPageComponent, LocaltimePipe, UsersPageComponent, + AppsPageComponent, + KeaAppTabComponent, ], imports: [ BrowserModule, diff --git a/webui/src/app/apps-page/apps-page.component.html b/webui/src/app/apps-page/apps-page.component.html new file mode 100644 index 0000000000000000000000000000000000000000..5669c735fa7fed01adc3e6962ec960b44217c865 --- /dev/null +++ b/webui/src/app/apps-page/apps-page.component.html @@ -0,0 +1,119 @@ + + +
+
+
+ {{ item.label }} +
+
+
+
+
+ + +
+
+
+ + + Filter apps: + + + + + + + +
+ +
+ +
+
+ + + + + + ID + Version + Status + Machine Address + Machine Hostname + Action + + + + + + {{ s.id }} + + + {{ s.version }} + + + + {{ d.niceName }} + + + + {{ s.machine.address }} + + + {{ s.machine.hostname }} + + + + + + + + + + + +
+ + +
+ +
diff --git a/webui/src/app/apps-page/apps-page.component.sass b/webui/src/app/apps-page/apps-page.component.sass new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/webui/src/app/apps-page/apps-page.component.spec.ts b/webui/src/app/apps-page/apps-page.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..9270bf5e7a52e5344289bea75e96bcd5de5a2990 --- /dev/null +++ b/webui/src/app/apps-page/apps-page.component.spec.ts @@ -0,0 +1,24 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing' + +import { AppsPageComponent } from './apps-page.component' + +describe('AppsPageComponent', () => { + let component: AppsPageComponent + let fixture: ComponentFixture + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [AppsPageComponent], + }).compileComponents() + })) + + beforeEach(() => { + fixture = TestBed.createComponent(AppsPageComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/webui/src/app/apps-page/apps-page.component.ts b/webui/src/app/apps-page/apps-page.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..0d0e84136756bd2496975274034616335890f4d1 --- /dev/null +++ b/webui/src/app/apps-page/apps-page.component.ts @@ -0,0 +1,264 @@ +import { Component, OnInit } from '@angular/core' +import { ActivatedRoute, ParamMap, Router, NavigationEnd } from '@angular/router' + +import { MessageService, MenuItem } from 'primeng/api' + +import { ServicesService } from '../backend/api/api' +import { LoadingService } from '../loading.service' + +function htmlizeExtVersion(app) { + if (app.details.extendedVersion) { + app.details.extendedVersion = app.details.extendedVersion.replace(/\n/g, '
') + } + for (const d of app.details.daemons) { + if (d.extendedVersion) { + d.extendedVersion = d.extendedVersion.replace(/\n/g, '
') + } + } +} + +function capitalize(txt) { + return txt.charAt(0).toUpperCase() + txt.slice(1) +} + +@Component({ + selector: 'app-apps-page', + templateUrl: './apps-page.component.html', + styleUrls: ['./apps-page.component.sass'], +}) +export class AppsPageComponent implements OnInit { + appType: string + // apps table + apps: any[] + totalApps: number + appMenuItems: MenuItem[] + + // action panel + filterText = '' + + // machine tabs + activeTabIdx = 0 + tabs: MenuItem[] + activeItem: MenuItem + openedApps: any + appTab: any + + constructor( + private route: ActivatedRoute, + private router: Router, + private servicesApi: ServicesService, + private msgSrv: MessageService, + private loadingService: LoadingService + ) {} + + switchToTab(index) { + if (this.activeTabIdx === index) { + return + } + this.activeTabIdx = index + this.activeItem = this.tabs[index] + if (index > 0) { + this.appTab = this.openedApps[index - 1] + } + } + + addAppTab(app) { + console.info('addAppTab', app) + this.openedApps.push({ + app, + activeDaemonTabIdx: 0, + }) + const capAppType = capitalize(app.type) + this.tabs.push({ + label: `${app.id}. ${capAppType}@${app.machine.address}`, + routerLink: '/apps/' + this.appType + '/' + app.id, + }) + } + + ngOnInit() { + this.appType = this.route.snapshot.params.srv + this.tabs = [{ label: capitalize(this.appType) + ' Apps', routerLink: '/apps/' + this.appType + '/all' }] + + this.apps = [] + this.appMenuItems = [ + { + label: 'Refresh', + icon: 'pi pi-refresh', + }, + ] + + this.openedApps = [] + + this.route.paramMap.subscribe((params: ParamMap) => { + const appIdStr = params.get('id') + if (appIdStr === 'all') { + this.switchToTab(0) + } else { + const appId = parseInt(appIdStr, 10) + + let found = false + // if tab for this app is already opened then switch to it + for (let idx = 0; idx < this.openedApps.length; idx++) { + const s = this.openedApps[idx].app + if (s.id === appId) { + console.info('found opened app', idx) + this.switchToTab(idx + 1) + found = true + } + } + + // if tab is not opened then search for list of apps if the one is present there, + // if so then open it in new tab and switch to it + if (!found) { + for (const s of this.apps) { + if (s.id === appId) { + console.info('found app in the list, opening it') + this.addAppTab(s) + this.switchToTab(this.tabs.length - 1) + found = true + break + } + } + } + + // if app is not loaded in list fetch it individually + if (!found) { + console.info('fetching app') + this.servicesApi.getApp(appId).subscribe( + data => { + htmlizeExtVersion(data) + this.addAppTab(data) + this.switchToTab(this.tabs.length - 1) + }, + err => { + let msg = err.statusText + if (err.error && err.error.message) { + msg = err.error.message + } + this.msgSrv.add({ + severity: 'error', + summary: 'Cannot get app', + detail: 'Getting app with ID ' + appId + ' erred: ' + msg, + life: 10000, + }) + this.router.navigate(['/apps/' + this.appType + '/all']) + } + ) + } + } + }) + } + + loadApps(event) { + let text + if (event.filters.text) { + text = event.filters.text.value + } + + this.servicesApi.getApps(event.first, event.rows, text, this.appType).subscribe(data => { + this.apps = data.items + this.totalApps = data.total + for (const s of this.apps) { + htmlizeExtVersion(s) + } + }) + } + + keyDownFilterText(appsTable, event) { + if (this.filterText.length >= 3 || event.key === 'Enter') { + appsTable.filter(this.filterText, 'text', 'equals') + } + } + + closeTab(event, idx) { + this.openedApps.splice(idx - 1, 1) + this.tabs.splice(idx, 1) + if (this.activeTabIdx === idx) { + this.switchToTab(idx - 1) + if (idx - 1 > 0) { + this.router.navigate(['/apps/' + this.appType + '/' + this.appTab.app.id]) + } else { + this.router.navigate(['/apps/' + this.appType + '/all']) + } + } else if (this.activeTabIdx > idx) { + this.activeTabIdx = this.activeTabIdx - 1 + } + if (event) { + event.preventDefault() + } + } + + _refreshAppState(app) { + this.servicesApi.getApp(app.id).subscribe( + data => { + this.msgSrv.add({ + severity: 'success', + summary: 'App refreshed', + detail: 'Refreshing succeeded.', + }) + + htmlizeExtVersion(data) + + // refresh app in app list + for (const s of this.apps) { + if (s.id === data.id) { + Object.assign(s, data) + break + } + } + // refresh machine in opened tab if present + for (const s of this.openedApps) { + if (s.app.id === data.id) { + Object.assign(s.app, data) + break + } + } + }, + err => { + let msg = err.statusText + if (err.error && err.error.message) { + msg = err.error.message + } + this.msgSrv.add({ + severity: 'error', + summary: 'Getting app state erred', + detail: 'Getting state of app erred: ' + msg, + life: 10000, + }) + } + ) + } + + showAppMenu(event, appMenu, app) { + appMenu.toggle(event) + + // connect method to refresh machine state + this.appMenuItems[0].command = () => { + this._refreshAppState(app) + } + } + + onRefreshApp(event) { + this._refreshAppState(this.appTab.app) + } + + refreshAppsList(appsTable) { + appsTable.onLazyLoad.emit(appsTable.createLazyLoadMetadata()) + } + + sortKeaDaemonsByImportance(app) { + const daemonMap = [] + for (const d of app.details.daemons) { + daemonMap[d.name] = d + } + const DMAP = [['dhcp4', 'DHCPv4'], ['dhcp6', 'DHCPv6'], ['d2', 'DDNS'], ['ca', 'CA'], ['netconf', 'NETCONF']] + const daemons = [] + for (const dm of DMAP) { + if (daemonMap[dm[0]] !== undefined) { + daemonMap[dm[0]].niceName = dm[1] + daemons.push(daemonMap[dm[0]]) + } + } + return daemons + } +} diff --git a/webui/src/app/kea-app-tab/kea-app-tab.component.html b/webui/src/app/kea-app-tab/kea-app-tab.component.html new file mode 100644 index 0000000000000000000000000000000000000000..edb993bdcdc4598d4a00038abf15ebfd94481325 --- /dev/null +++ b/webui/src/app/kea-app-tab/kea-app-tab.component.html @@ -0,0 +1,54 @@ +
+
+ +

Kea App {{ appTab.app.id }}.

+ Machine: + {{ appTab.app.machine.address }} +
+ +
+ + +
+
+ {{ item.label }} +
+
+
+
+
+
+
+ +
+
+ + + + + + + + + +
Version{{ daemon.version }}
Version Ext
+
+
+
diff --git a/webui/src/app/kea-app-tab/kea-app-tab.component.sass b/webui/src/app/kea-app-tab/kea-app-tab.component.sass new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/webui/src/app/kea-app-tab/kea-app-tab.component.spec.ts b/webui/src/app/kea-app-tab/kea-app-tab.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..1b61b32e6552c545f31511086abae913ae5db169 --- /dev/null +++ b/webui/src/app/kea-app-tab/kea-app-tab.component.spec.ts @@ -0,0 +1,24 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing' + +import { KeaAppTabComponent } from './kea-app-tab.component' + +describe('KeaAppTabComponent', () => { + let component: KeaAppTabComponent + let fixture: ComponentFixture + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [KeaAppTabComponent], + }).compileComponents() + })) + + beforeEach(() => { + fixture = TestBed.createComponent(KeaAppTabComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/webui/src/app/kea-app-tab/kea-app-tab.component.ts b/webui/src/app/kea-app-tab/kea-app-tab.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..a2c24f7174d69106cebd40a3e99bb398c0ec0eba --- /dev/null +++ b/webui/src/app/kea-app-tab/kea-app-tab.component.ts @@ -0,0 +1,71 @@ +import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core' + +import { MessageService, MenuItem } from 'primeng/api' + +@Component({ + selector: 'app-kea-daemons-tabs', + templateUrl: './kea-app-tab.component.html', + styleUrls: ['./kea-app-tab.component.sass'], +}) +export class KeaAppTabComponent implements OnInit { + private _appTab: any + @Output() refreshApp = new EventEmitter() + + tabs: MenuItem[] + activeTab: MenuItem + daemons: any[] = [] + daemon: any + + constructor() {} + + ngOnInit() { + console.info('this.app', this.appTab) + } + + @Input() + set appTab(appTab) { + this._appTab = appTab + + const daemonMap = [] + for (const d of appTab.app.details.daemons) { + daemonMap[d.name] = d + } + const DMAP = [['dhcp4', 'DHCPv4'], ['dhcp6', 'DHCPv6'], ['d2', 'DDNS'], ['ca', 'CA'], ['netconf', 'NETCONF']] + const daemons = [] + const tabs = [] + for (const dm of DMAP) { + if (daemonMap[dm[0]] !== undefined) { + daemonMap[dm[0]].niceName = dm[1] + daemons.push(daemonMap[dm[0]]) + + tabs.push({ + label: dm[1], + command: event => { + this.daemonTabSwitch(event.item) + }, + }) + } + } + this.daemons = daemons + this.daemon = this.daemons[appTab.activeDaemonTabIdx] + this.tabs = tabs + this.activeTab = this.tabs[appTab.activeDaemonTabIdx] + } + + get appTab() { + return this._appTab + } + + daemonTabSwitch(item) { + for (const d of this.daemons) { + if (d.niceName === item.label) { + this.daemon = d + break + } + } + } + + refreshAppState() { + this.refreshApp.emit(this._appTab.app.id) + } +} diff --git a/webui/src/app/machines-page/machines-page.component.html b/webui/src/app/machines-page/machines-page.component.html index 6bcd1e4e108ae6c4b9e0595934ea511247bc3477..6c00bc224f5c7dc6b0c8891688a784777f7564dc 100644 --- a/webui/src/app/machines-page/machines-page.component.html +++ b/webui/src/app/machines-page/machines-page.component.html @@ -65,8 +65,8 @@ /> - - + + @@ -108,7 +108,7 @@ Hostname Address Agent Version - Service + Apps CPUs CPUs Load Memory @@ -126,7 +126,22 @@ {{ m.address }}:{{ m.agentPort }} {{ m.agentVersion }} - {{ m.service }} + + + {{ s.type }} + + {{ m.cpus }} {{ m.cpusLoad }} {{ m.memory }} @@ -146,15 +161,28 @@ + +
-
-
+
+
+ +
+ +
+

System Information

@@ -203,7 +231,6 @@ - @@ -276,14 +303,27 @@
HostameAgent Version {{ machineTab.machine.agentVersion }}
CPUs {{ machineTab.machine.cpus }}
-
- +
+
+
+

Kea App

+ Active: + + + {{ srv.active ? 'yes' : 'no' }} + +
+ Version: {{ srv.version }} +
+ link to details +
+
diff --git a/webui/src/app/machines-page/machines-page.component.ts b/webui/src/app/machines-page/machines-page.component.ts index a97457c4a393c0b9fbf4c1159ba50e45bfeed870..4a7ee675d268af4b43a0b99518ca73d98762106f 100644 --- a/webui/src/app/machines-page/machines-page.component.ts +++ b/webui/src/app/machines-page/machines-page.component.ts @@ -6,7 +6,7 @@ import { MessageService, MenuItem } from 'primeng/api' import { ServicesService } from '../backend/api/api' import { LoadingService } from '../loading.service' -interface ServiceType { +interface AppType { name: string value: string } @@ -24,8 +24,8 @@ export class MachinesPageComponent implements OnInit { // action panel filterText = '' - serviceTypes: ServiceType[] - selectedServiceType: ServiceType + appTypes: AppType[] + selectedAppType: AppType // new machine newMachineDlgVisible = false @@ -75,7 +75,7 @@ export class MachinesPageComponent implements OnInit { this.tabs = [{ label: 'Machines', routerLink: '/machines/all' }] this.machines = [] - this.serviceTypes = [{ name: 'any', value: '' }, { name: 'BIND', value: 'bind' }, { name: 'Kea', value: 'kea' }] + this.appTypes = [{ name: 'any', value: '' }, { name: 'BIND', value: 'bind' }, { name: 'Kea', value: 'kea' }] this.machineMenuItems = [ { label: 'Refresh', @@ -91,7 +91,6 @@ export class MachinesPageComponent implements OnInit { this.route.paramMap.subscribe((params: ParamMap) => { const machineIdStr = params.get('id') - console.info('machineId', machineIdStr) if (machineIdStr === 'all') { this.switchToTab(0) } else { @@ -102,7 +101,6 @@ export class MachinesPageComponent implements OnInit { for (let idx = 0; idx < this.openedMachines.length; idx++) { const m = this.openedMachines[idx].machine if (m.id === machineId) { - console.info('found opened machine', idx) this.switchToTab(idx + 1) found = true } @@ -113,7 +111,6 @@ export class MachinesPageComponent implements OnInit { if (!found) { for (const m of this.machines) { if (m.id === machineId) { - console.info('found machine in the list, opening it') this.addMachineTab(m) this.switchToTab(this.tabs.length - 1) found = true @@ -124,7 +121,6 @@ export class MachinesPageComponent implements OnInit { // if machine is not loaded in list fetch it individually if (!found) { - console.info('fetching machine') this.servicesApi.getMachine(machineId).subscribe( data => { this.addMachineTab(data) @@ -156,12 +152,12 @@ export class MachinesPageComponent implements OnInit { text = event.filters.text.value } - let service - if (event.filters.service) { - service = event.filters.service.value + let app + if (event.filters.app) { + app = event.filters.app.value } - this.servicesApi.getMachines(event.first, event.rows, text, service).subscribe(data => { + this.servicesApi.getMachines(event.first, event.rows, text, app).subscribe(data => { this.machines = data.items this.totalMachines = data.total }) @@ -241,8 +237,8 @@ export class MachinesPageComponent implements OnInit { } } - filterByService(machinesTable) { - machinesTable.filter(this.selectedServiceType.value, 'service', 'equals') + filterByApp(machinesTable) { + machinesTable.filter(this.selectedAppType.value, 'app', 'equals') } closeTab(event, idx) {