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