added serve-http option
This commit is contained in:
parent
c0539ade72
commit
4a726291a2
9 changed files with 313 additions and 96 deletions
16
Readme.md
16
Readme.md
|
@ -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 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:
|
||||
````
|
||||
Usage of hetzner-sb-notifier:
|
||||
-alert-on-score int
|
||||
set alert on score
|
||||
-max-benchmark int
|
||||
-min-cpu-benchmark int
|
||||
set min benchmark
|
||||
-max-cpu-benchmark int
|
||||
set max benchmark (default 20000)
|
||||
-max-hdd-count int
|
||||
set max hdd count (default 15)
|
||||
|
@ -17,8 +17,6 @@ Usage of hetzner-sb-notifier:
|
|||
set max price (default 297)
|
||||
-max-ram int
|
||||
set max ram (default 256)
|
||||
-min-benchmark int
|
||||
set min benchmark
|
||||
-min-hdd-count int
|
||||
set min hdd count
|
||||
-min-hdd-size int
|
||||
|
@ -27,8 +25,14 @@ Usage of hetzner-sb-notifier:
|
|||
set min price
|
||||
-min-ram int
|
||||
set min ram
|
||||
-serve-http
|
||||
set serve http
|
||||
-serve-http-port int
|
||||
set serve http port (default 8080)
|
||||
````
|
||||
|
||||
## Http mode
|
||||
|
||||
## Example
|
||||
|
||||
./hetzner-sb-notifier --max-price 77 --min-ram 128 --min-hdd-count 2 --min-hdd-size 4096
|
||||
|
|
|
@ -1,51 +1,41 @@
|
|||
package crawler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mrahbar/my-bloody-hetzner-sb-notifier/hetzner"
|
||||
"os"
|
||||
"sort"
|
||||
"text/tabwriter"
|
||||
)
|
||||
|
||||
type Crawler struct {
|
||||
tabWriter *tabwriter.Writer
|
||||
type Parameter struct {
|
||||
MinPrice float64
|
||||
MaxPrice float64
|
||||
|
||||
minPrice float64
|
||||
maxPrice float64
|
||||
MinRam int64
|
||||
MaxRam int64
|
||||
|
||||
minRam int
|
||||
maxRam int
|
||||
MinHddSize int64
|
||||
MaxHddSize int64
|
||||
|
||||
minHddSize int
|
||||
maxHddSize int
|
||||
MinHddCount int64
|
||||
MaxHddCount int64
|
||||
|
||||
minHddCount int
|
||||
maxHddCount int
|
||||
|
||||
minBenchmark int
|
||||
maxBenchmark int
|
||||
MinBenchmark int64
|
||||
MaxBenchmark int64
|
||||
}
|
||||
|
||||
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{
|
||||
tabwriter.NewWriter(os.Stdout, 0, 8, 2, ' ', tabwriter.Debug|tabwriter.AlignRight),
|
||||
minPrice,
|
||||
maxPrice,
|
||||
minRam,
|
||||
maxRam,
|
||||
minHddSize,
|
||||
maxHddSize,
|
||||
minHddCount,
|
||||
maxHddCount,
|
||||
minBenchmark,
|
||||
maxBenchmark,
|
||||
parameter: parameter,
|
||||
}
|
||||
|
||||
return crawler
|
||||
}
|
||||
|
||||
func (c *Crawler) Filter(servers []hetzner.Server) []hetzner.Server {
|
||||
func (c *Crawler) Filter(servers []hetzner.Server) hetzner.Deals {
|
||||
var filteredServers []hetzner.Server
|
||||
for _, server := range servers {
|
||||
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 {
|
||||
return servers[i].Score() > servers[j].Score()
|
||||
})
|
||||
return filteredServers
|
||||
}
|
||||
|
||||
func (c *Crawler) Print(servers []hetzner.Server) {
|
||||
fmt.Fprintf(c.tabWriter, "%s\n", servers[0].Header())
|
||||
for _, server := range servers {
|
||||
fmt.Fprintf(c.tabWriter, "%s\n", server.ToString())
|
||||
deals := hetzner.Deals{
|
||||
ResultStats: hetzner.FilterResultStats{OriginalCount: len(servers), FilteredCount: len(filteredServers)},
|
||||
Servers: filteredServers,
|
||||
}
|
||||
c.tabWriter.Flush()
|
||||
return deals
|
||||
}
|
||||
|
||||
func (c *Crawler) isFiltered(server hetzner.Server) bool {
|
||||
filtered := true
|
||||
|
||||
priceParsed := server.ParsePrice()
|
||||
if server.CpuBenchmark >= c.minBenchmark && server.CpuBenchmark <= c.maxBenchmark &&
|
||||
priceParsed >= c.minPrice && priceParsed <= c.maxPrice &&
|
||||
server.Ram >= c.minRam && server.Ram <= c.maxRam &&
|
||||
server.TotalHdd() >= c.minHddSize && server.TotalHdd() <= c.maxHddSize &&
|
||||
server.HddCount >= c.minHddCount && server.HddCount <= c.maxHddCount {
|
||||
if server.CpuBenchmark >= c.parameter.MinBenchmark && server.CpuBenchmark <= c.parameter.MaxBenchmark &&
|
||||
priceParsed >= c.parameter.MinPrice && priceParsed <= c.parameter.MaxPrice &&
|
||||
server.Ram >= c.parameter.MinRam && server.Ram <= c.parameter.MaxRam &&
|
||||
server.TotalHdd() >= c.parameter.MinHddSize && server.TotalHdd() <= c.parameter.MaxHddSize &&
|
||||
server.HddCount >= c.parameter.MinHddCount && server.HddCount <= c.parameter.MaxHddCount {
|
||||
filtered = false
|
||||
}
|
||||
|
||||
|
|
1
go.mod
1
go.mod
|
@ -4,6 +4,7 @@ require (
|
|||
cloud.google.com/go v0.31.0
|
||||
github.com/golang/protobuf v1.2.0
|
||||
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/iochan v1.0.0 // indirect
|
||||
go.opencensus.io v0.18.0 // indirect
|
||||
|
|
|
@ -6,6 +6,16 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
type Deals struct {
|
||||
ResultStats FilterResultStats
|
||||
Servers []Server
|
||||
}
|
||||
|
||||
type FilterResultStats struct {
|
||||
OriginalCount int `json:"count"`
|
||||
FilteredCount int `json:"filtered"`
|
||||
}
|
||||
|
||||
type Offers struct {
|
||||
Server []Server `json:"server"`
|
||||
}
|
||||
|
@ -26,13 +36,13 @@ type Server struct {
|
|||
SetupPrice string `json:"setup_price"`
|
||||
|
||||
Cpu string `json:"cpu"`
|
||||
CpuBenchmark int `json:"cpu_benchmark"`
|
||||
CpuCount int `json:"cpu_count"`
|
||||
Ram int `json:"ram"`
|
||||
CpuBenchmark int64 `json:"cpu_benchmark"`
|
||||
CpuCount int64 `json:"cpu_count"`
|
||||
Ram int64 `json:"ram"`
|
||||
RamHr string `json:"ram_hr"`
|
||||
HddSize int `json:"hdd_size"`
|
||||
HddSize int64 `json:"hdd_size"`
|
||||
HddHr string `json:"hdd_hr"`
|
||||
HddCount int `json:"hdd_count"`
|
||||
HddCount int64 `json:"hdd_count"`
|
||||
SpecialHdd string `json:"specialHdd"`
|
||||
|
||||
NextReduce int `json:"next_reduce"`
|
||||
|
@ -43,7 +53,7 @@ type Server struct {
|
|||
IsEcc bool `json:"is_ecc"`
|
||||
}
|
||||
|
||||
func (s *Server) TotalHdd() int {
|
||||
func (s *Server) TotalHdd() int64 {
|
||||
return s.HddCount * s.HddSize
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
package instrumentation
|
||||
|
||||
type Instrumenter struct {
|
||||
projectID string
|
||||
}
|
||||
|
||||
func NewInstrumenter(projectID string) *Instrumenter {
|
||||
return &Instrumenter{
|
||||
projectID: projectID,
|
||||
}
|
||||
}
|
228
main.go
228
main.go
|
@ -3,92 +3,256 @@ package main
|
|||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/iancoleman/strcase"
|
||||
"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/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 (
|
||||
zeroIntValue = int64(0)
|
||||
zeroFloatValue = float64(0)
|
||||
|
||||
defaultMaxPriceValue = float64(297)
|
||||
defaultMaxRamValue = int64(256)
|
||||
defaultMaxHddSizeValue = int64(6144)
|
||||
defaultMaxHddCountValue = int64(15)
|
||||
defaultMaxCpuBenchmarkValue = int64(20000)
|
||||
|
||||
minPrice = flag.Float64(
|
||||
"min-price",
|
||||
flagMinPrice,
|
||||
0,
|
||||
"set min price",
|
||||
)
|
||||
maxPrice = flag.Float64(
|
||||
"max-price",
|
||||
flagMaxPrice,
|
||||
297,
|
||||
"set max price",
|
||||
)
|
||||
|
||||
minRam = flag.Int(
|
||||
"min-ram",
|
||||
minRam = flag.Int64(
|
||||
flagMinRam,
|
||||
0,
|
||||
"set min ram",
|
||||
)
|
||||
maxRam = flag.Int(
|
||||
"max-ram",
|
||||
maxRam = flag.Int64(
|
||||
flagMaxRam,
|
||||
256,
|
||||
"set max ram",
|
||||
)
|
||||
|
||||
minHddSize = flag.Int(
|
||||
"min-hdd-size",
|
||||
minHddSize = flag.Int64(
|
||||
flagMinHddSize,
|
||||
0,
|
||||
"set min hdd size",
|
||||
)
|
||||
maxHddSize = flag.Int(
|
||||
"max-hdd-size",
|
||||
maxHddSize = flag.Int64(
|
||||
flagMaxHddSize,
|
||||
6144,
|
||||
"set max hdd size",
|
||||
)
|
||||
|
||||
minHddCount = flag.Int(
|
||||
"min-hdd-count",
|
||||
minHddCount = flag.Int64(
|
||||
flagMinHddCount,
|
||||
0,
|
||||
"set min hdd count",
|
||||
)
|
||||
maxHddCount = flag.Int(
|
||||
"max-hdd-count",
|
||||
maxHddCount = flag.Int64(
|
||||
flagMaxHddCount,
|
||||
15,
|
||||
"set max hdd count",
|
||||
)
|
||||
|
||||
minBenchmark = flag.Int(
|
||||
"min-benchmark",
|
||||
minBenchmark = flag.Int64(
|
||||
flagMinBenchmark,
|
||||
0,
|
||||
"set min benchmark",
|
||||
)
|
||||
maxBenchmark = flag.Int(
|
||||
"max-benchmark",
|
||||
maxBenchmark = flag.Int64(
|
||||
flagMaxBenchmark,
|
||||
20000,
|
||||
"set max benchmark",
|
||||
)
|
||||
|
||||
alertOnScore = flag.Int(
|
||||
"alert-on-score",
|
||||
0,
|
||||
"set alert on score",
|
||||
serveHttp = flag.Bool(
|
||||
flagServeHttp,
|
||||
false,
|
||||
"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() {
|
||||
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{}
|
||||
err := client.NewClient().DoRequest(hetzner.MakeURL(), offers)
|
||||
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 {
|
||||
crawler := c.NewCrawler(*minPrice, *maxPrice, *minRam, *maxRam, *minHddSize, *maxHddSize, *minHddCount, *maxHddCount, *minBenchmark, *maxBenchmark)
|
||||
servers := crawler.Filter(offers.Server)
|
||||
c := crawler.NewCrawler(parameter)
|
||||
deals := c.Filter(offers.Server)
|
||||
|
||||
fmt.Printf("Got %d offers. Filtered offers: %d\n", len(offers.Server), len(servers))
|
||||
crawler.Print(servers)
|
||||
|
||||
//notifier := n.NewInstrumenter(*notifierRecipient, *notifierSender, *notifierPassword)
|
||||
switch output {
|
||||
case "json":
|
||||
writer.NewJsonWriter(w).Print(deals)
|
||||
break
|
||||
default:
|
||||
case "table":
|
||||
writer.NewTableWriter(w).Print(deals)
|
||||
break
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
fmt.Println("Got no offers.")
|
||||
return fmt.Errorf("got no offers.")
|
||||
}
|
||||
}
|
||||
|
|
27
writer/json.go
Normal file
27
writer/json.go
Normal 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
28
writer/table.go
Normal 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
7
writer/writer.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
package writer
|
||||
|
||||
import "github.com/mrahbar/my-bloody-hetzner-sb-notifier/hetzner"
|
||||
|
||||
type Writer interface {
|
||||
Print(deals hetzner.Deals)
|
||||
}
|
Reference in a new issue