This commit is contained in:
Jeffrey Paul 2019-11-05 15:32:09 -08:00
parent d33f093ab5
commit 1c7e2f11e0
11 changed files with 279 additions and 180 deletions

View File

@ -3,6 +3,7 @@ FROM golang:1.13 as builder
WORKDIR /go/src/github.com/sneak/feta WORKDIR /go/src/github.com/sneak/feta
COPY . . COPY . .
#RUN make lint && make build
RUN make build RUN make build
WORKDIR /go WORKDIR /go

View File

@ -25,10 +25,10 @@ ifneq ($(UNAME_S),Darwin)
GOFLAGS = -ldflags "-linkmode external -extldflags -static $(GOLDFLAGS)" GOFLAGS = -ldflags "-linkmode external -extldflags -static $(GOLDFLAGS)"
endif endif
default: run default: rundebug
rundebug: build rundebug: build
DEBUG=1 ./$(FN) GOTRACEBACK=all DEBUG=1 ./$(FN)
run: build run: build
./$(FN) ./$(FN)
@ -38,9 +38,13 @@ clean:
build: ./$(FN) build: ./$(FN)
lint:
go get -u golang.org/x/lint/golint
go get -u github.com/GeertJohan/fgt
fgt golint
go-get: go-get:
go get -v go get -v
cd cmd/$(FN) && go get -v
./$(FN): *.go cmd/*/*.go go-get ./$(FN): *.go cmd/*/*.go go-get
cd cmd/$(FN) && go build -o ../../$(FN) $(GOFLAGS) . cd cmd/$(FN) && go build -o ../../$(FN) $(GOFLAGS) .
@ -59,7 +63,7 @@ build-docker-image: clean
build-docker-image-dist: is_uncommitted clean build-docker-image-dist: is_uncommitted clean
docker build -t $(IMAGENAME):$(VERSION) -t $(IMAGENAME):latest -t $(IMAGENAME):$(BUILDTIMETAG) . docker build -t $(IMAGENAME):$(VERSION) -t $(IMAGENAME):latest -t $(IMAGENAME):$(BUILDTIMETAG) .
dist: build-docker-image dist: lint build-docker-image
-mkdir -p ./output -mkdir -p ./output
docker run --rm --entrypoint cat $(IMAGENAME) /bin/$(FN) > output/$(FN) docker run --rm --entrypoint cat $(IMAGENAME) /bin/$(FN) > output/$(FN)
docker save $(IMAGENAME) | bzip2 > output/$(BUILDTIMEFILENAME).$(FN).tbz2 docker save $(IMAGENAME) | bzip2 > output/$(BUILDTIMEFILENAME).$(FN).tbz2

76
apihandlers.go Normal file
View File

@ -0,0 +1,76 @@
package feta
import "time"
import "net/http"
import "encoding/json"
import "github.com/gin-gonic/gin"
type hash map[string]interface{}
func (a *TootArchiverAPIServer) instances() []hash {
resp := make([]hash, 0)
now := time.Now()
for _, v := range a.archiver.manager.listInstances() {
i := make(hash)
// FIXME figure out why a very short lock here deadlocks
v.Lock()
i["hostname"] = v.hostname
i["nextCheck"] = v.nextFetch.UTC().Format(time.RFC3339)
i["nextCheckAfter"] = (-1 * now.Sub(v.nextFetch)).String()
i["successCount"] = v.successCount
i["errorCount"] = v.errorCount
i["identified"] = v.identified
i["status"] = v.status
i["software"] = "unknown"
i["version"] = "unknown"
if v.identified {
i["software"] = v.serverImplementationString
i["version"] = v.serverVersionString
}
v.Unlock()
resp = append(resp, i)
}
return resp
}
func (a *TootArchiverAPIServer) getIndexHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
index := &gin.H{
"page": "index",
"instances": a.instances(),
"status": "ok",
"now": time.Now().UTC().Format(time.RFC3339),
"uptime": a.archiver.Uptime().String(),
}
json, err := json.Marshal(index)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(json)
}
}
func (a *TootArchiverAPIServer) getHealthCheckHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
resp := &gin.H{
"status": "ok",
"now": time.Now().UTC().Format(time.RFC3339),
"uptime": a.archiver.Uptime().String(),
}
json, err := json.Marshal(resp)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(json)
}
}

View File

@ -43,30 +43,8 @@ func (a *TootArchiverAPIServer) getRouter() *gin.Engine {
// attach logger middleware // attach logger middleware
r.Use(ginzerolog.Logger("gin")) r.Use(ginzerolog.Logger("gin"))
r.GET("/.well-known/healthcheck.json", func(c *gin.Context) { r.GET("/.well-known/healthcheck.json", gin.WrapF(a.getHealthCheckHandler()))
c.JSON(200, gin.H{ r.GET("/", gin.WrapF(a.getIndexHandler()))
"status": "ok",
"now": time.Now().UTC().Format(time.RFC3339),
"uptime": a.archiver.Uptime().String(),
})
})
r.GET("/", func(c *gin.Context) {
ir := a.archiver.manager.instanceSummaryReport()
il := a.archiver.manager.instanceListForApi()
c.JSON(200, gin.H{
"status": "ok",
"now": time.Now().UTC().Format(time.RFC3339),
"uptime": a.archiver.Uptime().String(),
"instanceSummary": gin.H{
"total": ir.total,
"up": ir.up,
"identified": ir.identified,
},
"instanceList": il,
})
})
return r return r
} }

View File

@ -26,7 +26,6 @@ func (a *TootArchiver) RunForever() {
newInstanceHostnameNotifications := make(chan InstanceHostname, 10000) newInstanceHostnameNotifications := make(chan InstanceHostname, 10000)
a.locator = NewInstanceLocator() a.locator = NewInstanceLocator()
a.manager = NewInstanceManager() a.manager = NewInstanceManager()
a.locator.AddInstanceNotificationChannel(newInstanceHostnameNotifications) a.locator.AddInstanceNotificationChannel(newInstanceHostnameNotifications)

View File

@ -1,36 +0,0 @@
package main
import (
"github.com/rs/zerolog/log"
)
var Version string
var Buildtime string
var Builduser string
var Buildarch string
type AppIdentity struct {
Version string
Buildtime string
Builduser string
Buildarch string
}
func GetAppIdentity() *AppIdentity {
i := new(AppIdentity)
i.Version = Version
i.Buildtime = Buildtime
i.Builduser = Builduser
i.Buildarch = Buildarch
return i
}
func identify() {
i := GetAppIdentity()
log.Info().
Str("version", i.Version).
Str("buildarch", i.Buildarch).
Str("buildtime", i.Buildtime).
Str("builduser", i.Builduser).
Msg("starting")
}

View File

@ -1,57 +1,15 @@
package main package main
import "os" import "os"
import "sync"
import "time"
import "github.com/rs/zerolog"
import "github.com/rs/zerolog/log"
import "golang.org/x/crypto/ssh/terminal"
import "github.com/sneak/feta" import "github.com/sneak/feta"
// these are filled in at link-time by the build scripts
var Version string
var Buildtime string
var Builduser string
var Buildarch string
func main() { func main() {
os.Exit(app()) os.Exit(feta.CLIEntry(Version, Buildtime, Builduser, Buildarch))
}
func app() int {
log.Logger = log.With().Caller().Logger()
if terminal.IsTerminal(int(os.Stdout.Fd())) {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
}
identify()
// always log in UTC
zerolog.TimestampFunc = func() time.Time {
return time.Now().UTC()
}
zerolog.SetGlobalLevel(zerolog.InfoLevel)
if os.Getenv("DEBUG") != "" {
zerolog.SetGlobalLevel(zerolog.DebugLevel)
}
archiver := feta.NewTootArchiver()
api := new(feta.TootArchiverAPIServer)
api.SetArchiver(archiver)
var wg sync.WaitGroup
// start api webserver goroutine
wg.Add(1)
go func() {
api.Serve()
wg.Done()
}()
wg.Add(1)
go func() {
archiver.RunForever()
wg.Done()
}()
wg.Wait()
return 0
} }

91
feta.go Normal file
View File

@ -0,0 +1,91 @@
package feta
import "os"
//import "os/signal"
import "sync"
//import "syscall"
import "time"
import "github.com/rs/zerolog"
import "github.com/rs/zerolog/log"
import "golang.org/x/crypto/ssh/terminal"
func CLIEntry(version string, buildtime string, buildarch string, builduser string) int {
f := new(FetaProcess)
f.version = version
f.buildtime = buildtime
f.buildarch = buildarch
f.builduser = builduser
f.setupLogging()
return f.runForever()
}
// FetaProcess is the main structure/process of this app
type FetaProcess struct {
version string
buildtime string
buildarch string
builduser string
archiver *TootArchiver
api *TootArchiverAPIServer
wg *sync.WaitGroup
//quit chan os.Signal
}
func (f *FetaProcess) identify() {
log.Info().
Str("version", f.version).
Str("buildtime", f.buildtime).
Str("buildarch", f.buildarch).
Str("builduser", f.builduser).
Msg("starting")
}
func (f *FetaProcess) setupLogging() {
log.Logger = log.With().Caller().Logger()
if terminal.IsTerminal(int(os.Stdout.Fd())) {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
}
// always log in UTC
zerolog.TimestampFunc = func() time.Time {
return time.Now().UTC()
}
zerolog.SetGlobalLevel(zerolog.InfoLevel)
if os.Getenv("DEBUG") != "" {
zerolog.SetGlobalLevel(zerolog.DebugLevel)
}
f.identify()
}
func (f *FetaProcess) runForever() int {
f.archiver = NewTootArchiver()
f.api = new(TootArchiverAPIServer)
f.api.SetArchiver(f.archiver)
//FIXME(sneak) get this channel into places that need to be shut down
//f.quit = make(chan os.Signal)
//signal.Notify(f.quit, syscall.SIGINT, syscall.SIGTERM)
f.wg = new(sync.WaitGroup)
// start api webserver goroutine
f.wg.Add(1)
go func() {
f.api.Serve()
f.wg.Done()
}()
f.wg.Add(1)
go func() {
f.archiver.RunForever()
f.wg.Done()
}()
f.wg.Wait()
return 0
}

View File

@ -14,6 +14,8 @@ import "github.com/rs/zerolog/log"
const NodeInfoSchemaVersionTwoName = "http://nodeinfo.diaspora.software/ns/schema/2.0" const NodeInfoSchemaVersionTwoName = "http://nodeinfo.diaspora.software/ns/schema/2.0"
const INSTANCE_NODEINFO_TIMEOUT = time.Second * 5
const INSTANCE_HTTP_TIMEOUT = time.Second * 60 const INSTANCE_HTTP_TIMEOUT = time.Second * 60
const INSTANCE_SPIDER_INTERVAL = time.Second * 60 const INSTANCE_SPIDER_INTERVAL = time.Second * 60
@ -40,25 +42,27 @@ const (
type Instance struct { type Instance struct {
sync.RWMutex sync.RWMutex
errorCount uint errorCount uint
successCount uint successCount uint
highestId int highestId int
hostname string hostname string
identified bool identified bool
fetching bool fetching bool
impl InstanceImplementation implementation InstanceImplementation
backend *InstanceBackend backend *InstanceBackend
status InstanceStatus status InstanceStatus
nextFetch time.Time nextFetch time.Time
nodeInfoUrl string nodeInfoUrl string
serverVersion string serverVersionString string
serverImplementationString string
fetchingLock sync.Mutex
} }
func NewInstance(hostname InstanceHostname) *Instance { func NewInstance(hostname InstanceHostname) *Instance {
self := new(Instance) self := new(Instance)
self.hostname = string(hostname) self.hostname = string(hostname)
self.status = InstanceStatusUnknown self.status = InstanceStatusUnknown
self.nextFetch = time.Now().Add(-1 * time.Second) self.setNextFetchAfter(1 * time.Second)
return self return self
} }
@ -75,6 +79,9 @@ func (self *Instance) setNextFetchAfter(d time.Duration) {
} }
func (self *Instance) Fetch() { func (self *Instance) Fetch() {
self.fetchingLock.Lock()
defer self.fetchingLock.Unlock()
err := self.detectNodeTypeIfNecessary() err := self.detectNodeTypeIfNecessary()
if err != nil { if err != nil {
self.setNextFetchAfter(INSTANCE_ERROR_INTERVAL) self.setNextFetchAfter(INSTANCE_ERROR_INTERVAL)
@ -85,8 +92,8 @@ func (self *Instance) Fetch() {
return return
} }
//self.setNextFetchAfter(INSTANCE_SPIDER_INTERVAL) self.setNextFetchAfter(INSTANCE_SPIDER_INTERVAL)
log.Info().Msgf("i (%s) should check for toots", self.hostname) //log.Info().Msgf("i (%s) IS NOW READY FOR FETCH", self.hostname)
} }
func (self *Instance) dueForFetch() bool { func (self *Instance) dueForFetch() bool {
@ -99,7 +106,7 @@ func (self *Instance) dueForFetch() bool {
func (self *Instance) nodeIdentified() bool { func (self *Instance) nodeIdentified() bool {
self.RLock() self.RLock()
defer self.RUnlock() defer self.RUnlock()
if self.impl > Unknown { if self.implementation > Unknown {
return true return true
} }
return false return false
@ -127,7 +134,7 @@ func (self *Instance) registerSuccess() {
self.successCount++ self.successCount++
} }
func (self *Instance) ApiReport() *gin.H { func (self *Instance) APIReport() *gin.H {
r := gin.H{} r := gin.H{}
return &r return &r
} }
@ -141,7 +148,7 @@ func (i *Instance) Up() bool {
func (i *Instance) fetchNodeInfoURL() error { func (i *Instance) fetchNodeInfoURL() error {
url := fmt.Sprintf("https://%s/.well-known/nodeinfo", i.hostname) url := fmt.Sprintf("https://%s/.well-known/nodeinfo", i.hostname)
var c = &http.Client{ var c = &http.Client{
Timeout: INSTANCE_HTTP_TIMEOUT, Timeout: INSTANCE_NODEINFO_TIMEOUT,
} }
log.Debug(). log.Debug().
@ -217,7 +224,7 @@ func (i *Instance) fetchNodeInfo() error {
} }
var c = &http.Client{ var c = &http.Client{
Timeout: INSTANCE_HTTP_TIMEOUT, Timeout: INSTANCE_NODEINFO_TIMEOUT,
} }
//FIXME make sure the nodeinfourl is on the same domain as the instance //FIXME make sure the nodeinfourl is on the same domain as the instance
@ -268,8 +275,8 @@ func (i *Instance) fetchNodeInfo() error {
Msg("received nodeinfo from instance") Msg("received nodeinfo from instance")
i.Lock() i.Lock()
defer i.Unlock() i.serverVersionString = ni.Software.Version
i.serverVersion = ni.Software.Version i.serverImplementationString = ni.Software.Name
ni.Software.Name = strings.ToLower(ni.Software.Name) ni.Software.Name = strings.ToLower(ni.Software.Name)
@ -278,22 +285,26 @@ func (i *Instance) fetchNodeInfo() error {
Str("hostname", i.hostname). Str("hostname", i.hostname).
Str("software", ni.Software.Name). Str("software", ni.Software.Name).
Msg("detected server software") Msg("detected server software")
i.registerSuccess()
i.identified = true i.identified = true
i.impl = Pleroma i.implementation = Pleroma
i.status = InstanceStatusIdentified i.status = InstanceStatusIdentified
i.Unlock()
i.registerSuccess()
return nil return nil
} else if ni.Software.Name == "mastodon" { } else if ni.Software.Name == "mastodon" {
i.registerSuccess() i.registerSuccess()
i.identified = true i.identified = true
i.impl = Mastodon i.implementation = Mastodon
i.status = InstanceStatusIdentified i.status = InstanceStatusIdentified
i.Unlock()
i.registerSuccess()
return nil return nil
} else { } else {
log.Error(). log.Error().
Str("hostname", i.hostname). Str("hostname", i.hostname).
Str("software", ni.Software.Name). Str("software", ni.Software.Name).
Msg("FIXME unknown server implementation") Msg("FIXME unknown server implementation")
i.Unlock()
i.registerError() i.registerError()
return errors.New("FIXME unknown server implementation") return errors.New("FIXME unknown server implementation")
} }

View File

@ -12,12 +12,16 @@ const INDEX_API_TIMEOUT = time.Second * 60
var USER_AGENT = "https://github.com/sneak/feta indexer bot; sneak@sneak.berlin for feedback" var USER_AGENT = "https://github.com/sneak/feta indexer bot; sneak@sneak.berlin for feedback"
// check with indices only hourly // INDEX_CHECK_INTERVAL defines the interval for downloading new lists from
// the index APIs run by mastodon/pleroma (default: 1h)
var INDEX_CHECK_INTERVAL = time.Second * 60 * 60 var INDEX_CHECK_INTERVAL = time.Second * 60 * 60
// check with indices after 10 mins if they failed // INDEX_ERROR_INTERVAL is used for when the index fetch/parse fails
// (default: 10m)
var INDEX_ERROR_INTERVAL = time.Second * 60 * 10 var INDEX_ERROR_INTERVAL = time.Second * 60 * 10
// LOG_REPORT_INTERVAL defines how long between logging internal
// stats/reporting for user supervision
var LOG_REPORT_INTERVAL = time.Second * 60 var LOG_REPORT_INTERVAL = time.Second * 60
const mastodonIndexUrl = "https://instances.social/list.json?q%5Busers%5D=&q%5Bsearch%5D=&strict=false" const mastodonIndexUrl = "https://instances.social/list.json?q%5Busers%5D=&q%5Bsearch%5D=&strict=false"
@ -26,6 +30,8 @@ const pleromaIndexUrl = "https://distsn.org/cgi-bin/distsn-pleroma-instances-api
type InstanceLocator struct { type InstanceLocator struct {
pleromaIndexNextRefresh *time.Time pleromaIndexNextRefresh *time.Time
mastodonIndexNextRefresh *time.Time mastodonIndexNextRefresh *time.Time
mastodonIndexFetchLock sync.Mutex
pleromaIndexFetchLock sync.Mutex
reportInstanceVia chan InstanceHostname reportInstanceVia chan InstanceHostname
sync.Mutex sync.Mutex
} }
@ -55,12 +61,20 @@ func (self *InstanceLocator) Locate() {
x := time.Now() x := time.Now()
for { for {
log.Info().Msg("InstanceLocator tick") log.Info().Msg("InstanceLocator tick")
if self.pleromaIndexNextRefresh.Before(time.Now()) { go func() {
self.locatePleroma() self.pleromaIndexFetchLock.Lock()
} if self.pleromaIndexNextRefresh.Before(time.Now()) {
if self.mastodonIndexNextRefresh.Before(time.Now()) { self.locatePleroma()
self.locateMastodon() }
} self.pleromaIndexFetchLock.Unlock()
}()
go func() {
self.mastodonIndexFetchLock.Lock()
if self.mastodonIndexNextRefresh.Before(time.Now()) {
self.locateMastodon()
}
self.mastodonIndexFetchLock.Unlock()
}()
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
if time.Now().After(x.Add(LOG_REPORT_INTERVAL)) { if time.Now().After(x.Add(LOG_REPORT_INTERVAL)) {
x = time.Now() x = time.Now()
@ -95,6 +109,9 @@ func (self *InstanceLocator) locateMastodon() {
self.mastodonIndexNextRefresh = &t self.mastodonIndexNextRefresh = &t
self.Unlock() self.Unlock()
return return
} else {
log.Info().
Msg("fetched mastodon index")
} }
defer resp.Body.Close() defer resp.Body.Close()

View File

@ -5,7 +5,7 @@ import "time"
import "fmt" import "fmt"
import "runtime" import "runtime"
import "github.com/gin-gonic/gin" //import "github.com/gin-gonic/gin"
import "github.com/rs/zerolog/log" import "github.com/rs/zerolog/log"
type InstanceBackend interface { type InstanceBackend interface {
@ -17,6 +17,7 @@ type InstanceManager struct {
instances map[InstanceHostname]*Instance instances map[InstanceHostname]*Instance
newInstanceNotifications chan InstanceHostname newInstanceNotifications chan InstanceHostname
startup time.Time startup time.Time
addLock sync.Mutex
} }
func NewInstanceManager() *InstanceManager { func NewInstanceManager() *InstanceManager {
@ -50,14 +51,14 @@ func (self *InstanceManager) logCaller(msg string) {
} }
func (self *InstanceManager) Lock() { func (self *InstanceManager) Lock() {
self.logCaller("instancemanager attempting to lock") //self.logCaller("instancemanager attempting to lock")
self.mu.Lock() self.mu.Lock()
self.logCaller("instancemanager locked") //self.logCaller("instancemanager locked")
} }
func (self *InstanceManager) Unlock() { func (self *InstanceManager) Unlock() {
self.mu.Unlock() self.mu.Unlock()
self.logCaller("instancemanager unlocked") //self.logCaller("instancemanager unlocked")
} }
func (self *InstanceManager) AddInstanceNotificationChannel(via chan InstanceHostname) { func (self *InstanceManager) AddInstanceNotificationChannel(via chan InstanceHostname) {
@ -67,12 +68,10 @@ func (self *InstanceManager) AddInstanceNotificationChannel(via chan InstanceHos
} }
func (self *InstanceManager) Manage() { func (self *InstanceManager) Manage() {
self.managerInfiniteLoop()
}
func (self *InstanceManager) managerInfiniteLoop() {
log.Info().Msg("InstanceManager starting") log.Info().Msg("InstanceManager starting")
go self.receiveNewInstanceHostnames() go func() {
self.receiveNewInstanceHostnames()
}()
self.startup = time.Now() self.startup = time.Now()
for { for {
log.Info().Msg("InstanceManager tick") log.Info().Msg("InstanceManager tick")
@ -83,16 +82,20 @@ func (self *InstanceManager) managerInfiniteLoop() {
func (self *InstanceManager) managerLoop() { func (self *InstanceManager) managerLoop() {
self.Lock() self.Lock()
defer self.Unlock() il := make([]*Instance, 0)
for _, v := range self.instances { for _, v := range self.instances {
// wrap in a new goroutine because this needs to iterate il = append(il, v)
// fast and unlock fast
go func() {
if v.dueForFetch() {
go v.Fetch()
}
}()
} }
self.Unlock()
for _, v := range il {
if v.dueForFetch() {
go func(i *Instance) {
i.Fetch()
}(v)
}
}
} }
func (self *InstanceManager) hostnameExists(newhn InstanceHostname) bool { func (self *InstanceManager) hostnameExists(newhn InstanceHostname) bool {
@ -107,11 +110,19 @@ func (self *InstanceManager) hostnameExists(newhn InstanceHostname) bool {
} }
func (self *InstanceManager) addInstanceByHostname(newhn InstanceHostname) { func (self *InstanceManager) addInstanceByHostname(newhn InstanceHostname) {
// only add it if we haven't seen the hostname before // we do these one at a time
self.addLock.Lock()
defer self.addLock.Unlock()
if self.hostnameExists(newhn) { if self.hostnameExists(newhn) {
return return
} }
i := NewInstance(newhn) i := NewInstance(newhn)
// we do node detection under the addLock to avoid thundering
// on startup
i.detectNodeTypeIfNecessary()
self.Lock() self.Lock()
defer self.Unlock() defer self.Unlock()
self.instances[newhn] = i self.instances[newhn] = i
@ -121,7 +132,11 @@ func (self *InstanceManager) receiveNewInstanceHostnames() {
var newhn InstanceHostname var newhn InstanceHostname
for { for {
newhn = <-self.newInstanceNotifications newhn = <-self.newInstanceNotifications
self.addInstanceByHostname(newhn) // receive them fast out of the channel, let the adding function lock to add
// them one at a time
go func() {
self.addInstanceByHostname(newhn)
}()
} }
} }
@ -166,21 +181,6 @@ func (self *InstanceManager) listInstances() []*Instance {
return out return out
} }
func (self *InstanceManager) instanceListForApi() []*gin.H {
var output []*gin.H
l := self.listInstances()
for _, v := range l {
id := &gin.H{
"hostname": v.hostname,
"up": v.Up(),
"nextFetch": string(time.Now().Sub(v.nextFetch)),
}
output = append(output, id)
}
return output
}
func (self *InstanceManager) instanceSummaryReport() *InstanceSummaryReport { func (self *InstanceManager) instanceSummaryReport() *InstanceSummaryReport {
self.Lock() self.Lock()
defer self.Unlock() defer self.Unlock()