github.com/mattyw/juju@v0.0.0-20140610034352-732aecd63861/cmd/juju/deploy.go (about)

     1  // Copyright 2012, 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package main
     5  
     6  import (
     7  	"errors"
     8  	"fmt"
     9  	"os"
    10  	"strings"
    11  
    12  	"github.com/juju/names"
    13  	"launchpad.net/gnuflag"
    14  
    15  	"github.com/juju/juju/charm"
    16  	"github.com/juju/juju/cmd"
    17  	"github.com/juju/juju/cmd/envcmd"
    18  	"github.com/juju/juju/constraints"
    19  	"github.com/juju/juju/environs/config"
    20  	"github.com/juju/juju/juju"
    21  	"github.com/juju/juju/juju/osenv"
    22  	"github.com/juju/juju/state/api"
    23  	"github.com/juju/juju/state/api/params"
    24  )
    25  
    26  type DeployCommand struct {
    27  	envcmd.EnvCommandBase
    28  	UnitCommandBase
    29  	CharmName    string
    30  	ServiceName  string
    31  	Config       cmd.FileVar
    32  	Constraints  constraints.Value
    33  	Networks     string
    34  	BumpRevision bool   // Remove this once the 1.16 support is dropped.
    35  	RepoPath     string // defaults to JUJU_REPOSITORY
    36  }
    37  
    38  const deployDoc = `
    39  <charm name> can be a charm URL, or an unambiguously condensed form of it;
    40  assuming a current series of "precise", the following forms will be accepted:
    41  
    42  For cs:precise/mysql
    43    mysql
    44    precise/mysql
    45  
    46  For cs:~user/precise/mysql
    47    cs:~user/mysql
    48  
    49  The current series is determined first by the default-series environment
    50  setting, followed by the preferred series for the charm in the charm store.
    51  
    52  In these cases, a versioned charm URL will be expanded as expected (for example,
    53  mysql-33 becomes cs:precise/mysql-33).
    54  
    55  However, for local charms, when the default-series is not specified in the
    56  environment, one must specify the series. For example:
    57    local:precise/mysql
    58  
    59  <service name>, if omitted, will be derived from <charm name>.
    60  
    61  Constraints can be specified when using deploy by specifying the --constraints
    62  flag.  When used with deploy, service-specific constraints are set so that later
    63  machines provisioned with add-unit will use the same constraints (unless changed
    64  by set-constraints).
    65  
    66  Charms can be deployed to a specific machine using the --to argument.
    67  If the destination is an LXC container the default is to use lxc-clone
    68  to create the container where possible. For Ubuntu deployments, lxc-clone
    69  is supported for the trusty OS series and later. A 'template' container is
    70  created with the name
    71    juju-<series>-template
    72  where <series> is the OS series, for example 'juju-trusty-template'.
    73  
    74  You can override the use of clone by changing the provider configuration:
    75    lxc-clone: false
    76  
    77  If you have the main container directory mounted on a btrfs partition,
    78  then the clone will be using btrfs snapshots to create the containers.
    79  This means that clones use up much less disk space.  If you do not have btrfs,
    80  lxc will attempt to use aufs (an overlay type filesystem). You can
    81  explicitly ask Juju to create full containers and not overlays by specifying
    82  the following in the provider configuration:
    83    lxc-clone-aufs: false
    84  
    85  Examples:
    86     juju deploy mysql --to 23       (deploy to machine 23)
    87     juju deploy mysql --to 24/lxc/3 (deploy to lxc container 3 on host machine 24)
    88     juju deploy mysql --to lxc:25   (deploy to a new lxc container on host machine 25)
    89  
    90     juju deploy mysql -n 5 --constraints mem=8G
    91     (deploy 5 instances of mysql with at least 8 GB of RAM each)
    92  
    93     juju deploy mysql --networks=storage,mynet --constraints networks=^logging,db
    94     (deploy mysql on machines with "storage", "mynet" and "db" networks,
    95      but not on machines with "logging" network, also configure "storage" and
    96      "mynet" networks)
    97  
    98  Like constraints, service-specific network requirements can be
    99  specified with the --networks argument, which takes a comma-delimited
   100  list of juju-specific network names. Networks can also be specified with
   101  constraints, but they only define what machine to pick, not what networks
   102  to configure on it. The --networks argument instructs juju to add all the
   103  networks specified with it to all new machines deployed to host units of
   104  the service. Not supported on all providers.
   105  
   106  See Also:
   107     juju help constraints
   108     juju help set-constraints
   109     juju help get-constraints
   110  `
   111  
   112  func (c *DeployCommand) Info() *cmd.Info {
   113  	return &cmd.Info{
   114  		Name:    "deploy",
   115  		Args:    "<charm name> [<service name>]",
   116  		Purpose: "deploy a new service",
   117  		Doc:     deployDoc,
   118  	}
   119  }
   120  
   121  func (c *DeployCommand) SetFlags(f *gnuflag.FlagSet) {
   122  	c.UnitCommandBase.SetFlags(f)
   123  	f.IntVar(&c.NumUnits, "n", 1, "number of service units to deploy for principal charms")
   124  	f.BoolVar(&c.BumpRevision, "u", false, "increment local charm directory revision (DEPRECATED)")
   125  	f.BoolVar(&c.BumpRevision, "upgrade", false, "")
   126  	f.Var(&c.Config, "config", "path to yaml-formatted service config")
   127  	f.Var(constraints.ConstraintsValue{Target: &c.Constraints}, "constraints", "set service constraints")
   128  	f.StringVar(&c.Networks, "networks", "", "bind the service to specific networks")
   129  	f.StringVar(&c.RepoPath, "repository", os.Getenv(osenv.JujuRepositoryEnvKey), "local charm repository")
   130  }
   131  
   132  func (c *DeployCommand) Init(args []string) error {
   133  	switch len(args) {
   134  	case 2:
   135  		if !names.IsService(args[1]) {
   136  			return fmt.Errorf("invalid service name %q", args[1])
   137  		}
   138  		c.ServiceName = args[1]
   139  		fallthrough
   140  	case 1:
   141  		if _, err := charm.InferURL(args[0], "fake"); err != nil {
   142  			return fmt.Errorf("invalid charm name %q", args[0])
   143  		}
   144  		c.CharmName = args[0]
   145  	case 0:
   146  		return errors.New("no charm specified")
   147  	default:
   148  		return cmd.CheckEmpty(args[2:])
   149  	}
   150  	return c.UnitCommandBase.Init(args)
   151  }
   152  
   153  func (c *DeployCommand) Run(ctx *cmd.Context) error {
   154  	client, err := juju.NewAPIClientFromName(c.EnvName)
   155  	if err != nil {
   156  		return err
   157  	}
   158  	defer client.Close()
   159  
   160  	attrs, err := client.EnvironmentGet()
   161  	if err != nil {
   162  		return err
   163  	}
   164  	conf, err := config.New(config.NoDefaults, attrs)
   165  	if err != nil {
   166  		return err
   167  	}
   168  
   169  	curl, err := resolveCharmURL(c.CharmName, client, conf)
   170  	if err != nil {
   171  		return err
   172  	}
   173  
   174  	repo, err := charm.InferRepository(curl.Reference, ctx.AbsPath(c.RepoPath))
   175  	if err != nil {
   176  		return err
   177  	}
   178  
   179  	repo = config.SpecializeCharmRepo(repo, conf)
   180  
   181  	curl, err = addCharmViaAPI(client, ctx, curl, repo)
   182  	if err != nil {
   183  		return err
   184  	}
   185  
   186  	if c.BumpRevision {
   187  		ctx.Infof("--upgrade (or -u) is deprecated and ignored; charms are always deployed with a unique revision.")
   188  	}
   189  
   190  	requestedNetworks, err := networkNamesToTags(parseNetworks(c.Networks))
   191  	if err != nil {
   192  		return err
   193  	}
   194  	// We need to ensure network names are valid below, but we don't need them here.
   195  	_, err = networkNamesToTags(c.Constraints.IncludeNetworks())
   196  	if err != nil {
   197  		return err
   198  	}
   199  	_, err = networkNamesToTags(c.Constraints.ExcludeNetworks())
   200  	if err != nil {
   201  		return err
   202  	}
   203  	haveNetworks := len(requestedNetworks) > 0 || c.Constraints.HaveNetworks()
   204  
   205  	charmInfo, err := client.CharmInfo(curl.String())
   206  	if err != nil {
   207  		return err
   208  	}
   209  
   210  	numUnits := c.NumUnits
   211  	if charmInfo.Meta.Subordinate {
   212  		if !constraints.IsEmpty(&c.Constraints) {
   213  			return errors.New("cannot use --constraints with subordinate service")
   214  		}
   215  		if numUnits == 1 && c.ToMachineSpec == "" {
   216  			numUnits = 0
   217  		} else {
   218  			return errors.New("cannot use --num-units or --to with subordinate service")
   219  		}
   220  	}
   221  	serviceName := c.ServiceName
   222  	if serviceName == "" {
   223  		serviceName = charmInfo.Meta.Name
   224  	}
   225  
   226  	var configYAML []byte
   227  	if c.Config.Path != "" {
   228  		configYAML, err = c.Config.Read(ctx)
   229  		if err != nil {
   230  			return err
   231  		}
   232  	}
   233  	err = client.ServiceDeployWithNetworks(
   234  		curl.String(),
   235  		serviceName,
   236  		numUnits,
   237  		string(configYAML),
   238  		c.Constraints,
   239  		c.ToMachineSpec,
   240  		requestedNetworks,
   241  	)
   242  	if params.IsCodeNotImplemented(err) {
   243  		if haveNetworks {
   244  			return errors.New("cannot use --networks/--constraints networks=...: not supported by the API server")
   245  		}
   246  		err = client.ServiceDeploy(
   247  			curl.String(),
   248  			serviceName,
   249  			numUnits,
   250  			string(configYAML),
   251  			c.Constraints,
   252  			c.ToMachineSpec)
   253  	}
   254  	return err
   255  }
   256  
   257  // addCharmViaAPI calls the appropriate client API calls to add the
   258  // given charm URL to state. Also displays the charm URL of the added
   259  // charm on stdout.
   260  func addCharmViaAPI(client *api.Client, ctx *cmd.Context, curl *charm.URL, repo charm.Repository) (*charm.URL, error) {
   261  	if curl.Revision < 0 {
   262  		latest, err := charm.Latest(repo, curl)
   263  		if err != nil {
   264  			return nil, err
   265  		}
   266  		curl = curl.WithRevision(latest)
   267  	}
   268  	switch curl.Schema {
   269  	case "local":
   270  		ch, err := repo.Get(curl)
   271  		if err != nil {
   272  			return nil, err
   273  		}
   274  		stateCurl, err := client.AddLocalCharm(curl, ch)
   275  		if err != nil {
   276  			return nil, err
   277  		}
   278  		curl = stateCurl
   279  	case "cs":
   280  		err := client.AddCharm(curl)
   281  		if err != nil {
   282  			return nil, err
   283  		}
   284  	default:
   285  		return nil, fmt.Errorf("unsupported charm URL schema: %q", curl.Schema)
   286  	}
   287  	ctx.Infof("Added charm %q to the environment.", curl)
   288  	return curl, nil
   289  }
   290  
   291  // parseNetworks returns a list of network names by parsing the
   292  // comma-delimited string value of --networks argument.
   293  func parseNetworks(networksValue string) []string {
   294  	parts := strings.Split(networksValue, ",")
   295  	var networks []string
   296  	for _, part := range parts {
   297  		network := strings.TrimSpace(part)
   298  		if network != "" {
   299  			networks = append(networks, network)
   300  		}
   301  	}
   302  	return networks
   303  }
   304  
   305  // networkNamesToTags returns the given network names converted to
   306  // tags, or an error.
   307  func networkNamesToTags(networks []string) ([]string, error) {
   308  	var tags []string
   309  	for _, network := range networks {
   310  		if !names.IsNetwork(network) {
   311  			return nil, fmt.Errorf("%q is not a valid network name", network)
   312  		}
   313  		tags = append(tags, names.NetworkTag(network))
   314  	}
   315  	return tags, nil
   316  }