mig/mig-loader/loader.go

650 строки
14 KiB
Go

// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Contributor: Aaron Meihm ameihm@mozilla.com [:alm]
// The MIG loader is a simple bootstrapping tool for MIG. It can be scheduled
// to run on a host system and download the newest available version of the
// agent. If the loader identifies a newer version of the agent available, it
// will download the required files from the API, replace the existing files,
// and notify any existing agent it should terminate.
package main
import (
"bufio"
"bytes"
"compress/gzip"
"crypto/sha256"
"encoding/json"
"flag"
"fmt"
"github.com/jvehent/cljs"
"io"
"io/ioutil"
"github.com/mozilla/mig"
"github.com/mozilla/mig/mig-agent/agentcontext"
"github.com/mozilla/mig/pgp"
"net/http"
"net/url"
"os"
"runtime"
"strings"
"sync"
)
var ctx Context
var haveChanges bool
var apiManifest *mig.ManifestResponse
var wg sync.WaitGroup
func initializeHaveBundle() (ret []mig.BundleDictionaryEntry, err error) {
defer func() {
if e := recover(); e != nil {
err = fmt.Errorf("initializeHaveBundle() -> %v", e)
}
}()
ret, err = mig.GetHostBundle()
if err != nil {
panic(err)
}
ret, err = mig.HashBundle(ret)
if err != nil {
panic(err)
}
logInfo("initialized local bundle information")
for _, x := range ret {
hv := x.SHA256
if hv == "" {
hv = "not found"
}
logInfo("%v %v -> %v", x.Name, x.Path, hv)
}
return
}
func requestManifest() (err error) {
defer func() {
if e := recover(); e != nil {
err = fmt.Errorf("requestManifest() -> %v", e)
}
}()
murl := APIURL + "manifest/agent/"
logInfo("requesting manifest from %v", murl)
mparam := mig.ManifestParameters{}
mparam.AgentIdentifier = ctx.AgentIdentifier
buf, err := json.Marshal(mparam)
if err != nil {
panic(err)
}
mstring := string(buf)
data := url.Values{"parameters": {mstring}}
r, err := http.NewRequest("POST", murl, strings.NewReader(data.Encode()))
if err != nil {
panic(err)
}
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
r.Header.Add("X-LOADERKEY", ctx.LoaderKey)
client := http.Client{}
resp, err := client.Do(r)
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
panic(err)
}
var resource *cljs.Resource
err = json.Unmarshal(body, &resource)
if err != nil {
panic(err)
}
if resp.StatusCode != http.StatusOK {
err = fmt.Errorf("HTTP %v, API call failed with error '%v' (code %s)", resp.StatusCode,
resource.Collection.Error.Message, resource.Collection.Error.Code)
panic(err)
}
// Extract our manifest from the response.
manifest, err := valueToManifest(resource.Collection.Items[0].Data[0].Value)
if err != nil {
panic(err)
}
apiManifest = &manifest
err = apiManifest.Validate()
if err != nil {
panic(err)
}
return checkManifestSignature(apiManifest, MANIFESTPGPKEYS[:])
}
func valueToManifest(v interface{}) (m mig.ManifestResponse, err error) {
b, err := json.Marshal(v)
if err != nil {
return
}
err = json.Unmarshal(b, &m)
return
}
func valueToFetchResponse(v interface{}) (m mig.ManifestFetchResponse, err error) {
b, err := json.Marshal(v)
if err != nil {
return
}
err = json.Unmarshal(b, &m)
return
}
func fetchFile(n string) (ret []byte, err error) {
defer func() {
if e := recover(); e != nil {
err = fmt.Errorf("fetchFile() -> %v", e)
}
}()
murl := APIURL + "manifest/fetch/"
logInfo("fetching file from %v", murl)
mparam := mig.ManifestParameters{}
mparam.AgentIdentifier = ctx.AgentIdentifier
mparam.Object = n
buf, err := json.Marshal(mparam)
if err != nil {
panic(err)
}
mstring := string(buf)
data := url.Values{"parameters": {mstring}}
r, err := http.NewRequest("POST", murl, strings.NewReader(data.Encode()))
if err != nil {
panic(err)
}
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
r.Header.Add("X-LOADERKEY", ctx.LoaderKey)
client := http.Client{}
resp, err := client.Do(r)
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
panic(err)
}
var resource *cljs.Resource
err = json.Unmarshal(body, &resource)
if err != nil {
panic(err)
}
// Extract fetch response.
fetchresp, err := valueToFetchResponse(resource.Collection.Items[0].Data[0].Value)
if err != nil {
panic(err)
}
// Decompress the returned file and return it as a byte slice.
b := bytes.NewBuffer(fetchresp.Data)
gz, err := gzip.NewReader(b)
if err != nil {
panic(err)
}
ret, err = ioutil.ReadAll(gz)
if err != nil {
panic(err)
}
return
}
func fetchAndReplace(entry mig.BundleDictionaryEntry, sig string) (err error) {
defer func() {
if e := recover(); e != nil {
err = fmt.Errorf("fetchAndReplace() -> %v", e)
}
}()
// Grab the new file from the API.
filebuf, err := fetchFile(entry.Name)
if err != nil {
panic(err)
}
// Stage the new file. Write the file recieved from the API to the
// file system and validate the signature of the new file to make
// sure it matches the signature from the manifest.
//
// Append .loader to the file name to use as the staged file path.
reppath := entry.Path + ".loader"
oldpath := entry.Path + ".old"
fd, err := os.OpenFile(reppath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY,
entry.Perm)
if err != nil {
panic(err)
}
_, err = fd.Write(filebuf)
if err != nil {
panic(err)
}
fd.Close()
// Validate the signature on the new file.
logInfo("validating staged file signature")
h := sha256.New()
fd, err = os.Open(reppath)
if err != nil {
panic(err)
}
buf := make([]byte, 4096)
for {
n, err := fd.Read(buf)
if err != nil {
if err == io.EOF {
break
}
fd.Close()
panic(err)
}
if n > 0 {
h.Write(buf[:n])
}
}
fd.Close()
if sig != fmt.Sprintf("%x", h.Sum(nil)) {
panic("staged file signature mismatch")
}
// Got this far, OK to proceed with the replacement.
// Rename existing file first
logInfo("renaming existing file")
os.Rename(entry.Path, oldpath)
// Replace target file
logInfo("installing staged file")
err = os.Rename(reppath, entry.Path)
if err != nil {
panic(err)
}
return
}
func checkEntry(entry mig.BundleDictionaryEntry) (err error) {
defer func() {
if e := recover(); e != nil {
err = fmt.Errorf("checkEntry() -> %v", e)
}
}()
var compare mig.ManifestEntry
logInfo("comparing %v %v", entry.Name, entry.Path)
found := false
for _, x := range apiManifest.Entries {
if x.Name == entry.Name {
compare = x
found = true
break
}
}
if !found {
logInfo("entry not in API manifest, ignoring")
return
}
hv := entry.SHA256
if hv == "" {
hv = "not found"
}
logInfo("we have %v", hv)
logInfo("they have %v", compare.SHA256)
if entry.SHA256 == compare.SHA256 {
logInfo("we have correct file, no need to replace")
return entryPermCheck(entry)
}
haveChanges = true
logInfo("refreshing %v", entry.Name)
err = fetchAndReplace(entry, compare.SHA256)
if err != nil {
panic(err)
}
return
}
// Check the file permissions set on a bundle entry and if they are
// incorrect fix the permissions
func entryPermCheck(entry mig.BundleDictionaryEntry) (err error) {
defer func() {
if e := recover(); e != nil {
err = fmt.Errorf("entryPermCheck() -> %v", e)
}
}()
var fi os.FileInfo
fi, err = os.Stat(entry.Path)
if err != nil {
panic(err)
}
if fi.Mode() != entry.Perm {
logInfo("%v has incorrect permissions, fixing", entry.Path)
err = os.Chmod(entry.Path, entry.Perm)
if err != nil {
panic(err)
}
}
return
}
// Compare the manifest that the API sent with our knowledge of what is
// currently installed. For each case there is a difference, we will
// request the new file and replace the existing entry.
func compareManifest(have []mig.BundleDictionaryEntry) (err error) {
defer func() {
if e := recover(); e != nil {
err = fmt.Errorf("compareManifest() -> %v", e)
}
}()
for _, x := range have {
err := checkEntry(x)
if err != nil {
panic(err)
}
}
return
}
func checkManifestSignature(mr *mig.ManifestResponse, keylist []string) (err error) {
defer func() {
if e := recover(); e != nil {
err = fmt.Errorf("checkManifestSignature() -> %v", e)
}
}()
var keys [][]byte
for _, pk := range keylist {
keys = append(keys, []byte(pk))
}
keyring, _, err := pgp.ArmoredKeysToKeyring(keys)
if err != nil {
panic(err)
}
cnt, err := mr.VerifySignatures(keyring)
if err != nil {
panic(err)
}
logInfo("%v valid signatures in manifest", cnt)
if cnt < REQUIREDSIGNATURES {
err = fmt.Errorf("Not enough valid signatures (got %v, need %v), rejecting",
cnt, REQUIREDSIGNATURES)
panic(err)
}
return
}
func initContext() (err error) {
defer func() {
if e := recover(); e != nil {
err = fmt.Errorf("initContext() -> %v", e)
}
}()
ctx.Logging, err = mig.InitLogger(LOGGINGCONF, "mig-loader")
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
wg.Add(1)
go func() {
var stop bool
for event := range ctx.Channels.Log {
stop, err = mig.ProcessLog(ctx.Logging, event)
if err != nil {
panic("unable to process log")
}
if stop {
break
}
}
wg.Done()
}()
logInfo("logging routine started")
ctx.LoaderKey = LOADERKEY
hints := agentcontext.AgentContextHints{
DiscoverPublicIP: DISCOVERPUBLICIP,
DiscoverAWSMeta: DISCOVERAWSMETA,
APIUrl: APIURL,
Proxies: PROXIES[:],
}
actx, err := agentcontext.NewAgentContext(ctx.Channels.Log, hints)
if err != nil {
panic(err)
}
ctx.AgentIdentifier = actx.ToAgent()
ctx.AgentIdentifier.Tags = TAGS
ctx, err = initKeyring(ctx)
if err != nil {
panic(err)
}
return
}
func logInfo(s string, args ...interface{}) {
ctx.Channels.Log <- mig.Log{Desc: fmt.Sprintf(s, args...)}.Info()
}
func logError(s string, args ...interface{}) {
ctx.Channels.Log <- mig.Log{Desc: fmt.Sprintf(s, args...)}.Err()
}
func doExit(v int) {
close(ctx.Channels.Log)
wg.Wait()
os.Exit(v)
}
// Return the path to the expected loader key file location
func getLoaderKeyfile() string {
switch runtime.GOOS {
case "linux":
return "/etc/mig/mig-loader.key"
case "darwin":
return "/etc/mig/mig-loader.key"
case "windows":
return "C:\\mig\\mig-loader.key"
}
panic("loader does not support this operating system")
return ""
}
func loaderInitializePathLinux() error {
path := os.Getenv("PATH")
if path != "" {
path = path + ":"
}
path = path + "/bin:/sbin:/usr/bin:/usr/sbin"
return os.Setenv("PATH", path)
}
func loaderInitializePathDarwin() error {
path := os.Getenv("PATH")
if path != "" {
path = path + ":"
}
path = path + "/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin"
return os.Setenv("PATH", path)
}
func loaderInitializePathWindows() error {
path := os.Getenv("PATH")
if path != "" {
path = path + ";"
}
path = path + "C:\\mig"
return os.Setenv("PATH", path)
}
func loaderInitializePath() error {
switch runtime.GOOS {
case "linux":
return loaderInitializePathLinux()
case "darwin":
return loaderInitializePathDarwin()
case "windows":
return loaderInitializePathWindows()
}
return fmt.Errorf("loader does not support this operating system")
}
// Attempt to obtain the loader key from the file system and override the
// built-in secret
func loadLoaderKey() error {
fd, err := os.Open(getLoaderKeyfile())
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
defer fd.Close()
buf, _, err := bufio.NewReader(fd).ReadLine()
if err != nil {
// Nothing in the loader key file
if err == io.EOF {
return nil
}
return err
}
// Also trim any leading and trailing spaces from the loader key
LOADERKEY = strings.Trim(string(buf), " ")
err = mig.ValidateLoaderPrefixAndKey(LOADERKEY)
if err != nil {
return err
}
ctx.LoaderKey = LOADERKEY
return nil
}
func main() {
var (
initialMode bool
runService bool
confPath string
checkOnly bool
err error
)
runtime.GOMAXPROCS(1)
flag.BoolVar(&checkOnly, "c", false, "only check if agent is running")
flag.StringVar(&confPath, "f", configDefault(), "Load configuration from file")
flag.BoolVar(&initialMode, "i", false, "initialization mode")
flag.BoolVar(&runService, "s", false, "persistent service mode")
flag.Parse()
ctx.Channels.Log = make(chan mig.Log, 37)
err = configLoad(confPath)
if err != nil {
logInfo("warning: unable to load configuration from %v, using built-in configuration", confPath)
} else {
logInfo("using external configuration from %v", confPath)
}
if runService {
err = serviceMode()
if err != nil {
logError("%v", err)
}
doExit(0)
}
err = initContext()
if err != nil {
logError("%v", err)
doExit(1)
}
err = loaderInitializePath()
if err != nil {
logError("%v", err)
doExit(1)
}
err = loadLoaderKey()
if err != nil {
logError("%v", err)
doExit(1)
}
// Do any service installation that might be required for this platform
if initialMode {
err = serviceDeploy()
if err != nil {
logError("%v", err)
doExit(1)
}
} else if checkOnly {
err = agentRunning()
if err != nil {
logInfo("agent does not appear to be running, trying to start")
err = runTriggers()
if err != nil {
logError("%v", err)
doExit(1)
}
} else {
logInfo("agent looks like it is running")
}
doExit(0)
}
// Get our current status from the file system.
have, err := initializeHaveBundle()
if err != nil {
logError("%v", err)
doExit(1)
}
// Retrieve our manifest from the API.
err = requestManifest()
if err != nil {
logError("%v", err)
doExit(1)
}
err = compareManifest(have)
if err != nil {
logError("%v", err)
doExit(1)
}
if haveChanges {
err = runTriggers()
if err != nil {
logError("%v", err)
doExit(1)
}
} else {
// If we don't have changes, just validate the agent is running,
// if it is not we will also execute the triggers to try to
// bump it.
err = agentRunning()
if err != nil {
logInfo("agent does not appear to be running, trying to start")
err = runTriggers()
if err != nil {
logError("%v", err)
doExit(1)
}
} else {
logInfo("agent looks like it is running")
}
}
doExit(0)
}