github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/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  		if params.IsCodeUnauthorized(err) {
   240  			common.PermissionsMessage(ctx.Stderr, "add a machine to this model")
   241  		}
   242  		return errors.Trace(err)
   243  	}
   244  	config, err := config.New(config.NoDefaults, configAttrs)
   245  	if err != nil {
   246  		return errors.Trace(err)
   247  	}
   248  
   249  	if c.Placement != nil && c.Placement.Scope == "ssh" {
   250  		logger.Infof("manual provisioning")
   251  		authKeys, err := common.ReadAuthorizedKeys(ctx, "")
   252  		if err != nil {
   253  			return errors.Annotate(err, "reading authorized-keys")
   254  		}
   255  		args := manual.ProvisionMachineArgs{
   256  			Host:           c.Placement.Directive,
   257  			Client:         client,
   258  			Stdin:          ctx.Stdin,
   259  			Stdout:         ctx.Stdout,
   260  			Stderr:         ctx.Stderr,
   261  			AuthorizedKeys: authKeys,
   262  			UpdateBehavior: &params.UpdateBehavior{
   263  				config.EnableOSRefreshUpdate(),
   264  				config.EnableOSUpgrade(),
   265  			},
   266  		}
   267  		machineId, err := manualProvisioner(args)
   268  		if err == nil {
   269  			ctx.Infof("created machine %v", machineId)
   270  		}
   271  		return err
   272  	}
   273  
   274  	logger.Infof("model provisioning")
   275  	if c.Placement != nil && c.Placement.Scope == "model-uuid" {
   276  		uuid, ok := client.ModelUUID()
   277  		if !ok {
   278  			return errors.New("API connection is controller-only (should never happen)")
   279  		}
   280  		c.Placement.Scope = uuid
   281  	}
   282  
   283  	if c.Placement != nil && c.Placement.Scope == instance.MachineScope {
   284  		// It does not make sense to add-machine <id>.
   285  		return errors.Errorf("machine-id cannot be specified when adding machines")
   286  	}
   287  
   288  	jobs := []multiwatcher.MachineJob{multiwatcher.JobHostUnits}
   289  
   290  	machineParams := params.AddMachineParams{
   291  		Placement:   c.Placement,
   292  		Series:      c.Series,
   293  		Constraints: c.Constraints,
   294  		Jobs:        jobs,
   295  		Disks:       c.Disks,
   296  	}
   297  	machines := make([]params.AddMachineParams, c.NumMachines)
   298  	for i := 0; i < c.NumMachines; i++ {
   299  		machines[i] = machineParams
   300  	}
   301  
   302  	var results []params.AddMachinesResult
   303  	// If storage is specified, we attempt to use a new API on the service facade.
   304  	if len(c.Disks) > 0 {
   305  		results, err = machineManager.AddMachines(machines)
   306  	} else {
   307  		results, err = client.AddMachines(machines)
   308  	}
   309  	if params.IsCodeOperationBlocked(err) {
   310  		return block.ProcessBlockedError(err, block.BlockChange)
   311  	}
   312  	if err != nil {
   313  		return errors.Trace(err)
   314  	}
   315  
   316  	errs := []error{}
   317  	for _, machineInfo := range results {
   318  		if machineInfo.Error != nil {
   319  			errs = append(errs, machineInfo.Error)
   320  			continue
   321  		}
   322  		machineId := machineInfo.Machine
   323  
   324  		if names.IsContainerMachine(machineId) {
   325  			ctx.Infof("created container %v", machineId)
   326  		} else {
   327  			ctx.Infof("created machine %v", machineId)
   328  		}
   329  	}
   330  	if len(errs) == 1 {
   331  		fmt.Fprint(ctx.Stderr, "failed to create 1 machine\n")
   332  		return errs[0]
   333  	}
   334  	if len(errs) > 1 {
   335  		fmt.Fprintf(ctx.Stderr, "failed to create %d machines\n", len(errs))
   336  		returnErr := []string{}
   337  		for _, e := range errs {
   338  			returnErr = append(returnErr, e.Error())
   339  		}
   340  		return errors.New(strings.Join(returnErr, ", "))
   341  	}
   342  	return nil
   343  }