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 ) ) )
}