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

     1  // Copyright 2017 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package introspect
     5  
     6  import (
     7  	"io"
     8  	"net"
     9  	"net/http"
    10  	"net/http/httputil"
    11  	"net/url"
    12  	"os"
    13  	"path/filepath"
    14  
    15  	"github.com/juju/cmd"
    16  	"github.com/juju/errors"
    17  	"github.com/juju/gnuflag"
    18  	"gopkg.in/juju/names.v2"
    19  
    20  	jujucmd "github.com/juju/juju/cmd"
    21  	"github.com/juju/juju/cmd/jujud/agent"
    22  	cmdutil "github.com/juju/juju/cmd/jujud/util"
    23  )
    24  
    25  type IntrospectCommand struct {
    26  	cmd.CommandBase
    27  	dataDir string
    28  	agent   string
    29  	path    string
    30  	listen  string
    31  
    32  	// IntrospectionSocketName returns the socket name
    33  	// for a given tag. If IntrospectionSocketName is nil,
    34  	// agent.DefaultIntrospectionSocketName is used.
    35  	IntrospectionSocketName func(names.Tag) string
    36  }
    37  
    38  const introspectCommandDoc = `
    39  Introspect Juju agents running on this machine.
    40  
    41  The juju-introspect command can be used to expose
    42  the agent's introspection socket via HTTP, using
    43  the --listen flag. e.g.
    44  
    45      juju-introspect --listen=:6060
    46  
    47  Otherwise, a single positional argument is required,
    48  which is the path to query. e.g.
    49  
    50      juju-introspect /debug/pprof/heap?debug=1
    51  
    52  By default, juju-introspect operates on the
    53  machine agent. If you wish to introspect a
    54  unit agent on the machine, you can specify the
    55  agent using --agent. e.g.
    56  
    57      juju-introspect --agent=unit-mysql-0 metrics
    58  `
    59  
    60  // Info returns usage information for the command.
    61  func (c *IntrospectCommand) Info() *cmd.Info {
    62  	return jujucmd.Info(&cmd.Info{
    63  		Name:    "juju-introspect",
    64  		Args:    "(--listen=...|<path>)",
    65  		Purpose: "introspect Juju agents running on this machine",
    66  		Doc:     introspectCommandDoc,
    67  	})
    68  }
    69  
    70  func (c *IntrospectCommand) SetFlags(f *gnuflag.FlagSet) {
    71  	c.CommandBase.SetFlags(f)
    72  	f.StringVar(&c.dataDir, "data-dir", cmdutil.DataDir, "Juju base data directory")
    73  	f.StringVar(&c.agent, "agent", "", "agent to introspect (defaults to machine agent)")
    74  	f.StringVar(&c.listen, "listen", "", "address on which to expose the introspection socket")
    75  }
    76  
    77  func (c *IntrospectCommand) Init(args []string) error {
    78  	if len(args) >= 1 {
    79  		c.path, args = args[0], args[1:]
    80  	}
    81  	if c.path == "" && c.listen == "" {
    82  		return errors.New("either a query path or a --listen address must be specified")
    83  	}
    84  	if c.path != "" && c.listen != "" {
    85  		return errors.New("a query path may not be specified with --listen")
    86  	}
    87  	return c.CommandBase.Init(args)
    88  }
    89  
    90  func (c *IntrospectCommand) Run(ctx *cmd.Context) error {
    91  	targetURL, err := url.Parse("http://unix.socket/" + c.path)
    92  	if err != nil {
    93  		return err
    94  	}
    95  
    96  	tag, err := c.getAgentTag()
    97  	if err != nil {
    98  		return err
    99  	}
   100  
   101  	getSocketName := c.IntrospectionSocketName
   102  	if getSocketName == nil {
   103  		getSocketName = agent.DefaultIntrospectionSocketName
   104  	}
   105  	socketName := "@" + getSocketName(tag)
   106  	if c.listen != "" {
   107  		listener, err := net.Listen("tcp", c.listen)
   108  		if err != nil {
   109  			return err
   110  		}
   111  		defer listener.Close()
   112  		ctx.Infof("Exposing %s introspection socket on %s", socketName, listener.Addr())
   113  		proxy := httputil.NewSingleHostReverseProxy(targetURL)
   114  		proxy.Transport = unixSocketHTTPTransport(socketName)
   115  		return http.Serve(listener, proxy)
   116  	}
   117  
   118  	ctx.Infof("Querying %s introspection socket: %s", socketName, c.path)
   119  	client := unixSocketHTTPClient(socketName)
   120  	resp, err := client.Get(targetURL.String())
   121  	if err != nil {
   122  		return err
   123  	}
   124  	defer resp.Body.Close()
   125  
   126  	if resp.StatusCode != http.StatusOK {
   127  		io.Copy(ctx.Stderr, resp.Body)
   128  		return errors.Errorf(
   129  			"response returned %d (%s)",
   130  			resp.StatusCode,
   131  			http.StatusText(resp.StatusCode),
   132  		)
   133  	}
   134  	_, err = io.Copy(ctx.Stdout, resp.Body)
   135  	return err
   136  }
   137  
   138  func (c *IntrospectCommand) getAgentTag() (names.Tag, error) {
   139  	if c.agent != "" {
   140  		return names.ParseTag(c.agent)
   141  	}
   142  	agentsDir := filepath.Join(c.dataDir, "agents")
   143  	dir, err := os.Open(agentsDir)
   144  	if err != nil {
   145  		return nil, errors.Annotate(err, "opening agents dir")
   146  	}
   147  	defer dir.Close()
   148  
   149  	entries, err := dir.Readdir(-1)
   150  	if err != nil {
   151  		return nil, errors.Annotate(err, "reading agents dir")
   152  	}
   153  	for _, info := range entries {
   154  		name := info.Name()
   155  		tag, err := names.ParseTag(name)
   156  		if err != nil {
   157  			continue
   158  		}
   159  		if tag.Kind() == names.MachineTagKind {
   160  			return tag, nil
   161  		}
   162  	}
   163  	return nil, errors.New("could not determine machine tag")
   164  }
   165  
   166  func unixSocketHTTPClient(socketPath string) *http.Client {
   167  	return &http.Client{
   168  		Transport: unixSocketHTTPTransport(socketPath),
   169  	}
   170  }
   171  
   172  func unixSocketHTTPTransport(socketPath string) *http.Transport {
   173  	return &http.Transport{
   174  		Dial: func(proto, addr string) (net.Conn, error) {
   175  			return net.Dial("unix", socketPath)
   176  		},
   177  	}
   178  }