github.com/cloud-green/juju@v0.0.0-20151002100041-a00291338d3d/cmd/juju/service/addunit.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  	"regexp"
     8  	"strings"
     9  
    10  	"github.com/juju/cmd"
    11  	"github.com/juju/errors"
    12  	"github.com/juju/names"
    13  	"launchpad.net/gnuflag"
    14  
    15  	"github.com/juju/juju/apiserver/params"
    16  	"github.com/juju/juju/cmd/envcmd"
    17  	"github.com/juju/juju/cmd/juju/block"
    18  	"github.com/juju/juju/environs/config"
    19  	"github.com/juju/juju/instance"
    20  	"github.com/juju/juju/provider"
    21  )
    22  
    23  // UnitCommandBase provides support for commands which deploy units. It handles the parsing
    24  // and validation of --to and --num-units arguments.
    25  type UnitCommandBase struct {
    26  	// PlacementSpec is the raw string command arg value used to specify placement directives.
    27  	PlacementSpec string
    28  	// Placement is the result of parsing the PlacementSpec arg value.
    29  	Placement []*instance.Placement
    30  	NumUnits  int
    31  }
    32  
    33  func (c *UnitCommandBase) SetFlags(f *gnuflag.FlagSet) {
    34  	f.IntVar(&c.NumUnits, "num-units", 1, "")
    35  	f.StringVar(&c.PlacementSpec, "to", "", "the machine, container or placement directive to deploy the unit in, bypasses constraints")
    36  }
    37  
    38  func (c *UnitCommandBase) Init(args []string) error {
    39  	if c.NumUnits < 1 {
    40  		return errors.New("--num-units must be a positive integer")
    41  	}
    42  	if c.PlacementSpec != "" {
    43  		// Older Juju versions just accept a single machine or container.
    44  		if IsMachineOrNewContainer(c.PlacementSpec) {
    45  			return nil
    46  		}
    47  		// Newer Juju versions accept a comma separated list of placement directives.
    48  		placementSpecs := strings.Split(c.PlacementSpec, ",")
    49  		c.Placement = make([]*instance.Placement, len(placementSpecs))
    50  		for i, spec := range placementSpecs {
    51  			placement, err := parsePlacement(spec)
    52  			if err != nil {
    53  				return errors.Errorf("invalid --to parameter %q", spec)
    54  			}
    55  			c.Placement[i] = placement
    56  		}
    57  	}
    58  	if len(c.Placement) > c.NumUnits {
    59  		logger.Warningf("%d unit(s) will be deployed, extra placement directives will be ignored", c.NumUnits)
    60  	}
    61  	return nil
    62  }
    63  
    64  func parsePlacement(spec string) (*instance.Placement, error) {
    65  	placement, err := instance.ParsePlacement(spec)
    66  	if err == instance.ErrPlacementScopeMissing {
    67  		spec = "env-uuid" + ":" + spec
    68  		placement, err = instance.ParsePlacement(spec)
    69  	}
    70  	if err != nil {
    71  		return nil, errors.Errorf("invalid --to parameter %q", spec)
    72  	}
    73  	return placement, nil
    74  }
    75  
    76  // TODO(anastasiamac) 2014-10-20 Bug#1383116
    77  // This exists to provide more context to the user about
    78  // why they cannot allocate units to machine 0. Remove
    79  // this when the local provider's machine 0 is a container.
    80  // TODO(cherylj) Unexport CheckProvider once deploy is moved under service
    81  func (c *UnitCommandBase) CheckProvider(conf *config.Config) error {
    82  	isMachineZero := c.PlacementSpec == "0"
    83  	for _, p := range c.Placement {
    84  		isMachineZero = isMachineZero || (p.Scope == instance.MachineScope && p.Directive == "0")
    85  	}
    86  	if conf.Type() == provider.Local && isMachineZero {
    87  		return errors.New("machine 0 is the state server for a local environment and cannot host units")
    88  	}
    89  	return nil
    90  }
    91  
    92  // TODO(cherylj) Unexport GetClientConfig and make it a standard function
    93  // once deploy is moved under service
    94  var GetClientConfig = func(client ServiceAddUnitAPI) (*config.Config, error) {
    95  	// Separated into a variable for easy overrides
    96  	attrs, err := client.EnvironmentGet()
    97  	if err != nil {
    98  		return nil, err
    99  	}
   100  
   101  	return config.New(config.NoDefaults, attrs)
   102  }
   103  
   104  // AddUnitCommand is responsible adding additional units to a service.
   105  type AddUnitCommand struct {
   106  	envcmd.EnvCommandBase
   107  	UnitCommandBase
   108  	ServiceName string
   109  	api         ServiceAddUnitAPI
   110  }
   111  
   112  const addUnitDoc = `
   113  Adding units to an existing service is a way to scale out an environment by
   114  deploying more instances of a service.  Add-unit must be called on services that
   115  have already been deployed via juju deploy.
   116  
   117  By default, services are deployed to newly provisioned machines.  Alternatively,
   118  service units can be added to a specific existing machine using the --to
   119  argument.
   120  
   121  Examples:
   122   juju service add-unit mysql -n 5          (Add 5 mysql units on 5 new machines)
   123   juju service add-unit mysql --to 23       (Add a mysql unit to machine 23)
   124   juju service add-unit mysql --to 24/lxc/3 (Add unit to lxc container 3 on host machine 24)
   125   juju service add-unit mysql --to lxc:25   (Add unit to a new lxc container on host machine 25)
   126  `
   127  
   128  func (c *AddUnitCommand) Info() *cmd.Info {
   129  	return &cmd.Info{
   130  		Name:    "add-unit",
   131  		Args:    "<service name>",
   132  		Purpose: "add one or more units of an already-deployed service",
   133  		Doc:     addUnitDoc,
   134  	}
   135  }
   136  
   137  func (c *AddUnitCommand) SetFlags(f *gnuflag.FlagSet) {
   138  	c.UnitCommandBase.SetFlags(f)
   139  	f.IntVar(&c.NumUnits, "n", 1, "number of service units to add")
   140  }
   141  
   142  func (c *AddUnitCommand) Init(args []string) error {
   143  	switch len(args) {
   144  	case 1:
   145  		c.ServiceName = args[0]
   146  	case 0:
   147  		return errors.New("no service specified")
   148  	}
   149  	if err := cmd.CheckEmpty(args[1:]); err != nil {
   150  		return err
   151  	}
   152  	return c.UnitCommandBase.Init(args)
   153  }
   154  
   155  // ServiceAddUnitAPI defines the methods on the client API
   156  // that the service add-unit command calls.
   157  type ServiceAddUnitAPI interface {
   158  	Close() error
   159  	EnvironmentUUID() string
   160  	AddServiceUnits(service string, numUnits int, machineSpec string) ([]string, error)
   161  	AddServiceUnitsWithPlacement(service string, numUnits int, placement []*instance.Placement) ([]string, error)
   162  	EnvironmentGet() (map[string]interface{}, error)
   163  }
   164  
   165  func (c *AddUnitCommand) getAPI() (ServiceAddUnitAPI, error) {
   166  	if c.api != nil {
   167  		return c.api, nil
   168  	}
   169  	return c.NewAPIClient()
   170  }
   171  
   172  // Run connects to the environment specified on the command line
   173  // and calls AddServiceUnits for the given service.
   174  func (c *AddUnitCommand) Run(_ *cmd.Context) error {
   175  	apiclient, err := c.getAPI()
   176  	if err != nil {
   177  		return err
   178  	}
   179  	defer apiclient.Close()
   180  
   181  	conf, err := GetClientConfig(apiclient)
   182  	if err != nil {
   183  		return err
   184  	}
   185  
   186  	if err := c.CheckProvider(conf); err != nil {
   187  		return err
   188  	}
   189  
   190  	for i, p := range c.Placement {
   191  		if p.Scope == "env-uuid" {
   192  			p.Scope = apiclient.EnvironmentUUID()
   193  		}
   194  		c.Placement[i] = p
   195  	}
   196  	if len(c.Placement) > 0 {
   197  		_, err = apiclient.AddServiceUnitsWithPlacement(c.ServiceName, c.NumUnits, c.Placement)
   198  		if err == nil {
   199  			return nil
   200  		}
   201  		if !params.IsCodeNotImplemented(err) {
   202  			return block.ProcessBlockedError(err, block.BlockChange)
   203  		}
   204  	}
   205  	if c.PlacementSpec != "" && !IsMachineOrNewContainer(c.PlacementSpec) {
   206  		return errors.Errorf("unsupported --to parameter %q", c.PlacementSpec)
   207  	}
   208  	if c.PlacementSpec != "" && c.NumUnits > 1 {
   209  		return errors.New("this version of Juju does not support --num-units > 1 with --to")
   210  	}
   211  	_, err = apiclient.AddServiceUnits(c.ServiceName, c.NumUnits, c.PlacementSpec)
   212  	return block.ProcessBlockedError(err, block.BlockChange)
   213  }
   214  
   215  // deployTarget describes the format a machine or container target must match to be valid.
   216  const deployTarget = "^(" + names.ContainerTypeSnippet + ":)?" + names.MachineSnippet + "$"
   217  
   218  var validMachineOrNewContainer = regexp.MustCompile(deployTarget)
   219  
   220  // IsMachineOrNewContainer returns whether spec is a valid machine id
   221  // or new container definition.
   222  func IsMachineOrNewContainer(spec string) bool {
   223  	return validMachineOrNewContainer.MatchString(spec)
   224  }