github.com/cloudbase/juju-core@v0.0.0-20140504232958-a7271ac7912f/cmd/juju/debughooks.go (about)

     1  // Copyright 2012, 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package main
     5  
     6  import (
     7  	"encoding/base64"
     8  	"errors"
     9  	"fmt"
    10  	"sort"
    11  
    12  	"launchpad.net/juju-core/charm/hooks"
    13  	"launchpad.net/juju-core/cmd"
    14  	"launchpad.net/juju-core/names"
    15  	"launchpad.net/juju-core/state/api/params"
    16  	unitdebug "launchpad.net/juju-core/worker/uniter/debug"
    17  )
    18  
    19  // DebugHooksCommand is responsible for launching a ssh shell on a given unit or machine.
    20  type DebugHooksCommand struct {
    21  	SSHCommand
    22  	hooks []string
    23  }
    24  
    25  const debugHooksDoc = `
    26  Interactively debug a hook remotely on a service unit.
    27  `
    28  
    29  func (c *DebugHooksCommand) Info() *cmd.Info {
    30  	return &cmd.Info{
    31  		Name:    "debug-hooks",
    32  		Args:    "<unit name> [hook names]",
    33  		Purpose: "launch a tmux session to debug a hook",
    34  		Doc:     debugHooksDoc,
    35  	}
    36  }
    37  
    38  func (c *DebugHooksCommand) Init(args []string) error {
    39  	if len(args) < 1 {
    40  		return errors.New("no unit name specified")
    41  	}
    42  	c.Target = args[0]
    43  	if !names.IsUnit(c.Target) {
    44  		return fmt.Errorf("%q is not a valid unit name", c.Target)
    45  	}
    46  
    47  	// If any of the hooks is "*", then debug all hooks.
    48  	c.hooks = append([]string{}, args[1:]...)
    49  	for _, h := range c.hooks {
    50  		if h == "*" {
    51  			c.hooks = nil
    52  			break
    53  		}
    54  	}
    55  	return nil
    56  }
    57  
    58  // getRelationNames1dot16 gets the list of relation hooks directly from the
    59  // database, in a fashion compatible with the API server in Juju 1.16 (which
    60  // doesn't have the ServiceCharmRelations API). This function can be removed
    61  // when we no longer maintain compatibility with 1.16
    62  func (c *DebugHooksCommand) getRelationNames1dot16() ([]string, error) {
    63  	err := c.ensureRawConn()
    64  	if err != nil {
    65  		return nil, err
    66  	}
    67  	unit, err := c.rawConn.State.Unit(c.Target)
    68  	if err != nil {
    69  		return nil, err
    70  	}
    71  	service, err := unit.Service()
    72  	if err != nil {
    73  		return nil, err
    74  	}
    75  	endpoints, err := service.Endpoints()
    76  	if err != nil {
    77  		return nil, err
    78  	}
    79  	relations := make([]string, len(endpoints))
    80  	for i, endpoint := range endpoints {
    81  		relations[i] = endpoint.Relation.Name
    82  	}
    83  	return relations, nil
    84  }
    85  
    86  func (c *DebugHooksCommand) getRelationNames(serviceName string) ([]string, error) {
    87  	relations, err := c.apiClient.ServiceCharmRelations(serviceName)
    88  	if params.IsCodeNotImplemented(err) {
    89  		logger.Infof("API server does not support Client.ServiceCharmRelations falling back to 1.16 compatibility mode (direct DB access)")
    90  		return c.getRelationNames1dot16()
    91  	}
    92  	if err != nil {
    93  		return nil, err
    94  	}
    95  	return relations, err
    96  }
    97  
    98  func (c *DebugHooksCommand) validateHooks() error {
    99  	if len(c.hooks) == 0 {
   100  		return nil
   101  	}
   102  	service := names.UnitService(c.Target)
   103  	relations, err := c.getRelationNames(service)
   104  	if err != nil {
   105  		return err
   106  	}
   107  
   108  	validHooks := make(map[string]bool)
   109  	for _, hook := range hooks.UnitHooks() {
   110  		validHooks[string(hook)] = true
   111  	}
   112  	for _, relation := range relations {
   113  		for _, hook := range hooks.RelationHooks() {
   114  			hook := fmt.Sprintf("%s-%s", relation, hook)
   115  			validHooks[hook] = true
   116  		}
   117  	}
   118  	for _, hook := range c.hooks {
   119  		if !validHooks[hook] {
   120  			names := make([]string, 0, len(validHooks))
   121  			for hookName, _ := range validHooks {
   122  				names = append(names, hookName)
   123  			}
   124  			sort.Strings(names)
   125  			logger.Infof("unknown hook %s, valid hook names: %v", hook, names)
   126  			return fmt.Errorf("unit %q does not contain hook %q", c.Target, hook)
   127  		}
   128  	}
   129  	return nil
   130  }
   131  
   132  // Run ensures c.Target is a unit, and resolves its address,
   133  // and connects to it via SSH to execute the debug-hooks
   134  // script.
   135  func (c *DebugHooksCommand) Run(ctx *cmd.Context) error {
   136  	var err error
   137  	c.apiClient, err = c.initAPIClient()
   138  	if err != nil {
   139  		return err
   140  	}
   141  	defer c.apiClient.Close()
   142  	err = c.validateHooks()
   143  	if err != nil {
   144  		return err
   145  	}
   146  	debugctx := unitdebug.NewHooksContext(c.Target)
   147  	script := base64.StdEncoding.EncodeToString([]byte(unitdebug.ClientScript(debugctx, c.hooks)))
   148  	innercmd := fmt.Sprintf(`F=$(mktemp); echo %s | base64 -d > $F; . $F`, script)
   149  	args := []string{fmt.Sprintf("sudo /bin/bash -c '%s'", innercmd)}
   150  	c.Args = args
   151  	return c.SSHCommand.Run(ctx)
   152  }