Skip to content
Permalink
Browse files

perkeepd, serverinit, gce: opaque-ify serverinit.Config, trim camlist…

…ored.go

This change has two major parts, which were interwoven enough to do
them in one change:

1) make serverinit.Config fully opaque, in prep for TOML configs #1134

2) shrink the massive server/perkeepd/camlistored.go file. It was out
   of control and had a bunch of code that better belonged
   elsewhere. This change moves a few hundred lines of code from
   camlistored.go into more logical places: internal/osutil/gce for
   GCE stuff, serverinit for config stuff (KeyRingAndId), etc.

I also added a TODO to make it possible to compile perkeepd without
any GCE stuff, which I saw as a possible and worthy goal only after
moving everything away.

Updates #1134

Change-Id: Iea6f84c5aca9c70b97806f4a201ec35e0f630e3b
  • Loading branch information
bradfitz committed May 13, 2018
1 parent 3b48afb commit f3f38f0c76a9bfc13aafe9a696cff9ae98c0f0c2
9 TODO
@@ -4,6 +4,15 @@ There are two TODO lists. This file (good for airplanes) and the online bug trac

Offline list:

-- add a build tag to allow perkeepd to be compiled without any GCE
support. (good for smaller binaries on Raspberry Pis or
whatnot). Most code has moved to the osutil/gce package, and
there's little gce usage now in perkeepd except for 2-3 things
bethind "if env.OnGCE" checks. those things inside the checks can
be func pointers registered by a file with a +build gce or
!without_gce, depending on which way we decide to go. But make sure
our test coverage builds both.

-- fix the presubmit's gofmt to be happy about emacs:

go fmt perkeep.org/cmd... perkeep.org/dev... perkeep.org/misc... perkeep.org/pkg... perkeep.org/server...
@@ -71,7 +71,8 @@ func (c *reindexdpCmd) RunCommand(args []string) error {
if err != nil {
return err
}
prefixes, ok := cfg.Obj["prefixes"].(map[string]interface{})
low := cfg.LowLevelJSONConfig()
prefixes, ok := low["prefixes"].(map[string]interface{})
if !ok {
return fmt.Errorf("No 'prefixes' object in low-level (or converted) config file %s", osutil.UserServerConfigPath())
}
@@ -57,11 +57,10 @@ func (c *dumpconfigCmd) RunCommand(args []string) error {
if err != nil {
return err
}
cfg.Obj["handlerConfig"] = true
ll, err := json.MarshalIndent(cfg.Obj, "", " ")
lowj, err := json.MarshalIndent(cfg.LowLevelJSONConfig(), "", " ")
if err != nil {
return err
}
_, err = os.Stdout.Write(ll)
_, err = os.Stdout.Write(lowj)
return err
}
@@ -25,8 +25,13 @@ import (
"log"
"os"
"path"
"strconv"
"strings"
"time"

"golang.org/x/oauth2/google"
compute "google.golang.org/api/compute/v1"
"google.golang.org/api/googleapi"
"perkeep.org/internal/osutil"
"perkeep.org/pkg/env"

@@ -138,3 +143,326 @@ func LogWriter() (w io.WriteCloser, err error) {
closer: logc,
}, nil
}

type gceInst struct {
cs *compute.Service
cis *compute.InstancesService
zone string
projectID string
name string
}

func gceInstance() (*gceInst, error) {
ctx := context.Background()
hc, err := google.DefaultClient(ctx)
if err != nil {
return nil, fmt.Errorf("error getting a default http client: %v", err)
}
cs, err := compute.New(hc)
if err != nil {
return nil, fmt.Errorf("error getting a compute service: %v", err)
}
cis := compute.NewInstancesService(cs)
projectID, err := metadata.ProjectID()
if err != nil {
return nil, fmt.Errorf("error getting projectID: %v", err)
}
zone, err := metadata.Zone()
if err != nil {
return nil, fmt.Errorf("error getting zone: %v", err)
}
name, err := metadata.InstanceName()
if err != nil {
return nil, fmt.Errorf("error getting instance name: %v", err)
}
return &gceInst{
cs: cs,
cis: cis,
zone: zone,
projectID: projectID,
name: name,
}, nil
}

// resetInstance reboots the GCE VM that this process is running in.
func resetInstance() error {
if !env.OnGCE() {
return errors.New("cannot reset instance if not on GCE")
}

ctx := context.Background()

inst, err := gceInstance()
if err != nil {
return err
}
cs, projectID, zone, name := inst.cis, inst.projectID, inst.zone, inst.name

call := cs.Reset(projectID, zone, name).Context(ctx)
op, err := call.Do()
if err != nil {
if googleapi.IsNotModified(err) {
return nil
}
return fmt.Errorf("error resetting instance: %v", err)
}
// TODO(mpl): refactor this whole pattern below into a func
opName := op.Name
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(500 * time.Millisecond):
}
op, err := inst.cs.ZoneOperations.Get(projectID, zone, opName).Context(ctx).Do()
if err != nil {
return fmt.Errorf("failed to get op %s: %v", opName, err)
}
switch op.Status {
case "PENDING", "RUNNING":
continue
case "DONE":
if op.Error != nil {
for _, operr := range op.Error.Errors {
log.Printf("operation error: %+v", operr)
}
return fmt.Errorf("operation error: %v", op.Error.Errors[0])
}
log.Print("Successfully reset instance")
return nil
default:
return fmt.Errorf("unknown operation status %q: %+v", op.Status, op)
}
}
}

// SetInstanceHostname sets the "camlistore-hostname" metadata on the GCE
// instance where perkeepd is running. The value set is the same as the one we
// register with the camlistore.net DNS, i.e. "<gpgKeyId>.camlistore.net", where
// <gpgKeyId> is the short form (8 trailing chars) of Perkeep's keyId.
func SetInstanceHostname(camliNetHostName string) error {
if !env.OnGCE() {
return nil
}

hostname, err := metadata.InstanceAttributeValue("camlistore-hostname")
if err != nil {
if _, ok := err.(metadata.NotDefinedError); !ok {
return fmt.Errorf("error getting existing camlistore-hostname: %v", err)
}
}
if err == nil && hostname != "" {
// we do not overwrite an existing value. it's not possible anyway, as the
// SetMetadata call won't allow it.
return nil
}

ctx := context.Background()
inst, err := gceInstance()
if err != nil {
return err
}
cs, projectID, zone, name := inst.cis, inst.projectID, inst.zone, inst.name

instance, err := cs.Get(projectID, zone, name).Context(ctx).Do()
if err != nil {
return fmt.Errorf("error getting instance: %v", err)
}
items := instance.Metadata.Items
items = append(items, &compute.MetadataItems{
Key: "camlistore-hostname",
Value: googleapi.String(camliNetHostName),
})
mdata := &compute.Metadata{
Items: items,
Fingerprint: instance.Metadata.Fingerprint,
}

call := cs.SetMetadata(projectID, zone, name, mdata).Context(ctx)
op, err := call.Do()
if err != nil {
if googleapi.IsNotModified(err) {
return nil
}
return fmt.Errorf("error setting instance hostname: %v", err)
}
// TODO(mpl): refactor this whole pattern below into a func
opName := op.Name
for {
// TODO(mpl): add a timeout maybe?
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(500 * time.Millisecond):
}
op, err := inst.cs.ZoneOperations.Get(projectID, zone, opName).Context(ctx).Do()
if err != nil {
return fmt.Errorf("failed to get op %s: %v", opName, err)
}
switch op.Status {
case "PENDING", "RUNNING":
continue
case "DONE":
if op.Error != nil {
for _, operr := range op.Error.Errors {
log.Printf("operation error: %+v", operr)
}
return fmt.Errorf("operation error: %v", op.Error.Errors[0])
}
log.Printf(`Successfully set "camlistore-hostname" to "%v" on instance`, camliNetHostName)
return nil
default:
return fmt.Errorf("unknown operation status %q: %+v", op.Status, op)
}
}
}

func exitf(pattern string, args ...interface{}) {
log.SetOutput(os.Stderr)
log.SetFlags(0)
log.Fatalf(pattern, args...)
}

// FixUserDataForPerkeepRename checks whether the value of "user-data"
// in the GCE metadata is up to date with the correct systemd service
// and docker image tarball based on the "perkeep" name. If not
// (i.e. they're the old "camlistore" based ones), it fixes said
// metadata. It returns whether the metadata was indeed changed, which
// indicates that the instance should be restarted for the change to
// take effect.
func FixUserDataForPerkeepRename() {
needsRestart, err := fixUserDataForPerkeepRename()
if err != nil {
exitf("Could not fix GCE user-data metadata: %v", err)
}
if needsRestart {
if err := resetInstance(); err != nil {
exitf("Could not reset instance: %v", err)
}
}
}

func fixUserDataForPerkeepRename() (needsRestart bool, err error) {
if !env.OnGCE() {
return false, nil
}

metadataKey := "user-data"

userData, err := metadata.InstanceAttributeValue(metadataKey)
if err != nil {
if _, ok := err.(metadata.NotDefinedError); !ok {
return false, fmt.Errorf("error getting existing user-data: %v", err)
}
}

goodExecStartPre := `ExecStartPre=/bin/bash -c '/usr/bin/curl https://storage.googleapis.com/camlistore-release/docker/perkeepd.tar.gz`
goodExecStart := `ExecStart=/opt/bin/systemd-docker run --rm -p 80:80 -p 443:443 --name %n -v /run/camjournald.sock:/run/camjournald.sock -v /var/lib/camlistore/tmp:/tmp --link=mysql.service:mysqldb perkeep/server`
goodServiceName := `- name: perkeepd.service`
if strings.Contains(userData, goodExecStartPre) &&
strings.Contains(userData, goodExecStart) &&
strings.Contains(userData, goodServiceName) {
// We're already a proper perkeep deployment, all good.
return false, nil
}

oldExecStartPre := `ExecStartPre=/bin/bash -c '/usr/bin/curl https://storage.googleapis.com/camlistore-release/docker/camlistored.tar.gz`
oldExecStart := `ExecStart=/opt/bin/systemd-docker run --rm -p 80:80 -p 443:443 --name %n -v /run/camjournald.sock:/run/camjournald.sock -v /var/lib/camlistore/tmp:/tmp --link=mysql.service:mysqldb camlistore/server`

// double-check that it's our launcher based instance, and not a custom thing,
// even though OnGCE is already a pretty strong barrier.
if !strings.Contains(userData, oldExecStartPre) {
return false, nil
}

oldServiceName := `- name: camlistored.service`
userData = strings.Replace(userData, oldExecStartPre, goodExecStartPre, 1)
userData = strings.Replace(userData, oldExecStart, goodExecStart, 1)
userData = strings.Replace(userData, oldServiceName, goodServiceName, 1)

ctx := context.Background()
inst, err := gceInstance()
if err != nil {
return false, err
}
cs, projectID, zone, name := inst.cis, inst.projectID, inst.zone, inst.name

instance, err := cs.Get(projectID, zone, name).Context(ctx).Do()
if err != nil {
return false, fmt.Errorf("error getting instance: %v", err)
}
items := instance.Metadata.Items
for k, v := range items {
if v.Key == metadataKey {
items[k] = &compute.MetadataItems{
Key: metadataKey,
Value: googleapi.String(userData),
}
break
}
}
mdata := &compute.Metadata{
Items: items,
Fingerprint: instance.Metadata.Fingerprint,
}

call := cs.SetMetadata(projectID, zone, name, mdata).Context(ctx)
op, err := call.Do()
if err != nil {
if googleapi.IsNotModified(err) {
return false, nil
}
return false, fmt.Errorf("error setting instance user-data: %v", err)
}
// TODO(mpl): refactor this whole pattern below into a func
opName := op.Name
for {
select {
case <-ctx.Done():
return false, ctx.Err()
case <-time.After(500 * time.Millisecond):
}
op, err := inst.cs.ZoneOperations.Get(projectID, zone, opName).Context(ctx).Do()
if err != nil {
return false, fmt.Errorf("failed to get op %s: %v", opName, err)
}
switch op.Status {
case "PENDING", "RUNNING":
continue
case "DONE":
if op.Error != nil {
for _, operr := range op.Error.Errors {
log.Printf("operation error: %+v", operr)
}
return false, fmt.Errorf("operation error: %v", op.Error.Errors[0])
}
log.Printf("Successfully corrected %v on instance", metadataKey)
return true, nil
default:
return false, fmt.Errorf("unknown operation status %q: %+v", op.Status, op)
}
}
}

// BlobpackedRecoveryValue returns the blobpacked recovery level (0, 1, 2) from
// the GCE instance metadata.
//
// The return type here is logically a blobpacked.RecoveryMode, but we're not
// importing that here to save a dependency.
func BlobpackedRecoveryValue() int {
recovery, err := metadata.InstanceAttributeValue("camlistore-recovery")
if err != nil {
if _, ok := err.(metadata.NotDefinedError); !ok {
log.Printf("error getting camlistore-recovery: %v", err)
}
return 0
}
if recovery == "" {
return 0
}
mode, err := strconv.Atoi(recovery)
if err != nil {
log.Printf("invalid int value for \"camlistore-recovery\": %v", err)
}
return mode
}

0 comments on commit f3f38f0

Please sign in to comment.
You can’t perform that action at this time.