github.com/mwhudson/juju@v0.0.0-20160512215208-90ff01f3497f/cmd/juju/service/set.go (about)

     1  // Copyright 2012-2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package service
     5  
     6  import (
     7  	"fmt"
     8  	"io/ioutil"
     9  	"os"
    10  	"strings"
    11  	"unicode/utf8"
    12  
    13  	"github.com/juju/cmd"
    14  	"github.com/juju/errors"
    15  	"github.com/juju/utils/keyvalues"
    16  	"launchpad.net/gnuflag"
    17  
    18  	"github.com/juju/juju/api/service"
    19  	"github.com/juju/juju/apiserver/params"
    20  	"github.com/juju/juju/cmd/juju/block"
    21  	"github.com/juju/juju/cmd/modelcmd"
    22  )
    23  
    24  // NewSetCommand returns a command used to set service attributes.
    25  func NewSetCommand() cmd.Command {
    26  	return modelcmd.Wrap(&setCommand{})
    27  }
    28  
    29  // setCommand updates the configuration of a service.
    30  type setCommand struct {
    31  	modelcmd.ModelCommandBase
    32  	ServiceName     string
    33  	SettingsStrings map[string]string
    34  	Options         []string
    35  	SettingsYAML    cmd.FileVar
    36  	SetDefault      bool
    37  	serviceApi      serviceAPI
    38  }
    39  
    40  var usageSetConfigSummary = `
    41  Sets configuration options for a service.`[1:]
    42  
    43  var usageSetConfigDetails = `
    44  Charms may, and frequently do, expose a number of configuration settings
    45  for a service to the user. These can be set at deploy time, but may be set
    46  at any time by using the `[1:] + "`juju set-config`" + ` command. The actual options
    47  vary per charm (you can check the charm documentation, or use ` + "`juju get-\nconfig`" +
    48  	` to check which options may be set).
    49  If ‘value’ begins with the ‘@’ character, it is interpreted as a filename
    50  and the actual value is read from it. The maximum size of the filename is
    51  5M.
    52  Values may be any UTF-8 encoded string. UTF-8 is accepted on the command
    53  line and in referenced files.
    54  See ` + "`juju status`" + ` for service names.
    55  
    56  Examples:
    57      juju set-config mysql dataset-size=80% backup_dir=/vol1/mysql/backups
    58      juju set-config apache2 --model mymodel --config /home/ubuntu/mysql.yaml
    59  
    60  See also: 
    61      get-config
    62      deploy
    63      status`
    64  
    65  const maxValueSize = 5242880
    66  
    67  // Info implements Command.Info.
    68  func (c *setCommand) Info() *cmd.Info {
    69  	return &cmd.Info{
    70  		Name:    "set-config",
    71  		Args:    "<service name> <service key>=<value> ...",
    72  		Purpose: usageSetConfigSummary,
    73  		Doc:     usageSetConfigDetails,
    74  		Aliases: []string{"set-configs"},
    75  	}
    76  }
    77  
    78  // SetFlags implements Command.SetFlags.
    79  func (c *setCommand) SetFlags(f *gnuflag.FlagSet) {
    80  	f.Var(&c.SettingsYAML, "config", "path to yaml-formatted service config")
    81  	f.BoolVar(&c.SetDefault, "to-default", false, "set service option values to default")
    82  }
    83  
    84  // Init implements Command.Init.
    85  func (c *setCommand) Init(args []string) error {
    86  	if len(args) == 0 || len(strings.Split(args[0], "=")) > 1 {
    87  		return errors.New("no service name specified")
    88  	}
    89  	if c.SettingsYAML.Path != "" && len(args) > 1 {
    90  		return errors.New("cannot specify --config when using key=value arguments")
    91  	}
    92  	c.ServiceName = args[0]
    93  	if c.SetDefault {
    94  		c.Options = args[1:]
    95  		if len(c.Options) == 0 {
    96  			return errors.New("no configuration options specified")
    97  		}
    98  		return nil
    99  	}
   100  	settings, err := keyvalues.Parse(args[1:], true)
   101  	if err != nil {
   102  		return err
   103  	}
   104  	c.SettingsStrings = settings
   105  	return nil
   106  }
   107  
   108  // serviceAPI defines the methods on the client API
   109  // that the service set command calls.
   110  type serviceAPI interface {
   111  	Close() error
   112  	Update(args params.ServiceUpdate) error
   113  	Get(service string) (*params.ServiceGetResults, error)
   114  	Set(service string, options map[string]string) error
   115  	Unset(service string, options []string) error
   116  }
   117  
   118  func (c *setCommand) getServiceAPI() (serviceAPI, error) {
   119  	if c.serviceApi != nil {
   120  		return c.serviceApi, nil
   121  	}
   122  	root, err := c.NewAPIRoot()
   123  	if err != nil {
   124  		return nil, errors.Trace(err)
   125  	}
   126  	return service.NewClient(root), nil
   127  }
   128  
   129  // Run updates the configuration of a service.
   130  func (c *setCommand) Run(ctx *cmd.Context) error {
   131  	apiclient, err := c.getServiceAPI()
   132  	if err != nil {
   133  		return err
   134  	}
   135  	defer apiclient.Close()
   136  
   137  	if c.SettingsYAML.Path != "" {
   138  		b, err := c.SettingsYAML.Read(ctx)
   139  		if err != nil {
   140  			return err
   141  		}
   142  		return block.ProcessBlockedError(apiclient.Update(params.ServiceUpdate{
   143  			ServiceName:  c.ServiceName,
   144  			SettingsYAML: string(b),
   145  		}), block.BlockChange)
   146  	} else if c.SetDefault {
   147  		return block.ProcessBlockedError(apiclient.Unset(c.ServiceName, c.Options), block.BlockChange)
   148  	} else if len(c.SettingsStrings) == 0 {
   149  		return nil
   150  	}
   151  	settings := map[string]string{}
   152  	for k, v := range c.SettingsStrings {
   153  		//empty string is also valid as a setting value
   154  		if v == "" {
   155  			settings[k] = v
   156  			continue
   157  		}
   158  
   159  		if v[0] != '@' {
   160  			if !utf8.ValidString(v) {
   161  				return fmt.Errorf("value for option %q contains non-UTF-8 sequences", k)
   162  			}
   163  			settings[k] = v
   164  			continue
   165  		}
   166  		nv, err := readValue(ctx, v[1:])
   167  		if err != nil {
   168  			return err
   169  		}
   170  		if !utf8.ValidString(nv) {
   171  			return fmt.Errorf("value for option %q contains non-UTF-8 sequences", k)
   172  		}
   173  		settings[k] = nv
   174  	}
   175  
   176  	result, err := apiclient.Get(c.ServiceName)
   177  	if err != nil {
   178  		return err
   179  	}
   180  
   181  	for k, v := range settings {
   182  		configValue := result.Config[k]
   183  
   184  		configValueMap, ok := configValue.(map[string]interface{})
   185  		if ok {
   186  			// convert the value to string and compare
   187  			if fmt.Sprintf("%v", configValueMap["value"]) == v {
   188  				logger.Warningf("the configuration setting %q already has the value %q", k, v)
   189  			}
   190  		}
   191  	}
   192  
   193  	return block.ProcessBlockedError(apiclient.Set(c.ServiceName, settings), block.BlockChange)
   194  }
   195  
   196  // readValue reads the value of an option out of the named file.
   197  // An empty content is valid, like in parsing the options. The upper
   198  // size is 5M.
   199  func readValue(ctx *cmd.Context, filename string) (string, error) {
   200  	absFilename := ctx.AbsPath(filename)
   201  	fi, err := os.Stat(absFilename)
   202  	if err != nil {
   203  		return "", fmt.Errorf("cannot read option from file %q: %v", filename, err)
   204  	}
   205  	if fi.Size() > maxValueSize {
   206  		return "", fmt.Errorf("size of option file is larger than 5M")
   207  	}
   208  	content, err := ioutil.ReadFile(ctx.AbsPath(filename))
   209  	if err != nil {
   210  		return "", fmt.Errorf("cannot read option from file %q: %v", filename, err)
   211  	}
   212  	return string(content), nil
   213  }