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

     1  // Copyright 2017 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  // The sla package contains the implementation of the juju sla
     5  // command.
     6  package sla
     7  
     8  import (
     9  	"encoding/json"
    10  	"fmt"
    11  	"io"
    12  
    13  	"github.com/gosuri/uitable"
    14  	"github.com/juju/cmd"
    15  	"github.com/juju/errors"
    16  	"github.com/juju/gnuflag"
    17  	"github.com/juju/loggo"
    18  	"github.com/juju/romulus/api/sla"
    19  	slawire "github.com/juju/romulus/wireformat/sla"
    20  	"gopkg.in/macaroon.v2-unstable"
    21  
    22  	"github.com/juju/juju/api"
    23  	"github.com/juju/juju/api/modelconfig"
    24  	jujucmd "github.com/juju/juju/cmd"
    25  	"github.com/juju/juju/cmd/juju/common"
    26  	rcmd "github.com/juju/juju/cmd/juju/romulus"
    27  	"github.com/juju/juju/cmd/modelcmd"
    28  	"github.com/juju/juju/jujuclient"
    29  )
    30  
    31  var logger = loggo.GetLogger("romulus.cmd.sla")
    32  
    33  // authorizationClient defines the interface of an api client that
    34  // the command uses to create an sla authorization macaroon.
    35  type authorizationClient interface {
    36  	// Authorize returns the sla authorization macaroon for the specified model,
    37  	Authorize(modelUUID, supportLevel, wallet string) (*slawire.SLAResponse, error)
    38  }
    39  
    40  type slaClient interface {
    41  	SetSLALevel(level, owner string, creds []byte) error
    42  	SLALevel() (string, error)
    43  }
    44  
    45  type slaLevel struct {
    46  	Model   string `json:"model,omitempty" yaml:"model,omitempty"`
    47  	SLA     string `json:"sla,omitempty" yaml:"sla,omitempty"`
    48  	Message string `json:"message,omitempty" yaml:"message,omitempty"`
    49  }
    50  
    51  var newSLAClient = func(conn api.Connection) slaClient {
    52  	return modelconfig.NewClient(conn)
    53  }
    54  
    55  var newAuthorizationClient = func(options ...sla.ClientOption) (authorizationClient, error) {
    56  	return sla.NewClient(options...)
    57  }
    58  
    59  var modelId = func(conn api.Connection) string {
    60  	// Our connection is model based so ignore the returned bool.
    61  	tag, _ := conn.ModelTag()
    62  	return tag.Id()
    63  }
    64  
    65  var newJujuClientStore = jujuclient.NewFileClientStore
    66  
    67  // NewSLACommand returns a new command that is used to set SLA credentials for a
    68  // deployed application.
    69  func NewSLACommand() cmd.Command {
    70  	slaCommand := &slaCommand{
    71  		newSLAClient:           newSLAClient,
    72  		newAuthorizationClient: newAuthorizationClient,
    73  	}
    74  	slaCommand.newAPIRoot = slaCommand.NewAPIRoot
    75  	return modelcmd.Wrap(slaCommand)
    76  }
    77  
    78  // slaCommand is a command-line tool for setting
    79  // Model.SLACredential for development & demonstration purposes.
    80  type slaCommand struct {
    81  	modelcmd.ModelCommandBase
    82  	out cmd.Output
    83  
    84  	newAPIRoot             func() (api.Connection, error)
    85  	newSLAClient           func(api.Connection) slaClient
    86  	newAuthorizationClient func(options ...sla.ClientOption) (authorizationClient, error)
    87  
    88  	Level  string
    89  	Budget string
    90  }
    91  
    92  // SetFlags sets additional flags for the support command.
    93  func (c *slaCommand) SetFlags(f *gnuflag.FlagSet) {
    94  	c.ModelCommandBase.SetFlags(f)
    95  	c.out.AddFlags(f, "tabular", map[string]cmd.Formatter{
    96  		"tabular": formatTabular,
    97  		"json":    cmd.FormatJson,
    98  		"yaml":    cmd.FormatYaml,
    99  	})
   100  	f.StringVar(&c.Budget, "budget", "", "the maximum spend for the model")
   101  }
   102  
   103  // Info implements cmd.Command.
   104  func (c *slaCommand) Info() *cmd.Info {
   105  	return jujucmd.Info(&cmd.Info{
   106  		Name:    "sla",
   107  		Args:    "<level>",
   108  		Purpose: "Set the SLA level for a model.",
   109  		Doc: `
   110  Set the support level for the model, effective immediately.
   111  Examples:
   112      # set the support level to essential
   113      juju sla essential
   114  
   115      # set the support level to essential with a maximum budget of $1000 in wallet 'personal'
   116      juju sla standard --budget personal:1000
   117  
   118      # display the current support level for the model.
   119      juju sla
   120  `,
   121  	})
   122  }
   123  
   124  // Init implements cmd.Command.
   125  func (c *slaCommand) Init(args []string) error {
   126  	if len(args) < 1 {
   127  		return nil
   128  	}
   129  	c.Level = args[0]
   130  	return c.ModelCommandBase.Init(args[1:])
   131  }
   132  
   133  func (c *slaCommand) requestSupportCredentials(modelUUID string) (string, string, []byte, error) {
   134  	fail := func(err error) (string, string, []byte, error) {
   135  		return "", "", nil, err
   136  	}
   137  	hc, err := c.BakeryClient()
   138  	if err != nil {
   139  		return fail(errors.Trace(err))
   140  	}
   141  	apiRoot, err := rcmd.GetMeteringURLForModelCmd(&c.ModelCommandBase)
   142  	if err != nil {
   143  		return fail(errors.Trace(err))
   144  	}
   145  	authClient, err := c.newAuthorizationClient(sla.APIRoot(apiRoot), sla.HTTPClient(hc))
   146  	if err != nil {
   147  		return fail(errors.Trace(err))
   148  	}
   149  	slaResp, err := authClient.Authorize(modelUUID, c.Level, c.Budget)
   150  	if err != nil {
   151  		err = common.MaybeTermsAgreementError(err)
   152  		if termErr, ok := errors.Cause(err).(*common.TermsRequiredError); ok {
   153  			return fail(errors.Trace(termErr.UserErr()))
   154  		}
   155  		return fail(errors.Trace(err))
   156  	}
   157  	ms := macaroon.Slice{slaResp.Credentials}
   158  	mbuf, err := json.Marshal(ms)
   159  	if err != nil {
   160  		return fail(errors.Trace(err))
   161  	}
   162  	return slaResp.Owner, slaResp.Message, mbuf, nil
   163  }
   164  
   165  func (c *slaCommand) displayCurrentLevel(client slaClient, modelName string, ctx *cmd.Context) error {
   166  	level, err := client.SLALevel()
   167  	if err != nil {
   168  		return errors.Trace(err)
   169  	}
   170  	return errors.Trace(c.out.Write(ctx, &slaLevel{
   171  		Model: modelName,
   172  		SLA:   level,
   173  	}))
   174  }
   175  
   176  // Run implements cmd.Command.
   177  func (c *slaCommand) Run(ctx *cmd.Context) error {
   178  	root, err := c.newAPIRoot()
   179  	if err != nil {
   180  		return errors.Trace(err)
   181  	}
   182  	client := c.newSLAClient(root)
   183  	modelId := modelId(root)
   184  	modelNameMap := modelNameMap()
   185  	modelName := modelId
   186  	if name, ok := modelNameMap[modelId]; ok {
   187  		modelName = name
   188  	}
   189  
   190  	if c.Level == "" {
   191  		return c.displayCurrentLevel(client, modelName, ctx)
   192  	}
   193  	owner, message, credentials, err := c.requestSupportCredentials(modelId)
   194  	if err != nil {
   195  		return errors.Trace(err)
   196  	}
   197  	if message != "" {
   198  		err = c.out.Write(ctx, &slaLevel{
   199  			Model:   modelName,
   200  			SLA:     c.Level,
   201  			Message: message,
   202  		})
   203  		if err != nil {
   204  			return errors.Trace(err)
   205  		}
   206  	}
   207  	err = client.SetSLALevel(c.Level, owner, credentials)
   208  	if err != nil {
   209  		return errors.Trace(err)
   210  	}
   211  	return nil
   212  }
   213  
   214  func formatTabular(writer io.Writer, value interface{}) error {
   215  	l, ok := value.(*slaLevel)
   216  	if !ok {
   217  		return errors.Errorf("expected value of type %T, got %T", l, value)
   218  	}
   219  	table := uitable.New()
   220  	table.MaxColWidth = 50
   221  	table.Wrap = true
   222  	for _, col := range []int{2, 3, 5} {
   223  		table.RightAlign(col)
   224  	}
   225  	table.AddRow("Model", "SLA", "Message")
   226  	table.AddRow(l.Model, l.SLA, l.Message)
   227  	fmt.Fprint(writer, table)
   228  	return nil
   229  }
   230  
   231  func modelNameMap() map[string]string {
   232  	store := newJujuClientStore()
   233  	uuidToName := map[string]string{}
   234  	controllers, err := store.AllControllers()
   235  	if err != nil {
   236  		logger.Warningf("failed to read juju client controller names")
   237  		return map[string]string{}
   238  	}
   239  	for cname := range controllers {
   240  		models, err := store.AllModels(cname)
   241  		if err != nil {
   242  			logger.Warningf("failed to read juju client model names")
   243  			return map[string]string{}
   244  		}
   245  		for mname, mdetails := range models {
   246  			uuidToName[mdetails.ModelUUID] = cname + ":" + mname
   247  		}
   248  	}
   249  	return uuidToName
   250  }