Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ FPM_ARGS ?= \

NAME = scw
SRC = cmd/scw
PACKAGES = pkg/api pkg/commands pkg/utils pkg/cli pkg/sshcommand pkg/config pkg/scwversion
PACKAGES = pkg/api pkg/commands pkg/utils pkg/cli pkg/sshcommand pkg/config pkg/scwversion pkg/pricing
REV = $(shell git rev-parse HEAD || echo "nogit")
TAG = $(shell git describe --tags --always || echo "nogit")
BUILDER = scaleway-cli-builder
Expand Down Expand Up @@ -72,7 +72,7 @@ $(INSTALL_LIST): %_install:
$(IREF_LIST): %_iref: pkg/scwversion/version.go
$(GOTEST) -i ./$*
$(TEST_LIST): %_test:
$(GOTEST) ./$*
$(GOTEST) -v ./$*
$(FMT_LIST): %_fmt:
$(GOFMT) ./$*

Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1130,10 +1130,11 @@ $ scw inspect myserver | jq '.[0].public_ip.address'

#### Features

* `scw info` now prints user/organization info from the API ([#142](https://github.com/scaleway/scaleway-cli/issues/130)
* `scw info` now prints user/organization info from the API ([#130](https://github.com/scaleway/scaleway-cli/issues/130)
* Added helpers to manipulate new `user_data` API ([#150](https://github.com/scaleway/scaleway-cli/issues/150))
* Support of `scw rm -f/--force` option ([#158](https://github.com/scaleway/scaleway-cli/issues/158))
* Added `scw _userdata local ...` option which interacts with the Metadata API without authentication ([#166](https://github.com/scaleway/scaleway-cli/issues/166))
* Initial version of `scw _billing` (price estimation tool) ([#118](https://github.com/scaleway/scaleway-cli/issues/118)

#### Fixes

Expand Down
8 changes: 5 additions & 3 deletions pkg/cli/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,10 @@ var Commands = []*Command{

cmdAttach,
cmdCommit,
cmdCompletion,
cmdCp,
cmdCreate,
cmdEvents,
cmdExec,
cmdFlushCache,
cmdHistory,
cmdImages,
cmdInfo,
Expand All @@ -27,7 +25,6 @@ var Commands = []*Command{
cmdLogin,
cmdLogout,
cmdLogs,
cmdPatch,
cmdPort,
cmdPs,
cmdRename,
Expand All @@ -43,4 +40,9 @@ var Commands = []*Command{
cmdUserdata,
cmdVersion,
cmdWait,

cmdBilling,
cmdCompletion,
cmdFlushCache,
cmdPatch,
}
2 changes: 1 addition & 1 deletion pkg/cli/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ var (
"version", "wait",
}
secretCommands []string = []string{
"_patch", "_completion", "_flush-cache", "_userdata",
"_patch", "_completion", "_flush-cache", "_userdata", "_billing",
}
publicOptions []string = []string{
"-h, --help=false",
Expand Down
94 changes: 94 additions & 0 deletions pkg/cli/x_billing.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Copyright (C) 2015 Scaleway. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE.md file.

package cli

import (
"fmt"
"math/big"
"text/tabwriter"
"time"

"github.com/scaleway/scaleway-cli/pkg/commands"
"github.com/scaleway/scaleway-cli/pkg/pricing"
"github.com/scaleway/scaleway-cli/pkg/utils"
"github.com/scaleway/scaleway-cli/vendor/github.com/Sirupsen/logrus"
"github.com/scaleway/scaleway-cli/vendor/github.com/docker/docker/pkg/units"
)

var cmdBilling = &Command{
Exec: runBilling,
UsageLine: "_billing [OPTIONS]",
Description: "",
Hidden: true,
Help: "Get resources billing estimation",
}

func init() {
cmdBilling.Flag.BoolVar(&billingHelp, []string{"h", "-help"}, false, "Print usage")
cmdBilling.Flag.BoolVar(&billingNoTrunc, []string{"-no-trunc"}, false, "Don't truncate output")
}

// BillingArgs are flags for the `RunBilling` function
type BillingArgs struct {
NoTrunc bool
}

// Flags
var billingHelp bool // -h, --help flag
var billingNoTrunc bool // --no-trunc flag

func runBilling(cmd *Command, rawArgs []string) error {
if billingHelp {
return cmd.PrintUsage()
}
if len(rawArgs) > 0 {
return cmd.PrintShortUsage()
}

// cli parsing
args := commands.PsArgs{
NoTrunc: billingNoTrunc,
}
ctx := cmd.GetContext(rawArgs)

logrus.Warn("")
logrus.Warn("Warning: 'scw _billing' is a work-in-progress price estimation tool")
logrus.Warn("For real usage, visit https://cloud.scaleway.com/#/billing")
logrus.Warn("")

// table
w := tabwriter.NewWriter(ctx.Stdout, 20, 1, 3, ' ', 0)
defer w.Flush()
fmt.Fprintf(w, "ID\tNAME\tSTARTED\tMONTH PRICE\n")

// servers
servers, err := cmd.API.GetServers(true, 0)
if err != nil {
return err
}

totalMonthPrice := new(big.Rat)

for _, server := range *servers {
if server.State != "running" {
continue
}
shortID := utils.TruncIf(server.Identifier, 8, !args.NoTrunc)
shortName := utils.TruncIf(utils.Wordify(server.Name), 25, !args.NoTrunc)
modificationTime, _ := time.Parse("2006-01-02T15:04:05.000000+00:00", server.ModificationDate)
modificationAgo := time.Now().UTC().Sub(modificationTime)
shortModificationDate := units.HumanDuration(modificationAgo)
usage := pricing.NewUsageByPath("/compute/c1/run")
usage.SetStartEnd(modificationTime, time.Now().UTC())

totalMonthPrice = totalMonthPrice.Add(totalMonthPrice, usage.Total())

fmt.Fprintf(w, "server/%s\t%s\t%s\t%s\n", shortID, shortName, shortModificationDate, usage.TotalString())
}

fmt.Fprintf(w, "TOTAL\t\t\t%s\n", pricing.PriceString(totalMonthPrice, "EUR"))

return nil
}
41 changes: 41 additions & 0 deletions pkg/pricing/basket.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package pricing

import (
"math/big"
"time"
)

type Basket []Usage

func NewBasket() *Basket {
return &Basket{}
}

func (b *Basket) Add(usage Usage) error {
*b = append(*b, usage)
return nil
}

func (b *Basket) Length() int {
return len(*b)
}

func (b *Basket) SetDuration(duration time.Duration) error {
var err error
for i, usage := range *b {
err = usage.SetDuration(duration)
if err != nil {
return err
}
(*b)[i] = usage
}
return nil
}

func (b *Basket) Total() *big.Rat {
total := new(big.Rat)
for _, usage := range *b {
total = total.Add(total, usage.Total())
}
return total
}
87 changes: 87 additions & 0 deletions pkg/pricing/basket_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package pricing

import (
"math/big"
"testing"
"time"

. "github.com/scaleway/scaleway-cli/vendor/github.com/smartystreets/goconvey/convey"
)

func TestNewBasket(t *testing.T) {
Convey("Testing NewBasket()", t, func() {
basket := NewBasket()
So(basket, ShouldNotBeNil)
So(basket.Length(), ShouldEqual, 0)
})
}

func TestBasket_Add(t *testing.T) {
Convey("Testing Basket.Add", t, FailureContinues, func() {
basket := NewBasket()
So(basket, ShouldNotBeNil)
So(basket.Length(), ShouldEqual, 0)

err := basket.Add(NewUsageByPathWithQuantity("/compute/c1/run", big.NewRat(1, 1)))
So(err, ShouldBeNil)
So(basket.Length(), ShouldEqual, 1)

err = basket.Add(NewUsageByPathWithQuantity("/compute/c1/run", big.NewRat(42, 1)))
So(err, ShouldBeNil)
So(basket.Length(), ShouldEqual, 2)

err = basket.Add(NewUsageByPathWithQuantity("/compute/c1/run", big.NewRat(600, 1)))
So(err, ShouldBeNil)
So(basket.Length(), ShouldEqual, 3)
})
}

func TestBasket_Total(t *testing.T) {
Convey("Testing Basket.Total", t, FailureContinues, func() {
Convey("3 compute instances", func() {
basket := NewBasket()
So(basket, ShouldNotBeNil)
So(basket.Total(), ShouldEqualBigRat, ratZero)

err := basket.Add(NewUsageByPathWithQuantity("/compute/c1/run", big.NewRat(1, 1)))
So(err, ShouldBeNil)
So(basket.Total(), ShouldEqualBigRat, big.NewRat(2, 1000)) // 0.002

err = basket.Add(NewUsageByPathWithQuantity("/compute/c1/run", big.NewRat(42, 1)))
So(err, ShouldBeNil)
So(basket.Total(), ShouldEqualBigRat, big.NewRat(4, 1000)) // 0.004

err = basket.Add(NewUsageByPathWithQuantity("/compute/c1/run", big.NewRat(600, 1)))
So(err, ShouldBeNil)
So(basket.Total(), ShouldEqualBigRat, big.NewRat(24, 1000)) // 0.024
})
Convey("1 compute instance with 2 volumes and 1 ip", func() {
basket := NewBasket()

basket.Add(NewUsageByPath("/compute/c1/run"))
basket.Add(NewUsageByPath("/ip/dynamic"))
basket.Add(NewUsageByPath("/storage/local/ssd/storage"))
basket.Add(NewUsageByPath("/storage/local/ssd/storage"))
So(basket.Length(), ShouldEqual, 4)

basket.SetDuration(1 * time.Minute)
So(basket.Total(), ShouldEqualBigRat, big.NewRat(8, 1000)) // 0.008

basket.SetDuration(1 * time.Hour)
So(basket.Total(), ShouldEqualBigRat, big.NewRat(8, 1000)) // 0.008

basket.SetDuration(2 * time.Hour)
So(basket.Total(), ShouldEqualBigRat, big.NewRat(12, 1000)) // 0.012

basket.SetDuration(2 * 24 * time.Hour)
So(basket.Total(), ShouldEqualBigRat, big.NewRat(196, 1000)) // 0.196

basket.SetDuration(30 * 24 * time.Hour)
So(basket.Total(), ShouldEqualBigRat, big.NewRat(2050, 1000)) // 2.05

// FIXME: this test is false, the capacity is per month
basket.SetDuration(365 * 24 * time.Hour)
So(basket.Total(), ShouldEqualBigRat, big.NewRat(2694, 1000)) // 2.694
})
})
}
85 changes: 85 additions & 0 deletions pkg/pricing/pricing.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package pricing

import (
"math/big"
"time"
)

type PricingObject struct {
Path string
Identifier string
Currency string
UsageUnit string
UnitPrice *big.Rat
UnitQuantity *big.Rat
UnitPriceCap *big.Rat
UsageGranularity time.Duration
}

type PricingList []PricingObject

// CurrentPricing tries to be up-to-date with the real pricing
// we cannot guarantee of these values since we hardcode values for now
// later, we should be able to call a dedicated pricing API
var CurrentPricing PricingList

func init() {
CurrentPricing = PricingList{
{
Path: "/compute/c1/run",
Identifier: "aaaaaaaa-aaaa-4aaa-8aaa-111111111112",
Currency: "EUR",
UnitPrice: big.NewRat(2, 1000), // 0.002
UnitQuantity: big.NewRat(60000, 1000), // 60
UnitPriceCap: big.NewRat(1000, 1000), // 1
UsageGranularity: time.Minute,
},
{
Path: "/ip/dynamic",
Identifier: "467116bf-4631-49fb-905b-e07701c21111",
Currency: "EUR",
UnitPrice: big.NewRat(2, 1000), // 0.002
UnitQuantity: big.NewRat(60000, 1000), // 60
UnitPriceCap: big.NewRat(990, 1000), // 0.99
UsageGranularity: time.Minute,
},
{
Path: "/ip/reserved",
Identifier: "467116bf-4631-49fb-905b-e07701c22222",
Currency: "EUR",
UnitPrice: big.NewRat(2, 1000), // 0.002
UnitQuantity: big.NewRat(60000, 1000), // 60
UnitPriceCap: big.NewRat(990, 1000), // 0.99
UsageGranularity: time.Minute,
},
{
Path: "/storage/local/ssd/storage",
Identifier: "bbbbbbbb-bbbb-4bbb-8bbb-111111111144",
Currency: "EUR",
UnitPrice: big.NewRat(2, 1000), // 0.002
UnitQuantity: big.NewRat(50000, 1000), // 50
UnitPriceCap: big.NewRat(1000, 1000), // 1
UsageGranularity: time.Hour,
},
}
}

// GetByPath returns an object matching a path
func (pl *PricingList) GetByPath(path string) *PricingObject {
for _, object := range *pl {
if object.Path == path {
return &object
}
}
return nil
}

// GetByIdentifier returns an object matching a identifier
func (pl *PricingList) GetByIdentifier(identifier string) *PricingObject {
for _, object := range *pl {
if object.Identifier == identifier {
return &object
}
}
return nil
}
Loading