added serve-http option

This commit is contained in:
Mahmoud Rahbar Azad 2018-10-26 22:31:39 +02:00
parent c0539ade72
commit 4a726291a2
WARNING! Although there is a key with this ID in the database it does not verify this commit! This commit is SUSPICIOUS.
GPG key ID: 7DBBD39E2BFEB784
9 changed files with 313 additions and 96 deletions

View file

@ -1,13 +1,13 @@
The aim of my-bloody-hetzner-sb-notifier is a simple CLI to fetch the current Hetzner Serverbörse deals and filter them according to CLI parameters sorted by score. The aim of my-bloody-hetzner-sb-notifier is a simple CLI to fetch the current Hetzner Serverbörse deals and filter them according to CLI parameters sorted by score.
The score is calculated from the amount of HDD space as well as RAM and CPU-Benchnmark for better comparability. For each offer a score is calculated from the amount of HDD space as well as RAM and CPU-Benchnmark for better comparability.
The CLI interface looks like this: The CLI interface looks like this:
```` ````
Usage of hetzner-sb-notifier: Usage of hetzner-sb-notifier:
-alert-on-score int -min-cpu-benchmark int
set alert on score set min benchmark
-max-benchmark int -max-cpu-benchmark int
set max benchmark (default 20000) set max benchmark (default 20000)
-max-hdd-count int -max-hdd-count int
set max hdd count (default 15) set max hdd count (default 15)
@ -17,8 +17,6 @@ Usage of hetzner-sb-notifier:
set max price (default 297) set max price (default 297)
-max-ram int -max-ram int
set max ram (default 256) set max ram (default 256)
-min-benchmark int
set min benchmark
-min-hdd-count int -min-hdd-count int
set min hdd count set min hdd count
-min-hdd-size int -min-hdd-size int
@ -27,8 +25,14 @@ Usage of hetzner-sb-notifier:
set min price set min price
-min-ram int -min-ram int
set min ram set min ram
-serve-http
set serve http
-serve-http-port int
set serve http port (default 8080)
```` ````
## Http mode
## Example ## Example
./hetzner-sb-notifier --max-price 77 --min-ram 128 --min-hdd-count 2 --min-hdd-size 4096 ./hetzner-sb-notifier --max-price 77 --min-ram 128 --min-hdd-count 2 --min-hdd-size 4096

View file

@ -1,51 +1,41 @@
package crawler package crawler
import ( import (
"fmt"
"github.com/mrahbar/my-bloody-hetzner-sb-notifier/hetzner" "github.com/mrahbar/my-bloody-hetzner-sb-notifier/hetzner"
"os"
"sort" "sort"
"text/tabwriter"
) )
type Crawler struct { type Parameter struct {
tabWriter *tabwriter.Writer MinPrice float64
MaxPrice float64
minPrice float64 MinRam int64
maxPrice float64 MaxRam int64
minRam int MinHddSize int64
maxRam int MaxHddSize int64
minHddSize int MinHddCount int64
maxHddSize int MaxHddCount int64
minHddCount int MinBenchmark int64
maxHddCount int MaxBenchmark int64
minBenchmark int
maxBenchmark int
} }
func NewCrawler(minPrice float64, maxPrice float64, minRam int, maxRam int, minHddSize int, maxHddSize int, minHddCount int, maxHddCount int, minBenchmark int, maxBenchmark int) *Crawler {
type Crawler struct {
parameter Parameter
}
func NewCrawler(parameter Parameter) *Crawler {
crawler := &Crawler{ crawler := &Crawler{
tabwriter.NewWriter(os.Stdout, 0, 8, 2, ' ', tabwriter.Debug|tabwriter.AlignRight), parameter: parameter,
minPrice,
maxPrice,
minRam,
maxRam,
minHddSize,
maxHddSize,
minHddCount,
maxHddCount,
minBenchmark,
maxBenchmark,
} }
return crawler return crawler
} }
func (c *Crawler) Filter(servers []hetzner.Server) []hetzner.Server { func (c *Crawler) Filter(servers []hetzner.Server) hetzner.Deals {
var filteredServers []hetzner.Server var filteredServers []hetzner.Server
for _, server := range servers { for _, server := range servers {
if !c.isFiltered(server) { if !c.isFiltered(server) {
@ -56,26 +46,23 @@ func (c *Crawler) Filter(servers []hetzner.Server) []hetzner.Server {
sort.Slice(servers, func(i, j int) bool { sort.Slice(servers, func(i, j int) bool {
return servers[i].Score() > servers[j].Score() return servers[i].Score() > servers[j].Score()
}) })
return filteredServers
}
func (c *Crawler) Print(servers []hetzner.Server) { deals := hetzner.Deals{
fmt.Fprintf(c.tabWriter, "%s\n", servers[0].Header()) ResultStats: hetzner.FilterResultStats{OriginalCount: len(servers), FilteredCount: len(filteredServers)},
for _, server := range servers { Servers: filteredServers,
fmt.Fprintf(c.tabWriter, "%s\n", server.ToString())
} }
c.tabWriter.Flush() return deals
} }
func (c *Crawler) isFiltered(server hetzner.Server) bool { func (c *Crawler) isFiltered(server hetzner.Server) bool {
filtered := true filtered := true
priceParsed := server.ParsePrice() priceParsed := server.ParsePrice()
if server.CpuBenchmark >= c.minBenchmark && server.CpuBenchmark <= c.maxBenchmark && if server.CpuBenchmark >= c.parameter.MinBenchmark && server.CpuBenchmark <= c.parameter.MaxBenchmark &&
priceParsed >= c.minPrice && priceParsed <= c.maxPrice && priceParsed >= c.parameter.MinPrice && priceParsed <= c.parameter.MaxPrice &&
server.Ram >= c.minRam && server.Ram <= c.maxRam && server.Ram >= c.parameter.MinRam && server.Ram <= c.parameter.MaxRam &&
server.TotalHdd() >= c.minHddSize && server.TotalHdd() <= c.maxHddSize && server.TotalHdd() >= c.parameter.MinHddSize && server.TotalHdd() <= c.parameter.MaxHddSize &&
server.HddCount >= c.minHddCount && server.HddCount <= c.maxHddCount { server.HddCount >= c.parameter.MinHddCount && server.HddCount <= c.parameter.MaxHddCount {
filtered = false filtered = false
} }

1
go.mod
View file

@ -4,6 +4,7 @@ require (
cloud.google.com/go v0.31.0 cloud.google.com/go v0.31.0
github.com/golang/protobuf v1.2.0 github.com/golang/protobuf v1.2.0
github.com/googleapis/gax-go v2.0.0+incompatible // indirect github.com/googleapis/gax-go v2.0.0+incompatible // indirect
github.com/iancoleman/strcase v0.0.0-20180726023541-3605ed457bf7 // indirect
github.com/mitchellh/gox v0.4.0 // indirect github.com/mitchellh/gox v0.4.0 // indirect
github.com/mitchellh/iochan v1.0.0 // indirect github.com/mitchellh/iochan v1.0.0 // indirect
go.opencensus.io v0.18.0 // indirect go.opencensus.io v0.18.0 // indirect

View file

@ -6,6 +6,16 @@ import (
"strings" "strings"
) )
type Deals struct {
ResultStats FilterResultStats
Servers []Server
}
type FilterResultStats struct {
OriginalCount int `json:"count"`
FilteredCount int `json:"filtered"`
}
type Offers struct { type Offers struct {
Server []Server `json:"server"` Server []Server `json:"server"`
} }
@ -26,13 +36,13 @@ type Server struct {
SetupPrice string `json:"setup_price"` SetupPrice string `json:"setup_price"`
Cpu string `json:"cpu"` Cpu string `json:"cpu"`
CpuBenchmark int `json:"cpu_benchmark"` CpuBenchmark int64 `json:"cpu_benchmark"`
CpuCount int `json:"cpu_count"` CpuCount int64 `json:"cpu_count"`
Ram int `json:"ram"` Ram int64 `json:"ram"`
RamHr string `json:"ram_hr"` RamHr string `json:"ram_hr"`
HddSize int `json:"hdd_size"` HddSize int64 `json:"hdd_size"`
HddHr string `json:"hdd_hr"` HddHr string `json:"hdd_hr"`
HddCount int `json:"hdd_count"` HddCount int64 `json:"hdd_count"`
SpecialHdd string `json:"specialHdd"` SpecialHdd string `json:"specialHdd"`
NextReduce int `json:"next_reduce"` NextReduce int `json:"next_reduce"`
@ -43,7 +53,7 @@ type Server struct {
IsEcc bool `json:"is_ecc"` IsEcc bool `json:"is_ecc"`
} }
func (s *Server) TotalHdd() int { func (s *Server) TotalHdd() int64 {
return s.HddCount * s.HddSize return s.HddCount * s.HddSize
} }

View file

@ -1,11 +0,0 @@
package instrumentation
type Instrumenter struct {
projectID string
}
func NewInstrumenter(projectID string) *Instrumenter {
return &Instrumenter{
projectID: projectID,
}
}

228
main.go
View file

@ -3,92 +3,256 @@ package main
import ( import (
"flag" "flag"
"fmt" "fmt"
"github.com/iancoleman/strcase"
"github.com/mrahbar/my-bloody-hetzner-sb-notifier/client" "github.com/mrahbar/my-bloody-hetzner-sb-notifier/client"
c "github.com/mrahbar/my-bloody-hetzner-sb-notifier/crawler" "github.com/mrahbar/my-bloody-hetzner-sb-notifier/crawler"
"github.com/mrahbar/my-bloody-hetzner-sb-notifier/hetzner" "github.com/mrahbar/my-bloody-hetzner-sb-notifier/hetzner"
"github.com/mrahbar/my-bloody-hetzner-sb-notifier/writer"
"io"
"net/http"
"net/http/httputil"
"os"
"strconv"
)
const (
flagMinPrice = "min-price"
flagMaxPrice = "max-price"
flagMinRam = "min-ram"
flagMaxRam = "max-ram"
flagMinHddSize = "min-hdd-size"
flagMaxHddSize = "max-hdd-size"
flagMinHddCount = "min-hdd-count"
flagMaxHddCount = "max-hdd-count"
flagMinBenchmark = "min-cpu-benchmark"
flagMaxBenchmark = "max-cpu-benchmark"
flagOutput = "output"
flagServeHttpPort = "serve-http-port"
flagServeHttp = "serve-http"
) )
var ( var (
zeroIntValue = int64(0)
zeroFloatValue = float64(0)
defaultMaxPriceValue = float64(297)
defaultMaxRamValue = int64(256)
defaultMaxHddSizeValue = int64(6144)
defaultMaxHddCountValue = int64(15)
defaultMaxCpuBenchmarkValue = int64(20000)
minPrice = flag.Float64( minPrice = flag.Float64(
"min-price", flagMinPrice,
0, 0,
"set min price", "set min price",
) )
maxPrice = flag.Float64( maxPrice = flag.Float64(
"max-price", flagMaxPrice,
297, 297,
"set max price", "set max price",
) )
minRam = flag.Int( minRam = flag.Int64(
"min-ram", flagMinRam,
0, 0,
"set min ram", "set min ram",
) )
maxRam = flag.Int( maxRam = flag.Int64(
"max-ram", flagMaxRam,
256, 256,
"set max ram", "set max ram",
) )
minHddSize = flag.Int( minHddSize = flag.Int64(
"min-hdd-size", flagMinHddSize,
0, 0,
"set min hdd size", "set min hdd size",
) )
maxHddSize = flag.Int( maxHddSize = flag.Int64(
"max-hdd-size", flagMaxHddSize,
6144, 6144,
"set max hdd size", "set max hdd size",
) )
minHddCount = flag.Int( minHddCount = flag.Int64(
"min-hdd-count", flagMinHddCount,
0, 0,
"set min hdd count", "set min hdd count",
) )
maxHddCount = flag.Int( maxHddCount = flag.Int64(
"max-hdd-count", flagMaxHddCount,
15, 15,
"set max hdd count", "set max hdd count",
) )
minBenchmark = flag.Int( minBenchmark = flag.Int64(
"min-benchmark", flagMinBenchmark,
0, 0,
"set min benchmark", "set min benchmark",
) )
maxBenchmark = flag.Int( maxBenchmark = flag.Int64(
"max-benchmark", flagMaxBenchmark,
20000, 20000,
"set max benchmark", "set max benchmark",
) )
alertOnScore = flag.Int( serveHttp = flag.Bool(
"alert-on-score", flagServeHttp,
0, false,
"set alert on score", "set serve http",
)
serveHttpPort = flag.Int(
flagServeHttpPort,
8080,
"set serve http port",
)
output = flag.String(
flagOutput,
"table",
"set output: one of table, json",
) )
) )
func main() { func main() {
flag.Parse() flag.Parse()
if *serveHttp {
runHttp()
} else {
p := crawler.Parameter{
MinPrice: *minPrice,
MaxPrice: *maxPrice,
MinRam: *minRam,
MaxRam: *maxRam,
MinHddSize: *minHddSize,
MaxHddSize: *maxHddSize,
MinHddCount: *minHddCount,
MaxHddCount: *maxHddCount,
MinBenchmark: *minBenchmark,
MaxBenchmark: *maxBenchmark,
}
run(os.Stdout, p, *output)
}
}
func runHttp() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
b, err := httputil.DumpRequest(r, false)
if err == nil {
fmt.Printf("Got request: %s", string(b))
}
parameter := crawler.Parameter{
MinPrice: zeroFloatValue,
MaxPrice: defaultMaxPriceValue,
MinRam: zeroIntValue,
MaxRam: defaultMaxRamValue,
MinHddSize: zeroIntValue,
MaxHddSize: defaultMaxHddSizeValue,
MinHddCount: zeroIntValue,
MaxHddCount: defaultMaxHddCountValue,
MinBenchmark: zeroIntValue,
MaxBenchmark: defaultMaxCpuBenchmarkValue,
}
output := "table"
values := r.URL.Query()
for k, v := range values {
value := v[0]
switch k {
case strcase.ToLowerCamel(flagMinPrice):
parameter.MinPrice = parseFloatFlag(w, strcase.ToLowerCamel(flagMinPrice), value)
break
case strcase.ToLowerCamel(flagMaxPrice):
parameter.MaxPrice = parseFloatFlag(w, strcase.ToLowerCamel(flagMaxPrice), value)
break
case strcase.ToLowerCamel(flagMinRam):
parameter.MinRam = parseIntFlag(w, strcase.ToLowerCamel(flagMinRam), value)
break
case strcase.ToLowerCamel(flagMaxRam):
parameter.MaxRam = parseIntFlag(w, strcase.ToLowerCamel(flagMaxRam), value)
break
case strcase.ToLowerCamel(flagMinHddSize):
parameter.MinHddSize = parseIntFlag(w, strcase.ToLowerCamel(flagMinHddSize), value)
break
case strcase.ToLowerCamel(flagMaxHddSize):
parameter.MaxHddSize = parseIntFlag(w, strcase.ToLowerCamel(flagMaxHddSize), value)
break
case strcase.ToLowerCamel(flagMinHddCount):
parameter.MinHddCount = parseIntFlag(w, strcase.ToLowerCamel(flagMinHddCount), value)
break
case strcase.ToLowerCamel(flagMaxHddCount):
parameter.MaxHddCount = parseIntFlag(w, strcase.ToLowerCamel(flagMaxHddCount), value)
break
case strcase.ToLowerCamel(flagMinBenchmark):
parameter.MinBenchmark = parseIntFlag(w, strcase.ToLowerCamel(flagMinBenchmark), value)
break
case strcase.ToLowerCamel(flagMaxBenchmark):
parameter.MaxBenchmark = parseIntFlag(w, strcase.ToLowerCamel(flagMaxBenchmark), value)
break
case strcase.ToLowerCamel(flagOutput):
output = value
break
}
}
w.WriteHeader(http.StatusOK)
run(w, parameter, output)
})
address := fmt.Sprintf(":%d", *serveHttpPort)
fmt.Printf("Running http server on address %s\n", address)
fmt.Println(http.ListenAndServe(address, nil))
}
func parseFloatFlag(w http.ResponseWriter, flag string, value string) float64 {
parseValue, err := strconv.ParseFloat(value, 32)
if err == nil {
return parseValue
} else {
writeBadRequestResponseForQueryParameter(w, flag, err)
return zeroFloatValue
}
}
func parseIntFlag(w http.ResponseWriter, flag string, value string) int64 {
parseValue, err := strconv.ParseInt(value, 10, 32)
if err == nil {
return parseValue
} else {
writeBadRequestResponseForQueryParameter(w, flag, err)
return zeroIntValue
}
}
func writeBadRequestResponseForQueryParameter(w http.ResponseWriter, parameter string, err error) {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(fmt.Sprintf("Error parsing query parameter %s: %s", parameter, err)))
}
func run(w io.Writer, parameter crawler.Parameter, output string) error {
offers := &hetzner.Offers{} offers := &hetzner.Offers{}
err := client.NewClient().DoRequest(hetzner.MakeURL(), offers) err := client.NewClient().DoRequest(hetzner.MakeURL(), offers)
if err != nil { if err != nil {
panic(fmt.Errorf("failed to get hetzner live data: %s", err)) return fmt.Errorf("failed to get hetzner live data: %s", err)
} }
if len(offers.Server) > 0 { if len(offers.Server) > 0 {
crawler := c.NewCrawler(*minPrice, *maxPrice, *minRam, *maxRam, *minHddSize, *maxHddSize, *minHddCount, *maxHddCount, *minBenchmark, *maxBenchmark) c := crawler.NewCrawler(parameter)
servers := crawler.Filter(offers.Server) deals := c.Filter(offers.Server)
fmt.Printf("Got %d offers. Filtered offers: %d\n", len(offers.Server), len(servers)) switch output {
crawler.Print(servers) case "json":
writer.NewJsonWriter(w).Print(deals)
//notifier := n.NewInstrumenter(*notifierRecipient, *notifierSender, *notifierPassword) break
default:
case "table":
writer.NewTableWriter(w).Print(deals)
break
}
return nil
} else { } else {
fmt.Println("Got no offers.") return fmt.Errorf("got no offers.")
} }
} }

27
writer/json.go Normal file
View file

@ -0,0 +1,27 @@
package writer
import (
"encoding/json"
"fmt"
"github.com/mrahbar/my-bloody-hetzner-sb-notifier/hetzner"
"io"
)
type JsonWriter struct {
output io.Writer
}
func NewJsonWriter(output io.Writer)*JsonWriter {
return &JsonWriter{
output:output,
}
}
func (c *JsonWriter) Print(deals hetzner.Deals) {
b, err := json.Marshal(deals)
if err != nil {
fmt.Fprintf(c.output, "{\"error\": \"%s\"}", err)
return
}
fmt.Fprint(c.output, string(b))
}

28
writer/table.go Normal file
View file

@ -0,0 +1,28 @@
package writer
import (
"fmt"
"github.com/mrahbar/my-bloody-hetzner-sb-notifier/hetzner"
"io"
"text/tabwriter"
)
type TableWriter struct {
tabWriter *tabwriter.Writer
}
func NewTableWriter(output io.Writer)*TableWriter {
return &TableWriter{
tabwriter.NewWriter(output, 0, 8, 2, ' ', tabwriter.Debug|tabwriter.AlignRight),
}
}
func (c *TableWriter) Print(deals hetzner.Deals) {
fmt.Fprintf(c.tabWriter,"Got %d offers. Filtered offers: %d\n", deals.ResultStats.OriginalCount, deals.ResultStats.FilteredCount)
fmt.Fprintf(c.tabWriter, "%s\n", deals.Servers[0].Header())
for _, server := range deals.Servers {
fmt.Fprintf(c.tabWriter, "%s\n", server.ToString())
}
c.tabWriter.Flush()
}

7
writer/writer.go Normal file
View file

@ -0,0 +1,7 @@
package writer
import "github.com/mrahbar/my-bloody-hetzner-sb-notifier/hetzner"
type Writer interface {
Print(deals hetzner.Deals)
}