cloudstack icon indicating copy to clipboard operation
cloudstack copied to clipboard

Quota: custom tariffs

Open GutoVeronezi opened this issue 3 years ago • 4 comments

ISSUE TYPE
  • Enhancement Request
COMPONENT NAME
Quota, billing
CLOUDSTACK VERSION

4.16/main

SUMMARY

This spec changes the Cloudstack's Quota Plugin to allow operators to customize tariffs based on characteristics of the billed resources.


Table of Contents

  • Problem description
    • Current workflows
      • Calculate usage
      • List tariff
      • Update tariff
  • Proposed changes
    • Proposed workflows
      • Calculate usage
      • List tariff
      • Update tariff
      • Create tariff
      • Delete tariff
    • Rules processing and variables
      • Default
      • RUNNING_VM
      • ALLOCATED_VM
      • VOLUME
      • TEMPLATE/ISO
      • SNAPSHOT
      • NETWORK_OFFERING
      • VM_SNAPSHOT
      • Others resources
    • Script samples
    • Billing example
  • Work items
  • Future works

Problem description

Currently, ACS's Quota Plugin accounts for different resources:

RUNNING_VM
ALLOCATED_VM
IP_ADDRESS
NETWORK_BYTES_SENT
NETWORK_BYTES_RECEIVED
VOLUME
TEMPLATE
ISO
SNAPSHOT
SECURITY_GROUP
LOAD_BALANCER_POLICY
PORT_FORWARDING_RULE
NETWORK_OFFERING
VPN_USERS
CPU_SPEED
vCPU
MEMORY
VM_DISK_IO_READ
VM_DISK_IO_WRITE
VM_DISK_BYTES_READ
VM_DISK_BYTES_WRITE
VM_SNAPSHOT

Each element assumes only one tariff/price, turning it into an one to one relationship:

model-resource-tariff

Therefore, for example, for every RUNNING_VM, we must apply the same tariff/price, as well as every ALLOCATED_VM. However, there are situations where one resource needs to have different tariffs/prices. These situations can be related to characteristics of the business, such as Windows (or other O.S.) licensing, performance of the primary storage where volumes are allocated, and who is the owner of the resource. Examples of mapped cases till now:

Characteristic Example
owner (account/domain/project) Owner X has a special contract and will pay a different price per resource.
volume of allocated resource for the owner If owner X has less than 10 VMs, the owner will pay Y, otherwise, Z.
O.S. VMs with Windows (or other built-in licensing) costs more.
storage tags Volumes with tag SSD NVME costs more.
host tags VMs with tag CPU platinum costs more.

Also, there are three (3) running VM resources in separated tariffs:

  • CPU_SPEED (CPU_CLOCK_RATE in ACS's enum);
  • vCPU (CPU_NUMBER in ACS's enum);
  • MEMORY (MEMORY in ACS's enum);

They are accounted together with RUNNING_VM, however, only to fixed service offerings.


Current workflows

Current calculate usage, list tariff and update tariff workflows are:


  • Calculate usage:
calculate-usage
  • List tariff: list-tariff

  • Update tariff: update-tariff

Proposed changes

This proposal intends to change the paradigm of the feature by allowing tariff customization and making the relationship between resource and tariff one to zero or many:

model-resource-tariff

For each one of the resources (listed in the section Problem description), operators will be able to create many tariffs as needed and define (or not) the activation rules for each tariff and its duration (the start date will always be required). Activation rules will be used to define if a tariff should be applied to a resource being rated; if no activation rule is provided, we assume that it is applied to all resources of the given type. When updating the tariff, the previous one will be removed and a new one will be created. However, to ensure traceability of the record, a common identifier will be kept.

As in this proposal the tariffs will be custom, instead of having one tariff type for each VM resource, the values will be injected as preset variables into the RUNNING_VM resource, allowing operators to charge them. The current vCPU, CPU_SPEED and MEMORY tariffs will be converted to RUNNING_VM tariffs with respective activation rules and their types will be removed from ACS.


Proposed workflows

Proposed calculate usage, list tariff and update tariff workflows are:


  • Calculate usage:
calculate-usage
  • List tariff:
list-tariff

This API will receive three (3) new parameters:

Parameters Description Required Value
name To retrieve tariff by its name. No The name of the tariff.
enddate To retrieve tariffs with end date less or equal to the parameter. No Any date (format yyyy-MM-dd).
listall To retrieve even removed tariffs. No true or false.

  • Update tariff:
update-tariff
Parameters Description Required Value
id UUID of the tariff to update. Yes The UUID of the tariff.
description Description of the tariff. No Any string (max 65535 characters).
value The price of the tariff. No Any float value.
activationrule The rule to apply the tariff. Null means that it will always by applied. No Any JavaScript code (max 65535 characters).
enddate Date when the tariff will stop to be applied. No Any date (format yyyy-MM-dd) from the current date and after or equal startdate.

The parameter usagetype will be kept, however it will not be used and a warning message is going to show that it is ignored for the request. Moreover, it will be removed in future releases.


Also, will be necessary to create two (2) new APIs:


  • Create tariff: this API will allow operators to create new tariffs for the listed resources;
create-tariff
Parameters Description Required Value
name An unique name for the tariff. Yes Any string (max 65535 characters).
description Description of the tariff. No Any string (max 65535 characters).
usagetype Resource type of the tariff. Yes Any of the resource types (listed in the section Problem description).
value The price of the tariff. Yes Any float value.
activationrule The rule to apply the tariff. Null means that it will always by applied. No Any JavaScript code (max 65535 characters).
startdate Date when the tariff will start to be applied. No Any date from the current date. If this parameter is not informed, the default value will be D+1.
enddate Date when the tariff will stop to be applied. No Any date from the current date and after or equal startdate.

  • Delete tariff: this API will mark the tariff as removed;
delete-tariff
Parameters Description Required Value
id The UUID of the tariff. Yes The UUID of the tariff.

Rules processing and variables

To process the activation rules, it will be used the library J2V8 which "...is a set of Java bindings for V8. J2V8 focuses on performance and tight integration with V8...". It has a considerable relevance, good performance and is easy to implement. Therefore, the activation rule expressions must be written in JavaScript code[^1] and return a boolean or number value[^2]. If there is no expression to be evaluated or the expression is empty, the tariff will always be applied.

Some variables will be pre-created into the code's context to give more flexibility to operators. Each resource type will have a series of variables corresponding to their characteristics:


Default

Variable Description
account.id UUID of the account owner of the resource.
account.name Name of the account owner of the resource.
account.role.id UUID of the role of the account owner of the resource (if exists).
account.role.name Name of the role of the account owner of the resource (if exists).
account.role.type Type of the role of the account owner of the resource (if exists).
domain.id UUID of the domain owner of the resource.
domain.name Name of the domain owner of the resource.
domain.path Path of the domain owner of the resource.
project.id UUID of the project owner of the resource (if exists).
project.name Name of the project owner of the resource (if exists).
resourceType Type of the record.
value.accountResources List of resources of the account between the start and end date of the usage record being calculated (i.e.: [{zoneId: ..., domainId:...}]).
zone.id UUID of the zone owner of the resource.
zone.name Name of the zone owner of the resource.

RUNNING_VM

Variable Description
value.host.id UUID of the host where the VM is running.
value.host.name Name of the host where the VM is running.
value.host.tags List of tags of the host where the VM is running (i.e.: ["a", "b"]).
value.id UUID of the VM.
value.name Name of the VM.
value.osName Name of the OS of the VM.
value.computeOffering.customized A boolean informing if the compute offering is customized or not.
value.computeOffering.id UUID of the compute offering with which VM was created.
value.computeOffering.name Name of the compute offering with which VM was created.
value.computingResources.cpuNumber Current VM's vCPUs.
value.computingResources.cpuSpeed Current VM's CPU speed (in Mhz).
value.computingResources.memory Current VM's memory (in MiB).
value.tags List of tags of the VM in the format key:value (i.e.: {"a":"b", "c":"d"}).
value.template.id UUID of the template with which VM was created.
value.template.name Name of the template with which VM was created.

ALLOCATED_VM

Variable Description
value.id UUID of the VM.
value.name Name of the VM.
value.osName Name of the OS of the VM.
value.computeOffering.customized A boolean informing if the compute offering is customized or not.
value.computeOffering.id UUID of the compute offering with which VM was created.
value.computeOffering.name Name of the compute offering with which VM was created.
value.tags List of tags of the VM in the format key:value (i.e.: {"a":"b", "c":"d"}).
value.template.id UUID of the template with which VM was created.
value.template.name Name of the template with which VM was created.

VOLUME

Variable Description
value.diskOffering.id UUID of the disk offering with which volume was created.
value.diskOffering.name Name of the disk offering with which volume was created.
value.id UUID of the volume.
value.name Name of the volume.
value.provisioningType Provisioning type of the resource. Values can be: thin, sparse or fat.
value.storage.id UUID of the storage where the volume is.
value.storage.name Name of the storage where the volume is.
value.storage.scope Scope of the storage where the volume is. Values can be: ZONE or CLUSTER.
value.storage.tags List of tags of the storage where the volume is (i.e.: ["a", "b"]).
value.tags List of tags of the volume in the format key:value (i.e.: {"a":"b", "c":"d"}).
value.size Size of the volume (in MiB).

TEMPLATE / ISO

Variable Description
value.id UUID of the template/ISO.
value.name Name of the template/ISO.
value.osName Name of the OS of the template/ISO.
value.tags List of tags of the template/ISO in the format key:value (i.e.: {"a":"b", "c":"d"}).
value.size Size of the template/ISO (in MiB).

SNAPSHOT

Variable Description
value.id UUID of the snapshot.
value.name Name of the snapshot.
value.size Size of the snapshot (in MiB).
value.snapshotType Type of the snapshot. Values can be: MANUAL, HOURLY, DAILY, WEEKLY and MONTHLY.
value.storage.id UUID of the storage where the snapshot is. The data will be from the primary storage if the global setting snapshot.backup.to.secondary is false, otherwise it will be from secondary storage.
value.storage.name Name of the storage where the snapshot is. The data will be from the primary storage if the global setting snapshot.backup.to.secondary is false, otherwise it will be from secondary storage.
value.storage.scope If the global setting snapshot.backup.to.secondary is false, the scope of the primary storage where the snapshot is (values can be: ZONE or CLUSTER), otherwise it will not exist.
value.storage.tags List of tags of the storage where the snapshot is (i.e.: ["a", "b"]). The data will be from the primary storage if the global setting snapshot.backup.to.secondary is false, otherwise it will not exist.
value.tags List of tags of the snapshot in the format key:value (i.e.: {"a":"b", "c":"d"}).

NETWORK_OFFERING

Variable Description
value.id UUID of the network offering.
value.name Name of the network offering.
value.tag Tag of the network offering.

VM_SNAPSHOT

Variable Description
value.id UUID of the VM snapshot.
value.name Name of the VM snapshot.
value.tags List of tags of the VM snapshot in the format key:value (i.e.: {"a":"b", "c":"d"}).
value.vmSnapshotType Type of the VM snapshot. Values can be: Disk or DiskAndMemory.

Others resources

Others resources will have only the Default preset variables.

Others resources:

IP_ADDRESS
NETWORK_BYTES_SENT
NETWORK_BYTES_RECEIVED
SECURITY_GROUP
LOAD_BALANCER_POLICY
PORT_FORWARDING_RULE
VPN_USERS

Script samples

  1. Owner (account/domain/project) of the resource (available to ALL resources):

    if (account.id == 'b29e84da-ed2e-47dc-9785-49231de8ff07') {
        true
    } else {
        false
    }
    

    Or just:

    account.id == 'b29e84da-ed2e-47dc-9785-49231de8ff07'
    
  2. Volume of allocated resource for the owner (available to ALL resources):

    value.accountResources.filter(resource => 
            resource.domainId == 'b5ea6ffb-fa80-455e-8b38-c9b7e3900cfd'
        ).length > 20
    
  3. Volume of allocated resource for the owner, resulting in the value of the tariff (available to ALL resources)[^3]:

    resourcesLength = value.accountResources.filter(resource => 
            resource.domainId == 'b5ea6ffb-fa80-455e-8b38-c9b7e3900cfd'
        ).length
    
    if (resourcesLength > 40) {
        20
    } else if (resourcesLength > 10) {
        25
    } else {
        30
    }
    
  4. Name of the O.S (available to resources RUNNING_VM and ALLOCATED_VM):

    ['Windows 10 (32-bit)',
     'Windows 10 (64-bit)',
     'Windows 2000 Advanced Server'].includes(value.osName)
    
  5. Storage tags (available to resources VOLUME and SNAPSHOT):

    value.storage.tags.includes('SSD') 
        && value.storage.tags.includes('NVME')
    
  6. Host tags (available to resource RUNNING_VM):

    value.host.tags.includes('CPU platinum')
    
  7. Billing the public IP[^4]. Therefore, if we want to provide one public IP free of charge to users, we can avoid billing source NAT IPs (available to resource IP_ADDRESS):

    resourceType !== 'SourceNat'
    

A setting will be created to define the timeout of the scripts. The default value will be two (2) seconds.


Billing exampe

The RUNNING_VM tariff costs 10 and there are 2 VMs.

The VM A belongs to af7bfdef-2c8f-44a7-9a0e-eb817d6cf821 and has the name promo-123-PersonalCloud.

The VM B belongs to 1e4100b8-e28b-4e76-814b-d0d77b27d7a7, has the name CompanyCloud and the host tag Best Performance.

With the current workflow, both VMs would be accounted with the same tariff/price. With the proposal, operators will be able to create different tariffs, like:

  1. Price: -1.5 Rule:

    value.name.includes('promo-123-')
    
  2. Price: -1.0 Rule:

    account.id == '1e4100b8-e28b-4e76-814b-d0d77b27d7a7'
    
  3. Price: 5.0 Rule:

    value.host.tags.includes('Best Performance')
    

At the end, both VMs will have different costs:

  • VM A: 10 (base) - 1.5 (rule 1) = 8.5

  • VM B: 10 (base) - 1.0 (rule 2) + 5.0 (rule 3) = 14.0


Work items

  • Create six (6) new columns in table cloud_usage.quota_tariff:
Column Nullable Updatable Description
uuid No No To identify the tariff.
name No No A name, defined by the user, to the tariff. This column will be used as common identifier along the tariff updates.
description Yes Yes To describe the tariff.
activation_rule Yes Yes To define when the tariff should be activated. Use null if the tariff is always activated.
removed Yes Yes To mark tariff as removed.
end_date Yes Yes To define the end of the tariff.
  • Migrate tariffs to new paradigm:
    • Tariffs before of the current will be marked as removed and will have the end_date equal to the effective_on of its next;
    • Tariffs after of the current will have its value as the difference between current tariff and its original value;
    • vCPU, CPU_SPEED and MEMORY tariffs will be converted to RUNNING_VM tariffs with respective activation rules;
  • Change QuotaTariffVO;
  • Change API quotaTariffList:
    • Create parameter name, enddate and listall.
    • Change API behavior to, if parameter listAll is not informed, only retrieve not removed tariffs;
  • Change API quotaTariffUpdate:
    • Mark previous tariff as removed, create a new one and copy the identifier (column name) to it.
  • Create API quotaTariffCreate:
    • This API will create a new tariff.
  • Create API quotaTariffDelete:
    • This API will mark the tariff as removed.
  • Create global setting to define the scripts' timeout.
  • Change API quotaUpdate:
    • Validate resources against tariffs' rules, sum the total price and calculate the usage.

Future works

This proposal regards to backend and database, therefore, front-end will not be addressed. In the future, we must change the UI to be compatible with the APIs.

Other proposals arising from this spec are:

  • Bill the Network resource in Quota.
  • Bill the VPC resource in Quota.
  • Change APIs quotaCredits and quotaBalance to allow operators to inform the credits' date and the payment's date or the processing's date. It is necessary due to how some payment methods work.
  • Change resource Tags behavior to indicate when it is an administrative Tag (created by the operator) or an user Tag.
  • Create account/domain setting to granular enabling of Quota.
  • Add VM details on RUNNING_VM. This item will need a special attention as we retrieve the values while processing the rating, meaning that users are able to bypass the tariff by changing the attributes that activate/deactivate a tariff right before the rating is executed.
  • Change the account balance calculation in the APIs quotaSummary and quotaBalance, as the current calculations is incorrect and does not represent the balance.
  • Improve API quotaStatement to, when using the parameter type, retrieve detailed data of the type instead of listing the other types with value 0.

[^1]: As the V8 object will be instantiated only once per cycle, operators must avoid declaring variables with the keywords const, var or let, otherwise it will throw the error Identifier has already been declared between the iterations. Instead of using const a = 1;, one must use a = 1;.

[^2]: It will automatically infer the type of the result: If the result is a number, like 1, 2.5, -3.0 and so on, it will consider the result as a number and will use it as the value of the tariff. Otherwise, it will try to convert the result to a boolean (true or false). If the result is true, it will use the tariffs value in the calculation. If the result is false or not a valid boolean, it will not add the tariff value to the calculation.

[^3]: If the else value (in this example, 30) is not provided, the script's result will be undefined and the the tariff won't be applied.

[^4]: Public IPs are bound to VPCs or isolated networks (not user VMs directly). Every first IP of a VPC or isolated network is a source NAT; additional, if added/allocated by the user, IPs have a null resourceType.

GutoVeronezi avatar Jan 24 '22 15:01 GutoVeronezi

interesting work @GutoVeronezi , nice.

DaanHoogland avatar Jan 25 '22 16:01 DaanHoogland

Congrats @GutoVeronezi - very detailed and interesting work. I see the PR is in draft, do you think it could be ready for 4.17?

nvazquez avatar Feb 04 '22 00:02 nvazquez

Hi, @nvazquez, thanks!

I think yes. I put it in draft because it is not addressing CPU_CLOCK_RATE, CPU_NUMBER and MEMORY yet, but as soon as I implement it, I'll turn it to ready.

GutoVeronezi avatar Feb 04 '22 11:02 GutoVeronezi

As explained the issue description, we choose J2V8 to be our JS engine based on its relevance, performance and implementation; However, we faced a dependency on a GCC version higher or equal to 4.9. CentOS7, by default, is shipped with GCC 4.8, therefore, to build the packages we would need to build a newer GCC version and install it. In order to avoid this barrier, we searched for another JS engine and found Nashorn Engine (standalone).

"The Nashorn JavaScript engine was first incorporated into JDK 8 via JEP 174 as a replacement for the Rhino scripting engine" (JEP 372). Due to the rapid pace of development of ECMAScript, the community found Nashorn challenging to maintain. Then, in JDK 11, they started the engine deprecation (JEP 335). After that, the OpenJDK community created a standalone version of the Nashorn Engine, which is available for JDK 11 and after.

Currently, ACS is developed under JDK 11, which still has the engine as built-in; however, looking at future compatibility, we decided to use the standalone Nashorn Engine version.

Nashorn Engine (standalone) will attend our necessities with JS processing inside Java, allowing users to write rules for Quota tariffs, equally as we achieved with J2V8.

GutoVeronezi avatar Aug 04 '22 18:08 GutoVeronezi