diff --git a/commander b/commander index 913f095..721c056 100755 --- a/commander +++ b/commander @@ -132,18 +132,18 @@ case $txt in svc=$(docker inspect $(docker ps --format "{{.Names}}" | grep -- -control-) | grep -i started | sed -e "s/[^:]*:\(.*\)/\1/" | sed -e "s/.*\"\(.*\)\".*/\1/") boulder=$(docker inspect $(docker ps --format "{{.Names}}" | grep -- -boulder-) | grep -i started | grep -v depends_on | sed -e "s/[^:]*:\(.*\)/\1/" | sed -e "s/.*\"\(.*\)\".*/\1/") labca=$(docker inspect $(docker ps --format "{{.Names}}" | grep -- -labca-) | grep -i started | grep -v depends_on | sed -e "s/[^:]*:\(.*\)/\1/" | sed -e "s/.*\"\(.*\)\".*/\1/") - echo "$nginx|$svc|$boulder|$labca" + mysql=$(docker inspect $(docker ps --format "{{.Names}}" | grep -- -bmysql-) | grep -i started | grep -v depends_on | sed -e "s/[^:]*:\(.*\)/\1/" | sed -e "s/.*\"\(.*\)\".*/\1/") + echo "$nginx|$svc|$boulder|$labca|$mysql" + exit 0 + ;; +"log-uptime") + timezone=$(cat /etc/timezone) + uptime=$(uptime -s) + echo "$timezone|$uptime" exit 0 ;; "log-stats") - timezone=$(cat /etc/timezone) - uptime=$(uptime -s) - procs=$(ps -ef --no-headers | wc -l) - total=$(free -b --si | grep ":" | head -1 | perl -p0e 's/.*?\s+(\d+)\s+.*/$1/') - avail=$(free -b --si | grep ":" | head -1 | perl -p0e 's/.*\s+(\d+)$/$1/') - let used=$total-$avail - echo "$timezone|$uptime|$procs|$used|$avail" - exit 0 + docker stats --no-stream -a | grep " boulder-" ;; "revoke-cert") read serial @@ -183,6 +183,12 @@ case $txt in sleep 15 wait_up $PS_LABCA &>>$LOGFILE ;; +"mysql-restart") + cd /boulder + set +e + COMPOSE_HTTP_TIMEOUT=120 docker-compose restart bmysql + set -e + ;; "svc-restart") cd /boulder set +e diff --git a/gui/dashboard.go b/gui/dashboard.go index 57e4185..7132acf 100644 --- a/gui/dashboard.go +++ b/gui/dashboard.go @@ -166,6 +166,15 @@ func _parseComponents(data string) []Component { parts := strings.Split(data, "|") + if len(parts) < 5 { + components = append(components, Component{Name: "Boulder (ACME)"}) + components = append(components, Component{Name: "Controller"}) + components = append(components, Component{Name: "LabCA Application"}) + components = append(components, Component{Name: "MySQL Database"}) + components = append(components, Component{Name: "NGINX Webserver"}) + return components + } + nginx, err := time.Parse(time.RFC3339Nano, parts[0]) nginxReal := "" nginxNice := "stopped" @@ -206,9 +215,20 @@ func _parseComponents(data string) []Component { labcaClass = "" } + mysql, err := time.Parse(time.RFC3339Nano, parts[4]) + mysqlReal := "" + mysqlNice := "stopped" + mysqlClass := "error" + if err == nil { + mysqlReal = mysql.Format("02-Jan-2006 15:04:05 MST") + mysqlNice = humanize.RelTime(mysql, time.Now(), "", "") + mysqlClass = "" + } + components = append(components, Component{Name: "Boulder (ACME)", Timestamp: boulderReal, TimestampRel: boulderNice, Class: boulderClass}) components = append(components, Component{Name: "Controller", Timestamp: svcReal, TimestampRel: svcNice, Class: svcClass}) components = append(components, Component{Name: "LabCA Application", Timestamp: labcaReal, TimestampRel: labcaNice, Class: labcaClass}) + components = append(components, Component{Name: "MySQL Database", Timestamp: mysqlReal, TimestampRel: mysqlNice, Class: mysqlClass}) components = append(components, Component{Name: "NGINX Webserver", Timestamp: nginxReal, TimestampRel: nginxNice, Class: nginxClass}) return components @@ -222,7 +242,16 @@ type Stat struct { Class string } -func _parseStats(data string) []Stat { +// The stats as reported by docker +type DockerStat struct { + Name string + MemUsage uint64 + MemLimit uint64 + MemPerc float64 + Pids uint64 +} + +func _parseStats(data string, components []Component) []Stat { var stats []Stat if data[len(data)-1:] == "\n" { @@ -249,24 +278,99 @@ func _parseStats(data string) []Stat { } stats = append(stats, Stat{Name: "System Uptime", Hint: sinceReal, Value: sinceNice}) - memUsed, err := strconv.ParseUint(parts[3], 10, 64) - if err != nil { - memUsed = 0 + if components == nil { + return stats } - memAvail, err := strconv.ParseUint(parts[4], 10, 64) - if err != nil { - memAvail = 0 + + stats = append(stats, Stat{Name: "Memory Limit", Value: ""}) + stats = append(stats, Stat{Name: "Memory Used", Value: ""}) + stats = append(stats, Stat{Name: "Memory Used [%]", Value: ""}) + + return stats +} + +// What we return as json +type AjaxStat struct { + Stat + MemoryUsed string + MemoryPerc string + NumPids int +} + +func parseDockerStats(data string) []AjaxStat { + var stats []AjaxStat + + dockerStats := []DockerStat{} + rawStats := strings.Split(data, "\n") + for _, rawStat := range rawStats { + if len(rawStat) > 0 { + elms := strings.Fields(rawStat) + if len(elms) > 13 { + stat := DockerStat{} + // CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS + // 817bdaec6daf boulder-boulder-1 0.07% 255.3MiB / 1GiB 24.93% 1.18MB / 339kB 0B / 0B 158 + stat.Name = elms[1] + x, err := humanize.ParseBigBytes(elms[3]) + if err == nil { + stat.MemUsage = x.Uint64() + } + x, err = humanize.ParseBigBytes(elms[5]) + if err == nil { + stat.MemLimit = x.Uint64() + } + y, err := strconv.ParseFloat(strings.Replace(elms[6], "%", "", -1), 64) + if err == nil { + stat.MemPerc = y + } + p, err := strconv.ParseUint(elms[13], 10, 64) + if err == nil { + stat.Pids = p + } + dockerStats = append(dockerStats, stat) + } + } + } + + // Update the component stats + totalMemUsage := uint64(0) + for _, docker := range dockerStats { + stat := AjaxStat{} + if strings.Contains(docker.Name, "-boulder-") { + stat.Name = "Boulder (ACME)" + } + if strings.Contains(docker.Name, "-control-") { + stat.Name = "Controller" + } + if strings.Contains(docker.Name, "-labca-") { + stat.Name = "LabCA Application" + } + if strings.Contains(docker.Name, "-nginx-") { + stat.Name = "NGINX Webserver" + } + if strings.Contains(docker.Name, "-bmysql-") { + stat.Name = "MySQL Database" + } + + stat.MemoryUsed = humanize.IBytes(docker.MemUsage) + stat.MemoryPerc = fmt.Sprintf("%s%%", humanize.FtoaWithDigits(docker.MemPerc, 1)) + stat.NumPids = int(docker.Pids) + + stats = append(stats, stat) + + totalMemUsage += docker.MemUsage } percMem := float64(0) - if (memUsed + memAvail) > 0 { - percMem = float64(100) * float64(memUsed) / float64(memUsed+memAvail) + if (dockerStats[0].MemLimit) > 0 { + percMem = float64(100) * float64(totalMemUsage) / float64(dockerStats[0].MemLimit) } - usedHuman := humanize.IBytes(memUsed) - availHuman := humanize.IBytes(memAvail) - percHuman := fmt.Sprintf("%s %%", humanize.FtoaWithDigits(percMem, 1)) + usedHuman := humanize.IBytes(totalMemUsage) + limitHuman := humanize.IBytes(dockerStats[0].MemLimit) + percHuman := fmt.Sprintf("%s%%", humanize.FtoaWithDigits(percMem, 1)) + stats = append(stats, AjaxStat{Stat: Stat{Name: "Memory Limit", Value: limitHuman}}) + stats = append(stats, AjaxStat{Stat: Stat{Name: "Memory Used", Value: usedHuman}}) class := "" if percMem > 75 { class = "warning" @@ -274,16 +378,7 @@ func _parseStats(data string) []Stat { if percMem > 90 { class = "error" } - stats = append(stats, Stat{Name: "Memory Usage", Value: percHuman, Class: class}) - stats = append(stats, Stat{Name: "Memory Used", Value: usedHuman}) - class = "" - if memAvail < 250000000 { - class = "warning" - } - if memAvail < 100000000 { - class = "error" - } - stats = append(stats, Stat{Name: "Memory Available", Value: availHuman, Class: class}) + stats = append(stats, AjaxStat{Stat: Stat{Name: "Memory Used [%]", Value: percHuman, Class: class}}) return stats } @@ -369,11 +464,10 @@ func CollectDashboardData(w http.ResponseWriter, r *http.Request) (map[string]in activity := getLog(w, r, "activity") dashboardData["Activity"] = _parseActivity(activity) - components := getLog(w, r, "components") - dashboardData["Components"] = _parseComponents(components) - - stats := getLog(w, r, "stats") - dashboardData["Stats"] = _parseStats(stats) + components := _parseComponents(getLog(w, r, "components")) + uptime := getLog(w, r, "uptime") + dashboardData["Stats"] = _parseStats(uptime, components) + dashboardData["Components"] = components return dashboardData, nil } diff --git a/gui/main.go b/gui/main.go index bb4caa6..0a8d85e 100644 --- a/gui/main.go +++ b/gui/main.go @@ -894,7 +894,8 @@ func (res *Result) ManageComponents(w http.ResponseWriter, r *http.Request, acti if (components[i].Name == "NGINX Webserver" && (action == "nginx-reload" || action == "nginx-restart")) || (components[i].Name == "Controller" && action == "svc-restart") || (components[i].Name == "Boulder (ACME)" && (action == "boulder-start" || action == "boulder-stop" || action == "boulder-restart")) || - (components[i].Name == "LabCA Application" && action == "labca-restart") { + (components[i].Name == "LabCA Application" && action == "labca-restart") || + (components[i].Name == "MySQL Database" && action == "mysql-restart") { res.Timestamp = components[i].Timestamp res.TimestampRel = components[i].TimestampRel res.Class = components[i].Class @@ -976,6 +977,7 @@ func _managePost(w http.ResponseWriter, r *http.Request) { "backup-delete", "backup-now", "cert-export", + "mysql-restart", "nginx-reload", "nginx-restart", "svc-restart", @@ -1109,17 +1111,20 @@ func _manageGet(w http.ResponseWriter, r *http.Request) { btn["Label"] = "Restart" components[i].Buttons = append(components[i].Buttons, btn) } - } - manageData["Components"] = components - stats := _parseStats(getLog(w, r, "stats")) - for _, stat := range stats { - if stat.Name == "System Uptime" { - manageData["ServerTimestamp"] = stat.Hint - manageData["ServerTimestampRel"] = stat.Value - break + if components[i].Name == "MySQL Database" { + components[i].LogURL = "" + components[i].LogTitle = "" + + btn := make(map[string]interface{}) + btn["Class"] = "btn-warning" + btn["Id"] = "mysql-restart" + btn["Title"] = "Restart the MySQL database server" + btn["Label"] = "Restart" + components[i].Buttons = append(components[i].Buttons, btn) } } + manageData["Components"] = components backupFiles := strings.Split(getLog(w, r, "backups"), "\n") backupFiles = backupFiles[:len(backupFiles)-1] @@ -2269,6 +2274,13 @@ func certRevokeHandler(w http.ResponseWriter, r *http.Request) { } } +func statsHandler(w http.ResponseWriter, r *http.Request) { + res := parseDockerStats(getLog(w, r, "stats")) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(res) +} + type navItem struct { Name string Icon string @@ -2600,6 +2612,7 @@ func main() { r := mux.NewRouter() r.HandleFunc("/", rootHandler).Methods("GET") + r.HandleFunc("/stats", statsHandler).Methods("GET") r.HandleFunc("/about", aboutHandler).Methods("GET") r.HandleFunc("/manage", manageHandler).Methods("GET", "POST") r.HandleFunc("/final", finalHandler).Methods("GET") diff --git a/gui/templates/views/dashboard.tmpl b/gui/templates/views/dashboard.tmpl index de97683..2451a51 100644 --- a/gui/templates/views/dashboard.tmpl +++ b/gui/templates/views/dashboard.tmpl @@ -136,18 +136,27 @@
-
+
{{ range $item := .Components }} - - + + + + + + + + + + {{ end }}
{{ $item.Name }} + {{ $item.Name }}Uptime {{ $item.TimestampRel }} {{ if $item.Class }}{{ end }}
Memory
Processes
@@ -158,7 +167,8 @@ {{ $item.Name }} - {{ $item.Value }} + + {{ if $item.Value }}{{ $item.Value }}{{ else }}{{ end }} {{ if $item.Class }}{{ end }} @@ -178,3 +188,53 @@
{{ end }} + +{{ define "tail" }} + +{{ end }} diff --git a/static/css/labca.css b/static/css/labca.css index f33b2d3..381e547 100644 --- a/static/css/labca.css +++ b/static/css/labca.css @@ -95,6 +95,19 @@ p.caption { padding: 4px 8px; } +td.pad-low-bottom { + padding-bottom: 4px !important; +} + +td.pad-low { + padding-top: 4px !important; + padding-bottom: 4px !important; +} + +td.pad-low-top { + padding-top: 4px !important; +} + .btn-reg { width: 5em; }