github.com/mattyw/juju@v0.0.0-20140610034352-732aecd63861/worker/uniter/context.go (about)

     1  // Copyright 2012, 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package uniter
     5  
     6  import (
     7  	"bufio"
     8  	"fmt"
     9  	"io"
    10  	"os"
    11  	"os/exec"
    12  	"path/filepath"
    13  	"sort"
    14  	"strings"
    15  	"sync"
    16  	"time"
    17  
    18  	"github.com/juju/loggo"
    19  	utilexec "github.com/juju/utils/exec"
    20  	"github.com/juju/utils/proxy"
    21  
    22  	"github.com/juju/juju/charm"
    23  	"github.com/juju/juju/state/api/params"
    24  	"github.com/juju/juju/state/api/uniter"
    25  	unitdebug "github.com/juju/juju/worker/uniter/debug"
    26  	"github.com/juju/juju/worker/uniter/jujuc"
    27  )
    28  
    29  type missingHookError struct {
    30  	hookName string
    31  }
    32  
    33  func (e *missingHookError) Error() string {
    34  	return e.hookName + " does not exist"
    35  }
    36  
    37  func IsMissingHookError(err error) bool {
    38  	_, ok := err.(*missingHookError)
    39  	return ok
    40  }
    41  
    42  // HookContext is the implementation of jujuc.Context.
    43  type HookContext struct {
    44  	unit *uniter.Unit
    45  
    46  	// privateAddress is the cached value of the unit's private
    47  	// address.
    48  	privateAddress string
    49  
    50  	// publicAddress is the cached value of the unit's public
    51  	// address.
    52  	publicAddress string
    53  
    54  	// configSettings holds the service configuration.
    55  	configSettings charm.Settings
    56  
    57  	// id identifies the context.
    58  	id string
    59  
    60  	// uuid is the universally unique identifier of the environment.
    61  	uuid string
    62  
    63  	// envName is the human friendly name of the environment.
    64  	envName string
    65  
    66  	// relationId identifies the relation for which a relation hook is
    67  	// executing. If it is -1, the context is not running a relation hook;
    68  	// otherwise, its value must be a valid key into the relations map.
    69  	relationId int
    70  
    71  	// remoteUnitName identifies the changing unit of the executing relation
    72  	// hook. It will be empty if the context is not running a relation hook,
    73  	// or if it is running a relation-broken hook.
    74  	remoteUnitName string
    75  
    76  	// relations contains the context for every relation the unit is a member
    77  	// of, keyed on relation id.
    78  	relations map[int]*ContextRelation
    79  
    80  	// apiAddrs contains the API server addresses.
    81  	apiAddrs []string
    82  
    83  	// serviceOwner contains the owner of the service
    84  	serviceOwner string
    85  
    86  	// proxySettings are the current proxy settings that the uniter knows about
    87  	proxySettings proxy.Settings
    88  }
    89  
    90  func NewHookContext(unit *uniter.Unit, id, uuid, envName string,
    91  	relationId int, remoteUnitName string, relations map[int]*ContextRelation,
    92  	apiAddrs []string, serviceOwner string, proxySettings proxy.Settings) (*HookContext, error) {
    93  	ctx := &HookContext{
    94  		unit:           unit,
    95  		id:             id,
    96  		uuid:           uuid,
    97  		envName:        envName,
    98  		relationId:     relationId,
    99  		remoteUnitName: remoteUnitName,
   100  		relations:      relations,
   101  		apiAddrs:       apiAddrs,
   102  		serviceOwner:   serviceOwner,
   103  		proxySettings:  proxySettings,
   104  	}
   105  	// Get and cache the addresses.
   106  	var err error
   107  	ctx.publicAddress, err = unit.PublicAddress()
   108  	if err != nil && !params.IsCodeNoAddressSet(err) {
   109  		return nil, err
   110  	}
   111  	ctx.privateAddress, err = unit.PrivateAddress()
   112  	if err != nil && !params.IsCodeNoAddressSet(err) {
   113  		return nil, err
   114  	}
   115  	return ctx, nil
   116  }
   117  
   118  func (ctx *HookContext) UnitName() string {
   119  	return ctx.unit.Name()
   120  }
   121  
   122  func (ctx *HookContext) PublicAddress() (string, bool) {
   123  	return ctx.publicAddress, ctx.publicAddress != ""
   124  }
   125  
   126  func (ctx *HookContext) PrivateAddress() (string, bool) {
   127  	return ctx.privateAddress, ctx.privateAddress != ""
   128  }
   129  
   130  func (ctx *HookContext) OpenPort(protocol string, port int) error {
   131  	return ctx.unit.OpenPort(protocol, port)
   132  }
   133  
   134  func (ctx *HookContext) ClosePort(protocol string, port int) error {
   135  	return ctx.unit.ClosePort(protocol, port)
   136  }
   137  
   138  func (ctx *HookContext) OwnerTag() string {
   139  	return ctx.serviceOwner
   140  }
   141  
   142  func (ctx *HookContext) ConfigSettings() (charm.Settings, error) {
   143  	if ctx.configSettings == nil {
   144  		var err error
   145  		ctx.configSettings, err = ctx.unit.ConfigSettings()
   146  		if err != nil {
   147  			return nil, err
   148  		}
   149  	}
   150  	result := charm.Settings{}
   151  	for name, value := range ctx.configSettings {
   152  		result[name] = value
   153  	}
   154  	return result, nil
   155  }
   156  
   157  func (ctx *HookContext) HookRelation() (jujuc.ContextRelation, bool) {
   158  	return ctx.Relation(ctx.relationId)
   159  }
   160  
   161  func (ctx *HookContext) RemoteUnitName() (string, bool) {
   162  	return ctx.remoteUnitName, ctx.remoteUnitName != ""
   163  }
   164  
   165  func (ctx *HookContext) Relation(id int) (jujuc.ContextRelation, bool) {
   166  	r, found := ctx.relations[id]
   167  	return r, found
   168  }
   169  
   170  func (ctx *HookContext) RelationIds() []int {
   171  	ids := []int{}
   172  	for id := range ctx.relations {
   173  		ids = append(ids, id)
   174  	}
   175  	return ids
   176  }
   177  
   178  // hookVars returns an os.Environ-style list of strings necessary to run a hook
   179  // such that it can know what environment it's operating in, and can call back
   180  // into ctx.
   181  func (ctx *HookContext) hookVars(charmDir, toolsDir, socketPath string) []string {
   182  	vars := []string{
   183  		"APT_LISTCHANGES_FRONTEND=none",
   184  		"DEBIAN_FRONTEND=noninteractive",
   185  		"PATH=" + toolsDir + ":" + os.Getenv("PATH"),
   186  		"CHARM_DIR=" + charmDir,
   187  		"JUJU_CONTEXT_ID=" + ctx.id,
   188  		"JUJU_AGENT_SOCKET=" + socketPath,
   189  		"JUJU_UNIT_NAME=" + ctx.unit.Name(),
   190  		"JUJU_ENV_UUID=" + ctx.uuid,
   191  		"JUJU_ENV_NAME=" + ctx.envName,
   192  		"JUJU_API_ADDRESSES=" + strings.Join(ctx.apiAddrs, " "),
   193  	}
   194  	if r, found := ctx.HookRelation(); found {
   195  		vars = append(vars, "JUJU_RELATION="+r.Name())
   196  		vars = append(vars, "JUJU_RELATION_ID="+r.FakeId())
   197  		name, _ := ctx.RemoteUnitName()
   198  		vars = append(vars, "JUJU_REMOTE_UNIT="+name)
   199  	}
   200  	vars = append(vars, ctx.proxySettings.AsEnvironmentValues()...)
   201  	return vars
   202  }
   203  
   204  func (ctx *HookContext) finalizeContext(process string, err error) error {
   205  	writeChanges := err == nil
   206  	for id, rctx := range ctx.relations {
   207  		if writeChanges {
   208  			if e := rctx.WriteSettings(); e != nil {
   209  				e = fmt.Errorf(
   210  					"could not write settings from %q to relation %d: %v",
   211  					process, id, e,
   212  				)
   213  				logger.Errorf("%v", e)
   214  				if err == nil {
   215  					err = e
   216  				}
   217  			}
   218  		}
   219  		rctx.ClearCache()
   220  	}
   221  	return err
   222  }
   223  
   224  // RunCommands executes the commands in an environment which allows it to to
   225  // call back into the hook context to execute jujuc tools.
   226  func (ctx *HookContext) RunCommands(commands, charmDir, toolsDir, socketPath string) (*utilexec.ExecResponse, error) {
   227  	env := ctx.hookVars(charmDir, toolsDir, socketPath)
   228  	result, err := utilexec.RunCommands(
   229  		utilexec.RunParams{
   230  			Commands:    commands,
   231  			WorkingDir:  charmDir,
   232  			Environment: env})
   233  	return result, ctx.finalizeContext("run commands", err)
   234  }
   235  
   236  func (ctx *HookContext) GetLogger(hookName string) loggo.Logger {
   237  	return loggo.GetLogger(fmt.Sprintf("unit.%s.%s", ctx.UnitName(), hookName))
   238  }
   239  
   240  // RunHook executes a hook in an environment which allows it to to call back
   241  // into the hook context to execute jujuc tools.
   242  func (ctx *HookContext) RunHook(hookName, charmDir, toolsDir, socketPath string) error {
   243  	var err error
   244  	env := ctx.hookVars(charmDir, toolsDir, socketPath)
   245  	debugctx := unitdebug.NewHooksContext(ctx.unit.Name())
   246  	if session, _ := debugctx.FindSession(); session != nil && session.MatchHook(hookName) {
   247  		logger.Infof("executing %s via debug-hooks", hookName)
   248  		err = session.RunHook(hookName, charmDir, env)
   249  	} else {
   250  		err = ctx.runCharmHook(hookName, charmDir, env)
   251  	}
   252  	return ctx.finalizeContext(hookName, err)
   253  }
   254  
   255  func (ctx *HookContext) runCharmHook(hookName, charmDir string, env []string) error {
   256  	hook, err := exec.LookPath(filepath.Join(charmDir, "hooks", hookName))
   257  	if err != nil {
   258  		if ee, ok := err.(*exec.Error); ok && os.IsNotExist(ee.Err) {
   259  			// Missing hook is perfectly valid, but worth mentioning.
   260  			logger.Infof("skipped %q hook (not implemented)", hookName)
   261  			return &missingHookError{hookName}
   262  		}
   263  		return err
   264  	}
   265  	ps := exec.Command(hook)
   266  	ps.Env = env
   267  	ps.Dir = charmDir
   268  	outReader, outWriter, err := os.Pipe()
   269  	if err != nil {
   270  		return fmt.Errorf("cannot make logging pipe: %v", err)
   271  	}
   272  	ps.Stdout = outWriter
   273  	ps.Stderr = outWriter
   274  	hookLogger := &hookLogger{
   275  		r:      outReader,
   276  		done:   make(chan struct{}),
   277  		logger: ctx.GetLogger(hookName),
   278  	}
   279  	go hookLogger.run()
   280  	err = ps.Start()
   281  	outWriter.Close()
   282  	if err == nil {
   283  		err = ps.Wait()
   284  	}
   285  	hookLogger.stop()
   286  	return err
   287  }
   288  
   289  type hookLogger struct {
   290  	r       io.ReadCloser
   291  	done    chan struct{}
   292  	mu      sync.Mutex
   293  	stopped bool
   294  	logger  loggo.Logger
   295  }
   296  
   297  func (l *hookLogger) run() {
   298  	defer close(l.done)
   299  	defer l.r.Close()
   300  	br := bufio.NewReaderSize(l.r, 4096)
   301  	for {
   302  		line, _, err := br.ReadLine()
   303  		if err != nil {
   304  			if err != io.EOF {
   305  				logger.Errorf("cannot read hook output: %v", err)
   306  			}
   307  			break
   308  		}
   309  		l.mu.Lock()
   310  		if l.stopped {
   311  			l.mu.Unlock()
   312  			return
   313  		}
   314  		l.logger.Infof("%s", line)
   315  		l.mu.Unlock()
   316  	}
   317  }
   318  
   319  func (l *hookLogger) stop() {
   320  	// We can see the process exit before the logger has processed
   321  	// all its output, so allow a moment for the data buffered
   322  	// in the pipe to be processed. We don't wait indefinitely though,
   323  	// because the hook may have started a background process
   324  	// that keeps the pipe open.
   325  	select {
   326  	case <-l.done:
   327  	case <-time.After(100 * time.Millisecond):
   328  	}
   329  	// We can't close the pipe asynchronously, so just
   330  	// stifle output instead.
   331  	l.mu.Lock()
   332  	l.stopped = true
   333  	l.mu.Unlock()
   334  }
   335  
   336  // SettingsMap is a map from unit name to relation settings.
   337  type SettingsMap map[string]params.RelationSettings
   338  
   339  // ContextRelation is the implementation of jujuc.ContextRelation.
   340  type ContextRelation struct {
   341  	ru *uniter.RelationUnit
   342  
   343  	// members contains settings for known relation members. Nil values
   344  	// indicate members whose settings have not yet been cached.
   345  	members SettingsMap
   346  
   347  	// settings allows read and write access to the relation unit settings.
   348  	settings *uniter.Settings
   349  
   350  	// cache is a short-term cache that enables consistent access to settings
   351  	// for units that are not currently participating in the relation. Its
   352  	// contents should be cleared whenever a new hook is executed.
   353  	cache SettingsMap
   354  }
   355  
   356  // NewContextRelation creates a new context for the given relation unit.
   357  // The unit-name keys of members supplies the initial membership.
   358  func NewContextRelation(ru *uniter.RelationUnit, members map[string]int64) *ContextRelation {
   359  	ctx := &ContextRelation{ru: ru, members: SettingsMap{}}
   360  	for unit := range members {
   361  		ctx.members[unit] = nil
   362  	}
   363  	ctx.ClearCache()
   364  	return ctx
   365  }
   366  
   367  // WriteSettings persists all changes made to the unit's relation settings.
   368  func (ctx *ContextRelation) WriteSettings() (err error) {
   369  	if ctx.settings != nil {
   370  		err = ctx.settings.Write()
   371  	}
   372  	return
   373  }
   374  
   375  // ClearCache discards all cached settings for units that are not members
   376  // of the relation, and all unwritten changes to the unit's relation settings.
   377  // including any changes to Settings that have not been written.
   378  func (ctx *ContextRelation) ClearCache() {
   379  	ctx.settings = nil
   380  	ctx.cache = make(SettingsMap)
   381  }
   382  
   383  // UpdateMembers ensures that the context is aware of every supplied
   384  // member unit. For each supplied member, the cached settings will be
   385  // overwritten.
   386  func (ctx *ContextRelation) UpdateMembers(members SettingsMap) {
   387  	for m, s := range members {
   388  		ctx.members[m] = s
   389  	}
   390  }
   391  
   392  // DeleteMember drops the membership and cache of a single remote unit, without
   393  // perturbing settings for the remaining members.
   394  func (ctx *ContextRelation) DeleteMember(unitName string) {
   395  	delete(ctx.members, unitName)
   396  }
   397  
   398  func (ctx *ContextRelation) Id() int {
   399  	return ctx.ru.Relation().Id()
   400  }
   401  
   402  func (ctx *ContextRelation) Name() string {
   403  	return ctx.ru.Endpoint().Name
   404  }
   405  
   406  func (ctx *ContextRelation) FakeId() string {
   407  	return fmt.Sprintf("%s:%d", ctx.Name(), ctx.ru.Relation().Id())
   408  }
   409  
   410  func (ctx *ContextRelation) UnitNames() (units []string) {
   411  	for unit := range ctx.members {
   412  		units = append(units, unit)
   413  	}
   414  	sort.Strings(units)
   415  	return units
   416  }
   417  
   418  func (ctx *ContextRelation) Settings() (jujuc.Settings, error) {
   419  	if ctx.settings == nil {
   420  		node, err := ctx.ru.Settings()
   421  		if err != nil {
   422  			return nil, err
   423  		}
   424  		ctx.settings = node
   425  	}
   426  	return ctx.settings, nil
   427  }
   428  
   429  func (ctx *ContextRelation) ReadSettings(unit string) (settings params.RelationSettings, err error) {
   430  	settings, member := ctx.members[unit]
   431  	if settings == nil {
   432  		if settings = ctx.cache[unit]; settings == nil {
   433  			settings, err = ctx.ru.ReadSettings(unit)
   434  			if err != nil {
   435  				return nil, err
   436  			}
   437  		}
   438  	}
   439  	if member {
   440  		ctx.members[unit] = settings
   441  	} else {
   442  		ctx.cache[unit] = settings
   443  	}
   444  	return settings, nil
   445  }