github.com/Pankov404/juju@v0.0.0-20150703034450-be266991dceb/cmd/juju/commands/deploy.go (about)

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