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: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ All notable changes to this project are documented here. The format is based on
`payload`) and `pcd_keymanager_container` (grouped secrets) resources plus a
`pcd_keymanager_secret` data source (optionally fetches the payload). Barbican identifies
objects by URL refs; the resources expose the full ref and use the bare UUID as the ID.
- Network QoS (Neutron `qos` extension) — Phase 3: `pcd_networking_qos_policy` and its three
rule types — `pcd_networking_qos_bandwidth_limit_rule`, `pcd_networking_qos_dscp_marking_rule`,
`pcd_networking_qos_minimum_bandwidth_rule` — plus a `pcd_networking_qos_policy` data source.
Rules are nested under a policy and imported by a composite `<qos_policy_id>/<rule_id>` ID.

- Registry documentation generation wired via `tfplugindocs` (`make generate`) — renders
`docs/` for every resource and data source plus the provider index from schema
Expand Down
1 change: 1 addition & 0 deletions DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ yet passable on this lab (reason noted). Generated registry docs are not committ
| Load balancing (Octavia) | `pcd_lb_loadbalancer`, `_listener`, `_pool`, `_member`, `_monitor`, `_l7policy`, `_l7rule` + `_loadbalancer` DS | **PENDING** — Phase 3, code-complete; per-LB wait-for-`ACTIVE` lifecycle, root-LB resolution for every child, echo-only churny fields. Full-tree acc test + examples written. Octavia is live on the lab (Step 0), but LB provisioning needs a working amphora/provider driver; not yet run live (credentials unavailable this session). |
| DNS (Designate) | `pcd_dns_zone`, `pcd_dns_recordset` + `pcd_dns_zone` DS | **PENDING** — Phase 3, code-complete; async create/update/delete → wait-for-`ACTIVE`/404. Acc test (zone + recordset + import) + examples written. Designate is live on the lab (Step 0) and DNS needs no compute/storage backend, so this should pass live — not yet run this session (credentials unavailable). |
| Key management (Barbican) | `pcd_keymanager_secret`, `pcd_keymanager_container` + `pcd_keymanager_secret` DS | **PENDING** — Phase 3, code-complete; write-only echo-only `payload`, URL-ref→UUID id handling, wait-for-`ACTIVE` only on create-with-payload. Acc test (secret + container + data source + import) + examples written. Barbican is live on the lab (Step 0) and needs no compute/storage backend, so this should pass live — not yet run this session (credentials unavailable). |
| Network QoS (Neutron) | `pcd_networking_qos_policy`, `_qos_bandwidth_limit_rule`, `_qos_dscp_marking_rule`, `_qos_minimum_bandwidth_rule` + `_qos_policy` DS | **PENDING** — Phase 3, code-complete; rules nested under a policy with composite `<policy_id>/<rule_id>` import, tags via the attributes-tags extension (`policies` type), `ForceNew` on `qos_policy_id`. Full-tree acc test (policy + all three rules + data source + import) + examples written. Depends only on the Neutron `qos` extension (no compute/storage backend), so this should pass live — not yet run this session (credentials unavailable). |

Both PENDING items are lab-side configuration gaps (Platform9 / lab-ops), not provider
defects; their acceptance tests flip green on a properly-configured PCD cloud.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
data "pcd_networking_qos_policy" "example" {
name = "tf-example-qos"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
terraform import pcd_networking_qos_bandwidth_limit_rule.example <qos_policy_id>/<rule_id>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
resource "pcd_networking_qos_policy" "example" {
name = "tf-example-qos"
}

resource "pcd_networking_qos_bandwidth_limit_rule" "example" {
qos_policy_id = pcd_networking_qos_policy.example.id
max_kbps = 3000
max_burst_kbps = 300
direction = "egress"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
terraform import pcd_networking_qos_dscp_marking_rule.example <qos_policy_id>/<rule_id>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
resource "pcd_networking_qos_policy" "example" {
name = "tf-example-qos"
}

resource "pcd_networking_qos_dscp_marking_rule" "example" {
qos_policy_id = pcd_networking_qos_policy.example.id
dscp_mark = 26
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
terraform import pcd_networking_qos_minimum_bandwidth_rule.example <qos_policy_id>/<rule_id>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
resource "pcd_networking_qos_policy" "example" {
name = "tf-example-qos"
}

resource "pcd_networking_qos_minimum_bandwidth_rule" "example" {
qos_policy_id = pcd_networking_qos_policy.example.id
min_kbps = 1000
direction = "egress"
}
1 change: 1 addition & 0 deletions examples/resources/pcd_networking_qos_policy/import.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
terraform import pcd_networking_qos_policy.example <qos_policy_id>
6 changes: 6 additions & 0 deletions examples/resources/pcd_networking_qos_policy/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
resource "pcd_networking_qos_policy" "example" {
name = "tf-example-qos"
description = "QoS policy managed by Terraform"
shared = true
tags = ["terraform", "qos"]
}
5 changes: 5 additions & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ func (p *pcdProvider) Resources(_ context.Context) []func() resource.Resource {
networking.NewPortSecgroupAssociateResource,
networking.NewRouterRouteResource,
networking.NewSubnetRouteResource,
networking.NewQoSPolicyResource,
networking.NewQoSBandwidthLimitRuleResource,
networking.NewQoSDSCPMarkingRuleResource,
networking.NewQoSMinimumBandwidthRuleResource,
compute.NewKeypairResource,
compute.NewInstanceResource,
compute.NewFlavorResource,
Expand Down Expand Up @@ -99,6 +103,7 @@ func (p *pcdProvider) DataSources(_ context.Context) []func() datasource.DataSou
networking.NewPortIDsDataSource,
networking.NewRouterDataSource,
networking.NewFloatingIPDataSource,
networking.NewQoSPolicyDataSource,
compute.NewFlavorDataSource,
compute.NewKeypairDataSource,
compute.NewAvailabilityZonesDataSource,
Expand Down
10 changes: 10 additions & 0 deletions internal/services/networking/networking.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,16 @@ func sortedIDsHash(ids []string) (sorted []string, id string) {
return sorted, fmt.Sprintf("%d", h.Sum32())
}

// splitQoSRuleID parses a composite "<qos_policy_id>/<rule_id>" import ID used by
// the QoS rule resources (which are nested under a policy).
func splitQoSRuleID(id string) (policyID, ruleID string, err error) {
parts := strings.SplitN(id, "/", 2)
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
return "", "", fmt.Errorf("expected import ID in the form <qos_policy_id>/<rule_id>, got %q", id)
}
return parts[0], parts[1], nil
}

// replaceTags sets the full tag list on a Neutron resource (networks, subnets,
// ports, ...) via the standard attributes-tags extension.
func replaceTags(ctx context.Context, client *gophercloud.ServiceClient, resourceType, id string, tags []string) error {
Expand Down
232 changes: 232 additions & 0 deletions internal/services/networking/qos_bandwidth_limit_rule_resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
// Copyright (c) Platform9 Systems, Inc.
// SPDX-License-Identifier: MPL-2.0
//
// Ported from terraform-provider-openstack v3.4.0
// (openstack/resource_openstack_networking_qos_bandwidth_limit_rule_v2.go),
// adapted for the terraform-plugin-framework and PCD.

package networking

import (
"context"
"fmt"
"net/http"

"github.com/gophercloud/gophercloud/v2"
"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/qos/rules"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"

"github.com/platform9/terraform-provider-pcd/internal/clients"
)

var (
_ resource.Resource = (*qosBandwidthLimitRuleResource)(nil)
_ resource.ResourceWithConfigure = (*qosBandwidthLimitRuleResource)(nil)
_ resource.ResourceWithImportState = (*qosBandwidthLimitRuleResource)(nil)
)

// NewQoSBandwidthLimitRuleResource is the factory registered with the provider.
func NewQoSBandwidthLimitRuleResource() resource.Resource {
return &qosBandwidthLimitRuleResource{}
}

type qosBandwidthLimitRuleResource struct {
config *clients.Config
}

type qosBandwidthLimitRuleModel struct {
ID types.String `tfsdk:"id"`
QoSPolicyID types.String `tfsdk:"qos_policy_id"`
MaxKBps types.Int64 `tfsdk:"max_kbps"`
MaxBurstKBps types.Int64 `tfsdk:"max_burst_kbps"`
Direction types.String `tfsdk:"direction"`
Region types.String `tfsdk:"region"`
}

func (r *qosBandwidthLimitRuleResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_networking_qos_bandwidth_limit_rule"
}

func (r *qosBandwidthLimitRuleResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
useState := []planmodifier.String{stringplanmodifier.UseStateForUnknown()}
resp.Schema = schema.Schema{
MarkdownDescription: "Manages a bandwidth-limit rule on a Neutron QoS policy in PCD.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{Computed: true, MarkdownDescription: "The rule ID.", PlanModifiers: useState},
"qos_policy_id": schema.StringAttribute{Required: true, MarkdownDescription: "The QoS policy this rule belongs to. Changing this forces a new resource.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
"max_kbps": schema.Int64Attribute{Required: true, MarkdownDescription: "The maximum rate in kbps."},
"max_burst_kbps": schema.Int64Attribute{Optional: true, Computed: true, MarkdownDescription: "The maximum burst size in kilobits.", PlanModifiers: []planmodifier.Int64{int64planmodifier.UseStateForUnknown()}},
"direction": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "The traffic direction: egress (default) or ingress.", PlanModifiers: useState},
"region": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "The region. Defaults to the provider's region.", PlanModifiers: useState},
},
}
}

func (r *qosBandwidthLimitRuleResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
r.config = configureClient(req.ProviderData, &resp.Diagnostics)
}

func (r *qosBandwidthLimitRuleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan qosBandwidthLimitRuleModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}

client, err := r.config.NetworkV2Client()
if err != nil {
resp.Diagnostics.AddError("networking: building v2 client", err.Error())
return
}

policyID := plan.QoSPolicyID.ValueString()
createOpts := rules.CreateBandwidthLimitRuleOpts{
MaxKBps: int(plan.MaxKBps.ValueInt64()),
MaxBurstKBps: int(plan.MaxBurstKBps.ValueInt64()),
Direction: plan.Direction.ValueString(),
}

rule, err := rules.CreateBandwidthLimitRule(ctx, client, policyID, createOpts).ExtractBandwidthLimitRule()
if err != nil {
resp.Diagnostics.AddError("networking: creating bandwidth-limit rule", err.Error())
return
}

notFound, readDiags := r.readInto(ctx, client, policyID, rule.ID, &plan)
resp.Diagnostics.Append(readDiags...)
if notFound {
resp.Diagnostics.AddError("networking: bandwidth-limit rule not found after create",
fmt.Sprintf("Rule %s was not found immediately after creation.", rule.ID))
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}

func (r *qosBandwidthLimitRuleResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state qosBandwidthLimitRuleModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}

client, err := r.config.NetworkV2Client()
if err != nil {
resp.Diagnostics.AddError("networking: building v2 client", err.Error())
return
}

notFound, diags := r.readInto(ctx, client, state.QoSPolicyID.ValueString(), state.ID.ValueString(), &state)
if notFound {
resp.Diagnostics.AddWarning("Bandwidth-limit rule not found",
fmt.Sprintf("Rule %s no longer exists and was removed from state.", state.ID.ValueString()))
resp.State.RemoveResource(ctx)
return
}
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}

func (r *qosBandwidthLimitRuleResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var plan, state qosBandwidthLimitRuleModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}

client, err := r.config.NetworkV2Client()
if err != nil {
resp.Diagnostics.AddError("networking: building v2 client", err.Error())
return
}

policyID := state.QoSPolicyID.ValueString()
updateOpts := rules.UpdateBandwidthLimitRuleOpts{}
if !plan.MaxKBps.Equal(state.MaxKBps) {
v := int(plan.MaxKBps.ValueInt64())
updateOpts.MaxKBps = &v
}
if !plan.MaxBurstKBps.Equal(state.MaxBurstKBps) {
v := int(plan.MaxBurstKBps.ValueInt64())
updateOpts.MaxBurstKBps = &v
}
if !plan.Direction.Equal(state.Direction) {
updateOpts.Direction = plan.Direction.ValueString()
}

if _, err := rules.UpdateBandwidthLimitRule(ctx, client, policyID, plan.ID.ValueString(), updateOpts).ExtractBandwidthLimitRule(); err != nil {
resp.Diagnostics.AddError("networking: updating bandwidth-limit rule", err.Error())
return
}

notFound, readDiags := r.readInto(ctx, client, policyID, plan.ID.ValueString(), &plan)
resp.Diagnostics.Append(readDiags...)
if notFound {
resp.Diagnostics.AddError("networking: bandwidth-limit rule not found after update",
fmt.Sprintf("Rule %s was not found immediately after update.", plan.ID.ValueString()))
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}

func (r *qosBandwidthLimitRuleResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var state qosBandwidthLimitRuleModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}

client, err := r.config.NetworkV2Client()
if err != nil {
resp.Diagnostics.AddError("networking: building v2 client", err.Error())
return
}

if err := rules.DeleteBandwidthLimitRule(ctx, client, state.QoSPolicyID.ValueString(), state.ID.ValueString()).ExtractErr(); err != nil {
if gophercloud.ResponseCodeIs(err, http.StatusNotFound) {
return
}
resp.Diagnostics.AddError("networking: deleting bandwidth-limit rule", err.Error())
}
}

func (r *qosBandwidthLimitRuleResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
policyID, ruleID, err := splitQoSRuleID(req.ID)
if err != nil {
resp.Diagnostics.AddError("Invalid import ID", err.Error())
return
}
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), ruleID)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("qos_policy_id"), policyID)...)
}

func (r *qosBandwidthLimitRuleResource) readInto(ctx context.Context, client *gophercloud.ServiceClient, policyID, ruleID string, m *qosBandwidthLimitRuleModel) (notFound bool, diags diag.Diagnostics) {
rule, err := rules.GetBandwidthLimitRule(ctx, client, policyID, ruleID).ExtractBandwidthLimitRule()
if err != nil {
if gophercloud.ResponseCodeIs(err, http.StatusNotFound) {
return true, diags
}
diags.AddError("networking: reading bandwidth-limit rule", err.Error())
return false, diags
}

m.ID = types.StringValue(rule.ID)
m.QoSPolicyID = types.StringValue(policyID)
m.MaxKBps = types.Int64Value(int64(rule.MaxKBps))
m.MaxBurstKBps = types.Int64Value(int64(rule.MaxBurstKBps))
m.Direction = types.StringValue(rule.Direction)
if m.Region.IsNull() || m.Region.IsUnknown() {
m.Region = types.StringValue(r.config.Region)
}
return false, diags
}
Loading
Loading