1
// Copyright 2013 Canonical Ltd.
2
// Licensed under the AGPLv3, see LICENCE file for details.
17
"github.com/juju/errors"
18
"github.com/juju/loggo"
19
"github.com/juju/utils"
20
"github.com/juju/utils/series"
21
"github.com/juju/utils/shell"
22
"github.com/juju/version"
23
"gopkg.in/juju/names.v2"
25
"github.com/juju/juju/api"
26
"github.com/juju/juju/apiserver/params"
27
"github.com/juju/juju/juju/paths"
28
"github.com/juju/juju/mongo"
29
"github.com/juju/juju/network"
30
"github.com/juju/juju/state/multiwatcher"
33
var logger = loggo.GetLogger("juju.agent")
36
// BootstrapNonce is used as a nonce for the initial controller machine.
37
BootstrapNonce = "user-admin:bootstrap"
39
// BootstrapMachineId is the ID of the initial controller machine.
40
BootstrapMachineId = "0"
42
// MachineLockName is the name of the mutex that the agent creates to
43
// ensure serialization of tasks such as uniter hook executions, juju-run,
45
MachineLockName = "machine-lock"
48
// These are base values used for the corresponding defaults.
50
logDir = paths.MustSucceed(paths.LogDir(series.HostSeries()))
51
dataDir = paths.MustSucceed(paths.DataDir(series.HostSeries()))
52
confDir = paths.MustSucceed(paths.ConfDir(series.HostSeries()))
53
metricsSpoolDir = paths.MustSucceed(paths.MetricsSpoolDir(series.HostSeries()))
56
// Agent exposes the agent's configuration to other components. This
57
// interface should probably be segregated (agent.ConfigGetter and
58
// agent.ConfigChanger?) but YAGNI *currently* advises against same.
59
type Agent interface {
61
// CurrentConfig returns a copy of the agent's configuration. No
62
// guarantees regarding ongoing correctness are made.
63
CurrentConfig() Config
65
// ChangeConfig allows clients to change the agent's configuration
66
// by supplying a callback that applies the changes.
67
ChangeConfig(ConfigMutator) error
70
// APIHostPortsSetter trivially wraps an Agent to implement
71
// worker/apiaddressupdater/APIAddressSetter.
72
type APIHostPortsSetter struct {
76
// SetAPIHostPorts is the APIAddressSetter interface.
77
func (s APIHostPortsSetter) SetAPIHostPorts(servers [][]network.HostPort) error {
78
return s.ChangeConfig(func(c ConfigSetter) error {
79
c.SetAPIHostPorts(servers)
84
// StateServingInfoSetter trivially wraps an Agent to implement
85
// worker/certupdater/SetStateServingInfo.
86
type StateServingInfoSetter struct {
90
// SetStateServingInfo is the SetStateServingInfo interface.
91
func (s StateServingInfoSetter) SetStateServingInfo(info params.StateServingInfo) error {
92
return s.ChangeConfig(func(c ConfigSetter) error {
93
c.SetStateServingInfo(info)
98
// Paths holds the directory paths used by the agent.
100
// DataDir is the data directory where each agent has a subdirectory
101
// containing the configuration files.
103
// LogDir is the log directory where all logs from all agents on
104
// the machine are written.
106
// MetricsSpoolDir is the spool directory where workloads store
107
// collected metrics.
108
MetricsSpoolDir string
109
// ConfDir is the directory where all config file for
110
// Juju agents are stored.
114
// Migrate assigns the directory locations specified from the new path configuration.
115
func (p *Paths) Migrate(newPaths Paths) {
116
if newPaths.DataDir != "" {
117
p.DataDir = newPaths.DataDir
119
if newPaths.LogDir != "" {
120
p.LogDir = newPaths.LogDir
122
if newPaths.MetricsSpoolDir != "" {
123
p.MetricsSpoolDir = newPaths.MetricsSpoolDir
125
if newPaths.ConfDir != "" {
126
p.ConfDir = newPaths.ConfDir
130
// NewPathsWithDefaults returns a Paths struct initialized with default locations if not otherwise specified.
131
func NewPathsWithDefaults(p Paths) Paths {
132
paths := DefaultPaths
134
paths.DataDir = p.DataDir
137
paths.LogDir = p.LogDir
139
if p.MetricsSpoolDir != "" {
140
paths.MetricsSpoolDir = p.MetricsSpoolDir
143
paths.ConfDir = p.ConfDir
149
// DefaultPaths defines the default paths for an agent.
150
DefaultPaths = Paths{
152
LogDir: path.Join(logDir, "juju"),
153
MetricsSpoolDir: metricsSpoolDir,
158
// SystemIdentity is the name of the file where the environment SSH key is kept.
159
const SystemIdentity = "system-identity"
162
LxcBridge = "LXC_BRIDGE"
163
LxdBridge = "LXD_BRIDGE"
164
ProviderType = "PROVIDER_TYPE"
165
ContainerType = "CONTAINER_TYPE"
166
Namespace = "NAMESPACE"
167
AgentServiceName = "AGENT_SERVICE_NAME"
168
MongoOplogSize = "MONGO_OPLOG_SIZE"
169
NumaCtlPreference = "NUMA_CTL_PREFERENCE"
172
// The Config interface is the sole way that the agent gets access to the
173
// configuration information for the machine and unit agents. There should
174
// only be one instance of a config object for any given agent, and this
175
// interface is passed between multiple go routines. The mutable methods are
176
// protected by a mutex, and it is expected that the caller doesn't modify any
177
// slice that may be returned.
179
// NOTE: should new mutating methods be added to this interface, consideration
180
// is needed around the synchronisation as a single instance is used in
181
// multiple go routines.
182
type Config interface {
183
// DataDir returns the data directory. Each agent has a subdirectory
184
// containing the configuration files.
187
// LogDir returns the log directory. All logs from all agents on
188
// the machine are written to this directory.
191
// SystemIdentityPath returns the path of the file where the environment
193
SystemIdentityPath() string
195
// Jobs returns a list of MachineJobs that need to run.
196
Jobs() []multiwatcher.MachineJob
198
// Tag returns the tag of the entity on whose behalf the state connection
202
// Dir returns the agent's directory.
205
// Nonce returns the nonce saved when the machine was provisioned
206
// TODO: make this one of the key/value pairs.
209
// CACert returns the CA certificate that is used to validate the state or
210
// API server's certificate.
213
// APIAddresses returns the addresses needed to connect to the api server
214
APIAddresses() ([]string, error)
216
// WriteCommands returns shell commands to write the agent configuration.
217
// It returns an error if the configuration does not have all the right
219
WriteCommands(renderer shell.Renderer) ([]string, error)
221
// StateServingInfo returns the details needed to run
222
// a controller and reports whether those details
224
StateServingInfo() (params.StateServingInfo, bool)
226
// APIInfo returns details for connecting to the API server and
227
// reports whether the details are available.
228
APIInfo() (*api.Info, bool)
230
// MongoInfo returns details for connecting to the controller's mongo
231
// database and reports whether those details are available
232
MongoInfo() (*mongo.MongoInfo, bool)
234
// OldPassword returns the fallback password when connecting to the
238
// UpgradedToVersion returns the version for which all upgrade steps have been
239
// successfully run, which is also the same as the initially deployed version.
240
UpgradedToVersion() version.Number
242
// Value returns the value associated with the key, or an empty string if
243
// the key is not found.
244
Value(key string) string
246
// Model returns the tag for the model that the agent belongs to.
247
Model() names.ModelTag
249
// MetricsSpoolDir returns the spool directory where workloads store
250
// collected metrics.
251
MetricsSpoolDir() string
253
// MongoVersion returns the version of mongo that the state server
255
MongoVersion() mongo.Version
258
type configSetterOnly interface {
259
// Clone returns a copy of the configuration that
260
// is unaffected by subsequent calls to the Set*
264
// SetOldPassword sets the password that is currently
265
// valid but needs to be changed. This is used as
267
SetOldPassword(oldPassword string)
269
// SetPassword sets the password to be used when
270
// connecting to the state.
271
SetPassword(newPassword string)
273
// SetValue updates the value for the specified key.
274
SetValue(key, value string)
276
// SetUpgradedToVersion sets the version that
277
// the agent has successfully upgraded to.
278
SetUpgradedToVersion(newVersion version.Number)
280
// SetAPIHostPorts sets the API host/port addresses to connect to.
281
SetAPIHostPorts(servers [][]network.HostPort)
283
// SetCACert sets the CA cert used for validating API connections.
286
// Migrate takes an existing agent config and applies the given
287
// parameters to change it.
289
// Only non-empty fields in newParams are used
290
// to change existing config settings. All changes are written
291
// atomically. UpgradedToVersion cannot be changed here, because
292
// Migrate is most likely called during an upgrade, so it will be
293
// changed at the end of the upgrade anyway, if successful.
295
// Migrate does not actually write the new configuration.
297
// Note that if the configuration file moves location,
298
// (if DataDir is set), the the caller is responsible for removing
299
// the old configuration.
300
Migrate(MigrateParams) error
302
// SetStateServingInfo sets the information needed
303
// to run a controller
304
SetStateServingInfo(info params.StateServingInfo)
306
// SetMongoVersion sets the passed version as currently in use.
307
SetMongoVersion(mongo.Version)
310
// LogFileName returns the filename for the Agent's log file.
311
func LogFilename(c Config) string {
312
return filepath.Join(c.LogDir(), c.Tag().String()+".log")
315
type ConfigMutator func(ConfigSetter) error
317
type ConfigWriter interface {
318
// Write writes the agent configuration.
322
type ConfigSetter interface {
327
type ConfigSetterWriter interface {
333
// MigrateParams holds agent config values to change in a
334
// Migrate call. Empty fields will be ignored. DeleteValues
335
// specifies a list of keys to delete.
336
type MigrateParams struct {
338
Jobs []multiwatcher.MachineJob
339
DeleteValues []string
340
Values map[string]string
344
// Ensure that the configInternal struct implements the Config interface.
345
var _ Config = (*configInternal)(nil)
347
type connectionDetails struct {
352
func (d *connectionDetails) clone() *connectionDetails {
357
newd.addresses = append([]string{}, d.addresses...)
361
type configInternal struct {
362
configFilePath string
367
jobs []multiwatcher.MachineJob
368
upgradedToVersion version.Number
370
stateDetails *connectionDetails
371
apiDetails *connectionDetails
373
servingInfo *params.StateServingInfo
374
values map[string]string
378
// AgentConfigParams holds the parameters required to create
379
// a new AgentConfig.
380
type AgentConfigParams struct {
382
Jobs []multiwatcher.MachineJob
383
UpgradedToVersion version.Number
388
StateAddresses []string
389
APIAddresses []string
391
Values map[string]string
392
MongoVersion mongo.Version
395
// NewAgentConfig returns a new config object suitable for use for a
396
// machine or unit agent.
397
func NewAgentConfig(configParams AgentConfigParams) (ConfigSetterWriter, error) {
398
if configParams.Paths.DataDir == "" {
399
return nil, errors.Trace(requiredError("data directory"))
401
if configParams.Tag == nil {
402
return nil, errors.Trace(requiredError("entity tag"))
404
switch configParams.Tag.(type) {
405
case names.MachineTag, names.UnitTag:
406
// these are the only two type of tags that can represent an agent
408
return nil, errors.Errorf("entity tag must be MachineTag or UnitTag, got %T", configParams.Tag)
410
if configParams.UpgradedToVersion == version.Zero {
411
return nil, errors.Trace(requiredError("upgradedToVersion"))
413
if configParams.Password == "" {
414
return nil, errors.Trace(requiredError("password"))
416
if uuid := configParams.Model.Id(); uuid == "" {
417
return nil, errors.Trace(requiredError("model"))
418
} else if !names.IsValidModel(uuid) {
419
return nil, errors.Errorf("%q is not a valid model uuid", uuid)
421
if len(configParams.CACert) == 0 {
422
return nil, errors.Trace(requiredError("CA certificate"))
424
// Note that the password parts of the state and api information are
425
// blank. This is by design: we want to generate a secure password
426
// for new agents. So, we create this config without a current password
427
// which signals to apicaller worker that it should try to connect using old password.
428
// When/if this connection is successful, apicaller worker will generate
429
// a new secure password and update this agent's config.
430
config := &configInternal{
431
paths: NewPathsWithDefaults(configParams.Paths),
432
jobs: configParams.Jobs,
433
upgradedToVersion: configParams.UpgradedToVersion,
434
tag: configParams.Tag,
435
nonce: configParams.Nonce,
436
model: configParams.Model,
437
caCert: configParams.CACert,
438
oldPassword: configParams.Password,
439
values: configParams.Values,
440
mongoVersion: configParams.MongoVersion.String(),
443
if len(configParams.StateAddresses) > 0 {
444
config.stateDetails = &connectionDetails{
445
addresses: configParams.StateAddresses,
448
if len(configParams.APIAddresses) > 0 {
449
config.apiDetails = &connectionDetails{
450
addresses: configParams.APIAddresses,
453
if err := config.check(); err != nil {
456
if config.values == nil {
457
config.values = make(map[string]string)
459
config.configFilePath = ConfigPath(config.paths.DataDir, config.tag)
463
// NewStateMachineConfig returns a configuration suitable for
464
// a machine running the controller.
465
func NewStateMachineConfig(configParams AgentConfigParams, serverInfo params.StateServingInfo) (ConfigSetterWriter, error) {
466
if serverInfo.Cert == "" {
467
return nil, errors.Trace(requiredError("controller cert"))
469
if serverInfo.PrivateKey == "" {
470
return nil, errors.Trace(requiredError("controller key"))
472
if serverInfo.CAPrivateKey == "" {
473
return nil, errors.Trace(requiredError("ca cert key"))
475
if serverInfo.StatePort == 0 {
476
return nil, errors.Trace(requiredError("state port"))
478
if serverInfo.APIPort == 0 {
479
return nil, errors.Trace(requiredError("api port"))
481
config, err := NewAgentConfig(configParams)
485
config.SetStateServingInfo(serverInfo)
489
// BaseDir returns the directory containing the data directories for
491
func BaseDir(dataDir string) string {
492
// Note: must use path, not filepath, as this function is
493
// (indirectly) used by the client on Windows.
494
return path.Join(dataDir, "agents")
497
// Dir returns the agent-specific data directory.
498
func Dir(dataDir string, tag names.Tag) string {
499
// Note: must use path, not filepath, as this
500
// function is used by the client on Windows.
501
return path.Join(BaseDir(dataDir), tag.String())
504
// ConfigPath returns the full path to the agent config file.
505
// NOTE: Delete this once all agents accept --config instead
506
// of --data-dir - it won't be needed anymore.
507
func ConfigPath(dataDir string, tag names.Tag) string {
508
return filepath.Join(Dir(dataDir, tag), agentConfigFilename)
511
// ReadConfig reads configuration data from the given location.
512
func ReadConfig(configFilePath string) (ConfigSetterWriter, error) {
515
config *configInternal
517
configData, err := ioutil.ReadFile(configFilePath)
519
return nil, fmt.Errorf("cannot read agent config %q: %v", configFilePath, err)
521
format, config, err = parseConfigData(configData)
525
logger.Debugf("read agent config, format %q", format.version())
526
config.configFilePath = configFilePath
530
func (c0 *configInternal) Clone() Config {
532
// Deep copy only fields which may be affected
533
// by ConfigSetter methods.
534
c1.stateDetails = c0.stateDetails.clone()
535
c1.apiDetails = c0.apiDetails.clone()
536
c1.jobs = append([]multiwatcher.MachineJob{}, c0.jobs...)
537
c1.values = make(map[string]string, len(c0.values))
538
for key, val := range c0.values {
544
func (config *configInternal) Migrate(newParams MigrateParams) error {
545
config.paths.Migrate(newParams.Paths)
546
config.configFilePath = ConfigPath(config.paths.DataDir, config.tag)
547
if len(newParams.Jobs) > 0 {
548
config.jobs = make([]multiwatcher.MachineJob, len(newParams.Jobs))
549
copy(config.jobs, newParams.Jobs)
551
for _, key := range newParams.DeleteValues {
552
delete(config.values, key)
554
for key, value := range newParams.Values {
555
if config.values == nil {
556
config.values = make(map[string]string)
558
config.values[key] = value
560
if newParams.Model.Id() != "" {
561
config.model = newParams.Model
563
if err := config.check(); err != nil {
564
return fmt.Errorf("migrated agent config is invalid: %v", err)
569
func (c *configInternal) SetUpgradedToVersion(newVersion version.Number) {
570
c.upgradedToVersion = newVersion
573
func (c *configInternal) SetAPIHostPorts(servers [][]network.HostPort) {
574
if c.apiDetails == nil {
578
for _, serverHostPorts := range servers {
579
hps := network.PrioritizeInternalHostPorts(serverHostPorts, false)
580
addrs = append(addrs, hps...)
582
c.apiDetails.addresses = addrs
583
logger.Infof("API server address details %q written to agent config as %q", servers, addrs)
586
func (c *configInternal) SetCACert(cert string) {
590
func (c *configInternal) SetValue(key, value string) {
592
delete(c.values, key)
594
c.values[key] = value
598
func (c *configInternal) SetOldPassword(oldPassword string) {
599
c.oldPassword = oldPassword
602
func (c *configInternal) SetPassword(newPassword string) {
603
if c.stateDetails != nil {
604
c.stateDetails.password = newPassword
606
if c.apiDetails != nil {
607
c.apiDetails.password = newPassword
611
func (c *configInternal) Write() error {
612
data, err := c.fileContents()
616
// Make sure the config dir gets created.
617
configDir := filepath.Dir(c.configFilePath)
618
if err := os.MkdirAll(configDir, 0755); err != nil {
619
return fmt.Errorf("cannot create agent config dir %q: %v", configDir, err)
621
return utils.AtomicWriteFile(c.configFilePath, data, 0600)
624
func requiredError(what string) error {
625
return fmt.Errorf("%s not found in configuration", what)
628
func (c *configInternal) File(name string) string {
629
return path.Join(c.Dir(), name)
632
func (c *configInternal) DataDir() string {
633
return c.paths.DataDir
636
func (c *configInternal) MetricsSpoolDir() string {
637
return c.paths.MetricsSpoolDir
640
func (c *configInternal) LogDir() string {
641
return c.paths.LogDir
644
func (c *configInternal) SystemIdentityPath() string {
645
return filepath.Join(c.paths.DataDir, SystemIdentity)
648
func (c *configInternal) Jobs() []multiwatcher.MachineJob {
652
func (c *configInternal) Nonce() string {
656
func (c *configInternal) UpgradedToVersion() version.Number {
657
return c.upgradedToVersion
660
func (c *configInternal) CACert() string {
664
func (c *configInternal) Value(key string) string {
668
func (c *configInternal) StateServingInfo() (params.StateServingInfo, bool) {
669
if c.servingInfo == nil {
670
return params.StateServingInfo{}, false
672
return *c.servingInfo, true
675
func (c *configInternal) SetStateServingInfo(info params.StateServingInfo) {
676
c.servingInfo = &info
679
func (c *configInternal) APIAddresses() ([]string, error) {
680
if c.apiDetails == nil {
681
return []string{}, errors.New("No apidetails in config")
683
return append([]string{}, c.apiDetails.addresses...), nil
686
func (c *configInternal) OldPassword() string {
690
func (c *configInternal) Tag() names.Tag {
694
func (c *configInternal) Model() names.ModelTag {
698
func (c *configInternal) Dir() string {
699
return Dir(c.paths.DataDir, c.tag)
702
func (c *configInternal) check() error {
703
if c.stateDetails == nil && c.apiDetails == nil {
704
return errors.Trace(requiredError("state or API addresses"))
706
if c.stateDetails != nil {
707
if err := checkAddrs(c.stateDetails.addresses, "controller address"); err != nil {
711
if c.apiDetails != nil {
712
if err := checkAddrs(c.apiDetails.addresses, "API server address"); err != nil {
719
// MongoVersion implements Config.
720
func (c *configInternal) MongoVersion() mongo.Version {
721
v, err := mongo.NewVersion(c.mongoVersion)
728
// SetMongoVersion implements configSetterOnly.
729
func (c *configInternal) SetMongoVersion(v mongo.Version) {
730
c.mongoVersion = v.String()
733
var validAddr = regexp.MustCompile("^.+:[0-9]+$")
735
func checkAddrs(addrs []string, what string) error {
737
return errors.Trace(requiredError(what))
739
for _, a := range addrs {
740
if !validAddr.MatchString(a) {
741
return errors.Errorf("invalid %s %q", what, a)
747
func (c *configInternal) fileContents() ([]byte, error) {
748
data, err := currentFormat.marshal(c)
753
fmt.Fprintf(&buf, "%s%s\n", formatPrefix, currentFormat.version())
755
return buf.Bytes(), nil
758
// WriteCommands is defined on Config interface.
759
func (c *configInternal) WriteCommands(renderer shell.Renderer) ([]string, error) {
760
data, err := c.fileContents()
762
return nil, errors.Trace(err)
764
commands := renderer.MkdirAll(c.Dir())
765
filename := c.File(agentConfigFilename)
766
commands = append(commands, renderer.WriteFile(filename, data)...)
767
commands = append(commands, renderer.Chmod(filename, 0600)...)
771
// APIInfo is defined on Config interface.
772
func (c *configInternal) APIInfo() (*api.Info, bool) {
773
if c.apiDetails == nil || c.apiDetails.addresses == nil {
776
servingInfo, isController := c.StateServingInfo()
777
addrs := c.apiDetails.addresses
779
port := servingInfo.APIPort
780
localAPIAddr := net.JoinHostPort("localhost", strconv.Itoa(port))
782
for _, addr := range addrs {
783
if addr == localAPIAddr {
789
addrs = append(addrs, localAPIAddr)
794
Password: c.apiDetails.password,
802
// MongoInfo is defined on Config interface.
803
func (c *configInternal) MongoInfo() (info *mongo.MongoInfo, ok bool) {
804
ssi, ok := c.StateServingInfo()
808
addr := net.JoinHostPort("127.0.0.1", strconv.Itoa(ssi.StatePort))
809
return &mongo.MongoInfo{
811
Addrs: []string{addr},
814
Password: c.stateDetails.password,