launchpad.net/~rogpeppe/juju-core/500-errgo-fix@v0.0.0-20140213181702-000000002356/juju/conn.go (about)

     1  // Copyright 2012, 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package juju
     5  
     6  import (
     7  	stderrors "errors"
     8  	"fmt"
     9  	"io/ioutil"
    10  	"net/url"
    11  	"os"
    12  	"time"
    13  
    14  	"launchpad.net/juju-core/charm"
    15  	"launchpad.net/juju-core/environs"
    16  	"launchpad.net/juju-core/environs/config"
    17  	"launchpad.net/juju-core/environs/configstore"
    18  	"launchpad.net/juju-core/errors"
    19  	"launchpad.net/juju-core/juju/osenv"
    20  	"launchpad.net/juju-core/log"
    21  	"launchpad.net/juju-core/state"
    22  	"launchpad.net/juju-core/utils"
    23  	"launchpad.net/juju-core/utils/ssh"
    24  )
    25  
    26  // Conn holds a connection to a juju environment and its
    27  // associated state.
    28  type Conn struct {
    29  	Environ environs.Environ
    30  	State   *state.State
    31  }
    32  
    33  var redialStrategy = utils.AttemptStrategy{
    34  	Total: 60 * time.Second,
    35  	Delay: 250 * time.Millisecond,
    36  }
    37  
    38  // NewConn returns a new Conn that uses the
    39  // given environment. The environment must have already
    40  // been bootstrapped.
    41  func NewConn(environ environs.Environ) (*Conn, error) {
    42  	info, _, err := environ.StateInfo()
    43  	if err != nil {
    44  		return nil, err
    45  	}
    46  	password := environ.Config().AdminSecret()
    47  	if password == "" {
    48  		return nil, fmt.Errorf("cannot connect without admin-secret")
    49  	}
    50  	err = environs.CheckEnvironment(environ)
    51  	if err != nil {
    52  		return nil, err
    53  	}
    54  
    55  	info.Password = password
    56  	opts := state.DefaultDialOpts()
    57  	st, err := state.Open(info, opts)
    58  	if errors.IsUnauthorizedError(err) {
    59  		log.Noticef("juju: authorization error while connecting to state server; retrying")
    60  		// We can't connect with the administrator password,;
    61  		// perhaps this was the first connection and the
    62  		// password has not been changed yet.
    63  		info.Password = utils.UserPasswordHash(password, utils.CompatSalt)
    64  
    65  		// We try for a while because we might succeed in
    66  		// connecting to mongo before the state has been
    67  		// initialized and the initial password set.
    68  		for a := redialStrategy.Start(); a.Next(); {
    69  			st, err = state.Open(info, opts)
    70  			if !errors.IsUnauthorizedError(err) {
    71  				break
    72  			}
    73  		}
    74  		if err != nil {
    75  			return nil, err
    76  		}
    77  		if err := st.SetAdminMongoPassword(password); err != nil {
    78  			return nil, err
    79  		}
    80  	} else if err != nil {
    81  		return nil, err
    82  	}
    83  	conn := &Conn{
    84  		Environ: environ,
    85  		State:   st,
    86  	}
    87  	if err := conn.updateSecrets(); err != nil {
    88  		conn.Close()
    89  		return nil, fmt.Errorf("unable to push secrets: %v", err)
    90  	}
    91  	return conn, nil
    92  }
    93  
    94  // NewConnFromState returns a Conn that uses an Environ
    95  // made by reading the environment configuration.
    96  // The resulting Conn uses the given State - closing
    97  // it will close that State.
    98  func NewConnFromState(st *state.State) (*Conn, error) {
    99  	cfg, err := st.EnvironConfig()
   100  	if err != nil {
   101  		return nil, err
   102  	}
   103  	environ, err := environs.New(cfg)
   104  	if err != nil {
   105  		return nil, err
   106  	}
   107  	return &Conn{
   108  		Environ: environ,
   109  		State:   st,
   110  	}, nil
   111  }
   112  
   113  // NewConnFromName returns a Conn pointing at the environName environment, or the
   114  // default environment if not specified.
   115  func NewConnFromName(environName string) (*Conn, error) {
   116  	store, err := configstore.Default()
   117  	if err != nil {
   118  		return nil, err
   119  	}
   120  	environ, err := environs.NewFromName(environName, store)
   121  	if err != nil {
   122  		return nil, err
   123  	}
   124  	return NewConn(environ)
   125  }
   126  
   127  // Close terminates the connection to the environment and releases
   128  // any associated resources.
   129  func (c *Conn) Close() error {
   130  	return c.State.Close()
   131  }
   132  
   133  // updateSecrets writes secrets into the environment when there are none.
   134  // This is done because environments such as ec2 offer no way to securely
   135  // deliver the secrets onto the machine, so the bootstrap is done with the
   136  // whole environment configuration but without secrets, and then secrets
   137  // are delivered on the first communication with the running environment.
   138  func (c *Conn) updateSecrets() error {
   139  	secrets, err := c.Environ.Provider().SecretAttrs(c.Environ.Config())
   140  	if err != nil {
   141  		return err
   142  	}
   143  	cfg, err := c.State.EnvironConfig()
   144  	if err != nil {
   145  		return err
   146  	}
   147  	attrs := cfg.AllAttrs()
   148  	for k, v := range secrets {
   149  		if _, exists := attrs[k]; exists {
   150  			// Environment already has secrets. Won't send again.
   151  			return nil
   152  		} else {
   153  			attrs[k] = v
   154  		}
   155  	}
   156  	newcfg, err := config.New(config.NoDefaults, attrs)
   157  	if err != nil {
   158  		return err
   159  	}
   160  	return c.State.SetEnvironConfig(newcfg, cfg)
   161  }
   162  
   163  // PutCharm uploads the given charm to provider storage, and adds a
   164  // state.Charm to the state.  The charm is not uploaded if a charm with
   165  // the same URL already exists in the state.
   166  // If bumpRevision is true, the charm must be a local directory,
   167  // and the revision number will be incremented before pushing.
   168  func (conn *Conn) PutCharm(curl *charm.URL, repo charm.Repository, bumpRevision bool) (*state.Charm, error) {
   169  	if curl.Revision == -1 {
   170  		rev, err := charm.Latest(repo, curl)
   171  		if err != nil {
   172  			return nil, fmt.Errorf("cannot get latest charm revision: %v", err)
   173  		}
   174  		curl = curl.WithRevision(rev)
   175  	}
   176  	ch, err := repo.Get(curl)
   177  	if err != nil {
   178  		return nil, fmt.Errorf("cannot get charm: %v", err)
   179  	}
   180  	if bumpRevision {
   181  		chd, ok := ch.(*charm.Dir)
   182  		if !ok {
   183  			return nil, fmt.Errorf("cannot increment revision of charm %q: not a directory", curl)
   184  		}
   185  		if err = chd.SetDiskRevision(chd.Revision() + 1); err != nil {
   186  			return nil, fmt.Errorf("cannot increment revision of charm %q: %v", curl, err)
   187  		}
   188  		curl = curl.WithRevision(chd.Revision())
   189  	}
   190  	if sch, err := conn.State.Charm(curl); err == nil {
   191  		return sch, nil
   192  	}
   193  	return conn.addCharm(curl, ch)
   194  }
   195  
   196  func (conn *Conn) addCharm(curl *charm.URL, ch charm.Charm) (*state.Charm, error) {
   197  	var f *os.File
   198  	name := charm.Quote(curl.String())
   199  	switch ch := ch.(type) {
   200  	case *charm.Dir:
   201  		var err error
   202  		if f, err = ioutil.TempFile("", name); err != nil {
   203  			return nil, err
   204  		}
   205  		defer os.Remove(f.Name())
   206  		defer f.Close()
   207  		err = ch.BundleTo(f)
   208  		if err != nil {
   209  			return nil, fmt.Errorf("cannot bundle charm: %v", err)
   210  		}
   211  		if _, err := f.Seek(0, 0); err != nil {
   212  			return nil, err
   213  		}
   214  	case *charm.Bundle:
   215  		var err error
   216  		if f, err = os.Open(ch.Path); err != nil {
   217  			return nil, fmt.Errorf("cannot read charm bundle: %v", err)
   218  		}
   219  		defer f.Close()
   220  	default:
   221  		return nil, fmt.Errorf("unknown charm type %T", ch)
   222  	}
   223  	digest, size, err := utils.ReadSHA256(f)
   224  	if err != nil {
   225  		return nil, err
   226  	}
   227  	if _, err := f.Seek(0, 0); err != nil {
   228  		return nil, err
   229  	}
   230  	stor := conn.Environ.Storage()
   231  	log.Infof("writing charm to storage [%d bytes]", size)
   232  	if err := stor.Put(name, f, size); err != nil {
   233  		return nil, fmt.Errorf("cannot put charm: %v", err)
   234  	}
   235  	ustr, err := stor.URL(name)
   236  	if err != nil {
   237  		return nil, fmt.Errorf("cannot get storage URL for charm: %v", err)
   238  	}
   239  	u, err := url.Parse(ustr)
   240  	if err != nil {
   241  		return nil, fmt.Errorf("cannot parse storage URL: %v", err)
   242  	}
   243  	log.Infof("adding charm to state")
   244  	sch, err := conn.State.AddCharm(ch, curl, u, digest)
   245  	if err != nil {
   246  		return nil, fmt.Errorf("cannot add charm: %v", err)
   247  	}
   248  	return sch, nil
   249  }
   250  
   251  // InitJujuHome initializes the charm, environs/config and utils/ssh packages
   252  // to use default paths based on the $JUJU_HOME or $HOME environment variables.
   253  // This function should be called before calling NewConn or Conn.Deploy.
   254  func InitJujuHome() error {
   255  	jujuHome := osenv.JujuHomeDir()
   256  	if jujuHome == "" {
   257  		return stderrors.New(
   258  			"cannot determine juju home, required environment variables are not set")
   259  	}
   260  	osenv.SetJujuHome(jujuHome)
   261  	charm.CacheDir = osenv.JujuHomePath("charmcache")
   262  	if err := ssh.LoadClientKeys(osenv.JujuHomePath("ssh")); err != nil {
   263  		return fmt.Errorf("cannot load ssh client keys: %v", err)
   264  	}
   265  	return nil
   266  }