Skip to content

Commit f8fc935

Browse files
committed
Merge pull request #146 from moul/feature-billing
Feature billing
2 parents 98d31eb + c1651df commit f8fc935

File tree

13 files changed

+766
-7
lines changed

13 files changed

+766
-7
lines changed

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ FPM_ARGS ?= \
2525

2626
NAME = scw
2727
SRC = cmd/scw
28-
PACKAGES = pkg/api pkg/commands pkg/utils pkg/cli pkg/sshcommand pkg/config pkg/scwversion
28+
PACKAGES = pkg/api pkg/commands pkg/utils pkg/cli pkg/sshcommand pkg/config pkg/scwversion pkg/pricing
2929
REV = $(shell git rev-parse HEAD || echo "nogit")
3030
TAG = $(shell git describe --tags --always || echo "nogit")
3131
BUILDER = scaleway-cli-builder
@@ -72,7 +72,7 @@ $(INSTALL_LIST): %_install:
7272
$(IREF_LIST): %_iref: pkg/scwversion/version.go
7373
$(GOTEST) -i ./$*
7474
$(TEST_LIST): %_test:
75-
$(GOTEST) ./$*
75+
$(GOTEST) -v ./$*
7676
$(FMT_LIST): %_fmt:
7777
$(GOFMT) ./$*
7878

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1130,10 +1130,11 @@ $ scw inspect myserver | jq '.[0].public_ip.address'
11301130

11311131
#### Features
11321132

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

11381139
#### Fixes
11391140

pkg/cli/commands.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,10 @@ var Commands = []*Command{
1313

1414
cmdAttach,
1515
cmdCommit,
16-
cmdCompletion,
1716
cmdCp,
1817
cmdCreate,
1918
cmdEvents,
2019
cmdExec,
21-
cmdFlushCache,
2220
cmdHistory,
2321
cmdImages,
2422
cmdInfo,
@@ -27,7 +25,6 @@ var Commands = []*Command{
2725
cmdLogin,
2826
cmdLogout,
2927
cmdLogs,
30-
cmdPatch,
3128
cmdPort,
3229
cmdPs,
3330
cmdRename,
@@ -43,4 +40,9 @@ var Commands = []*Command{
4340
cmdUserdata,
4441
cmdVersion,
4542
cmdWait,
43+
44+
cmdBilling,
45+
cmdCompletion,
46+
cmdFlushCache,
47+
cmdPatch,
4648
}

pkg/cli/test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ var (
1717
"version", "wait",
1818
}
1919
secretCommands []string = []string{
20-
"_patch", "_completion", "_flush-cache", "_userdata",
20+
"_patch", "_completion", "_flush-cache", "_userdata", "_billing",
2121
}
2222
publicOptions []string = []string{
2323
"-h, --help=false",

pkg/cli/x_billing.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// Copyright (C) 2015 Scaleway. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE.md file.
4+
5+
package cli
6+
7+
import (
8+
"fmt"
9+
"math/big"
10+
"text/tabwriter"
11+
"time"
12+
13+
"github.com/scaleway/scaleway-cli/pkg/commands"
14+
"github.com/scaleway/scaleway-cli/pkg/pricing"
15+
"github.com/scaleway/scaleway-cli/pkg/utils"
16+
"github.com/scaleway/scaleway-cli/vendor/github.com/Sirupsen/logrus"
17+
"github.com/scaleway/scaleway-cli/vendor/github.com/docker/docker/pkg/units"
18+
)
19+
20+
var cmdBilling = &Command{
21+
Exec: runBilling,
22+
UsageLine: "_billing [OPTIONS]",
23+
Description: "",
24+
Hidden: true,
25+
Help: "Get resources billing estimation",
26+
}
27+
28+
func init() {
29+
cmdBilling.Flag.BoolVar(&billingHelp, []string{"h", "-help"}, false, "Print usage")
30+
cmdBilling.Flag.BoolVar(&billingNoTrunc, []string{"-no-trunc"}, false, "Don't truncate output")
31+
}
32+
33+
// BillingArgs are flags for the `RunBilling` function
34+
type BillingArgs struct {
35+
NoTrunc bool
36+
}
37+
38+
// Flags
39+
var billingHelp bool // -h, --help flag
40+
var billingNoTrunc bool // --no-trunc flag
41+
42+
func runBilling(cmd *Command, rawArgs []string) error {
43+
if billingHelp {
44+
return cmd.PrintUsage()
45+
}
46+
if len(rawArgs) > 0 {
47+
return cmd.PrintShortUsage()
48+
}
49+
50+
// cli parsing
51+
args := commands.PsArgs{
52+
NoTrunc: billingNoTrunc,
53+
}
54+
ctx := cmd.GetContext(rawArgs)
55+
56+
logrus.Warn("")
57+
logrus.Warn("Warning: 'scw _billing' is a work-in-progress price estimation tool")
58+
logrus.Warn("For real usage, visit https://cloud.scaleway.com/#/billing")
59+
logrus.Warn("")
60+
61+
// table
62+
w := tabwriter.NewWriter(ctx.Stdout, 20, 1, 3, ' ', 0)
63+
defer w.Flush()
64+
fmt.Fprintf(w, "ID\tNAME\tSTARTED\tMONTH PRICE\n")
65+
66+
// servers
67+
servers, err := cmd.API.GetServers(true, 0)
68+
if err != nil {
69+
return err
70+
}
71+
72+
totalMonthPrice := new(big.Rat)
73+
74+
for _, server := range *servers {
75+
if server.State != "running" {
76+
continue
77+
}
78+
shortID := utils.TruncIf(server.Identifier, 8, !args.NoTrunc)
79+
shortName := utils.TruncIf(utils.Wordify(server.Name), 25, !args.NoTrunc)
80+
modificationTime, _ := time.Parse("2006-01-02T15:04:05.000000+00:00", server.ModificationDate)
81+
modificationAgo := time.Now().UTC().Sub(modificationTime)
82+
shortModificationDate := units.HumanDuration(modificationAgo)
83+
usage := pricing.NewUsageByPath("/compute/c1/run")
84+
usage.SetStartEnd(modificationTime, time.Now().UTC())
85+
86+
totalMonthPrice = totalMonthPrice.Add(totalMonthPrice, usage.Total())
87+
88+
fmt.Fprintf(w, "server/%s\t%s\t%s\t%s\n", shortID, shortName, shortModificationDate, usage.TotalString())
89+
}
90+
91+
fmt.Fprintf(w, "TOTAL\t\t\t%s\n", pricing.PriceString(totalMonthPrice, "EUR"))
92+
93+
return nil
94+
}

pkg/pricing/basket.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package pricing
2+
3+
import (
4+
"math/big"
5+
"time"
6+
)
7+
8+
type Basket []Usage
9+
10+
func NewBasket() *Basket {
11+
return &Basket{}
12+
}
13+
14+
func (b *Basket) Add(usage Usage) error {
15+
*b = append(*b, usage)
16+
return nil
17+
}
18+
19+
func (b *Basket) Length() int {
20+
return len(*b)
21+
}
22+
23+
func (b *Basket) SetDuration(duration time.Duration) error {
24+
var err error
25+
for i, usage := range *b {
26+
err = usage.SetDuration(duration)
27+
if err != nil {
28+
return err
29+
}
30+
(*b)[i] = usage
31+
}
32+
return nil
33+
}
34+
35+
func (b *Basket) Total() *big.Rat {
36+
total := new(big.Rat)
37+
for _, usage := range *b {
38+
total = total.Add(total, usage.Total())
39+
}
40+
return total
41+
}

pkg/pricing/basket_test.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package pricing
2+
3+
import (
4+
"math/big"
5+
"testing"
6+
"time"
7+
8+
. "github.com/scaleway/scaleway-cli/vendor/github.com/smartystreets/goconvey/convey"
9+
)
10+
11+
func TestNewBasket(t *testing.T) {
12+
Convey("Testing NewBasket()", t, func() {
13+
basket := NewBasket()
14+
So(basket, ShouldNotBeNil)
15+
So(basket.Length(), ShouldEqual, 0)
16+
})
17+
}
18+
19+
func TestBasket_Add(t *testing.T) {
20+
Convey("Testing Basket.Add", t, FailureContinues, func() {
21+
basket := NewBasket()
22+
So(basket, ShouldNotBeNil)
23+
So(basket.Length(), ShouldEqual, 0)
24+
25+
err := basket.Add(NewUsageByPathWithQuantity("/compute/c1/run", big.NewRat(1, 1)))
26+
So(err, ShouldBeNil)
27+
So(basket.Length(), ShouldEqual, 1)
28+
29+
err = basket.Add(NewUsageByPathWithQuantity("/compute/c1/run", big.NewRat(42, 1)))
30+
So(err, ShouldBeNil)
31+
So(basket.Length(), ShouldEqual, 2)
32+
33+
err = basket.Add(NewUsageByPathWithQuantity("/compute/c1/run", big.NewRat(600, 1)))
34+
So(err, ShouldBeNil)
35+
So(basket.Length(), ShouldEqual, 3)
36+
})
37+
}
38+
39+
func TestBasket_Total(t *testing.T) {
40+
Convey("Testing Basket.Total", t, FailureContinues, func() {
41+
Convey("3 compute instances", func() {
42+
basket := NewBasket()
43+
So(basket, ShouldNotBeNil)
44+
So(basket.Total(), ShouldEqualBigRat, ratZero)
45+
46+
err := basket.Add(NewUsageByPathWithQuantity("/compute/c1/run", big.NewRat(1, 1)))
47+
So(err, ShouldBeNil)
48+
So(basket.Total(), ShouldEqualBigRat, big.NewRat(2, 1000)) // 0.002
49+
50+
err = basket.Add(NewUsageByPathWithQuantity("/compute/c1/run", big.NewRat(42, 1)))
51+
So(err, ShouldBeNil)
52+
So(basket.Total(), ShouldEqualBigRat, big.NewRat(4, 1000)) // 0.004
53+
54+
err = basket.Add(NewUsageByPathWithQuantity("/compute/c1/run", big.NewRat(600, 1)))
55+
So(err, ShouldBeNil)
56+
So(basket.Total(), ShouldEqualBigRat, big.NewRat(24, 1000)) // 0.024
57+
})
58+
Convey("1 compute instance with 2 volumes and 1 ip", func() {
59+
basket := NewBasket()
60+
61+
basket.Add(NewUsageByPath("/compute/c1/run"))
62+
basket.Add(NewUsageByPath("/ip/dynamic"))
63+
basket.Add(NewUsageByPath("/storage/local/ssd/storage"))
64+
basket.Add(NewUsageByPath("/storage/local/ssd/storage"))
65+
So(basket.Length(), ShouldEqual, 4)
66+
67+
basket.SetDuration(1 * time.Minute)
68+
So(basket.Total(), ShouldEqualBigRat, big.NewRat(8, 1000)) // 0.008
69+
70+
basket.SetDuration(1 * time.Hour)
71+
So(basket.Total(), ShouldEqualBigRat, big.NewRat(8, 1000)) // 0.008
72+
73+
basket.SetDuration(2 * time.Hour)
74+
So(basket.Total(), ShouldEqualBigRat, big.NewRat(12, 1000)) // 0.012
75+
76+
basket.SetDuration(2 * 24 * time.Hour)
77+
So(basket.Total(), ShouldEqualBigRat, big.NewRat(196, 1000)) // 0.196
78+
79+
basket.SetDuration(30 * 24 * time.Hour)
80+
So(basket.Total(), ShouldEqualBigRat, big.NewRat(2050, 1000)) // 2.05
81+
82+
// FIXME: this test is false, the capacity is per month
83+
basket.SetDuration(365 * 24 * time.Hour)
84+
So(basket.Total(), ShouldEqualBigRat, big.NewRat(2694, 1000)) // 2.694
85+
})
86+
})
87+
}

pkg/pricing/pricing.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package pricing
2+
3+
import (
4+
"math/big"
5+
"time"
6+
)
7+
8+
type PricingObject struct {
9+
Path string
10+
Identifier string
11+
Currency string
12+
UsageUnit string
13+
UnitPrice *big.Rat
14+
UnitQuantity *big.Rat
15+
UnitPriceCap *big.Rat
16+
UsageGranularity time.Duration
17+
}
18+
19+
type PricingList []PricingObject
20+
21+
// CurrentPricing tries to be up-to-date with the real pricing
22+
// we cannot guarantee of these values since we hardcode values for now
23+
// later, we should be able to call a dedicated pricing API
24+
var CurrentPricing PricingList
25+
26+
func init() {
27+
CurrentPricing = PricingList{
28+
{
29+
Path: "/compute/c1/run",
30+
Identifier: "aaaaaaaa-aaaa-4aaa-8aaa-111111111112",
31+
Currency: "EUR",
32+
UnitPrice: big.NewRat(2, 1000), // 0.002
33+
UnitQuantity: big.NewRat(60000, 1000), // 60
34+
UnitPriceCap: big.NewRat(1000, 1000), // 1
35+
UsageGranularity: time.Minute,
36+
},
37+
{
38+
Path: "/ip/dynamic",
39+
Identifier: "467116bf-4631-49fb-905b-e07701c21111",
40+
Currency: "EUR",
41+
UnitPrice: big.NewRat(2, 1000), // 0.002
42+
UnitQuantity: big.NewRat(60000, 1000), // 60
43+
UnitPriceCap: big.NewRat(990, 1000), // 0.99
44+
UsageGranularity: time.Minute,
45+
},
46+
{
47+
Path: "/ip/reserved",
48+
Identifier: "467116bf-4631-49fb-905b-e07701c22222",
49+
Currency: "EUR",
50+
UnitPrice: big.NewRat(2, 1000), // 0.002
51+
UnitQuantity: big.NewRat(60000, 1000), // 60
52+
UnitPriceCap: big.NewRat(990, 1000), // 0.99
53+
UsageGranularity: time.Minute,
54+
},
55+
{
56+
Path: "/storage/local/ssd/storage",
57+
Identifier: "bbbbbbbb-bbbb-4bbb-8bbb-111111111144",
58+
Currency: "EUR",
59+
UnitPrice: big.NewRat(2, 1000), // 0.002
60+
UnitQuantity: big.NewRat(50000, 1000), // 50
61+
UnitPriceCap: big.NewRat(1000, 1000), // 1
62+
UsageGranularity: time.Hour,
63+
},
64+
}
65+
}
66+
67+
// GetByPath returns an object matching a path
68+
func (pl *PricingList) GetByPath(path string) *PricingObject {
69+
for _, object := range *pl {
70+
if object.Path == path {
71+
return &object
72+
}
73+
}
74+
return nil
75+
}
76+
77+
// GetByIdentifier returns an object matching a identifier
78+
func (pl *PricingList) GetByIdentifier(identifier string) *PricingObject {
79+
for _, object := range *pl {
80+
if object.Identifier == identifier {
81+
return &object
82+
}
83+
}
84+
return nil
85+
}

0 commit comments

Comments
 (0)