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 @@ + +
+ + +
+

+ + + + +
+
+ + + +
+ +

Executing action...

+
+ + +

+

+
+ +
+ +
+ Close +
+ +
+
\ No newline at end of file diff --git a/web/vtctld/app.js b/web/vtctld/app.js index e4660e8ba6..df0b0c5eb8 100644 --- a/web/vtctld/app.js +++ b/web/vtctld/app.js @@ -89,14 +89,16 @@ app.controller('AppCtrl', function($scope, $mdSidenav, $route, $location, }; }); -app.controller('KeyspacesCtrl', function($scope, $mdDialog, keyspaces, shards) { +app.controller('KeyspacesCtrl', function($scope, keyspaces, shards, actions) { $scope.keyspaceActions = [ - 'Validate Keyspace', - 'Validate Permissions', - 'Validate Schema', - 'Validate Version' + {name: 'ValidateKeyspace', title: 'Validate Keyspace'}, + {name: 'ValidateSchemaKeyspace', title: 'Validate Schema'}, + {name: 'ValidateVersionKeyspace', title: 'Validate Version'}, + {name: 'ValidatePermissionsKeyspace', title: 'Validate Permissions'}, ]; + $scope.actions = actions; + $scope.refreshData = function() { // Get list of keyspace names. keyspaces.query(function(ksnames) { @@ -114,15 +116,30 @@ app.controller('KeyspacesCtrl', function($scope, $mdDialog, keyspaces, shards) { }); app.controller('ShardCtrl', function($scope, $routeParams, $timeout, - shards, tablets, tabletinfo) { + shards, tablets, tabletinfo, actions) { var keyspace = $routeParams.keyspace; var shard = $routeParams.shard; $scope.keyspace = {name: keyspace}; $scope.shard = {name: shard}; + $scope.actions = actions; + + $scope.shardActions = [ + {name: 'ValidateShard', title: 'Validate Shard'}, + {name: 'ValidateSchemaShard', title: 'Validate Schema'}, + {name: 'ValidateVersionShard', title: 'Validate Version'}, + {name: 'ValidatePermissionsShard', title: 'Validate Permissions'}, + ]; $scope.tabletActions = [ - 'Ping', 'Scrap', 'Delete', 'Reload Schema' + {name: 'Ping', title: 'Ping'}, + + {name: 'RefreshState', title: 'Refresh State', confirm: 'This will tell the tablet to re-read its topology record and adjust its state accordingly.'}, + {name: 'ReloadSchema', title: 'Reload Schema', confirm: 'This will tell the tablet to refresh its schema cache by querying mysqld.'}, + + {name: 'ScrapTablet', title: 'Scrap', confirm: 'This will tell the tablet to remove itself from serving.'}, + {name: 'ScrapTabletForce', title: 'Scrap (force)', confirm: 'This will externally remove the tablet from serving, without telling the tablet.'}, + {name: 'DeleteTablet', title: 'Delete', confirm: 'This will delete the tablet record from topology.'}, ]; $scope.tabletType = function(tablet) { @@ -240,20 +257,101 @@ app.controller('TopoCtrl', function($scope, $routeParams) { $scope.path = $routeParams.path; }); +app.factory('actions', function($mdDialog, keyspaces, shards, tablets) { + var svc = {}; + + function actionDialogController($scope, $mdDialog, action, result) { + $scope.hide = function() { $mdDialog.hide(); }; + $scope.action = action; + $scope.result = result; + } + + function showResult(ev, action, result) { + $mdDialog.show({ + controller: actionDialogController, + templateUrl: 'action-dialog.html', + parent: angular.element(document.body), + targetEvent: ev, + locals: { + action: action, + result: result + } + }); + } + + function confirm(ev, action, doIt) { + if (action.confirm) { + var dialog = $mdDialog.confirm() + .parent(angular.element(document.body)) + .title('Confirm ' + action.title) + .content(action.confirm) + .ariaLabel('Confirm action') + .ok('OK') + .cancel('Cancel') + .targetEvent(ev); + $mdDialog.show(dialog).then(doIt); + } else { + doIt(); + } + } + + svc.applyKeyspace = function(ev, action, keyspace) { + confirm(ev, action, function() { + var result = keyspaces.action({ + keyspace: keyspace, action: action.name + }, ''); + showResult(ev, action, result); + }); + }; + + svc.applyShard = function(ev, action, keyspace, shard) { + confirm(ev, action, function() { + var result = shards.action({ + keyspace: keyspace, + shard: shard, + action: action.name + }, ''); + showResult(ev, action, result); + }); + }; + + svc.applyTablet = function(ev, action, tabletAlias) { + confirm(ev, action, function() { + var result = tablets.action({ + tablet: tabletAlias.Cell+'-'+tabletAlias.Uid, + action: action.name + }, ''); + showResult(ev, action, result); + }); + }; + + svc.label = function(action) { + return action.confirm ? action.title + '...' : action.title; + }; + + return svc; +}); + app.factory('cells', function($resource) { return $resource('/api/cells/'); }); app.factory('keyspaces', function($resource) { - return $resource('/api/keyspaces/:keyspace'); + return $resource('/api/keyspaces/:keyspace', {}, { + 'action': {method: 'POST'} + }); }); app.factory('shards', function($resource) { - return $resource('/api/shards/:keyspace/:shard'); + return $resource('/api/shards/:keyspace/:shard', {}, { + 'action': {method: 'POST'} + }); }); app.factory('tablets', function($resource) { - return $resource('/api/tablets/:tablet'); + return $resource('/api/tablets/:tablet', {}, { + 'action': {method: 'POST'} + }); }); app.factory('tabletinfo', function($resource) { diff --git a/web/vtctld/keyspaces.html b/web/vtctld/keyspaces.html index 49fc7404f4..4ebb4461e7 100644 --- a/web/vtctld/keyspaces.html +++ b/web/vtctld/keyspaces.html @@ -15,11 +15,13 @@

{{keyspace.name}}

- + - {{action}} + + {{actions.label(action)}} + diff --git a/web/vtctld/shard.html b/web/vtctld/shard.html index 96366f61fe..d674a193de 100644 --- a/web/vtctld/shard.html +++ b/web/vtctld/shard.html @@ -1,7 +1,22 @@ -

Shard Record

+ +
+

Shard Record

+ + + + + + + + {{actions.label(action)}} + + + +
+
@@ -44,11 +59,13 @@

{{tablet.Alias.Cell}}-{{tablet.Alias.Uid}} [{{tabletType(tablet)}}]

- + - {{action}} + + {{actions.label(action)}} +