boulder/cmd/shell.go

309 lines
9.3 KiB
Go

// This package provides utilities that underlie the specific commands.
// The idea is to make the specific command files very small, e.g.:
//
// func main() {
// app := cmd.NewAppShell("command-name")
// app.Action = func(c cmd.Config) {
// // command logic
// }
// app.Run()
// }
//
// All commands share the same invocation pattern. They take a single
// parameter "-config", which is the name of a JSON file containing
// the configuration for the app. This JSON file is unmarshalled into
// a Config object, which is provided to the app.
package cmd
import (
"encoding/json"
"encoding/pem"
"errors"
_ "expvar" // For DebugServer, below.
"fmt"
"io/ioutil"
"log"
"log/syslog"
"net"
"net/http"
_ "net/http/pprof" // HTTP performance profiling, added transparently to HTTP APIs
"os"
"path"
"runtime"
"time"
"github.com/go-sql-driver/mysql"
cfsslLog "github.com/cloudflare/cfssl/log"
"github.com/codegangsta/cli"
"github.com/letsencrypt/boulder/core"
blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/metrics"
)
// AppShell contains CLI Metadata
type AppShell struct {
Action func(Config, metrics.Statter, blog.Logger)
Config func(*cli.Context, Config) Config
App *cli.App
}
// Version returns a string representing the version of boulder running.
func Version() string {
return fmt.Sprintf("0.1.0 [%s]", core.GetBuildID())
}
// NewAppShell creates a basic AppShell object containing CLI metadata
func NewAppShell(name, usage string) (shell *AppShell) {
app := cli.NewApp()
app.Name = name
app.Usage = usage
app.Version = Version()
app.Author = "Boulder contributors"
app.Email = "ca-dev@letsencrypt.org"
app.Flags = []cli.Flag{
cli.StringFlag{
Name: "config",
Value: "config.json",
EnvVar: "BOULDER_CONFIG",
Usage: "Path to Config JSON",
},
}
return &AppShell{App: app}
}
// Run begins the application context, reading config and passing
// control to the default commandline action.
func (as *AppShell) Run() {
as.App.Action = func(c *cli.Context) {
configFileName := c.GlobalString("config")
configJSON, err := ioutil.ReadFile(configFileName)
FailOnError(err, "Unable to read config file")
var config Config
err = json.Unmarshal(configJSON, &config)
FailOnError(err, "Failed to read configuration")
if as.Config != nil {
config = as.Config(c, config)
}
// Provide default values for each service's AMQP config section.
if config.WFE.AMQP == nil {
config.WFE.AMQP = config.AMQP
}
if config.CA.AMQP == nil {
config.CA.AMQP = config.AMQP
if config.CA.AMQP != nil && config.AMQP.CA != nil {
config.CA.AMQP.ServiceQueue = config.AMQP.CA.Server
}
}
if config.RA.AMQP == nil {
config.RA.AMQP = config.AMQP
if config.RA.AMQP != nil && config.AMQP.RA != nil {
config.RA.AMQP.ServiceQueue = config.AMQP.RA.Server
}
}
if config.SA.AMQP == nil {
config.SA.AMQP = config.AMQP
if config.SA.AMQP != nil && config.AMQP.SA != nil {
config.SA.AMQP.ServiceQueue = config.AMQP.SA.Server
}
}
if config.VA.AMQP == nil {
config.VA.AMQP = config.AMQP
if config.VA.AMQP != nil && config.AMQP.VA != nil {
config.VA.AMQP.ServiceQueue = config.AMQP.VA.Server
}
}
if config.Mailer.AMQP == nil {
config.Mailer.AMQP = config.AMQP
}
if config.OCSPUpdater.AMQP == nil {
config.OCSPUpdater.AMQP = config.AMQP
}
if config.OCSPResponder.AMQP == nil {
config.OCSPResponder.AMQP = config.AMQP
}
if config.Publisher.AMQP == nil {
config.Publisher.AMQP = config.AMQP
if config.Publisher.AMQP != nil && config.AMQP.Publisher != nil {
config.Publisher.AMQP.ServiceQueue = config.AMQP.Publisher.Server
}
}
stats, logger := StatsAndLogging(config.Statsd, config.Syslog)
logger.Info(as.VersionString())
// If as.Action generates a panic, this will log it to syslog.
// AUDIT[ Error Conditions ] 9cc4d537-8534-4970-8665-4b382abe82f3
defer logger.AuditPanic()
as.Action(config, stats, logger)
}
err := as.App.Run(os.Args)
FailOnError(err, "Failed to run application")
}
// mysqlLogger proxies blog.AuditLogger to provide a Print(...) method.
type mysqlLogger struct {
blog.Logger
}
func (m mysqlLogger) Print(v ...interface{}) {
m.Err(fmt.Sprintf("[mysql] %s", fmt.Sprint(v...)))
}
// cfsslLogger provides two additional methods that are expected by CFSSL's
// logger but not supported by Boulder's Logger.
type cfsslLogger struct {
blog.Logger
}
func (cl cfsslLogger) Crit(msg string) {
cl.Err(msg)
}
func (cl cfsslLogger) Emerg(msg string) {
cl.Err(msg)
}
// StatsAndLogging constructs a Statter and an AuditLogger based on its config
// parameters, and return them both. Crashes if any setup fails.
// Also sets the constructed AuditLogger as the default logger.
func StatsAndLogging(statConf StatsdConfig, logConf SyslogConfig) (metrics.Statter, blog.Logger) {
stats, err := metrics.NewStatter(statConf.Server, statConf.Prefix)
FailOnError(err, "Couldn't connect to statsd")
tag := path.Base(os.Args[0])
syslogger, err := syslog.Dial(
logConf.Network,
logConf.Server,
syslog.LOG_INFO|syslog.LOG_LOCAL0, // default, overridden by log calls
tag)
FailOnError(err, "Could not connect to Syslog")
stdoutLoglevel := int(syslog.LOG_DEBUG)
if logConf.StdoutLevel != nil {
stdoutLoglevel = *logConf.StdoutLevel
}
syslogLogLevel := int(syslog.LOG_DEBUG)
if logConf.SyslogLevel != nil {
syslogLogLevel = *logConf.SyslogLevel
}
logger, err := blog.New(syslogger, stdoutLoglevel, syslogLogLevel)
FailOnError(err, "Could not connect to Syslog")
_ = blog.Set(logger)
cfsslLog.SetLogger(cfsslLogger{logger})
_ = mysql.SetLogger(mysqlLogger{logger})
return stats, logger
}
// VersionString produces a friendly Application version string
func (as *AppShell) VersionString() string {
return fmt.Sprintf("Versions: %s=(%s %s) Golang=(%s) BuildHost=(%s)", as.App.Name, core.GetBuildID(), core.GetBuildTime(), runtime.Version(), core.GetBuildHost())
}
// FailOnError exits and prints an error message if we encountered a problem
func FailOnError(err error, msg string) {
if err != nil {
// AUDIT[ Error Conditions ] 9cc4d537-8534-4970-8665-4b382abe82f3
logger := blog.Get()
logger.Err(fmt.Sprintf("%s: %s", msg, err))
fmt.Fprintf(os.Stderr, "%s: %s\n", msg, err)
os.Exit(1)
}
}
// ProfileCmd runs forever, sending Go runtime statistics to StatsD.
func ProfileCmd(profileName string, stats metrics.Statter) {
var memoryStats runtime.MemStats
prevNumGC := int64(0)
c := time.Tick(1 * time.Second)
for range c {
runtime.ReadMemStats(&memoryStats)
// Gather goroutine count
stats.Gauge(fmt.Sprintf("%s.Gostats.Goroutines", profileName), int64(runtime.NumGoroutine()), 1.0)
// Gather various heap metrics
stats.Gauge(fmt.Sprintf("%s.Gostats.Heap.Alloc", profileName), int64(memoryStats.HeapAlloc), 1.0)
stats.Gauge(fmt.Sprintf("%s.Gostats.Heap.Objects", profileName), int64(memoryStats.HeapObjects), 1.0)
stats.Gauge(fmt.Sprintf("%s.Gostats.Heap.Idle", profileName), int64(memoryStats.HeapIdle), 1.0)
stats.Gauge(fmt.Sprintf("%s.Gostats.Heap.InUse", profileName), int64(memoryStats.HeapInuse), 1.0)
stats.Gauge(fmt.Sprintf("%s.Gostats.Heap.Released", profileName), int64(memoryStats.HeapReleased), 1.0)
// Gather various GC related metrics
if memoryStats.NumGC > 0 {
totalRecentGC := uint64(0)
realBufSize := uint32(256)
if memoryStats.NumGC < 256 {
realBufSize = memoryStats.NumGC
}
for _, pause := range memoryStats.PauseNs {
totalRecentGC += pause
}
gcPauseAvg := totalRecentGC / uint64(realBufSize)
lastGC := memoryStats.PauseNs[(memoryStats.NumGC+255)%256]
stats.Timing(fmt.Sprintf("%s.Gostats.Gc.PauseAvg", profileName), int64(gcPauseAvg), 1.0)
stats.Gauge(fmt.Sprintf("%s.Gostats.Gc.LastPause", profileName), int64(lastGC), 1.0)
}
stats.Gauge(fmt.Sprintf("%s.Gostats.Gc.NextAt", profileName), int64(memoryStats.NextGC), 1.0)
// Send both a counter and a gauge here we can much more easily observe
// the GC rate (versus the raw number of GCs) in graphing tools that don't
// like deltas
stats.Gauge(fmt.Sprintf("%s.Gostats.Gc.Count", profileName), int64(memoryStats.NumGC), 1.0)
gcInc := int64(memoryStats.NumGC) - prevNumGC
stats.Inc(fmt.Sprintf("%s.Gostats.Gc.Rate", profileName), gcInc, 1.0)
prevNumGC += gcInc
}
}
// LoadCert loads a PEM-formatted certificate from the provided path, returning
// it as a byte array, or an error if it couldn't be decoded.
func LoadCert(path string) (cert []byte, err error) {
if path == "" {
err = errors.New("Issuer certificate was not provided in config.")
return
}
pemBytes, err := ioutil.ReadFile(path)
if err != nil {
return
}
block, _ := pem.Decode(pemBytes)
if block == nil || block.Type != "CERTIFICATE" {
err = errors.New("Invalid certificate value returned")
return
}
cert = block.Bytes
return
}
// DebugServer starts a server to receive debug information. Typical
// usage is to start it in a goroutine, configured with an address
// from the appropriate configuration object:
//
// go cmd.DebugServer(c.XA.DebugAddr)
func DebugServer(addr string) {
if addr == "" {
log.Fatalf("unable to boot debug server because no address was given for it. Set debugAddr.")
}
ln, err := net.Listen("tcp", addr)
if err != nil {
log.Fatalf("unable to boot debug server on %#v", addr)
}
err = http.Serve(ln, nil)
if err != nil {
log.Fatalf("unable to boot debug server: %v", err)
}
}