github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/cmd/juju/machine/add.go (about)

     1  // Copyright 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package machine
     5  
     6  import (
     7  	"fmt"
     8  	"strings"
     9  
    10  	"github.com/juju/cmd"
    11  	"github.com/juju/errors"
    12  	"github.com/juju/gnuflag"
    13  	"gopkg.in/juju/names.v2"
    14  
    15  	"github.com/juju/juju/api/machinemanager"
    16  	"github.com/juju/juju/api/modelconfig"
    17  	"github.com/juju/juju/apiserver/params"
    18  	"github.com/juju/juju/cmd/juju/block"
    19  	"github.com/juju/juju/cmd/juju/common"
    20  	"github.com/juju/juju/cmd/modelcmd"
    21  	"github.com/juju/juju/constraints"
    22  	"github.com/juju/juju/environs/config"
    23  	"github.com/juju/juju/environs/manual"
    24  	"github.com/juju/juju/instance"
    25  	"github.com/juju/juju/state/multiwatcher"
    26  	"github.com/juju/juju/storage"
    27  )
    28  
    29  var addMachineDoc = `
    30  Juju supports adding machines using provider-specific machine instances
    31  (EC2 instances, OpenStack servers, MAAS nodes, etc.); existing machines
    32  running a supported operating system (see "manual provisioning" below),
    33  and containers on machines. Machines are created in a clean state and
    34  ready to have units deployed.
    35  
    36  Without any parameters, add machine will allocate a new provider-specific
    37  machine (multiple, if "-n" is provided). When adding a new machine, you
    38  may specify constraints for the machine to be provisioned; the provider
    39  will interpret these constraints in order to decide what kind of machine
    40  to allocate.
    41  
    42  If a container type is specified (e.g. "lxd"), then add machine will
    43  allocate a container of that type on a new provider-specific machine. It is
    44  also possible to add containers to existing machines using the format
    45  <container type>:<machine number>. Constraints cannot be combined with
    46  deploying a container to an existing machine. The currently supported
    47  container types are: $CONTAINER_TYPES$.
    48  
    49  Manual provisioning is the process of installing Juju on an existing machine
    50  and bringing it under Juju's management; currently this requires that the
    51  machine be running Ubuntu, that it be accessible via SSH, and be running on
    52  the same network as the API server.
    53  
    54  It is possible to override or augment constraints by passing provider-specific
    55  "placement directives" as an argument; these give the provider additional
    56  information about how to allocate the machine. For example, one can direct the
    57  MAAS provider to acquire a particular node by specifying its hostname.
    58  For more information on placement directives, see "juju help placement".
    59  
    60  Examples:
    61     juju add-machine                      (starts a new machine)
    62     juju add-machine -n 2                 (starts 2 new machines)
    63     juju add-machine lxd                  (starts a new machine with an lxd container)
    64     juju add-machine lxd -n 2             (starts 2 new machines with an lxd container)
    65     juju add-machine lxd:4                (starts a new lxd container on machine 4)
    66     juju add-machine --constraints mem=8G (starts a machine with at least 8GB RAM)
    67     juju add-machine ssh:user@10.10.0.3   (manually provisions a machine with ssh)
    68     juju add-machine zone=us-east-1a      (start a machine in zone us-east-1a on AWS)
    69     juju add-machine maas2.name           (acquire machine maas2.name on MAAS)
    70  
    71  See also:
    72      remove-machine
    73  `
    74  
    75  func init() {
    76  	containerTypes := make([]string, len(instance.ContainerTypes))
    77  	for i, t := range instance.ContainerTypes {
    78  		containerTypes[i] = string(t)
    79  	}
    80  	addMachineDoc = strings.Replace(
    81  		addMachineDoc,
    82  		"$CONTAINER_TYPES$",
    83  		strings.Join(containerTypes, ", "),
    84  		-1,
    85  	)
    86  }
    87  
    88  // NewAddCommand returns a command that adds a machine to a model.
    89  func NewAddCommand() cmd.Command {
    90  	return modelcmd.Wrap(&addCommand{})
    91  }
    92  
    93  // addCommand starts a new machine and registers it in the model.
    94  type addCommand struct {
    95  	modelcmd.ModelCommandBase
    96  	api               AddMachineAPI
    97  	modelConfigAPI    ModelConfigAPI
    98  	machineManagerAPI MachineManagerAPI
    99  	// If specified, use this series, else use the model default-series
   100  	Series string
   101  	// If specified, these constraints are merged with those already in the model.
   102  	Constraints constraints.Value
   103  	// If specified, these constraints are merged with those already in the model.
   104  	ConstraintsStr string
   105  	// Placement is passed verbatim to the API, to be parsed and evaluated server-side.
   106  	Placement *instance.Placement
   107  	// NumMachines is the number of machines to add.
   108  	NumMachines int
   109  	// Disks describes disks that are to be attached to the machine.
   110  	Disks []storage.Constraints
   111  }
   112  
   113  func (c *addCommand) Info() *cmd.Info {
   114  	return &cmd.Info{
   115  		Name:    "add-machine",
   116  		Args:    "[<container>:machine | <container> | ssh:[user@]host | placement]",
   117  		Purpose: "Start a new, empty machine and optionally a container, or add a container to a machine.",
   118  		Doc:     addMachineDoc,
   119  	}
   120  }
   121  
   122  func (c *addCommand) SetFlags(f *gnuflag.FlagSet) {
   123  	c.ModelCommandBase.SetFlags(f)
   124  	f.StringVar(&c.Series, "series", "", "The charm series")
   125  	f.IntVar(&c.NumMachines, "n", 1, "The number of machines to add")
   126  	f.StringVar(&c.ConstraintsStr, "constraints", "", "Additional machine constraints")
   127  	f.Var(disksFlag{&c.Disks}, "disks", "Constraints for disks to attach to the machine")
   128  }
   129  
   130  func (c *addCommand) Init(args []string) error {
   131  	if c.Constraints.Container != nil {
   132  		return errors.Errorf("container constraint %q not allowed when adding a machine", *c.Constraints.Container)
   133  	}
   134  	placement, err := cmd.ZeroOrOneArgs(args)
   135  	if err != nil {
   136  		return err
   137  	}
   138  	c.Placement, err = instance.ParsePlacement(placement)
   139  	if err == instance.ErrPlacementScopeMissing {
   140  		placement = "model-uuid" + ":" + placement
   141  		c.Placement, err = instance.ParsePlacement(placement)
   142  	}
   143  	if err != nil {
   144  		return err
   145  	}
   146  	if c.NumMachines > 1 && c.Placement != nil && c.Placement.Directive != "" {
   147  		return errors.New("cannot use -n when specifying a placement directive")
   148  	}
   149  	return nil
   150  }
   151  
   152  type AddMachineAPI interface {
   153  	AddMachines([]params.AddMachineParams) ([]params.AddMachinesResult, error)
   154  	Close() error
   155  	ForceDestroyMachines(machines ...string) error
   156  	ModelUUID() (string, bool)
   157  	ProvisioningScript(params.ProvisioningScriptParams) (script string, err error)
   158  }
   159  
   160  type ModelConfigAPI interface {
   161  	ModelGet() (map[string]interface{}, error)
   162  	Close() error
   163  }
   164  
   165  type MachineManagerAPI interface {
   166  	AddMachines([]params.AddMachineParams) ([]params.AddMachinesResult, error)
   167  	BestAPIVersion() int
   168  	Close() error
   169  }
   170  
   171  var manualProvisioner = manual.ProvisionMachine
   172  
   173  func (c *addCommand) getClientAPI() (AddMachineAPI, error) {
   174  	if c.api != nil {
   175  		return c.api, nil
   176  	}
   177  	return c.NewAPIClient()
   178  }
   179  
   180  func (c *addCommand) getModelConfigAPI() (ModelConfigAPI, error) {
   181  	if c.modelConfigAPI != nil {
   182  		return c.modelConfigAPI, nil
   183  	}
   184  	api, err := c.NewAPIRoot()
   185  	if err != nil {
   186  		return nil, errors.Annotate(err, "opening API connection")
   187  	}
   188  	return modelconfig.NewClient(api), nil
   189  
   190  }
   191  
   192  func (c *addCommand) NewMachineManagerClient() (*machinemanager.Client, error) {
   193  	root, err := c.NewAPIRoot()
   194  	if err != nil {
   195  		return nil, errors.Trace(err)
   196  	}
   197  	return machinemanager.NewClient(root), nil
   198  }
   199  
   200  func (c *addCommand) getMachineManagerAPI() (MachineManagerAPI, error) {
   201  	if c.machineManagerAPI != nil {
   202  		return c.machineManagerAPI, nil
   203  	}
   204  	return c.NewMachineManagerClient()
   205  }
   206  
   207  func (c *addCommand) Run(ctx *cmd.Context) error {
   208  	var err error
   209  	c.Constraints, err = common.ParseConstraints(ctx, c.ConstraintsStr)
   210  	if err != nil {
   211  		return err
   212  	}
   213  	client, err := c.getClientAPI()
   214  	if err != nil {
   215  		return errors.Trace(err)
   216  	}
   217  	defer client.Close()
   218  
   219  	var machineManager MachineManagerAPI
   220  	if len(c.Disks) > 0 {
   221  		machineManager, err = c.getMachineManagerAPI()
   222  		if err != nil {
   223  			return errors.Trace(err)
   224  		}
   225  		defer machineManager.Close()
   226  		if machineManager.BestAPIVersion() < 1 {
   227  			return errors.New("cannot add machines with disks: not supported by the API server")
   228  		}
   229  	}
   230  
   231  	logger.Infof("load config")
   232  	modelConfigClient, err := c.getModelConfigAPI()
   233  	if err != nil {
   234  		return errors.Trace(err)
   235  	}
   236  	defer modelConfigClient.Close()
   237  	configAttrs, err := modelConfigClient.ModelGet()
   238  	if err != nil {
   239  		return errors.Trace(err)
   240  	}
   241  	config, err := config.New(config.NoDefaults, configAttrs)
   242  	if err != nil {
   243  		return errors.Trace(err)
   244  	}
   245  
   246  	if c.Placement != nil && c.Placement.Scope == "ssh" {
   247  		logger.Infof("manual provisioning")
   248  		authKeys, err := common.ReadAuthorizedKeys(ctx, "")
   249  		if err != nil {
   250  			return errors.Annotate(err, "reading authorized-keys")
   251  		}
   252  		args := manual.ProvisionMachineArgs{
   253  			Host:           c.Placement.Directive,
   254  			Client:         client,
   255  			Stdin:          ctx.Stdin,
   256  			Stdout:         ctx.Stdout,
   257  			Stderr:         ctx.Stderr,
   258  			AuthorizedKeys: authKeys,
   259  			UpdateBehavior: &params.UpdateBehavior{
   260  				config.EnableOSRefreshUpdate(),
   261  				config.EnableOSUpgrade(),
   262  			},
   263  		}
   264  		machineId, err := manualProvisioner(args)
   265  		if err == nil {
   266  			ctx.Infof("created machine %v", machineId)
   267  		}
   268  		return err
   269  	}
   270  
   271  	logger.Infof("model provisioning")
   272  	if c.Placement != nil && c.Placement.Scope == "model-uuid" {
   273  		uuid, ok := client.ModelUUID()
   274  		if !ok {
   275  			return errors.New("API connection is controller-only (should never happen)")
   276  		}
   277  		c.Placement.Scope = uuid
   278  	}
   279  
   280  	if c.Placement != nil && c.Placement.Scope == instance.MachineScope {
   281  		// It does not make sense to add-machine <id>.
   282  		return errors.Errorf("machine-id cannot be specified when adding machines")
   283  	}
   284  
   285  	jobs := []multiwatcher.MachineJob{multiwatcher.JobHostUnits}
   286  
   287  	machineParams := params.AddMachineParams{
   288  		Placement:   c.Placement,
   289  		Series:      c.Series,
   290  		Constraints: c.Constraints,
   291  		Jobs:        jobs,
   292  		Disks:       c.Disks,
   293  	}
   294  	machines := make([]params.AddMachineParams, c.NumMachines)
   295  	for i := 0; i < c.NumMachines; i++ {
   296  		machines[i] = machineParams
   297  	}
   298  
   299  	var results []params.AddMachinesResult
   300  	// If storage is specified, we attempt to use a new API on the service facade.
   301  	if len(c.Disks) > 0 {
   302  		results, err = machineManager.AddMachines(machines)
   303  	} else {
   304  		results, err = client.AddMachines(machines)
   305  	}
   306  	if params.IsCodeOperationBlocked(err) {
   307  		return block.ProcessBlockedError(err, block.BlockChange)
   308  	}
   309  	if err != nil {
   310  		return errors.Trace(err)
   311  	}
   312  
   313  	errs := []error{}
   314  	for _, machineInfo := range results {
   315  		if machineInfo.Error != nil {
   316  			errs = append(errs, machineInfo.Error)
   317  			continue
   318  		}
   319  		machineId := machineInfo.Machine
   320  
   321  		if names.IsContainerMachine(machineId) {
   322  			ctx.Infof("created container %v", machineId)
   323  		} else {
   324  			ctx.Infof("created machine %v", machineId)
   325  		}
   326  	}
   327  	if len(errs) == 1 {
   328  		fmt.Fprint(ctx.Stderr, "failed to create 1 machine\n")
   329  		return errs[0]
   330  	}
   331  	if len(errs) > 1 {
   332  		fmt.Fprintf(ctx.Stderr, "failed to create %d machines\n", len(errs))
   333  		returnErr := []string{}
   334  		for _, e := range errs {
   335  			returnErr = append(returnErr, e.Error())
   336  		}
   337  		return errors.New(strings.Join(returnErr, ", "))
   338  	}
   339  	return nil
   340  }