diff --git a/go/cmd/vtctld/api.go b/go/cmd/vtctld/api.go
index c26e6ff793..f60f5da415 100644
--- a/go/cmd/vtctld/api.go
+++ b/go/cmd/vtctld/api.go
@@ -16,7 +16,7 @@ import (
const apiPrefix = "/api/"
-func handleGet(collection string, getFunc func(*http.Request) (interface{}, error)) {
+func handleCollection(collection string, getFunc func(*http.Request) (interface{}, error)) {
http.HandleFunc(apiPrefix+collection+"/", func(w http.ResponseWriter, r *http.Request) {
// Get the requested object.
obj, err := getFunc(r)
@@ -60,46 +60,79 @@ func getItemPath(url string) string {
return parts[1]
}
-func initAPI(ctx context.Context, ts topo.Server) {
+func initAPI(ctx context.Context, ts topo.Server, actions *ActionRepository) {
tabletHealthCache := newTabletHealthCache(ts)
- // Get Cells
- handleGet("cells", func(r *http.Request) (interface{}, error) {
+ // Cells
+ handleCollection("cells", func(r *http.Request) (interface{}, error) {
if getItemPath(r.URL.Path) != "" {
return nil, errors.New("cells can only be listed, not retrieved")
}
return ts.GetKnownCells(ctx)
})
- // Get Keyspaces
- handleGet("keyspaces", func(r *http.Request) (interface{}, error) {
+ // Keyspaces
+ handleCollection("keyspaces", func(r *http.Request) (interface{}, error) {
keyspace := getItemPath(r.URL.Path)
+
+ // List all keyspaces.
if keyspace == "" {
return ts.GetKeyspaces(ctx)
}
+
+ // Perform an action on a keyspace.
+ if r.Method == "POST" {
+ if err := r.ParseForm(); err != nil {
+ return nil, err
+ }
+ action := r.FormValue("action")
+ if action == "" {
+ return nil, errors.New("must specify action")
+ }
+ return actions.ApplyKeyspaceAction(ctx, action, keyspace, r), nil
+ }
+
+ // Get the keyspace record.
return ts.GetKeyspace(ctx, keyspace)
})
- // Get Shards
- handleGet("shards", func(r *http.Request) (interface{}, error) {
+ // Shards
+ handleCollection("shards", func(r *http.Request) (interface{}, error) {
shardPath := getItemPath(r.URL.Path)
if !strings.Contains(shardPath, "/") {
return nil, fmt.Errorf("invalid shard path: %q", shardPath)
}
parts := strings.SplitN(shardPath, "/", 2)
- if parts[1] == "" {
- // It's just a keyspace. List the shards.
- return ts.GetShardNames(ctx, parts[0])
+ keyspace := parts[0]
+ shard := parts[1]
+
+ // List the shards in a keyspace.
+ if shard == "" {
+ return ts.GetShardNames(ctx, keyspace)
}
- // It's a keyspace/shard reference.
- return ts.GetShard(ctx, parts[0], parts[1])
+
+ // Perform an action on a shard.
+ if r.Method == "POST" {
+ if err := r.ParseForm(); err != nil {
+ return nil, err
+ }
+ action := r.FormValue("action")
+ if action == "" {
+ return nil, errors.New("must specify action")
+ }
+ return actions.ApplyShardAction(ctx, action, keyspace, shard, r), nil
+ }
+
+ // Get the shard record.
+ return ts.GetShard(ctx, keyspace, shard)
})
- // Get Tablets
- handleGet("tablets", func(r *http.Request) (interface{}, error) {
+ // Tablets
+ handleCollection("tablets", func(r *http.Request) (interface{}, error) {
tabletPath := getItemPath(r.URL.Path)
+
+ // List tablets based on query params.
if tabletPath == "" {
- // List tablets based on query params.
if err := r.ParseForm(); err != nil {
return nil, err
}
@@ -125,7 +158,7 @@ func initAPI(ctx context.Context, ts topo.Server) {
return ts.GetTabletsByCell(ctx, cell)
}
- // Tablet Health
+ // Get tablet health.
if parts := strings.Split(tabletPath, "/"); len(parts) == 2 && parts[1] == "health" {
tabletAlias, err := topo.ParseTabletAliasString(parts[0])
if err != nil {
@@ -134,16 +167,29 @@ func initAPI(ctx context.Context, ts topo.Server) {
return tabletHealthCache.Get(ctx, tabletAlias)
}
- // Get a specific tablet.
tabletAlias, err := topo.ParseTabletAliasString(tabletPath)
if err != nil {
return nil, err
}
+
+ // Perform an action on a tablet.
+ if r.Method == "POST" {
+ if err := r.ParseForm(); err != nil {
+ return nil, err
+ }
+ action := r.FormValue("action")
+ if action == "" {
+ return nil, errors.New("must specify action")
+ }
+ return actions.ApplyTabletAction(ctx, action, tabletAlias, r), nil
+ }
+
+ // Get the tablet record.
return ts.GetTablet(ctx, tabletAlias)
})
- // Get EndPoints
- handleGet("endpoints", func(r *http.Request) (interface{}, error) {
+ // EndPoints
+ handleCollection("endpoints", func(r *http.Request) (interface{}, error) {
// We expect cell/keyspace/shard/tabletType.
epPath := getItemPath(r.URL.Path)
parts := strings.Split(epPath, "/")
diff --git a/go/cmd/vtctld/vtctld.go b/go/cmd/vtctld/vtctld.go
index 33259fa511..9db292ed2a 100644
--- a/go/cmd/vtctld/vtctld.go
+++ b/go/cmd/vtctld/vtctld.go
@@ -126,6 +126,15 @@ func main() {
return "", wr.TabletManagerClient().Ping(ctx, ti)
})
+ actionRepo.RegisterTabletAction("RefreshState", acl.ADMIN,
+ func(ctx context.Context, wr *wrangler.Wrangler, tabletAlias topo.TabletAlias, r *http.Request) (string, error) {
+ ti, err := wr.TopoServer().GetTablet(ctx, tabletAlias)
+ if err != nil {
+ return "", err
+ }
+ return "", wr.TabletManagerClient().RefreshState(ctx, ti)
+ })
+
actionRepo.RegisterTabletAction("ScrapTablet", acl.ADMIN,
func(ctx context.Context, wr *wrangler.Wrangler, tabletAlias topo.TabletAlias, r *http.Request) (string, error) {
// refuse to scrap tablets that are not spare
@@ -310,7 +319,7 @@ func main() {
}
// Serve the REST API for the vtctld web app.
- initAPI(context.Background(), ts)
+ initAPI(context.Background(), ts, actionRepo)
// vschema viewer
http.HandleFunc("/vschema", func(w http.ResponseWriter, r *http.Request) {
@@ -320,7 +329,7 @@ func main() {
}
schemafier, ok := ts.(topo.Schemafier)
if !ok {
- httpErrorf(w, r, "%s", fmt.Errorf("%T doesn's support schemafier API", ts))
+ httpErrorf(w, r, "%s", fmt.Errorf("%T doesn't support schemafier API", ts))
}
var data struct {
Error error
diff --git a/test/tablet.py b/test/tablet.py
index 79440d876b..47ff85cac4 100644
--- a/test/tablet.py
+++ b/test/tablet.py
@@ -404,7 +404,13 @@ class Tablet(object):
if not self.proc:
Tablet.tablets_running += 1
self.proc = utils.run_bg(args, stderr=stderr_fd, extra_env=extra_env)
+
+ log_message = "Started vttablet: %s (%s) with pid: %s - Log files: %s/vttablet.*.{INFO,WARNING,ERROR,FATAL}.*.%s" % \
+ (self.tablet_uid, self.tablet_alias, self.proc.pid, environment.vtlogroot, self.proc.pid)
+ # This may race with the stderr output from the process (though that's usually empty).
+ stderr_fd.write(log_message + '\n')
stderr_fd.close()
+ logging.debug(log_message)
# wait for query service to be in the right state
if wait_for_state:
diff --git a/web/vtctld/action-dialog.html b/web/vtctld/action-dialog.html
new file mode 100644
index 0000000000..b3e58d9c35
--- /dev/null
+++ b/web/vtctld/action-dialog.html
@@ -0,0 +1,33 @@
+