github.com/mhilton/juju-juju@v0.0.0-20150901100907-a94dd2c73455/worker/rsyslog/worker.go (about) 1 // Copyright 2014 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package rsyslog 5 6 import ( 7 "crypto/tls" 8 "crypto/x509" 9 "fmt" 10 "io" 11 "net" 12 "os" 13 "os/user" 14 "path/filepath" 15 "runtime" 16 "strconv" 17 "time" 18 19 "github.com/juju/errors" 20 "github.com/juju/loggo" 21 "github.com/juju/names" 22 rsyslog "github.com/juju/syslog" 23 "github.com/juju/utils" 24 25 "github.com/juju/juju/agent" 26 apirsyslog "github.com/juju/juju/api/rsyslog" 27 "github.com/juju/juju/api/watcher" 28 "github.com/juju/juju/cert" 29 "github.com/juju/juju/utils/syslog" 30 "github.com/juju/juju/version" 31 "github.com/juju/juju/worker" 32 ) 33 34 var logger = loggo.GetLogger("juju.worker.rsyslog") 35 36 var ( 37 rsyslogConfDir = "/etc/rsyslog.d" 38 logDir = agent.DefaultLogDir 39 syslogTargets = []*rsyslog.Writer{} 40 ) 41 42 // RsyslogMode describes how to configure rsyslog. 43 type RsyslogMode int 44 45 const ( 46 RsyslogModeInvalid RsyslogMode = iota 47 // RsyslogModeForwarding is the mode in which 48 // rsyslog will be configured to forward logging 49 // to state servers. 50 RsyslogModeForwarding 51 // RsyslogModeAccumulate is the mode in which 52 // rsyslog will be configured to accumulate logging 53 // from other machines into an "all-machines.log". 54 RsyslogModeAccumulate 55 ) 56 57 // RsyslogConfigHandler implements worker.NotifyWatchHandler, watching 58 // environment configuration changes and generating new rsyslog 59 // configuration. 60 type RsyslogConfigHandler struct { 61 st *apirsyslog.State 62 mode RsyslogMode 63 syslogConfig *syslog.SyslogConfig 64 rsyslogConfPath string 65 tag names.Tag 66 // We store the syslog-port and rsyslog-ca-cert 67 // values after writing the rsyslog configuration, 68 // so we can decide whether a change has occurred. 69 syslogPort int 70 rsyslogCACert string 71 rsyslogCAKey string 72 } 73 74 // certPair holds the path and contents for a certificate. 75 type certPair struct { 76 path string 77 data string 78 } 79 80 var _ worker.NotifyWatchHandler = (*RsyslogConfigHandler)(nil) 81 82 func syslogUser() string { 83 var user string 84 switch version.Current.OS { 85 case version.CentOS: 86 user = "root" 87 default: 88 user = "syslog" 89 } 90 91 return user 92 } 93 94 var NewRsyslogConfigWorker = newRsyslogConfigWorker 95 96 // newRsyslogConfigWorker returns a worker.Worker that uses 97 // WatchForRsyslogChanges and updates rsyslog configuration based 98 // on changes. The worker will remove the configuration file 99 // on teardown. 100 func newRsyslogConfigWorker(st *apirsyslog.State, mode RsyslogMode, tag names.Tag, namespace string, stateServerAddrs []string, jujuConfigDir string) (worker.Worker, error) { 101 if version.Current.OS == version.Windows && mode == RsyslogModeAccumulate { 102 return worker.NewNoOpWorker(), nil 103 } 104 handler, err := newRsyslogConfigHandler(st, mode, tag, namespace, stateServerAddrs, jujuConfigDir) 105 if err != nil { 106 return nil, err 107 } 108 logger.Debugf("starting rsyslog worker mode %v for %q %q", mode, tag, namespace) 109 return worker.NewNotifyWorker(handler), nil 110 } 111 112 func newRsyslogConfigHandler(st *apirsyslog.State, mode RsyslogMode, tag names.Tag, namespace string, stateServerAddrs []string, jujuConfigDir string) (*RsyslogConfigHandler, error) { 113 if namespace != "" { 114 jujuConfigDir += "-" + namespace 115 } 116 jujuConfigDir = filepath.Join(jujuConfigDir, "rsyslog") 117 if err := os.MkdirAll(jujuConfigDir, 0755); err != nil { 118 return nil, errors.Trace(err) 119 } 120 121 syslogConfig := &syslog.SyslogConfig{ 122 LogFileName: tag.String(), 123 LogDir: logDir, 124 JujuConfigDir: jujuConfigDir, 125 Port: 0, 126 Namespace: namespace, 127 StateServerAddresses: stateServerAddrs, 128 } 129 if mode == RsyslogModeAccumulate { 130 syslog.NewAccumulateConfig(syslogConfig) 131 } else { 132 syslog.NewForwardConfig(syslogConfig) 133 } 134 135 // Historically only machine-0 includes the namespace in the log 136 // dir/file; for backwards compatibility we continue the tradition. 137 if tag != names.NewMachineTag("0") { 138 namespace = "" 139 } 140 switch tag := tag.(type) { 141 case names.MachineTag: 142 if namespace == "" { 143 syslogConfig.ConfigFileName = "25-juju.conf" 144 } else { 145 syslogConfig.ConfigFileName = fmt.Sprintf("25-juju-%s.conf", namespace) 146 } 147 default: 148 syslogConfig.ConfigFileName = fmt.Sprintf("26-juju-%s.conf", tag) 149 } 150 151 syslogConfig.ConfigDir = rsyslogConfDir 152 syslogConfig.LogDir = logDir 153 if namespace != "" { 154 syslogConfig.LogDir += "-" + namespace 155 } 156 157 return &RsyslogConfigHandler{ 158 st: st, 159 mode: mode, 160 syslogConfig: syslogConfig, 161 tag: tag, 162 }, nil 163 } 164 165 func (h *RsyslogConfigHandler) SetUp() (watcher.NotifyWatcher, error) { 166 if h.mode == RsyslogModeAccumulate { 167 if err := h.ensureCA(); err != nil { 168 return nil, errors.Annotate(err, "failed to write rsyslog certificates") 169 } 170 171 if err := h.ensureLogrotate(); err != nil { 172 return nil, errors.Annotate(err, "failed to write rsyslog logrotate scripts") 173 } 174 175 } 176 return h.st.WatchForRsyslogChanges(h.tag.String()) 177 } 178 179 var restartRsyslog = syslog.Restart 180 var dialSyslog = rsyslog.Dial 181 182 func (h *RsyslogConfigHandler) TearDown() error { 183 if err := os.Remove(h.syslogConfig.ConfigFilePath()); err == nil { 184 restartRsyslog() 185 } 186 return nil 187 } 188 189 // composeTLS generates a new client certificate for connecting to the rsyslog server. 190 // We explicitly set the ServerName field, this ensures that even if we are connecting 191 // via an IP address and are using an old certificate (pre 1.20.9), we can still 192 // successfully connect. 193 func (h *RsyslogConfigHandler) composeTLS(caCert string) (*tls.Config, error) { 194 cert := x509.NewCertPool() 195 ok := cert.AppendCertsFromPEM([]byte(caCert)) 196 if !ok { 197 return nil, errors.Errorf("Failed to parse rsyslog root certificate") 198 } 199 return &tls.Config{ 200 RootCAs: cert, 201 ServerName: "juju-rsyslog", 202 }, nil 203 } 204 205 func (h *RsyslogConfigHandler) replaceRemoteLogger(caCert string) error { 206 tlsConf, err := h.composeTLS(caCert) 207 if err != nil { 208 return err 209 } 210 211 var newLoggers []*rsyslog.Writer 212 var wrapLoggers []io.Writer 213 for _, j := range h.syslogConfig.StateServerAddresses { 214 host, _, err := net.SplitHostPort(j) 215 if err != nil { 216 // No port was found 217 host = j 218 } 219 target := fmt.Sprintf("%s:%d", host, h.syslogConfig.Port) 220 namespace := h.syslogConfig.Namespace 221 if namespace != "" { 222 namespace = "-" + namespace 223 } 224 logTag := "juju" + namespace + "-" + h.tag.String() 225 logger.Debugf("making syslog connection for %q to %s", logTag, target) 226 writer, err := dialSyslog("tcp", target, rsyslog.LOG_DEBUG, logTag, tlsConf) 227 if err != nil { 228 return err 229 } 230 wrapLoggers = append(wrapLoggers, writer) 231 newLoggers = append(newLoggers, writer) 232 } 233 wapper := io.MultiWriter(wrapLoggers...) 234 writer := loggo.NewSimpleWriter(wapper, &loggo.DefaultFormatter{}) 235 236 loggo.RemoveWriter("syslog") 237 err = loggo.RegisterWriter("syslog", writer, loggo.TRACE) 238 if err != nil { 239 return err 240 } 241 242 // Close old targets 243 for _, j := range syslogTargets { 244 if err := j.Close(); err != nil { 245 logger.Warningf("Failed to close syslog writer: %s", err) 246 } 247 } 248 // record new targets 249 syslogTargets = newLoggers 250 return nil 251 } 252 253 func (h *RsyslogConfigHandler) Handle(_ <-chan struct{}) error { 254 cfg, err := h.st.GetRsyslogConfig(h.tag.String()) 255 if err != nil { 256 return errors.Annotate(err, "cannot get environ config") 257 } 258 259 rsyslogCACert := cfg.CACert 260 if rsyslogCACert == "" { 261 return nil 262 } 263 264 rsyslogCAKey := cfg.CAKey 265 if rsyslogCAKey == "" { 266 return nil 267 } 268 269 h.syslogConfig.Port = cfg.Port 270 if h.mode == RsyslogModeForwarding { 271 if err := writeFileAtomic(h.syslogConfig.CACertPath(), []byte(rsyslogCACert), 0644, 0, 0); err != nil { 272 return errors.Annotate(err, "cannot write CA certificate") 273 } 274 if err := h.replaceRemoteLogger(rsyslogCACert); err != nil { 275 return err 276 } 277 } else { 278 rsyslogCertPEM, rsyslogKeyPEM, err := h.rsyslogServerCerts(rsyslogCACert, rsyslogCAKey) 279 if err != nil { 280 return errors.Trace(err) 281 } 282 283 if err := writeCertificates([]certPair{ 284 {h.syslogConfig.ServerCertPath(), rsyslogCertPEM}, 285 {h.syslogConfig.ServerKeyPath(), rsyslogKeyPEM}, 286 {h.syslogConfig.CACertPath(), rsyslogCACert}, 287 }); err != nil { 288 return errors.Trace(err) 289 } 290 291 data, err := h.syslogConfig.Render() 292 if err != nil { 293 return errors.Annotate(err, "failed to render rsyslog configuration file") 294 } 295 if err := writeFileAtomic(h.syslogConfig.ConfigFilePath(), []byte(data), 0644, 0, 0); err != nil { 296 return errors.Annotate(err, "failed to write rsyslog configuration file") 297 } 298 logger.Debugf("Reloading rsyslog configuration") 299 if err := restartRsyslog(); err != nil { 300 logger.Errorf("failed to reload rsyslog configuration") 301 return errors.Annotate(err, "cannot restart rsyslog") 302 } 303 } 304 // Record config values so we don't try again. 305 // Do this last so we recover from intermittent 306 // failures. 307 h.syslogPort = cfg.Port 308 h.rsyslogCACert = rsyslogCACert 309 h.rsyslogCAKey = rsyslogCAKey 310 return nil 311 } 312 313 var lookupUser = func(username string) (uid, gid int, err error) { 314 u, err := user.Lookup(username) 315 if err != nil { 316 return -1, -1, err 317 } 318 uid, err = strconv.Atoi(u.Uid) 319 if err != nil { 320 return -1, -1, err 321 } 322 gid, err = strconv.Atoi(u.Gid) 323 if err != nil { 324 return -1, -1, err 325 } 326 return uid, gid, nil 327 } 328 329 func localIPS() ([]string, error) { 330 var ips []string 331 addrs, err := net.InterfaceAddrs() 332 if err != nil { 333 return nil, err 334 } 335 for _, j := range addrs { 336 ip, _, err := net.ParseCIDR(j.String()) 337 if err != nil { 338 return nil, err 339 } 340 if ip.IsLoopback() { 341 continue 342 } 343 ips = append(ips, ip.String()) 344 } 345 return ips, nil 346 } 347 348 func (h *RsyslogConfigHandler) rsyslogHosts() ([]string, error) { 349 var hosts []string 350 cfg, err := h.st.GetRsyslogConfig(h.tag.String()) 351 if err != nil { 352 return nil, err 353 } 354 for _, j := range cfg.HostPorts { 355 if j.Value != "" { 356 hosts = append(hosts, j.Address.Value) 357 } 358 } 359 360 // Explicitly add the '*' wildcard host. This will ensure that rsyslog 361 // clients will always be able to connect even if their hostnames and/or IPAddresses 362 // are changed. This also ensures we can continue to use SSL for our rsyslog connections 363 // and we can avoid having to use the skipVerify flag. 364 hosts = append(hosts, "*") 365 366 return hosts, nil 367 } 368 369 // ensureCA ensures that a CA certificate and key exist in state, 370 // to be picked up by all rsyslog workers in the environment. 371 func (h *RsyslogConfigHandler) ensureCA() error { 372 // We never write the CA key to local disk, so 373 // we must check state to know whether or not 374 // we need to generate new certs and keys. 375 cfg, err := h.st.GetRsyslogConfig(h.tag.String()) 376 if err != nil { 377 return errors.Annotate(err, "cannot get environ config") 378 } 379 if cfg.CACert != "" && cfg.CAKey != "" { 380 return nil 381 } 382 383 // Generate a new CA and server cert/key pairs, and 384 // publish to state. Rsyslog workers will observe 385 // this and generate certificates and keys for 386 // rsyslog in response. 387 expiry := time.Now().UTC().AddDate(10, 0, 0) 388 caCertPEM, caKeyPEM, err := cert.NewCA("rsyslog", expiry) 389 if err != nil { 390 return err 391 } 392 return h.st.SetRsyslogCert(caCertPEM, caKeyPEM) 393 } 394 395 // writeCertificates persists any certPair to disk. If any 396 // of the write attempts fail it will return an error immediately. 397 // It is up to the caller to ensure the order of pairs represents 398 // a suitable order in the case of failue. 399 func writeCertificates(pairs []certPair) error { 400 // Files must be chowned to syslog:adm. 401 syslogUid, syslogGid, err := lookupUser(syslogUser()) 402 if err != nil { 403 return err 404 } 405 406 for _, pair := range pairs { 407 if err := writeFileAtomic(pair.path, []byte(pair.data), 0600, syslogUid, syslogGid); err != nil { 408 return err 409 } 410 } 411 return nil 412 } 413 414 // rsyslogServerCerts generates new certificates for RsyslogConfigHandler 415 // using the provider caCert and caKey. This is used during the setup of the 416 // rsyslog worker as well as when handling any changes to the rsyslog configuration, 417 // usually adding and removing of state machines through ensure-availability. 418 func (h *RsyslogConfigHandler) rsyslogServerCerts(caCert, caKey string) (string, string, error) { 419 if caCert == "" { 420 return "", "", errors.New("CACert is not set") 421 } 422 if caKey == "" { 423 return "", "", errors.New("CAKey is not set") 424 } 425 426 expiry := time.Now().UTC().AddDate(10, 0, 0) 427 // Add rsyslog servers in the subjectAltName so we can 428 // successfully validate when connectiong via SSL 429 hosts, err := h.rsyslogHosts() 430 if err != nil { 431 return "", "", err 432 } 433 // Add local IPs to SAN. When connecting via IP address, 434 // the client will validate the server against any IP in 435 // the subjectAltName. We add all local ips to make sure 436 // this does not cause an error 437 ips, err := localIPS() 438 if err != nil { 439 return "", "", err 440 } 441 hosts = append(hosts, ips...) 442 return cert.NewServer(caCert, caKey, expiry, hosts) 443 } 444 445 // ensureLogrotate ensures that the logrotate 446 // configuration file and logrotate helper script 447 // exist in the log directory and creates them if they do not. 448 func (h *RsyslogConfigHandler) ensureLogrotate() error { 449 // Files must be chowned to syslog 450 syslogUid, syslogGid, err := lookupUser(syslogUser()) 451 if err != nil { 452 return err 453 } 454 455 logrotateConfPath := h.syslogConfig.LogrotateConfPath() 456 // check for the logrotate conf 457 if _, err := os.Stat(logrotateConfPath); os.IsNotExist(err) { 458 logrotateConfFile, err := h.syslogConfig.LogrotateConfFile() 459 if err != nil { 460 return err 461 } 462 // create the logrotate conf 463 if err := writeFileAtomic(logrotateConfPath, logrotateConfFile, 0600, syslogUid, syslogGid); err != nil { 464 return err 465 } 466 } else { 467 return err 468 } 469 470 logrotateHelperPath := h.syslogConfig.LogrotateHelperPath() 471 // check for the logrotate helper 472 if _, err := os.Stat(logrotateHelperPath); os.IsNotExist(err) { 473 logrotateHelperFile, err := h.syslogConfig.LogrotateHelperFile() 474 if err != nil { 475 return err 476 } 477 // create the logrotate helper 478 if err := writeFileAtomic(logrotateHelperPath, logrotateHelperFile, 0700, syslogUid, syslogGid); err != nil { 479 return err 480 } 481 } else { 482 return err 483 } 484 485 return nil 486 } 487 488 func writeFileAtomic(path string, data []byte, mode os.FileMode, uid, gid int) error { 489 chmodAndChown := func(f *os.File) error { 490 // f.Chmod() and f.Chown() are not implemented on Windows 491 // There is currently no good way of doing file permission 492 // management for Windows, directly from Go. The behavior of os.Chmod() 493 // is different from its linux implementation. 494 if runtime.GOOS == "windows" { 495 return nil 496 } 497 if err := f.Chmod(mode); err != nil { 498 return err 499 } 500 if uid != 0 { 501 if err := f.Chown(uid, gid); err != nil { 502 return err 503 } 504 } 505 return nil 506 } 507 return utils.AtomicWriteFileAndChange(path, data, chmodAndChown) 508 }