github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/juju/commands/debughooks.go (about)

     1  // Copyright 2012, 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package commands
     5  
     6  import (
     7  	"encoding/base64"
     8  	"fmt"
     9  
    10  	"github.com/juju/cmd"
    11  	"github.com/juju/collections/set"
    12  	"github.com/juju/errors"
    13  	"gopkg.in/juju/charm.v6/hooks"
    14  	"gopkg.in/juju/names.v2"
    15  
    16  	"github.com/juju/juju/api/action"
    17  	"github.com/juju/juju/api/application"
    18  	"github.com/juju/juju/apiserver/params"
    19  	jujucmd "github.com/juju/juju/cmd"
    20  	"github.com/juju/juju/cmd/modelcmd"
    21  	"github.com/juju/juju/network/ssh"
    22  	unitdebug "github.com/juju/juju/worker/uniter/runner/debug"
    23  )
    24  
    25  func newDebugHooksCommand(hostChecker ssh.ReachableChecker) cmd.Command {
    26  	c := new(debugHooksCommand)
    27  	c.getActionAPI = c.newActionsAPI
    28  	c.setHostChecker(hostChecker)
    29  	return modelcmd.Wrap(c)
    30  }
    31  
    32  // debugHooksCommand is responsible for launching a ssh shell on a given unit or machine.
    33  type debugHooksCommand struct {
    34  	sshCommand
    35  	hooks []string
    36  
    37  	getActionAPI func() (ActionsAPI, error)
    38  }
    39  
    40  const debugHooksDoc = `
    41  Interactively debug hooks or actions remotely on an application unit.
    42  
    43  See the "juju help ssh" for information about SSH related options
    44  accepted by the debug-hooks command.
    45  `
    46  
    47  func (c *debugHooksCommand) Info() *cmd.Info {
    48  	return jujucmd.Info(&cmd.Info{
    49  		Name:    "debug-hooks",
    50  		Args:    "<unit name> [hook or action names]",
    51  		Purpose: "Launch a tmux session to debug hooks and/or actions.",
    52  		Doc:     debugHooksDoc,
    53  	})
    54  }
    55  
    56  func (c *debugHooksCommand) Init(args []string) error {
    57  	if len(args) < 1 {
    58  		return errors.Errorf("no unit name specified")
    59  	}
    60  	c.Target = args[0]
    61  	if !names.IsValidUnit(c.Target) {
    62  		return errors.Errorf("%q is not a valid unit name", c.Target)
    63  	}
    64  
    65  	// If any of the hooks is "*", then debug all hooks.
    66  	c.hooks = append([]string{}, args[1:]...)
    67  	for _, h := range c.hooks {
    68  		if h == "*" {
    69  			c.hooks = nil
    70  			break
    71  		}
    72  	}
    73  	return nil
    74  }
    75  
    76  type charmRelationsAPI interface {
    77  	CharmRelations(applicationName string) ([]string, error)
    78  }
    79  
    80  type ActionsAPI interface {
    81  	ApplicationCharmActions(params.Entity) (map[string]params.ActionSpec, error)
    82  }
    83  
    84  func (c *debugHooksCommand) getApplicationAPI() (charmRelationsAPI, error) {
    85  	root, err := c.NewAPIRoot()
    86  	if err != nil {
    87  		return nil, errors.Trace(err)
    88  	}
    89  	return application.NewClient(root), nil
    90  }
    91  
    92  func (c *debugHooksCommand) newActionsAPI() (ActionsAPI, error) {
    93  	root, err := c.NewAPIRoot()
    94  	if err != nil {
    95  		return nil, err
    96  	}
    97  	return action.NewClient(root), nil
    98  }
    99  
   100  func (c *debugHooksCommand) validateHooksOrActions() error {
   101  	if len(c.hooks) == 0 {
   102  		return nil
   103  	}
   104  	appName, err := names.UnitApplication(c.Target)
   105  	if err != nil {
   106  		return err
   107  	}
   108  
   109  	// Get a set of valid hooks.
   110  	validHooks, err := c.getValidHooks(appName)
   111  	if err != nil {
   112  		return err
   113  	}
   114  
   115  	// Get a set of valid actions.
   116  	validActions, err := c.getValidActions(appName)
   117  	if err != nil {
   118  		return err
   119  	}
   120  
   121  	// Is passed argument a valid hook or action name?
   122  	// If not valid, err out.
   123  	allValid := validHooks.Union(validActions)
   124  	for _, hook := range c.hooks {
   125  		if !allValid.Contains(hook) {
   126  			return errors.Errorf("unit %q contains neither hook nor action %q, valid actions are %v and valid hooks are %v",
   127  				c.Target,
   128  				hook,
   129  				validActions.SortedValues(),
   130  				validHooks.SortedValues(),
   131  			)
   132  		}
   133  	}
   134  	return nil
   135  }
   136  
   137  func (c *debugHooksCommand) getValidActions(appName string) (set.Strings, error) {
   138  	appTag := names.NewApplicationTag(appName)
   139  	actionAPI, err := c.getActionAPI()
   140  	if err != nil {
   141  		return nil, err
   142  	}
   143  
   144  	allActions, err := actionAPI.ApplicationCharmActions(params.Entity{Tag: appTag.String()})
   145  	if err != nil {
   146  		return nil, err
   147  	}
   148  
   149  	validActions := set.NewStrings()
   150  	for name := range allActions {
   151  		validActions.Add(name)
   152  	}
   153  	return validActions, nil
   154  }
   155  
   156  func (c *debugHooksCommand) getValidHooks(appName string) (set.Strings, error) {
   157  	applicationAPI, err := c.getApplicationAPI()
   158  	if err != nil {
   159  		return nil, err
   160  	}
   161  	relations, err := applicationAPI.CharmRelations(appName)
   162  	if err != nil {
   163  		return nil, err
   164  	}
   165  
   166  	validHooks := set.NewStrings()
   167  	for _, hook := range hooks.UnitHooks() {
   168  		validHooks.Add(string(hook))
   169  	}
   170  	for _, relation := range relations {
   171  		for _, hook := range hooks.RelationHooks() {
   172  			hook := fmt.Sprintf("%s-%s", relation, hook)
   173  			validHooks.Add(hook)
   174  		}
   175  	}
   176  	return validHooks, nil
   177  }
   178  
   179  // Run ensures c.Target is a unit, and resolves its address,
   180  // and connects to it via SSH to execute the debug-hooks
   181  // script.
   182  func (c *debugHooksCommand) Run(ctx *cmd.Context) error {
   183  	err := c.initRun()
   184  	if err != nil {
   185  		return err
   186  	}
   187  	defer c.cleanupRun()
   188  	err = c.validateHooksOrActions()
   189  	if err != nil {
   190  		return err
   191  	}
   192  	debugctx := unitdebug.NewHooksContext(c.Target)
   193  	script := base64.StdEncoding.EncodeToString([]byte(unitdebug.ClientScript(debugctx, c.hooks)))
   194  	innercmd := fmt.Sprintf(`F=$(mktemp); echo %s | base64 -d > $F; . $F`, script)
   195  	args := []string{fmt.Sprintf("sudo /bin/bash -c '%s'", innercmd)}
   196  	c.Args = args
   197  	return c.sshCommand.Run(ctx)
   198  }