mig/mig-loader/loader.go

407 строки
8.9 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 (
"bytes"
"compress/gzip"
"crypto/sha256"
"encoding/json"
"fmt"
"github.com/jvehent/cljs"
"io"
"io/ioutil"
"mig.ninja/mig"
"mig.ninja/mig/mig-agent/agentcontext"
"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)
}
ctx.Channels.Log <- mig.Log{Desc: "initialized local bundle information"}
for _, x := range ret {
hv := x.SHA256
if hv == "" {
hv = "not found"
}
ctx.Channels.Log <- mig.Log{Desc: fmt.Sprintf("%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"
ctx.Channels.Log <- mig.Log{Desc: fmt.Sprintf("requesting manifest from %v", murl)}
mparam := mig.ManifestParameters{}
mparam.LoaderKey = "secret"
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")
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 our manifest from the response.
manifest, err := valueToManifest(resource.Collection.Items[0].Data[0].Value)
if err != nil {
panic(err)
}
apiManifest = &manifest
return
}
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"
ctx.Channels.Log <- mig.Log{Desc: fmt.Sprintf("fetching file from %v", murl)}
mparam := mig.ManifestParameters{}
mparam.AgentIdentifier = ctx.AgentIdentifier
mparam.Object = n
mparam.LoaderKey = "secret"
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")
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"
fd, err := os.OpenFile(reppath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0700)
if err != nil {
panic(err)
}
_, err = fd.Write(filebuf)
if err != nil {
panic(err)
}
fd.Close()
// Validate the signature on the new file.
ctx.Channels.Log <- mig.Log{Desc: "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.
ctx.Channels.Log <- mig.Log{Desc: "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
ctx.Channels.Log <- mig.Log{Desc: fmt.Sprintf("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 {
ctx.Channels.Log <- mig.Log{Desc: "entry not in API manifest, ignoring"}
return
}
hv := entry.SHA256
if hv == "" {
hv = "not found"
}
ctx.Channels.Log <- mig.Log{Desc: fmt.Sprintf("we have %v", hv)}
ctx.Channels.Log <- mig.Log{Desc: fmt.Sprintf("they have %v", compare.SHA256)}
if entry.SHA256 == compare.SHA256 {
ctx.Channels.Log <- mig.Log{Desc: "nothing to do here"}
return
}
haveChanges = true
ctx.Channels.Log <- mig.Log{Desc: fmt.Sprintf("refreshing %v", entry.Name)}
err = fetchAndReplace(entry, compare.SHA256)
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 initContext() (ctx Context, err error) {
defer func() {
if e := recover(); e != nil {
err = fmt.Errorf("initContext() -> %v", e)
}
}()
ctx.Channels.Log = make(chan mig.Log, 37)
ctx.Logging.Level = "debug"
ctx.Logging.Mode = "stdout"
ctx.Logging, err = mig.InitLogger(ctx.Logging, "mig-loader")
if err != nil {
panic(err)
}
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()
return
}
func doExit(v int) {
close(ctx.Channels.Log)
wg.Wait()
os.Exit(v)
}
func main() {
var err error
runtime.GOMAXPROCS(1)
ctx, err = initContext()
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()
}()
ctx.Channels.Log <- mig.Log{Desc: "logging routine started"}
// Get our current status from the file system.
have, err := initializeHaveBundle()
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
doExit(1)
}
// Retrieve our manifest from the API.
err = requestManifest()
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
doExit(1)
}
err = compareManifest(have)
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
doExit(1)
}
if haveChanges {
err = runTriggers()
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
doExit(1)
}
}
doExit(0)
}