From 4a726291a26f0fb2ad61cde5eee2a3ee8c681403 Mon Sep 17 00:00:00 2001 From: Mahmoud Rahbar Azad Date: Fri, 26 Oct 2018 22:31:39 +0200 Subject: [PATCH] added serve-http option --- Readme.md | 16 +- crawler/crawler.go | 69 ++++----- go.mod | 1 + hetzner/types.go | 22 ++- instrumentation/instrumentation.go | 11 -- main.go | 228 +++++++++++++++++++++++++---- writer/json.go | 27 ++++ writer/table.go | 28 ++++ writer/writer.go | 7 + 9 files changed, 313 insertions(+), 96 deletions(-) delete mode 100644 instrumentation/instrumentation.go create mode 100644 writer/json.go create mode 100644 writer/table.go create mode 100644 writer/writer.go diff --git a/Readme.md b/Readme.md index 7f1f48d..ad2ffe0 100644 --- a/Readme.md +++ b/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 diff --git a/crawler/crawler.go b/crawler/crawler.go index d01b7ac..cfc21cc 100644 --- a/crawler/crawler.go +++ b/crawler/crawler.go @@ -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 } diff --git a/go.mod b/go.mod index df68788..8c35265 100644 --- a/go.mod +++ b/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 diff --git a/hetzner/types.go b/hetzner/types.go index 865cc11..6366550 100644 --- a/hetzner/types.go +++ b/hetzner/types.go @@ -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 } diff --git a/instrumentation/instrumentation.go b/instrumentation/instrumentation.go deleted file mode 100644 index 74836c5..0000000 --- a/instrumentation/instrumentation.go +++ /dev/null @@ -1,11 +0,0 @@ -package instrumentation - -type Instrumenter struct { - projectID string -} - -func NewInstrumenter(projectID string) *Instrumenter { - return &Instrumenter{ - projectID: projectID, - } -} diff --git a/main.go b/main.go index 02f66b4..091443b 100644 --- a/main.go +++ b/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.") } } diff --git a/writer/json.go b/writer/json.go new file mode 100644 index 0000000..cd45f46 --- /dev/null +++ b/writer/json.go @@ -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)) +} diff --git a/writer/table.go b/writer/table.go new file mode 100644 index 0000000..630800d --- /dev/null +++ b/writer/table.go @@ -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() +} diff --git a/writer/writer.go b/writer/writer.go new file mode 100644 index 0000000..88fdbf4 --- /dev/null +++ b/writer/writer.go @@ -0,0 +1,7 @@ +package writer + +import "github.com/mrahbar/my-bloody-hetzner-sb-notifier/hetzner" + +type Writer interface { + Print(deals hetzner.Deals) +}