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 @@
+
Version | +{{ daemon.version }} | +
Version Ext | ++ |