blob: 3a9b0f979c682b050d4a5118aecd0c383204a36d [file]
package main
import (
"encoding/json"
"flag"
"fmt"
"html/template"
"net/http"
"os"
"path/filepath"
"runtime"
"sort"
"strings"
"time"
"github.com/coreos/go-systemd/dbus"
"github.com/gorilla/mux"
"github.com/skia-dev/glog"
"go.skia.org/infra/go/common"
"go.skia.org/infra/go/packages"
"go.skia.org/infra/go/systemd"
"go.skia.org/infra/go/util"
)
var (
// indexTemplate is the main index.html page we serve.
indexTemplate *template.Template = nil
// dbc is the dbus connection we use to talk to systemd.
dbc *dbus.Conn
hostname = ""
ACTIONS = []string{"start", "stop", "restart"}
)
// flags
var (
doOauth = flag.Bool("oauth", true, "Run through the OAuth 2.0 flow on startup, otherwise use a GCE service account.")
graphiteServer = flag.String("graphite_server", "skia-monitoring:2003", "Where is Graphite metrics ingestion server running.")
installedPackagesFile = flag.String("installed_packages_file", "installed_packages.json", "Path to the file where to cache the list of installed debs.")
local = flag.Bool("local", false, "Running locally if true. As opposed to in production.")
oauthCacheFile = flag.String("oauth_cache_file", "google_storage_token.data", "Path to the file where to cache cache the oauth credentials.")
port = flag.String("port", ":10114", "HTTP service address (e.g., ':8000')")
resourcesDir = flag.String("resources_dir", "", "The directory to find templates, JS, and CSS files. If blank the current directory will be used.")
)
type UnitStatusSlice []*systemd.UnitStatus
func (p UnitStatusSlice) Len() int { return len(p) }
func (p UnitStatusSlice) Less(i, j int) bool { return p[i].Status.Name < p[j].Status.Name }
func (p UnitStatusSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
func loadResouces() {
if *resourcesDir == "" {
_, filename, _, _ := runtime.Caller(0)
*resourcesDir = filepath.Join(filepath.Dir(filename), "../..")
}
indexTemplate = template.Must(template.New("").Delims("{%", "%}").ParseFiles(
filepath.Join(*resourcesDir, "templates", "index.html"),
filepath.Join(*resourcesDir, "templates", "titlebar.html"),
filepath.Join(*resourcesDir, "templates", "header.html"),
))
}
// ChangeResult is the serialized JSON response from changeHandler.
type ChangeResult struct {
Result string `json:"result"`
}
func Init() {
var err error
dbc, err = dbus.New()
if err != nil {
glog.Fatalf("Failed to initialize dbus: %s", err)
}
hostname, err = os.Hostname()
if err != nil {
glog.Fatalf("Unable to retrieve hostname: %s", err)
}
loadResouces()
}
// changeHandler changes the status of a service.
//
// Takes the following query parameters:
//
// name - The name of the service.
// action - The action to perform. One of ["start", "stop", "restart"].
//
// The response is of the form:
//
// {
// "result": "started"
// }
//
func changeHandler(w http.ResponseWriter, r *http.Request) {
var err error
if err := r.ParseForm(); err != nil {
util.ReportError(w, r, err, "Failed to parse form.")
return
}
action := r.Form.Get("action")
if !util.In(action, ACTIONS) {
util.ReportError(w, r, fmt.Errorf("Not a valid action: %s", action), "Invalid action.")
return
}
name := r.Form.Get("name")
if name == "" {
util.ReportError(w, r, fmt.Errorf("Not a valid service name: %s", name), "Invalid service name.")
return
}
if *local {
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(ChangeResult{"started"}); err != nil {
glog.Errorf("Failed to write or encode output: %s", err)
}
return
}
ch := make(chan string)
switch action {
case "start":
_, err = dbc.StartUnit(name, "replace", ch)
case "stop":
_, err = dbc.StopUnit(name, "replace", ch)
case "restart":
_, err = dbc.RestartUnit(name, "replace", ch)
}
if err != nil {
util.ReportError(w, r, err, "Action failed.")
return
}
res := ChangeResult{}
res.Result = <-ch
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(res); err != nil {
glog.Errorf("Failed to write or encode output: %s", err)
}
}
// serviceOnly returns only units that are services.
func serviceOnly(units []*systemd.UnitStatus) []*systemd.UnitStatus {
ret := []*systemd.UnitStatus{}
for _, u := range units {
if strings.HasSuffix(u.Status.Name, ".service") {
ret = append(ret, u)
}
}
return ret
}
// filterService returns only units with names in filter.
func filterService(units []*systemd.UnitStatus, filter map[string]bool) []*systemd.UnitStatus {
ret := []*systemd.UnitStatus{}
for _, u := range units {
if filter[u.Status.Name] {
ret = append(ret, u)
}
}
return ret
}
// filterUnits fitlers down the units to only the interesting ones.
func filterUnits(units []*systemd.UnitStatus) []*systemd.UnitStatus {
units = serviceOnly(units)
sort.Sort(UnitStatusSlice(units))
// Filter the list down to just services installed by push packages.
installedPackages, err := packages.FromLocalFile(*installedPackagesFile)
if err != nil {
return units
}
allPackages, err := packages.AllAvailableByPackageName(store)
if err != nil {
return units
}
allServices := map[string]bool{}
for _, p := range installedPackages {
for _, name := range allPackages[p].Services {
allServices[name] = true
}
}
return filterService(units, allServices)
}
func listUnits() ([]*systemd.UnitStatus, error) {
unitStatus, err := dbc.ListUnits()
units := make([]*systemd.UnitStatus, 0, len(unitStatus))
if err == nil {
for _, st := range unitStatus {
cpst := st
units = append(units, &systemd.UnitStatus{
Status: &cpst,
})
}
} else {
if *local {
// If running locally the above will fail because we aren't on systemd
// yet, so return some dummy data.
units = []*systemd.UnitStatus{
&systemd.UnitStatus{
Status: &dbus.UnitStatus{
Name: "test.service",
SubState: "running",
},
Props: map[string]interface{}{
"ExecMainStartTimestamp": time.Now().Add(-5*time.Minute).Unix() * 1000000,
},
},
&systemd.UnitStatus{
Status: &dbus.UnitStatus{
Name: "something.service",
SubState: "halted",
},
Props: map[string]interface{}{
"ExecMainStartTimestamp": time.Now().Add(-2*time.Hour).Unix() * 1000000,
},
},
}
} else {
return nil, fmt.Errorf("Failed to list units: %s", err)
}
}
if !*local {
units = filterUnits(units)
// Now fill in the Props for each unit.
var err error
for _, unit := range units {
unit.Props, err = dbc.GetUnitTypeProperties(unit.Status.Name, "Service")
if err != nil {
glog.Errorf("Failed to get props for the unit %s: %s", unit.Status.Name, err)
}
}
}
return units, nil
}
// listHandler returns the list of units.
func listHandler(w http.ResponseWriter, r *http.Request) {
units, err := listUnits()
if err != nil {
util.ReportError(w, r, err, "Failed to list units.")
return
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(units); err != nil {
glog.Errorf("Failed to write or encode output: %s", err)
}
}
// IndexBody is the context for evaluating the index.html template.
type IndexBody struct {
Hostname string
Units []*systemd.UnitStatus
}
// mainHandler handles the GET of the main page.
func mainHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
if *local {
loadResouces()
}
units, err := listUnits()
if err != nil {
util.ReportError(w, r, err, "Failed to list units.")
return
}
context := &IndexBody{
Hostname: hostname,
Units: units,
}
w.Header().Set("Content-Type", "text/html")
if err := indexTemplate.ExecuteTemplate(w, "index.html", context); err != nil {
glog.Errorln("Failed to expand template:", err)
}
}
}
func main() {
common.InitWithMetrics("pulld", graphiteServer)
Init()
pullInit()
r := mux.NewRouter()
r.PathPrefix("/res/").HandlerFunc(util.MakeResourceHandler(*resourcesDir))
r.HandleFunc("/", mainHandler).Methods("GET")
r.HandleFunc("/_/list", listHandler).Methods("GET")
r.HandleFunc("/_/change", changeHandler).Methods("POST")
r.HandleFunc("/pullpullpull", pullHandler)
http.Handle("/", util.LoggingGzipRequestResponse(r))
glog.Infoln("Ready to serve.")
glog.Fatal(http.ListenAndServe(*port, nil))
}