Skip to content

Commit

Permalink
add liquid-cronus
Browse files Browse the repository at this point in the history
  • Loading branch information
majewsky committed Sep 18, 2024
1 parent cc14932 commit 065e67c
Show file tree
Hide file tree
Showing 4 changed files with 256 additions and 0 deletions.
19 changes: 19 additions & 0 deletions docs/liquids/cronus.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Liquid: `cronus`

This liquid provides support for the email service Cronus (SAP Converged Cloud internal only).

- The suggested service type is `liquid-cronus`.
- The suggested area is `email`.

## Service-specific configuration

None.

## Rates

| Rates | Unit | Capabilities |
| ---------------------- | ---- | --------------- |
| `attachment_size` | `B` | HasUsage = true |
| `data_transfer_in` | `B` | HasUsage = true |
| `data_transfer_out` | `B` | HasUsage = true |
| `recipients` | None | HasUsage = true |
1 change: 1 addition & 0 deletions docs/liquids/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ If the service does not provide LIQUID support itself, you can use one of the li

- [`archer`](./archer.md) for the endpoint injection service [Archer](https:/sapcc/archer)
- [`cinder`](./cinder.md) for the block storage service Cinder
- [`cronus`](./cronus.md) for the email service Cronus (SAP Converged Cloud internal only)
- [`designate`](./designate.md) for the DNS service Designate
- [`manila`](./manila.md) for the shared file system storage service Manila
- [`neutron`](./neutron.md) for the networking service Neutron
Expand Down
78 changes: 78 additions & 0 deletions internal/liquids/cronus/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*******************************************************************************
*
* Copyright 2020 SAP SE
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You should have received a copy of the License along with this
* program. If not, you may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*******************************************************************************/

package cronus

import (
"context"
"net/http"

"github.com/gophercloud/gophercloud/v2"
)

// Client is a gophercloud.ServiceClient for the Cronus v1 API.
type Client struct {
*gophercloud.ServiceClient
}

func NewClient(provider *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*Client, error) {
serviceType := "email-aws"
eo.ApplyDefaults(serviceType)

url, err := provider.EndpointLocator(eo)
if err != nil {
return nil, err
}
return &Client{
ServiceClient: &gophercloud.ServiceClient{
ProviderClient: provider,
Endpoint: url,
Type: serviceType,
},
}, nil
}

// Usage contains Cronus usage data for a single project.
type Usage struct {
AttachmentsSize uint64 `json:"attachments_size"`
DataTransferIn uint64 `json:"data_transfer_in"`
DataTransferOut uint64 `json:"data_transfer_out"`
Recipients uint64 `json:"recipients"`
StartDate string `json:"start"`
EndDate string `json:"end"`
}

// GetUsage returns usage data for a single project.
func (c Client) GetUsage(ctx context.Context, projectUUID string, previous bool) (Usage, error) {
url := c.ServiceURL("v1", "usage", projectUUID)
if previous {
url += "?prev=true"
}

var result gophercloud.Result
_, result.Err = c.Get(ctx, url, &result.Body, &gophercloud.RequestOpts{ //nolint:bodyclose // already closed by gophercloud
OkCodes: []int{http.StatusOK},
})

var data struct {
Usage Usage `json:"usage"`
}
err := result.ExtractInto(&data)
return data.Usage, err
}
158 changes: 158 additions & 0 deletions internal/liquids/cronus/liquid.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/*******************************************************************************
*
* Copyright 2020-2024 SAP SE
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You should have received a copy of the License along with this
* program. If not, you may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*******************************************************************************/

package cronus

import (
"context"
"encoding/json"
"fmt"
"math/big"

"github.com/gophercloud/gophercloud/v2"
"github.com/sapcc/go-api-declarations/limes"
"github.com/sapcc/go-api-declarations/liquid"
"github.com/sapcc/go-bits/logg"
)

type Logic struct {
// connections
CronusV1 *Client `json:"-"`
}

// Init implements the liquidapi.Logic interface.
func (l *Logic) Init(ctx context.Context, provider *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (err error) {
l.CronusV1, err = NewClient(provider, eo)
return err
}

// BuildServiceInfo implements the liquidapi.Logic interface.
func (l *Logic) BuildServiceInfo(ctx context.Context) (liquid.ServiceInfo, error) {
return liquid.ServiceInfo{
Version: 1,
Rates: map[liquid.RateName]liquid.RateInfo{
"attachment_size": {HasUsage: true, Unit: limes.UnitBytes},
"data_transfer_in": {HasUsage: true, Unit: limes.UnitBytes},
"data_transfer_out": {HasUsage: true, Unit: limes.UnitBytes},
"recipients": {HasUsage: true, Unit: limes.UnitNone},
},
}, nil
}

// ScanCapacity implements the liquidapi.Logic interface.
func (l *Logic) ScanCapacity(ctx context.Context, req liquid.ServiceCapacityRequest, serviceInfo liquid.ServiceInfo) (liquid.ServiceCapacityReport, error) {
// no resources report capacity
return liquid.ServiceCapacityReport{InfoVersion: serviceInfo.Version}, nil
}

// The payload format for this liquid's SerializedState.
type cronusState struct {
PreviousTotals struct {
AttachmentsSize *big.Int `json:"attachments_size"`
DataTransferIn *big.Int `json:"data_transfer_in"`
DataTransferOut *big.Int `json:"data_transfer_out"`
Recipients *big.Int `json:"recipients"`
} `json:"previous_totals"`
CurrentPeriod struct {
StartDate string `json:"start"`
} `json:"current_period"`
}

// ScanUsage implements the liquidapi.Logic interface.
func (l *Logic) ScanUsage(ctx context.Context, projectUUID string, req liquid.ServiceUsageRequest, serviceInfo liquid.ServiceInfo) (liquid.ServiceUsageReport, error) {
// decode previous SerializedState
var state cronusState
if len(req.SerializedState) == 0 {
// on first scrape, start with a default value that causes us to open a new billing period immediately down below
state.PreviousTotals.AttachmentsSize = big.NewInt(0)
state.PreviousTotals.DataTransferIn = big.NewInt(0)
state.PreviousTotals.DataTransferOut = big.NewInt(0)
state.PreviousTotals.Recipients = big.NewInt(0)
state.CurrentPeriod.StartDate = "1970-01-01"
} else {
err := json.Unmarshal([]byte(req.SerializedState), &state)
if err != nil {
return liquid.ServiceUsageReport{}, fmt.Errorf("cannot decode prevSerializedState: %w", err)
}
}

// get usage for the current billing period
currentUsage, err := l.CronusV1.GetUsage(ctx, projectUUID, false)
if err != nil {
return liquid.ServiceUsageReport{}, err
}
logg.Debug("currentUsage = %#v", currentUsage)

// if a new billing period has started, add the previous billing period's
// final tally into `state.PreviousTotals`
var newSerializedState json.RawMessage
if state.CurrentPeriod.StartDate == currentUsage.StartDate {
newSerializedState = req.SerializedState
} else {
prevUsage, err := l.CronusV1.GetUsage(ctx, projectUUID, true)
if err != nil {
return liquid.ServiceUsageReport{}, err
}
logg.Debug("prevUsage = %#v", prevUsage)
if state.CurrentPeriod.StartDate != prevUsage.StartDate && state.CurrentPeriod.StartDate != "1970-01-01" {
return liquid.ServiceUsageReport{}, fmt.Errorf(
"cannot start new billing period: expected previous billing period to end by %s, but actually ended %s",
state.CurrentPeriod.StartDate, prevUsage.StartDate,
)
}

state.PreviousTotals.AttachmentsSize = bigintPlusUint64(state.PreviousTotals.AttachmentsSize, prevUsage.AttachmentsSize)
state.PreviousTotals.DataTransferIn = bigintPlusUint64(state.PreviousTotals.DataTransferIn, prevUsage.DataTransferIn)
state.PreviousTotals.DataTransferOut = bigintPlusUint64(state.PreviousTotals.DataTransferOut, prevUsage.DataTransferOut)
state.PreviousTotals.Recipients = bigintPlusUint64(state.PreviousTotals.Recipients, prevUsage.Recipients)
state.CurrentPeriod.StartDate = currentUsage.StartDate

newSerializedStateBytes, err := json.Marshal(state)
if err != nil {
return liquid.ServiceUsageReport{}, fmt.Errorf("cannot serialize new state: %w", err)
}
newSerializedState = json.RawMessage(newSerializedStateBytes)
}

// obtain the current running totals by adding the current billing period's
// running tally to the previous totals
buildRateReport := func(previous *big.Int, current uint64) *liquid.RateUsageReport {
return &liquid.RateUsageReport{
PerAZ: liquid.InAnyAZ(liquid.AZRateUsageReport{
Usage: bigintPlusUint64(previous, current),
}),
}
}
return liquid.ServiceUsageReport{
Rates: map[liquid.RateName]*liquid.RateUsageReport{
"attachment_size": buildRateReport(state.PreviousTotals.AttachmentsSize, currentUsage.AttachmentsSize),
"data_transfer_in": buildRateReport(state.PreviousTotals.DataTransferIn, currentUsage.DataTransferIn),
"data_transfer_out": buildRateReport(state.PreviousTotals.DataTransferOut, currentUsage.DataTransferOut),
"recipients": buildRateReport(state.PreviousTotals.Recipients, currentUsage.Recipients),
},
SerializedState: newSerializedState,
}, nil
}

func bigintPlusUint64(a *big.Int, u uint64) *big.Int {
var b big.Int
b.SetUint64(u)
var c big.Int
return c.Add(a, &b)
}

0 comments on commit 065e67c

Please sign in to comment.