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