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  }