2013-06-05 17:18:27 +00:00
/ *
Copyright 2013 The Camlistore Authors .
Licensed under the Apache License , Version 2.0 ( the "License" ) ;
you may not use this file except in compliance with the License .
You may obtain a copy of the License at
http : //www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing , software
distributed under the License is distributed on an "AS IS" BASIS ,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
See the License for the specific language governing permissions and
limitations under the License .
* /
package server
import (
2014-03-17 03:13:47 +00:00
"encoding/json"
2016-07-20 00:35:20 +00:00
"errors"
2014-03-17 03:13:47 +00:00
"fmt"
"html"
2015-04-02 16:17:18 +00:00
"io"
2014-03-17 03:13:47 +00:00
"log"
2013-06-05 17:18:27 +00:00
"net/http"
2014-08-29 23:03:44 +00:00
"os"
2014-08-29 21:11:58 +00:00
"reflect"
2014-03-18 04:17:46 +00:00
"regexp"
2015-04-02 16:17:18 +00:00
"runtime"
2014-03-17 03:13:47 +00:00
"strings"
2014-08-29 21:11:58 +00:00
"time"
2013-06-05 17:18:27 +00:00
"camlistore.org/pkg/blobserver"
"camlistore.org/pkg/buildinfo"
2015-04-02 16:17:18 +00:00
"camlistore.org/pkg/env"
2013-06-05 17:18:27 +00:00
"camlistore.org/pkg/httputil"
2014-03-17 03:13:47 +00:00
"camlistore.org/pkg/index"
2014-12-19 15:31:59 +00:00
"camlistore.org/pkg/osutil"
2014-08-29 21:11:58 +00:00
"camlistore.org/pkg/search"
2014-12-19 15:31:59 +00:00
"camlistore.org/pkg/server/app"
2014-08-29 23:03:44 +00:00
"camlistore.org/pkg/types/camtypes"
2016-07-20 00:35:20 +00:00
2016-09-07 18:27:11 +00:00
"cloud.google.com/go/compute/metadata"
"go4.org/jsonconfig"
2013-06-05 17:18:27 +00:00
)
// StatusHandler publishes server status information.
type StatusHandler struct {
2014-03-17 03:13:47 +00:00
prefix string
handlerFinder blobserver . FindHandlerByTyper
2013-06-05 17:18:27 +00:00
}
func init ( ) {
blobserver . RegisterHandlerConstructor ( "status" , newStatusFromConfig )
}
2014-08-29 21:11:58 +00:00
var _ blobserver . HandlerIniter = ( * StatusHandler ) ( nil )
2013-06-05 17:18:27 +00:00
func newStatusFromConfig ( ld blobserver . Loader , conf jsonconfig . Obj ) ( h http . Handler , err error ) {
2013-09-22 23:03:47 +00:00
if err := conf . Validate ( ) ; err != nil {
return nil , err
}
2014-03-17 03:13:47 +00:00
return & StatusHandler {
prefix : ld . MyPrefix ( ) ,
handlerFinder : ld ,
} , nil
2013-06-05 17:18:27 +00:00
}
2014-08-29 21:11:58 +00:00
func ( sh * StatusHandler ) InitHandler ( hl blobserver . FindHandlerByTyper ) error {
_ , h , err := hl . FindHandlerByType ( "search" )
if err == blobserver . ErrHandlerTypeNotFound {
return nil
}
if err != nil {
return err
}
go func ( ) {
var lastSend * status
for {
cur := sh . currentStatus ( )
if reflect . DeepEqual ( cur , lastSend ) {
// TODO: something better. get notified on interesting events.
time . Sleep ( 10 * time . Second )
continue
}
lastSend = cur
js , _ := json . MarshalIndent ( cur , "" , " " )
h . ( * search . Handler ) . SendStatusUpdate ( js )
}
} ( )
return nil
}
2013-06-05 17:18:27 +00:00
func ( sh * StatusHandler ) ServeHTTP ( rw http . ResponseWriter , req * http . Request ) {
2013-06-12 09:17:30 +00:00
suffix := httputil . PathSuffix ( req )
2014-12-19 15:31:59 +00:00
if suffix == "restart" {
sh . serveRestart ( rw , req )
return
}
2014-03-17 03:13:47 +00:00
if ! httputil . IsGet ( req ) {
http . Error ( rw , "Illegal status method." , http . StatusMethodNotAllowed )
2013-06-09 17:25:00 +00:00
return
2013-06-05 17:18:27 +00:00
}
2014-03-17 03:13:47 +00:00
switch suffix {
case "status.json" :
sh . serveStatusJSON ( rw , req )
case "" :
sh . serveStatusHTML ( rw , req )
default :
2015-02-21 12:22:11 +00:00
http . Error ( rw , "Illegal status path." , http . StatusNotFound )
2013-06-05 17:18:27 +00:00
}
}
2014-03-17 03:13:47 +00:00
type status struct {
2014-08-30 13:41:19 +00:00
Version string ` json:"version" `
Errors [ ] camtypes . StatusError ` json:"errors,omitempty" `
Sync map [ string ] syncStatus ` json:"sync" `
Storage map [ string ] storageStatus ` json:"storage" `
importerRoot string
rootPrefix string
2014-08-16 18:58:16 +00:00
ImporterAccounts interface { } ` json:"importerAccounts" `
2014-03-17 03:13:47 +00:00
}
2014-08-29 23:03:44 +00:00
func ( st * status ) addError ( msg , url string ) {
st . Errors = append ( st . Errors , camtypes . StatusError {
Error : msg ,
URL : url ,
} )
}
2014-03-18 04:17:46 +00:00
func ( st * status ) isHandler ( pfx string ) bool {
2014-08-30 13:41:19 +00:00
if pfx == st . importerRoot {
2014-08-16 18:58:16 +00:00
return true
}
2014-03-18 04:17:46 +00:00
if _ , ok := st . Sync [ pfx ] ; ok {
return true
}
if _ , ok := st . Storage [ pfx ] ; ok {
return true
}
return false
}
2014-03-17 03:13:47 +00:00
type storageStatus struct {
Primary bool ` json:"primary,omitempty" `
IsIndex bool ` json:"isIndex,omitempty" `
Type string ` json:"type" `
2014-08-29 21:11:58 +00:00
ApproxBlobs int ` json:"approxBlobs,omitempty" `
ApproxBytes int ` json:"approxBytes,omitempty" `
2014-03-17 03:13:47 +00:00
ImplStatus interface { } ` json:"implStatus,omitempty" `
2013-06-05 17:18:27 +00:00
}
2014-03-17 03:13:47 +00:00
func ( sh * StatusHandler ) currentStatus ( ) * status {
res := & status {
2013-06-09 17:25:00 +00:00
Version : buildinfo . Version ( ) ,
2014-03-17 03:13:47 +00:00
Storage : make ( map [ string ] storageStatus ) ,
2014-03-18 04:17:46 +00:00
Sync : make ( map [ string ] syncStatus ) ,
2014-03-17 03:13:47 +00:00
}
2014-08-29 23:03:44 +00:00
if v := os . Getenv ( "CAMLI_FAKE_STATUS_ERROR" ) ; v != "" {
res . addError ( v , "/status/#fakeerror" )
}
2014-03-17 03:13:47 +00:00
_ , hi , err := sh . handlerFinder . FindHandlerByType ( "root" )
if err != nil {
2014-08-29 23:03:44 +00:00
res . addError ( fmt . Sprintf ( "Error finding root handler: %v" , err ) , "" )
2014-03-17 03:13:47 +00:00
return res
}
rh := hi . ( * RootHandler )
res . rootPrefix = rh . Prefix
2014-08-16 18:58:16 +00:00
if pfx , h , err := sh . handlerFinder . FindHandlerByType ( "importer" ) ; err == nil {
2014-08-30 13:41:19 +00:00
res . importerRoot = pfx
2014-08-16 18:58:16 +00:00
as := h . ( interface {
2014-08-29 23:03:44 +00:00
AccountsStatus ( ) ( interface { } , [ ] camtypes . StatusError )
2014-08-16 18:58:16 +00:00
} )
2014-08-29 23:03:44 +00:00
var errs [ ] camtypes . StatusError
res . ImporterAccounts , errs = as . AccountsStatus ( )
res . Errors = append ( res . Errors , errs ... )
2014-08-16 18:58:16 +00:00
}
2014-03-17 03:13:47 +00:00
types , handlers := sh . handlerFinder . AllHandlers ( )
2014-03-18 04:17:46 +00:00
// Sync
for pfx , h := range handlers {
sh , ok := h . ( * SyncHandler )
if ! ok {
continue
}
res . Sync [ pfx ] = sh . currentStatus ( )
}
2014-03-17 03:13:47 +00:00
// Storage
for pfx , typ := range types {
if ! strings . HasPrefix ( typ , "storage-" ) {
continue
}
h := handlers [ pfx ]
_ , isIndex := h . ( * index . Index )
res . Storage [ pfx ] = storageStatus {
Type : strings . TrimPrefix ( typ , "storage-" ) ,
Primary : pfx == rh . BlobRoot ,
IsIndex : isIndex ,
}
2013-06-05 17:18:27 +00:00
}
2014-03-17 03:13:47 +00:00
return res
}
func ( sh * StatusHandler ) serveStatusJSON ( rw http . ResponseWriter , req * http . Request ) {
httputil . ReturnJSON ( rw , sh . currentStatus ( ) )
}
2016-07-20 00:35:20 +00:00
func ( sh * StatusHandler ) googleCloudConsole ( ) ( string , error ) {
if ! env . OnGCE ( ) {
return "" , errors . New ( "not on GCE" )
}
projID , err := metadata . ProjectID ( )
if err != nil {
return "" , fmt . Errorf ( "Error getting project ID: %v" , err )
}
return "https://console.cloud.google.com/compute/instances?project=" + projID , nil
}
2014-03-18 04:17:46 +00:00
var quotedPrefix = regexp . MustCompile ( ` [;"]/(\S+?/)[&"] ` )
2014-03-17 03:13:47 +00:00
func ( sh * StatusHandler ) serveStatusHTML ( rw http . ResponseWriter , req * http . Request ) {
st := sh . currentStatus ( )
f := func ( p string , a ... interface { } ) {
2015-04-02 16:17:18 +00:00
if len ( a ) == 0 {
io . WriteString ( rw , p )
} else {
fmt . Fprintf ( rw , p , a ... )
}
2014-03-17 03:13:47 +00:00
}
2015-04-02 16:17:18 +00:00
f ( "<html><head><title>camlistored status</title></head>" )
f ( "<body>" )
f ( "<h1>camlistored status</h1>" )
f ( "<h2>Versions</h2><ul>" )
var envStr string
if env . OnGCE ( ) {
envStr = " (on GCE)"
}
f ( "<li><b>Camlistore</b>: %s%s</li>" , html . EscapeString ( buildinfo . Version ( ) ) , envStr )
f ( "<li><b>Go</b>: %s/%s %s, cgo=%v</li>" , runtime . GOOS , runtime . GOARCH , runtime . Version ( ) , cgoEnabled )
f ( "<li><b>djpeg</b>: %s" , html . EscapeString ( buildinfo . DjpegStatus ( ) ) )
f ( "</ul>" )
f ( "<h2>Logs</h2><ul>" )
f ( " <li><a href='/debug/config'>/debug/config</a> - server config</li>\n" )
if env . OnGCE ( ) {
2015-11-19 15:49:46 +00:00
f ( " <li><a href='/debug/logs/camlistored'>camlistored logs on Google Cloud Logging</a></li>\n" )
f ( " <li><a href='/debug/logs/system'>system logs from Google Compute Engine</a></li>\n" )
2015-04-02 16:17:18 +00:00
}
f ( "</ul>" )
f ( "<h2>Admin</h2>" )
2016-07-20 00:35:20 +00:00
f ( "<ul>" )
f ( " <li><form method='post' action='restart' onsubmit='return confirm(\"Really restart now?\")'><button>restart server</button>" )
f ( "<input type='checkbox' name='reindex'> reindex</form></li>" )
if env . OnGCE ( ) {
console , err := sh . googleCloudConsole ( )
if err != nil {
log . Printf ( "error getting Google Cloud Console URL: %v" , err )
} else {
f ( " <li><b>Updating:</b> When a new image for Camlistore on GCE is available, you can update by hitting \"Reset\" (or \"Stop\", then \"Start\") for your instance on your <a href='%s'>Google Cloud Console</a>.<br>Alternatively, you can ssh to your instance and restart the Camlistore service with: <b>sudo systemctl restart camlistored</b>.</li>" , console )
}
}
f ( "</ul>" )
2015-04-02 16:17:18 +00:00
f ( "<h2>Handlers</h2>" )
2014-03-17 03:13:47 +00:00
f ( "<p>As JSON: <a href='status.json'>status.json</a>; and the <a href='%s?camli.mode=config'>discovery JSON</a>.</p>" , st . rootPrefix )
f ( "<p>Not yet pretty HTML UI:</p>" )
js , err := json . MarshalIndent ( st , "" , " " )
if err != nil {
log . Printf ( "JSON marshal error: %v" , err )
}
2014-03-18 04:17:46 +00:00
jsh := html . EscapeString ( string ( js ) )
jsh = quotedPrefix . ReplaceAllStringFunc ( jsh , func ( in string ) string {
pfx := in [ 1 : len ( in ) - 1 ]
if st . isHandler ( pfx ) {
return fmt . Sprintf ( "%s<a href='%s'>%s</a>%s" , in [ : 1 ] , pfx , pfx , in [ len ( in ) - 1 : ] )
}
return in
} )
f ( "<pre>%s</pre>" , jsh )
2013-06-05 17:18:27 +00:00
}
2014-12-19 15:31:59 +00:00
func ( sh * StatusHandler ) serveRestart ( rw http . ResponseWriter , req * http . Request ) {
if req . Method != "POST" {
http . Error ( rw , "POST to restart" , http . StatusMethodNotAllowed )
return
}
_ , handlers := sh . handlerFinder . AllHandlers ( )
for _ , h := range handlers {
ah , ok := h . ( * app . Handler )
if ! ok {
continue
}
log . Printf ( "Sending SIGINT to %s" , ah . ProgramName ( ) )
err := ah . Quit ( )
if err != nil {
msg := fmt . Sprintf ( "Not restarting: couldn't interrupt app %s: %v" , ah . ProgramName ( ) , err )
log . Printf ( msg )
http . Error ( rw , msg , http . StatusInternalServerError )
return
}
}
2016-05-06 17:53:44 +00:00
reindex := ( req . FormValue ( "reindex" ) == "on" )
2014-12-19 15:31:59 +00:00
log . Println ( "Restarting camlistored" )
rw . Header ( ) . Set ( "Connection" , "close" )
http . Redirect ( rw , req , sh . prefix , http . StatusFound )
if f , ok := rw . ( http . Flusher ) ; ok {
f . Flush ( )
}
2016-05-06 17:53:44 +00:00
osutil . RestartProcess ( fmt . Sprintf ( "-reindex=%t" , reindex ) )
2014-12-19 15:31:59 +00:00
}
2015-04-02 16:17:18 +00:00
var cgoEnabled bool