github.com/rogpeppe/juju@v0.0.0-20140613142852-6337964b789e/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  	"github.com/juju/charm"
    15  	"github.com/juju/errors"
    16  	"github.com/juju/loggo"
    17  	"github.com/juju/utils"
    18  
    19  	"github.com/juju/juju/environs"
    20  	"github.com/juju/juju/environs/configstore"
    21  	"github.com/juju/juju/juju/osenv"
    22  	"github.com/juju/juju/mongo"
    23  	"github.com/juju/juju/state"
    24  	"github.com/juju/juju/utils/ssh"
    25  )
    26  
    27  var logger = loggo.GetLogger("juju.conn")
    28  
    29  // Conn holds a connection to a juju environment and its
    30  // associated state.
    31  type Conn struct {
    32  	Environ environs.Environ
    33  	State   *state.State
    34  }
    35  
    36  var redialStrategy = utils.AttemptStrategy{
    37  	Total: 60 * time.Second,
    38  	Delay: 250 * time.Millisecond,
    39  }
    40  
    41  // NewConn returns a new Conn that uses the
    42  // given environment. The environment must have already
    43  // been bootstrapped.
    44  func NewConn(environ environs.Environ) (*Conn, error) {
    45  	info, _, err := environ.StateInfo()
    46  	if err != nil {
    47  		return nil, err
    48  	}
    49  	password := environ.Config().AdminSecret()
    50  	if password == "" {
    51  		return nil, fmt.Errorf("cannot connect without admin-secret")
    52  	}
    53  	err = environs.CheckEnvironment(environ)
    54  	if err != nil {
    55  		return nil, err
    56  	}
    57  
    58  	info.Password = password
    59  	opts := mongo.DefaultDialOpts()
    60  	st, err := state.Open(info, opts, environs.NewStatePolicy())
    61  	if errors.IsUnauthorized(err) {
    62  		logger.Infof("authorization error while connecting to state server; retrying")
    63  		// We can't connect with the administrator password,;
    64  		// perhaps this was the first connection and the
    65  		// password has not been changed yet.
    66  		info.Password = utils.UserPasswordHash(password, utils.CompatSalt)
    67  
    68  		// We try for a while because we might succeed in
    69  		// connecting to mongo before the state has been
    70  		// initialized and the initial password set.
    71  		for a := redialStrategy.Start(); a.Next(); {
    72  			st, err = state.Open(info, opts, environs.NewStatePolicy())
    73  			if !errors.IsUnauthorized(err) {
    74  				break
    75  			}
    76  		}
    77  		if err != nil {
    78  			return nil, err
    79  		}
    80  		if err := st.SetAdminMongoPassword(password); err != nil {
    81  			return nil, err
    82  		}
    83  	} else if err != nil {
    84  		return nil, err
    85  	}
    86  	conn := &Conn{
    87  		Environ: environ,
    88  		State:   st,
    89  	}
    90  	if err := conn.updateSecrets(); err != nil {
    91  		conn.Close()
    92  		return nil, fmt.Errorf("unable to push secrets: %v", err)
    93  	}
    94  	return conn, nil
    95  }
    96  
    97  // NewConnFromState returns a Conn that uses an Environ
    98  // made by reading the environment configuration.
    99  // The resulting Conn uses the given State - closing
   100  // it will close that State.
   101  func NewConnFromState(st *state.State) (*Conn, error) {
   102  	cfg, err := st.EnvironConfig()
   103  	if err != nil {
   104  		return nil, err
   105  	}
   106  	environ, err := environs.New(cfg)
   107  	if err != nil {
   108  		return nil, err
   109  	}
   110  	return &Conn{
   111  		Environ: environ,
   112  		State:   st,
   113  	}, nil
   114  }
   115  
   116  // NewConnFromName returns a Conn pointing at the environName environment, or the
   117  // default environment if not specified.
   118  func NewConnFromName(environName string) (*Conn, error) {
   119  	store, err := configstore.Default()
   120  	if err != nil {
   121  		return nil, err
   122  	}
   123  	environ, err := environs.NewFromName(environName, store)
   124  	if err != nil {
   125  		return nil, err
   126  	}
   127  	return NewConn(environ)
   128  }
   129  
   130  // Close terminates the connection to the environment and releases
   131  // any associated resources.
   132  func (c *Conn) Close() error {
   133  	return c.State.Close()
   134  }
   135  
   136  // updateSecrets writes secrets into the environment when there are none.
   137  // This is done because environments such as ec2 offer no way to securely
   138  // deliver the secrets onto the machine, so the bootstrap is done with the
   139  // whole environment configuration but without secrets, and then secrets
   140  // are delivered on the first communication with the running environment.
   141  func (c *Conn) updateSecrets() error {
   142  	secrets, err := c.Environ.Provider().SecretAttrs(c.Environ.Config())
   143  	if err != nil {
   144  		return err
   145  	}
   146  	cfg, err := c.State.EnvironConfig()
   147  	if err != nil {
   148  		return err
   149  	}
   150  	secretAttrs := make(map[string]interface{})
   151  	attrs := cfg.AllAttrs()
   152  	for k, v := range secrets {
   153  		if _, exists := attrs[k]; exists {
   154  			// Environment already has secrets. Won't send again.
   155  			return nil
   156  		} else {
   157  			secretAttrs[k] = v
   158  		}
   159  	}
   160  	return c.State.UpdateEnvironConfig(secretAttrs, nil, nil)
   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  	logger.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  	logger.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  }