🌱🏠 instant least-authority port-forwarding (with automatic HTTPS) for anyone, anywhere! We **really** don't want your TLS private keys, you can keep them 😃 https://greenhouse.server.garden/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

319 lines
10 KiB

package main
import (
"bytes"
"crypto/md5"
"crypto/sha256"
"encoding/binary"
"fmt"
"html/template"
"log"
"math"
mathRand "math/rand"
"net/http"
"strconv"
"strings"
"time"
)
func registerAdminPanelRoutes(app *FrontendApp) {
app.handleWithSpecificUser("/admin", app.AdminTenantId, func(responseWriter http.ResponseWriter, request *http.Request, session Session) {
rawHash := sha256.Sum256([]byte(session.SessionId))
hashOfSessionId := fmt.Sprintf("%x", rawHash[:8])
if request.Method == "POST" {
postedHashOfSessionId := request.PostFormValue("hashOfSessionId")
log.Println(hashOfSessionId, postedHashOfSessionId)
if postedHashOfSessionId != hashOfSessionId {
app.setFlash(responseWriter, session, "error", "anti-CSRF (cross site request forgery) validation failed. Please try again.\n")
http.Redirect(responseWriter, request, "/admin", http.StatusFound)
return
}
action := request.PostFormValue("action")
if action == "delete-from-db" {
instance := request.PostFormValue("instance")
split := strings.Split(instance, "-")
if len(split) != 2 {
(*session.Flash)["error"] += fmt.Sprintf("invalid instance '%s'. expected <provider>-<provider_id>\n", instance)
} else {
err := app.Model.DeleteVPSInstance(split[0], split[1])
if err != nil {
app.unhandledError(responseWriter, request, err)
return
}
}
} else if action == "add-to-db" {
instance := request.PostFormValue("instance")
split := strings.Split(instance, "-")
if len(split) != 2 {
(*session.Flash)["error"] += fmt.Sprintf("invalid instance '%s'. expected <provider>-<provider_id>\n", instance)
} else {
err := app.Model.CreateVPSInstance(&VPSInstance{
ServiceProvider: split[0],
ProviderInstanceId: split[1],
IPV4: request.PostFormValue("ipv4"),
BytesMonthly: DEFAULT_INSTANCE_MONTHLY_BYTES,
})
if err != nil {
app.unhandledError(responseWriter, request, err)
return
}
}
} else if action == "reallocate" {
app.setFlash(responseWriter, session, "info", "reallocate has been kicked off in the background\n")
go (func() {
err := app.Backend.Reallocate(true, true)
if err != nil {
log.Printf("\reallocate failed! %+v\n\n", err)
}
})()
} else if action == "start_daemon" {
if app.Ingress.DaemonIsRunning {
app.setFlash(responseWriter, session, "error", "daemon is already running\n")
log.Println("daemon is already running")
} else {
err := app.Ingress.StartGreenhouseDaemon()
if err != nil {
app.setFlash(responseWriter, session, "error", fmt.Sprintf("start_daemon failed: %+v\n", err))
log.Printf("start_daemon failed: %+v\n", err)
} else {
app.setFlash(responseWriter, session, "info", "daemon appears to be running!\n")
log.Println("daemon appears to be running!")
}
}
} else if action == "configure_daemon" {
if !app.Ingress.DaemonIsRunning {
app.setFlash(responseWriter, session, "error", "daemon isn't running\n")
log.Println("daemon isn't running")
} else {
err := app.Ingress.ConfigureGreenhouseDaemon()
if err != nil {
app.setFlash(responseWriter, session, "error", fmt.Sprintf("configure_daemon failed: %+v\n", err))
log.Printf("configure_daemon failed: %+v\n", err)
} else {
app.setFlash(responseWriter, session, "info", "daemon appears to be running!\n")
log.Println("daemon appears to be running!")
}
}
} else if action == "daemon_status" {
responseString, err := app.Ingress.GetGreenhouseDaemonStatus()
if err != nil {
app.setFlash(responseWriter, session, "error", fmt.Sprintf("daemon_status failed: %+v\n", err))
log.Printf("daemon_status failed: %+v\n", err)
} else {
app.setFlash(responseWriter, session, "info", fmt.Sprintf("daemon status: %s\n", responseString))
log.Printf("daemon status: %s\n", responseString)
}
} else {
app.setFlash(responseWriter, session, "error", fmt.Sprintf("Unknown action '%s'\n", action))
}
http.Redirect(responseWriter, request, "/admin", http.StatusFound)
return
}
//TODO
desiredInstancesPerTenant := 2
tenantPinDuration := 6 * time.Hour
billingYear, billingMonth, _, _, amountOfMonthElapsed := getBillingTimeInfo()
validVpsInstances, dbOnlyInstances, cloudOnlyInstances, err := app.Backend.GetInstances()
if err != nil {
app.unhandledError(responseWriter, request, err)
return
}
tenants, err := app.Model.GetTenants()
if err != nil {
app.unhandledError(responseWriter, request, err)
return
}
tenantVpsInstanceRows, err := app.Model.GetTenantVPSInstanceRows(billingYear, billingMonth)
if err != nil {
app.unhandledError(responseWriter, request, err)
return
}
// if you update the following loop, consider updating the similar one in backend.go 😬
allocations := map[string]map[int]bool{}
pinned := map[string]map[int]bool{}
for _, row := range tenantVpsInstanceRows {
vpsInstanceId := row.GetVPSInstanceId()
vpsInstance, hasVPSInstance := validVpsInstances[vpsInstanceId]
if hasVPSInstance {
vpsInstance.Bytes += row.Bytes
_, hasAllocationMap := allocations[vpsInstanceId]
if !hasAllocationMap {
allocations[vpsInstanceId] = map[int]bool{}
pinned[vpsInstanceId] = map[int]bool{}
}
if row.Active {
allocations[vpsInstanceId][row.TenantId] = true
}
if row.DeactivatedAt != nil && row.DeactivatedAt.Add(tenantPinDuration).After(time.Now()) {
pinned[vpsInstanceId][row.TenantId] = true
}
}
}
healthStatus := app.Backend.HealthcheckInstances(validVpsInstances)
type TenantDisplay struct {
Name string
Color template.CSS
}
type Thermometer struct {
Height1 int
Color1 template.CSS
Height2 int
Color2 template.CSS
}
type VPSInstanceForDisplay struct {
VPSInstance
CurrentTenants []TenantDisplay
PinnedTenants []TenantDisplay
ThermometerReal Thermometer
ThermometerProjected Thermometer
Healthy string
}
display := map[string]*VPSInstanceForDisplay{}
for k, v := range validVpsInstances {
healthy := "healthy"
if !healthStatus[k] {
healthy = "unhealthy"
}
mapToTenantDisplay := func(vpsInstances *map[string]map[int]bool, vpsInstanceId string) []TenantDisplay {
toReturn := []TenantDisplay{}
tenantIds, hasTenantIds := (*vpsInstances)[vpsInstanceId]
if hasTenantIds {
for id := range tenantIds {
toReturn = append(toReturn, TenantDisplay{
Name: strconv.Itoa(id),
Color: template.CSS(getTenantColor(id)),
})
}
}
return toReturn
}
display[k] = &VPSInstanceForDisplay{
VPSInstance: *v,
CurrentTenants: mapToTenantDisplay(&allocations, k),
PinnedTenants: mapToTenantDisplay(&pinned, k),
Healthy: healthy,
}
// TODO handle BytesMonthly when node is created in the middle of the month / near end of month
projectedUsage := getInstanceProjectedUsage(v, &allocations, &tenants, desiredInstancesPerTenant, amountOfMonthElapsed)
allowanceBytes := int64(amountOfMonthElapsed * float64(v.BytesMonthly))
usage := (float64(projectedUsage) / float64(v.BytesMonthly)) * float64(0.5)
display[k].ThermometerReal.Color1 = template.CSS("cyan")
if allowanceBytes > v.Bytes {
display[k].ThermometerReal.Color2 = template.CSS("#ddd")
display[k].ThermometerReal.Height1 = int((float64(v.Bytes) / float64(v.BytesMonthly)) * float64(100))
display[k].ThermometerReal.Height2 = int((float64(allowanceBytes-v.Bytes) / float64(v.BytesMonthly)) * float64(100))
} else {
display[k].ThermometerReal.Color2 = template.CSS("orange")
display[k].ThermometerReal.Height1 = int((float64(allowanceBytes) / float64(v.BytesMonthly)) * float64(100))
display[k].ThermometerReal.Height2 = int((float64(allowanceBytes-v.Bytes) / float64(v.BytesMonthly)) * float64(100))
}
display[k].ThermometerProjected.Color1 = template.CSS("green")
if usage < 0.5 {
display[k].ThermometerProjected.Color2 = template.CSS("#ddd")
display[k].ThermometerProjected.Height1 = int(usage * float64(100))
display[k].ThermometerProjected.Height2 = int((float64(0.5) - usage) * float64(100))
} else {
display[k].ThermometerProjected.Color2 = template.CSS("orange")
display[k].ThermometerProjected.Height1 = 50
display[k].ThermometerProjected.Height2 = int((usage - float64(0.5)) * float64(100))
}
}
data := struct {
ValidVPSInstances map[string]*VPSInstanceForDisplay
DBOnlyVPSInstances map[string]*VPSInstance
CloudOnlyVPSInstances map[string]*VPSInstance
HashOfSessionId string
}{
ValidVPSInstances: display,
DBOnlyVPSInstances: dbOnlyInstances,
CloudOnlyVPSInstances: cloudOnlyInstances,
HashOfSessionId: hashOfSessionId,
}
app.buildPageFromTemplate(responseWriter, request, session, "admin.html", data)
})
}
var tenantColors = map[int]string{}
func getTenantColor(tenantId int) string {
if _, has := tenantColors[tenantId]; has {
return tenantColors[tenantId]
}
var randomInt int64
colorHashArray := md5.Sum([]byte(strconv.Itoa(tenantId)))
colorHash := bytes.NewReader(colorHashArray[0:16])
err := binary.Read(colorHash, binary.LittleEndian, &randomInt)
if err != nil {
panic(err)
}
randomSource := mathRand.NewSource(randomInt)
toReturn := hsvColor(
float64(randomSource.Int63()%360),
float64(0.68)+(float64(randomSource.Int63()%80)/float64(255)),
float64(0.68)+(float64(randomSource.Int63()%80)/float64(255)),
)
tenantColors[tenantId] = toReturn
return toReturn
}
func hsvColor(H, S, V float64) string {
Hp := H / 60.0
C := V * S
X := C * (1.0 - math.Abs(math.Mod(Hp, 2.0)-1.0))
m := V - C
r, g, b := 0.0, 0.0, 0.0
switch {
case 0.0 <= Hp && Hp < 1.0:
r = C
g = X
case 1.0 <= Hp && Hp < 2.0:
r = X
g = C
case 2.0 <= Hp && Hp < 3.0:
g = C
b = X
case 3.0 <= Hp && Hp < 4.0:
g = X
b = C
case 4.0 <= Hp && Hp < 5.0:
r = X
b = C
case 5.0 <= Hp && Hp < 6.0:
r = C
b = X
}
return fmt.Sprintf("rgb(%d, %d, %d)", int((m+r)*float64(255)), int((m+g)*float64(255)), int((m+b)*float64(255)))
}