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  }