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 }