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 }