diff --git a/README.md b/README.md index 8e161c5..870f5a0 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,9 @@ provider "tomswallapi" { | `tomswallapi_proxy_arp` / `_proxy_ndp` | id | proxy ARP/NDP on a `device` | | `tomswallapi_arp_rule` | id | ARP-level rule on a `device` | | `tomswallapi_maclist` | id | MAC/IP verification on a `device` interface | +| `tomswallapi_mangle` | id | packet-mangling rule on a `device` | +| `tomswallapi_accounting` | id | traffic-accounting rule on a `device` | +| `tomswallapi_tc_device` / `_tc_class` / `_tc_filter` / `_tc_interface` / `_tc_priority` | id | traffic-shaping on a `device` | ## Example diff --git a/internal/provider/provider.go b/internal/provider/provider.go index b5e8d8e..b4bb7fd 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -91,6 +91,13 @@ func (p *tomswallProvider) Resources(_ context.Context) []func() resource.Resour NewProxyNDPResource, NewArpRuleResource, NewMaclistResource, + NewMangleResource, + NewAccountingResource, + NewTCDeviceResource, + NewTCClassResource, + NewTCFilterResource, + NewTCInterfaceResource, + NewTCPriorityResource, } } diff --git a/internal/provider/resource_accounting.go b/internal/provider/resource_accounting.go new file mode 100644 index 0000000..08c3df6 --- /dev/null +++ b/internal/provider/resource_accounting.go @@ -0,0 +1,182 @@ +package provider + +import ( + "context" + "strconv" + + "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/types" +) + +var ( + _ resource.Resource = &accountingResource{} + _ resource.ResourceWithImportState = &accountingResource{} +) + +type accountingResource struct{ client *apiClient } + +type accountingModel struct { + ID types.Int64 `tfsdk:"id"` + Device types.String `tfsdk:"device"` + Action types.String `tfsdk:"action"` + Section types.String `tfsdk:"section"` + Chain types.String `tfsdk:"chain"` + Source types.String `tfsdk:"source"` + Dest types.String `tfsdk:"dest"` + Proto types.String `tfsdk:"proto"` + DPort types.List `tfsdk:"dport"` + SPort types.List `tfsdk:"sport"` + Mark types.String `tfsdk:"mark"` + Comment types.String `tfsdk:"comment"` +} + +type accountingAPI struct { + ID int64 `json:"id,omitempty"` + Device string `json:"device"` + Action string `json:"action"` + Section string `json:"section,omitempty"` + Chain string `json:"chain,omitempty"` + Source string `json:"source,omitempty"` + Dest string `json:"dest,omitempty"` + Proto string `json:"proto,omitempty"` + DPort []string `json:"dport,omitempty"` + SPort []string `json:"sport,omitempty"` + Mark string `json:"mark,omitempty"` + Comment string `json:"comment,omitempty"` +} + +func NewAccountingResource() resource.Resource { return &accountingResource{} } + +func (r *accountingResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_accounting" +} + +func (r *accountingResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "A traffic-accounting rule on a device.", + Attributes: map[string]schema.Attribute{ + "id": schema.Int64Attribute{Computed: true, PlanModifiers: []planmodifier.Int64{int64planmodifier.UseStateForUnknown()}}, + "device": schema.StringAttribute{Description: "Owning device.", Required: true}, + "action": schema.StringAttribute{Required: true}, + "section": schema.StringAttribute{Optional: true}, + "chain": schema.StringAttribute{Optional: true}, + "source": schema.StringAttribute{Optional: true}, + "dest": schema.StringAttribute{Optional: true}, + "proto": schema.StringAttribute{Optional: true}, + "dport": schema.ListAttribute{Optional: true, ElementType: types.StringType}, + "sport": schema.ListAttribute{Optional: true, ElementType: types.StringType}, + "mark": schema.StringAttribute{Optional: true}, + "comment": schema.StringAttribute{Optional: true}, + }, + } +} + +func (r *accountingResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.client = configureClient(req, resp) +} + +func (r *accountingResource) body(ctx context.Context, plan accountingModel, diags *diag.Diagnostics) accountingAPI { + return accountingAPI{ + Device: plan.Device.ValueString(), Action: plan.Action.ValueString(), Section: plan.Section.ValueString(), + Chain: plan.Chain.ValueString(), Source: plan.Source.ValueString(), Dest: plan.Dest.ValueString(), + Proto: plan.Proto.ValueString(), DPort: listToStrings(ctx, plan.DPort, diags), SPort: listToStrings(ctx, plan.SPort, diags), + Mark: plan.Mark.ValueString(), Comment: plan.Comment.ValueString(), + } +} + +func (r *accountingResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan accountingModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + body := r.body(ctx, plan, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + var out accountingAPI + if err := r.client.post(ctx, "/api/v1/accounting", body, &out); err != nil { + resp.Diagnostics.AddError("create accounting failed", err.Error()) + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, r.toModel(ctx, out, plan, &resp.Diagnostics))...) +} + +func (r *accountingResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan, state accountingModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + body := r.body(ctx, plan, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + var out accountingAPI + if err := r.client.post(ctx, "/api/v1/accounting", body, &out); err != nil { + resp.Diagnostics.AddError("recreate accounting failed", err.Error()) + return + } + if id := state.ID.ValueInt64(); id != 0 { + if err := r.client.del(ctx, "/api/v1/accounting/"+strconv.FormatInt(id, 10)); err != nil && !isNotFound(err) { + resp.Diagnostics.AddError("delete old accounting failed", err.Error()) + return + } + } + resp.Diagnostics.Append(resp.State.Set(ctx, r.toModel(ctx, out, plan, &resp.Diagnostics))...) +} + +func (r *accountingResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state accountingModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + var out accountingAPI + if err := r.client.get(ctx, "/api/v1/accounting/"+strconv.FormatInt(state.ID.ValueInt64(), 10), &out); err != nil { + if isNotFound(err) { + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError("read accounting failed", err.Error()) + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, r.toModel(ctx, out, state, &resp.Diagnostics))...) +} + +func (r *accountingResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state accountingModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if err := r.client.del(ctx, "/api/v1/accounting/"+strconv.FormatInt(state.ID.ValueInt64(), 10)); err != nil && !isNotFound(err) { + resp.Diagnostics.AddError("delete accounting failed", err.Error()) + } +} + +func (r *accountingResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + id, err := strconv.ParseInt(req.ID, 10, 64) + if err != nil { + resp.Diagnostics.AddError("invalid import ID", "accounting id must be an integer") + return + } + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), id)...) +} + +func (r *accountingResource) toModel(ctx context.Context, api accountingAPI, prior accountingModel, diags *diag.Diagnostics) accountingModel { + return accountingModel{ + ID: types.Int64Value(api.ID), Device: types.StringValue(api.Device), Action: types.StringValue(api.Action), + Section: optionalString(api.Section, prior.Section), Chain: optionalString(api.Chain, prior.Chain), + Source: optionalString(api.Source, prior.Source), Dest: optionalString(api.Dest, prior.Dest), + Proto: optionalString(api.Proto, prior.Proto), DPort: optionalList(ctx, api.DPort, prior.DPort, diags), + SPort: optionalList(ctx, api.SPort, prior.SPort, diags), Mark: optionalString(api.Mark, prior.Mark), + Comment: optionalString(api.Comment, prior.Comment), + } +} diff --git a/internal/provider/resource_mangle.go b/internal/provider/resource_mangle.go new file mode 100644 index 0000000..fad7f4d --- /dev/null +++ b/internal/provider/resource_mangle.go @@ -0,0 +1,211 @@ +package provider + +import ( + "context" + "strconv" + + "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/types" +) + +var ( + _ resource.Resource = &mangleResource{} + _ resource.ResourceWithImportState = &mangleResource{} +) + +type mangleResource struct{ client *apiClient } + +type mangleModel struct { + ID types.Int64 `tfsdk:"id"` + Device types.String `tfsdk:"device"` + Action types.String `tfsdk:"action"` + Chain types.String `tfsdk:"chain"` + MarkValue types.String `tfsdk:"mark_value"` + Source types.String `tfsdk:"source"` + Dest types.String `tfsdk:"dest"` + Proto types.String `tfsdk:"proto"` + DPort types.List `tfsdk:"dport"` + SPort types.List `tfsdk:"sport"` + User types.String `tfsdk:"user"` + Mark types.String `tfsdk:"mark"` + Length types.String `tfsdk:"length"` + TOS types.String `tfsdk:"tos"` + Helper types.String `tfsdk:"helper"` + Probability types.Float64 `tfsdk:"probability"` + Comment types.String `tfsdk:"comment"` +} + +type mangleAPI struct { + ID int64 `json:"id,omitempty"` + Device string `json:"device"` + Action string `json:"action"` + Chain string `json:"chain,omitempty"` + MarkValue string `json:"mark_value,omitempty"` + Source string `json:"source,omitempty"` + Dest string `json:"dest,omitempty"` + Proto string `json:"proto,omitempty"` + DPort []string `json:"dport,omitempty"` + SPort []string `json:"sport,omitempty"` + User string `json:"user,omitempty"` + Mark string `json:"mark,omitempty"` + Length string `json:"length,omitempty"` + TOS string `json:"tos,omitempty"` + Helper string `json:"helper,omitempty"` + Probability *float64 `json:"probability,omitempty"` + Comment string `json:"comment,omitempty"` +} + +func NewMangleResource() resource.Resource { return &mangleResource{} } + +func (r *mangleResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_mangle" +} + +func (r *mangleResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "A packet-mangling rule (marking, TOS, etc.) on a device.", + Attributes: map[string]schema.Attribute{ + "id": schema.Int64Attribute{Computed: true, PlanModifiers: []planmodifier.Int64{int64planmodifier.UseStateForUnknown()}}, + "device": schema.StringAttribute{Description: "Owning device.", Required: true}, + "action": schema.StringAttribute{Required: true}, + "chain": schema.StringAttribute{Optional: true}, + "mark_value": schema.StringAttribute{Optional: true}, + "source": schema.StringAttribute{Optional: true}, + "dest": schema.StringAttribute{Optional: true}, + "proto": schema.StringAttribute{Optional: true}, + "dport": schema.ListAttribute{Optional: true, ElementType: types.StringType}, + "sport": schema.ListAttribute{Optional: true, ElementType: types.StringType}, + "user": schema.StringAttribute{Optional: true}, + "mark": schema.StringAttribute{Optional: true}, + "length": schema.StringAttribute{Optional: true}, + "tos": schema.StringAttribute{Optional: true}, + "helper": schema.StringAttribute{Optional: true}, + "probability": schema.Float64Attribute{Optional: true}, + "comment": schema.StringAttribute{Optional: true}, + }, + } +} + +func (r *mangleResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.client = configureClient(req, resp) +} + +func (r *mangleResource) body(ctx context.Context, plan mangleModel, diags *diag.Diagnostics) mangleAPI { + b := mangleAPI{ + Device: plan.Device.ValueString(), Action: plan.Action.ValueString(), Chain: plan.Chain.ValueString(), + MarkValue: plan.MarkValue.ValueString(), Source: plan.Source.ValueString(), Dest: plan.Dest.ValueString(), + Proto: plan.Proto.ValueString(), DPort: listToStrings(ctx, plan.DPort, diags), SPort: listToStrings(ctx, plan.SPort, diags), + User: plan.User.ValueString(), Mark: plan.Mark.ValueString(), Length: plan.Length.ValueString(), + TOS: plan.TOS.ValueString(), Helper: plan.Helper.ValueString(), Comment: plan.Comment.ValueString(), + } + if !plan.Probability.IsNull() && !plan.Probability.IsUnknown() { + p := plan.Probability.ValueFloat64() + b.Probability = &p + } + return b +} + +func (r *mangleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan mangleModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + body := r.body(ctx, plan, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + var out mangleAPI + if err := r.client.post(ctx, "/api/v1/mangle", body, &out); err != nil { + resp.Diagnostics.AddError("create mangle failed", err.Error()) + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, r.toModel(ctx, out, plan, &resp.Diagnostics))...) +} + +func (r *mangleResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan, state mangleModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + body := r.body(ctx, plan, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + var out mangleAPI + if err := r.client.post(ctx, "/api/v1/mangle", body, &out); err != nil { + resp.Diagnostics.AddError("recreate mangle failed", err.Error()) + return + } + if id := state.ID.ValueInt64(); id != 0 { + if err := r.client.del(ctx, "/api/v1/mangle/"+strconv.FormatInt(id, 10)); err != nil && !isNotFound(err) { + resp.Diagnostics.AddError("delete old mangle failed", err.Error()) + return + } + } + resp.Diagnostics.Append(resp.State.Set(ctx, r.toModel(ctx, out, plan, &resp.Diagnostics))...) +} + +func (r *mangleResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state mangleModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + var out mangleAPI + if err := r.client.get(ctx, "/api/v1/mangle/"+strconv.FormatInt(state.ID.ValueInt64(), 10), &out); err != nil { + if isNotFound(err) { + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError("read mangle failed", err.Error()) + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, r.toModel(ctx, out, state, &resp.Diagnostics))...) +} + +func (r *mangleResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state mangleModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if err := r.client.del(ctx, "/api/v1/mangle/"+strconv.FormatInt(state.ID.ValueInt64(), 10)); err != nil && !isNotFound(err) { + resp.Diagnostics.AddError("delete mangle failed", err.Error()) + } +} + +func (r *mangleResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + id, err := strconv.ParseInt(req.ID, 10, 64) + if err != nil { + resp.Diagnostics.AddError("invalid import ID", "mangle id must be an integer") + return + } + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), id)...) +} + +func (r *mangleResource) toModel(ctx context.Context, api mangleAPI, prior mangleModel, diags *diag.Diagnostics) mangleModel { + m := mangleModel{ + ID: types.Int64Value(api.ID), Device: types.StringValue(api.Device), Action: types.StringValue(api.Action), + Chain: optionalString(api.Chain, prior.Chain), MarkValue: optionalString(api.MarkValue, prior.MarkValue), + Source: optionalString(api.Source, prior.Source), Dest: optionalString(api.Dest, prior.Dest), + Proto: optionalString(api.Proto, prior.Proto), DPort: optionalList(ctx, api.DPort, prior.DPort, diags), + SPort: optionalList(ctx, api.SPort, prior.SPort, diags), User: optionalString(api.User, prior.User), + Mark: optionalString(api.Mark, prior.Mark), Length: optionalString(api.Length, prior.Length), + TOS: optionalString(api.TOS, prior.TOS), Helper: optionalString(api.Helper, prior.Helper), + Comment: optionalString(api.Comment, prior.Comment), + } + if api.Probability != nil { + m.Probability = types.Float64Value(*api.Probability) + } else { + m.Probability = prior.Probability + } + return m +} diff --git a/internal/provider/resource_tc_class.go b/internal/provider/resource_tc_class.go new file mode 100644 index 0000000..d82e619 --- /dev/null +++ b/internal/provider/resource_tc_class.go @@ -0,0 +1,158 @@ +package provider + +import ( + "context" + "strconv" + + "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/types" +) + +var ( + _ resource.Resource = &tcClassResource{} + _ resource.ResourceWithImportState = &tcClassResource{} +) + +type tcClassResource struct{ client *apiClient } + +type tcClassModel struct { + ID types.Int64 `tfsdk:"id"` + Device types.String `tfsdk:"device"` + Interface types.String `tfsdk:"interface"` + Mark types.Int64 `tfsdk:"mark"` + Rate types.String `tfsdk:"rate"` + Ceil types.String `tfsdk:"ceil"` + Priority types.Int64 `tfsdk:"priority"` + Comment types.String `tfsdk:"comment"` +} + +type tcClassAPI struct { + ID int64 `json:"id,omitempty"` + Device string `json:"device"` + Interface string `json:"interface"` + Mark int `json:"mark,omitempty"` + Rate string `json:"rate,omitempty"` + Ceil string `json:"ceil,omitempty"` + Priority int `json:"priority,omitempty"` + Comment string `json:"comment,omitempty"` +} + +func NewTCClassResource() resource.Resource { return &tcClassResource{} } + +func (r *tcClassResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_tc_class" +} + +func (r *tcClassResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "A traffic-shaping class on a device.", + Attributes: map[string]schema.Attribute{ + "id": schema.Int64Attribute{Computed: true, PlanModifiers: []planmodifier.Int64{int64planmodifier.UseStateForUnknown()}}, + "device": schema.StringAttribute{Description: "Owning device.", Required: true}, + "interface": schema.StringAttribute{Description: "iface:class or iface:parent:class.", Required: true}, + "mark": schema.Int64Attribute{Optional: true}, + "rate": schema.StringAttribute{Optional: true}, + "ceil": schema.StringAttribute{Optional: true}, + "priority": schema.Int64Attribute{Optional: true}, + "comment": schema.StringAttribute{Optional: true}, + }, + } +} + +func (r *tcClassResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.client = configureClient(req, resp) +} + +func (r *tcClassResource) body(plan tcClassModel) tcClassAPI { + return tcClassAPI{ + Device: plan.Device.ValueString(), Interface: plan.Interface.ValueString(), Mark: int(plan.Mark.ValueInt64()), + Rate: plan.Rate.ValueString(), Ceil: plan.Ceil.ValueString(), Priority: int(plan.Priority.ValueInt64()), + Comment: plan.Comment.ValueString(), + } +} + +func (r *tcClassResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan tcClassModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + var out tcClassAPI + if err := r.client.post(ctx, "/api/v1/tc-classes", r.body(plan), &out); err != nil { + resp.Diagnostics.AddError("create tc_class failed", err.Error()) + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, r.toModel(out, plan))...) +} + +func (r *tcClassResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan, state tcClassModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + var out tcClassAPI + if err := r.client.post(ctx, "/api/v1/tc-classes", r.body(plan), &out); err != nil { + resp.Diagnostics.AddError("recreate tc_class failed", err.Error()) + return + } + if id := state.ID.ValueInt64(); id != 0 { + if err := r.client.del(ctx, "/api/v1/tc-classes/"+strconv.FormatInt(id, 10)); err != nil && !isNotFound(err) { + resp.Diagnostics.AddError("delete old tc_class failed", err.Error()) + return + } + } + resp.Diagnostics.Append(resp.State.Set(ctx, r.toModel(out, plan))...) +} + +func (r *tcClassResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state tcClassModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + var out tcClassAPI + if err := r.client.get(ctx, "/api/v1/tc-classes/"+strconv.FormatInt(state.ID.ValueInt64(), 10), &out); err != nil { + if isNotFound(err) { + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError("read tc_class failed", err.Error()) + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, r.toModel(out, state))...) +} + +func (r *tcClassResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state tcClassModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if err := r.client.del(ctx, "/api/v1/tc-classes/"+strconv.FormatInt(state.ID.ValueInt64(), 10)); err != nil && !isNotFound(err) { + resp.Diagnostics.AddError("delete tc_class failed", err.Error()) + } +} + +func (r *tcClassResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + id, err := strconv.ParseInt(req.ID, 10, 64) + if err != nil { + resp.Diagnostics.AddError("invalid import ID", "tc_class id must be an integer") + return + } + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), id)...) +} + +func (r *tcClassResource) toModel(api tcClassAPI, prior tcClassModel) tcClassModel { + return tcClassModel{ + ID: types.Int64Value(api.ID), Device: types.StringValue(api.Device), Interface: types.StringValue(api.Interface), + Mark: optionalInt64(api.Mark, prior.Mark), Rate: optionalString(api.Rate, prior.Rate), + Ceil: optionalString(api.Ceil, prior.Ceil), Priority: optionalInt64(api.Priority, prior.Priority), + Comment: optionalString(api.Comment, prior.Comment), + } +} diff --git a/internal/provider/resource_tc_device.go b/internal/provider/resource_tc_device.go new file mode 100644 index 0000000..ea1905c --- /dev/null +++ b/internal/provider/resource_tc_device.go @@ -0,0 +1,151 @@ +package provider + +import ( + "context" + "strconv" + + "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/types" +) + +var ( + _ resource.Resource = &tcDeviceResource{} + _ resource.ResourceWithImportState = &tcDeviceResource{} +) + +type tcDeviceResource struct{ client *apiClient } + +type tcDeviceModel struct { + ID types.Int64 `tfsdk:"id"` + Device types.String `tfsdk:"device"` + Interface types.String `tfsdk:"interface"` + InBandwidth types.String `tfsdk:"in_bandwidth"` + OutBandwidth types.String `tfsdk:"out_bandwidth"` + Comment types.String `tfsdk:"comment"` +} + +type tcDeviceAPI struct { + ID int64 `json:"id,omitempty"` + Device string `json:"device"` + Interface string `json:"interface"` + InBandwidth string `json:"in_bandwidth,omitempty"` + OutBandwidth string `json:"out_bandwidth,omitempty"` + Comment string `json:"comment,omitempty"` +} + +func NewTCDeviceResource() resource.Resource { return &tcDeviceResource{} } + +func (r *tcDeviceResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_tc_device" +} + +func (r *tcDeviceResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "A traffic-shaping device (root qdisc) on a device's interface.", + Attributes: map[string]schema.Attribute{ + "id": schema.Int64Attribute{Computed: true, PlanModifiers: []planmodifier.Int64{int64planmodifier.UseStateForUnknown()}}, + "device": schema.StringAttribute{Description: "Owning device.", Required: true}, + "interface": schema.StringAttribute{Required: true}, + "in_bandwidth": schema.StringAttribute{Optional: true}, + "out_bandwidth": schema.StringAttribute{Optional: true}, + "comment": schema.StringAttribute{Optional: true}, + }, + } +} + +func (r *tcDeviceResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.client = configureClient(req, resp) +} + +func (r *tcDeviceResource) body(plan tcDeviceModel) tcDeviceAPI { + return tcDeviceAPI{ + Device: plan.Device.ValueString(), Interface: plan.Interface.ValueString(), + InBandwidth: plan.InBandwidth.ValueString(), OutBandwidth: plan.OutBandwidth.ValueString(), + Comment: plan.Comment.ValueString(), + } +} + +func (r *tcDeviceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan tcDeviceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + var out tcDeviceAPI + if err := r.client.post(ctx, "/api/v1/tc-devices", r.body(plan), &out); err != nil { + resp.Diagnostics.AddError("create tc_device failed", err.Error()) + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, r.toModel(out, plan))...) +} + +func (r *tcDeviceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan, state tcDeviceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + var out tcDeviceAPI + if err := r.client.post(ctx, "/api/v1/tc-devices", r.body(plan), &out); err != nil { + resp.Diagnostics.AddError("recreate tc_device failed", err.Error()) + return + } + if id := state.ID.ValueInt64(); id != 0 { + if err := r.client.del(ctx, "/api/v1/tc-devices/"+strconv.FormatInt(id, 10)); err != nil && !isNotFound(err) { + resp.Diagnostics.AddError("delete old tc_device failed", err.Error()) + return + } + } + resp.Diagnostics.Append(resp.State.Set(ctx, r.toModel(out, plan))...) +} + +func (r *tcDeviceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state tcDeviceModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + var out tcDeviceAPI + if err := r.client.get(ctx, "/api/v1/tc-devices/"+strconv.FormatInt(state.ID.ValueInt64(), 10), &out); err != nil { + if isNotFound(err) { + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError("read tc_device failed", err.Error()) + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, r.toModel(out, state))...) +} + +func (r *tcDeviceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state tcDeviceModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if err := r.client.del(ctx, "/api/v1/tc-devices/"+strconv.FormatInt(state.ID.ValueInt64(), 10)); err != nil && !isNotFound(err) { + resp.Diagnostics.AddError("delete tc_device failed", err.Error()) + } +} + +func (r *tcDeviceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + id, err := strconv.ParseInt(req.ID, 10, 64) + if err != nil { + resp.Diagnostics.AddError("invalid import ID", "tc_device id must be an integer") + return + } + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), id)...) +} + +func (r *tcDeviceResource) toModel(api tcDeviceAPI, prior tcDeviceModel) tcDeviceModel { + return tcDeviceModel{ + ID: types.Int64Value(api.ID), Device: types.StringValue(api.Device), Interface: types.StringValue(api.Interface), + InBandwidth: optionalString(api.InBandwidth, prior.InBandwidth), OutBandwidth: optionalString(api.OutBandwidth, prior.OutBandwidth), + Comment: optionalString(api.Comment, prior.Comment), + } +} diff --git a/internal/provider/resource_tc_filter.go b/internal/provider/resource_tc_filter.go new file mode 100644 index 0000000..c0033c6 --- /dev/null +++ b/internal/provider/resource_tc_filter.go @@ -0,0 +1,183 @@ +package provider + +import ( + "context" + "strconv" + + "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/types" +) + +var ( + _ resource.Resource = &tcFilterResource{} + _ resource.ResourceWithImportState = &tcFilterResource{} +) + +type tcFilterResource struct{ client *apiClient } + +type tcFilterModel struct { + ID types.Int64 `tfsdk:"id"` + Device types.String `tfsdk:"device"` + Class types.String `tfsdk:"class"` + Source types.String `tfsdk:"source"` + Dest types.String `tfsdk:"dest"` + Proto types.String `tfsdk:"proto"` + DPort types.List `tfsdk:"dport"` + SPort types.List `tfsdk:"sport"` + TOS types.String `tfsdk:"tos"` + Length types.Int64 `tfsdk:"length"` + Priority types.Int64 `tfsdk:"priority"` + Comment types.String `tfsdk:"comment"` +} + +type tcFilterAPI struct { + ID int64 `json:"id,omitempty"` + Device string `json:"device"` + Class string `json:"class"` + Source string `json:"source,omitempty"` + Dest string `json:"dest,omitempty"` + Proto string `json:"proto,omitempty"` + DPort []string `json:"dport,omitempty"` + SPort []string `json:"sport,omitempty"` + TOS string `json:"tos,omitempty"` + Length int `json:"length,omitempty"` + Priority int `json:"priority,omitempty"` + Comment string `json:"comment,omitempty"` +} + +func NewTCFilterResource() resource.Resource { return &tcFilterResource{} } + +func (r *tcFilterResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_tc_filter" +} + +func (r *tcFilterResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "A traffic-classification filter (packet → class) on a device.", + Attributes: map[string]schema.Attribute{ + "id": schema.Int64Attribute{Computed: true, PlanModifiers: []planmodifier.Int64{int64planmodifier.UseStateForUnknown()}}, + "device": schema.StringAttribute{Description: "Owning device.", Required: true}, + "class": schema.StringAttribute{Description: "interface:class target.", Required: true}, + "source": schema.StringAttribute{Optional: true}, + "dest": schema.StringAttribute{Optional: true}, + "proto": schema.StringAttribute{Optional: true}, + "dport": schema.ListAttribute{Optional: true, ElementType: types.StringType}, + "sport": schema.ListAttribute{Optional: true, ElementType: types.StringType}, + "tos": schema.StringAttribute{Optional: true}, + "length": schema.Int64Attribute{Optional: true}, + "priority": schema.Int64Attribute{Optional: true}, + "comment": schema.StringAttribute{Optional: true}, + }, + } +} + +func (r *tcFilterResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.client = configureClient(req, resp) +} + +func (r *tcFilterResource) body(ctx context.Context, plan tcFilterModel, diags *diag.Diagnostics) tcFilterAPI { + return tcFilterAPI{ + Device: plan.Device.ValueString(), Class: plan.Class.ValueString(), Source: plan.Source.ValueString(), + Dest: plan.Dest.ValueString(), Proto: plan.Proto.ValueString(), + DPort: listToStrings(ctx, plan.DPort, diags), SPort: listToStrings(ctx, plan.SPort, diags), + TOS: plan.TOS.ValueString(), Length: int(plan.Length.ValueInt64()), Priority: int(plan.Priority.ValueInt64()), + Comment: plan.Comment.ValueString(), + } +} + +func (r *tcFilterResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan tcFilterModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + body := r.body(ctx, plan, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + var out tcFilterAPI + if err := r.client.post(ctx, "/api/v1/tc-filters", body, &out); err != nil { + resp.Diagnostics.AddError("create tc_filter failed", err.Error()) + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, r.toModel(ctx, out, plan, &resp.Diagnostics))...) +} + +func (r *tcFilterResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan, state tcFilterModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + body := r.body(ctx, plan, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + var out tcFilterAPI + if err := r.client.post(ctx, "/api/v1/tc-filters", body, &out); err != nil { + resp.Diagnostics.AddError("recreate tc_filter failed", err.Error()) + return + } + if id := state.ID.ValueInt64(); id != 0 { + if err := r.client.del(ctx, "/api/v1/tc-filters/"+strconv.FormatInt(id, 10)); err != nil && !isNotFound(err) { + resp.Diagnostics.AddError("delete old tc_filter failed", err.Error()) + return + } + } + resp.Diagnostics.Append(resp.State.Set(ctx, r.toModel(ctx, out, plan, &resp.Diagnostics))...) +} + +func (r *tcFilterResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state tcFilterModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + var out tcFilterAPI + if err := r.client.get(ctx, "/api/v1/tc-filters/"+strconv.FormatInt(state.ID.ValueInt64(), 10), &out); err != nil { + if isNotFound(err) { + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError("read tc_filter failed", err.Error()) + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, r.toModel(ctx, out, state, &resp.Diagnostics))...) +} + +func (r *tcFilterResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state tcFilterModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if err := r.client.del(ctx, "/api/v1/tc-filters/"+strconv.FormatInt(state.ID.ValueInt64(), 10)); err != nil && !isNotFound(err) { + resp.Diagnostics.AddError("delete tc_filter failed", err.Error()) + } +} + +func (r *tcFilterResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + id, err := strconv.ParseInt(req.ID, 10, 64) + if err != nil { + resp.Diagnostics.AddError("invalid import ID", "tc_filter id must be an integer") + return + } + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), id)...) +} + +func (r *tcFilterResource) toModel(ctx context.Context, api tcFilterAPI, prior tcFilterModel, diags *diag.Diagnostics) tcFilterModel { + return tcFilterModel{ + ID: types.Int64Value(api.ID), Device: types.StringValue(api.Device), Class: types.StringValue(api.Class), + Source: optionalString(api.Source, prior.Source), Dest: optionalString(api.Dest, prior.Dest), + Proto: optionalString(api.Proto, prior.Proto), DPort: optionalList(ctx, api.DPort, prior.DPort, diags), + SPort: optionalList(ctx, api.SPort, prior.SPort, diags), TOS: optionalString(api.TOS, prior.TOS), + Length: optionalInt64(api.Length, prior.Length), Priority: optionalInt64(api.Priority, prior.Priority), + Comment: optionalString(api.Comment, prior.Comment), + } +} diff --git a/internal/provider/resource_tc_interface.go b/internal/provider/resource_tc_interface.go new file mode 100644 index 0000000..1cae54c --- /dev/null +++ b/internal/provider/resource_tc_interface.go @@ -0,0 +1,153 @@ +package provider + +import ( + "context" + "strconv" + + "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/types" +) + +var ( + _ resource.Resource = &tcInterfaceResource{} + _ resource.ResourceWithImportState = &tcInterfaceResource{} +) + +type tcInterfaceResource struct{ client *apiClient } + +type tcInterfaceModel struct { + ID types.Int64 `tfsdk:"id"` + Device types.String `tfsdk:"device"` + Interface types.String `tfsdk:"interface"` + Type types.String `tfsdk:"type"` + InBandwidth types.String `tfsdk:"in_bandwidth"` + OutBandwidth types.String `tfsdk:"out_bandwidth"` + Comment types.String `tfsdk:"comment"` +} + +type tcInterfaceAPI struct { + ID int64 `json:"id,omitempty"` + Device string `json:"device"` + Interface string `json:"interface"` + Type string `json:"type,omitempty"` + InBandwidth string `json:"in_bandwidth,omitempty"` + OutBandwidth string `json:"out_bandwidth,omitempty"` + Comment string `json:"comment,omitempty"` +} + +func NewTCInterfaceResource() resource.Resource { return &tcInterfaceResource{} } + +func (r *tcInterfaceResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_tc_interface" +} + +func (r *tcInterfaceResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Simple per-interface traffic shaping on a device.", + Attributes: map[string]schema.Attribute{ + "id": schema.Int64Attribute{Computed: true, PlanModifiers: []planmodifier.Int64{int64planmodifier.UseStateForUnknown()}}, + "device": schema.StringAttribute{Description: "Owning device.", Required: true}, + "interface": schema.StringAttribute{Required: true}, + "type": schema.StringAttribute{Description: "external or internal.", Optional: true}, + "in_bandwidth": schema.StringAttribute{Optional: true}, + "out_bandwidth": schema.StringAttribute{Optional: true}, + "comment": schema.StringAttribute{Optional: true}, + }, + } +} + +func (r *tcInterfaceResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.client = configureClient(req, resp) +} + +func (r *tcInterfaceResource) body(plan tcInterfaceModel) tcInterfaceAPI { + return tcInterfaceAPI{ + Device: plan.Device.ValueString(), Interface: plan.Interface.ValueString(), Type: plan.Type.ValueString(), + InBandwidth: plan.InBandwidth.ValueString(), OutBandwidth: plan.OutBandwidth.ValueString(), Comment: plan.Comment.ValueString(), + } +} + +func (r *tcInterfaceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan tcInterfaceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + var out tcInterfaceAPI + if err := r.client.post(ctx, "/api/v1/tc-interfaces", r.body(plan), &out); err != nil { + resp.Diagnostics.AddError("create tc_interface failed", err.Error()) + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, r.toModel(out, plan))...) +} + +func (r *tcInterfaceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan, state tcInterfaceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + var out tcInterfaceAPI + if err := r.client.post(ctx, "/api/v1/tc-interfaces", r.body(plan), &out); err != nil { + resp.Diagnostics.AddError("recreate tc_interface failed", err.Error()) + return + } + if id := state.ID.ValueInt64(); id != 0 { + if err := r.client.del(ctx, "/api/v1/tc-interfaces/"+strconv.FormatInt(id, 10)); err != nil && !isNotFound(err) { + resp.Diagnostics.AddError("delete old tc_interface failed", err.Error()) + return + } + } + resp.Diagnostics.Append(resp.State.Set(ctx, r.toModel(out, plan))...) +} + +func (r *tcInterfaceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state tcInterfaceModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + var out tcInterfaceAPI + if err := r.client.get(ctx, "/api/v1/tc-interfaces/"+strconv.FormatInt(state.ID.ValueInt64(), 10), &out); err != nil { + if isNotFound(err) { + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError("read tc_interface failed", err.Error()) + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, r.toModel(out, state))...) +} + +func (r *tcInterfaceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state tcInterfaceModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if err := r.client.del(ctx, "/api/v1/tc-interfaces/"+strconv.FormatInt(state.ID.ValueInt64(), 10)); err != nil && !isNotFound(err) { + resp.Diagnostics.AddError("delete tc_interface failed", err.Error()) + } +} + +func (r *tcInterfaceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + id, err := strconv.ParseInt(req.ID, 10, 64) + if err != nil { + resp.Diagnostics.AddError("invalid import ID", "tc_interface id must be an integer") + return + } + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), id)...) +} + +func (r *tcInterfaceResource) toModel(api tcInterfaceAPI, prior tcInterfaceModel) tcInterfaceModel { + return tcInterfaceModel{ + ID: types.Int64Value(api.ID), Device: types.StringValue(api.Device), Interface: types.StringValue(api.Interface), + Type: optionalString(api.Type, prior.Type), InBandwidth: optionalString(api.InBandwidth, prior.InBandwidth), + OutBandwidth: optionalString(api.OutBandwidth, prior.OutBandwidth), Comment: optionalString(api.Comment, prior.Comment), + } +} diff --git a/internal/provider/resource_tc_priority.go b/internal/provider/resource_tc_priority.go new file mode 100644 index 0000000..0b0d0f7 --- /dev/null +++ b/internal/provider/resource_tc_priority.go @@ -0,0 +1,175 @@ +package provider + +import ( + "context" + "strconv" + + "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/types" +) + +var ( + _ resource.Resource = &tcPriorityResource{} + _ resource.ResourceWithImportState = &tcPriorityResource{} +) + +type tcPriorityResource struct{ client *apiClient } + +type tcPriorityModel struct { + ID types.Int64 `tfsdk:"id"` + Device types.String `tfsdk:"device"` + Band types.Int64 `tfsdk:"band"` + Proto types.String `tfsdk:"proto"` + DPort types.List `tfsdk:"dport"` + SPort types.List `tfsdk:"sport"` + Address types.String `tfsdk:"address"` + Interface types.String `tfsdk:"interface"` + Helper types.String `tfsdk:"helper"` + Comment types.String `tfsdk:"comment"` +} + +type tcPriorityAPI struct { + ID int64 `json:"id,omitempty"` + Device string `json:"device"` + Band int `json:"band"` + Proto string `json:"proto,omitempty"` + DPort []string `json:"dport,omitempty"` + SPort []string `json:"sport,omitempty"` + Address string `json:"address,omitempty"` + Interface string `json:"interface,omitempty"` + Helper string `json:"helper,omitempty"` + Comment string `json:"comment,omitempty"` +} + +func NewTCPriorityResource() resource.Resource { return &tcPriorityResource{} } + +func (r *tcPriorityResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_tc_priority" +} + +func (r *tcPriorityResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "A simple priority-band classification on a device.", + Attributes: map[string]schema.Attribute{ + "id": schema.Int64Attribute{Computed: true, PlanModifiers: []planmodifier.Int64{int64planmodifier.UseStateForUnknown()}}, + "device": schema.StringAttribute{Description: "Owning device.", Required: true}, + "band": schema.Int64Attribute{Description: "Priority band (1, 2, or 3).", Required: true}, + "proto": schema.StringAttribute{Optional: true}, + "dport": schema.ListAttribute{Optional: true, ElementType: types.StringType}, + "sport": schema.ListAttribute{Optional: true, ElementType: types.StringType}, + "address": schema.StringAttribute{Optional: true}, + "interface": schema.StringAttribute{Optional: true}, + "helper": schema.StringAttribute{Optional: true}, + "comment": schema.StringAttribute{Optional: true}, + }, + } +} + +func (r *tcPriorityResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.client = configureClient(req, resp) +} + +func (r *tcPriorityResource) body(ctx context.Context, plan tcPriorityModel, diags *diag.Diagnostics) tcPriorityAPI { + return tcPriorityAPI{ + Device: plan.Device.ValueString(), Band: int(plan.Band.ValueInt64()), Proto: plan.Proto.ValueString(), + DPort: listToStrings(ctx, plan.DPort, diags), SPort: listToStrings(ctx, plan.SPort, diags), + Address: plan.Address.ValueString(), Interface: plan.Interface.ValueString(), + Helper: plan.Helper.ValueString(), Comment: plan.Comment.ValueString(), + } +} + +func (r *tcPriorityResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan tcPriorityModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + body := r.body(ctx, plan, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + var out tcPriorityAPI + if err := r.client.post(ctx, "/api/v1/tc-priorities", body, &out); err != nil { + resp.Diagnostics.AddError("create tc_priority failed", err.Error()) + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, r.toModel(ctx, out, plan, &resp.Diagnostics))...) +} + +func (r *tcPriorityResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan, state tcPriorityModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + body := r.body(ctx, plan, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + var out tcPriorityAPI + if err := r.client.post(ctx, "/api/v1/tc-priorities", body, &out); err != nil { + resp.Diagnostics.AddError("recreate tc_priority failed", err.Error()) + return + } + if id := state.ID.ValueInt64(); id != 0 { + if err := r.client.del(ctx, "/api/v1/tc-priorities/"+strconv.FormatInt(id, 10)); err != nil && !isNotFound(err) { + resp.Diagnostics.AddError("delete old tc_priority failed", err.Error()) + return + } + } + resp.Diagnostics.Append(resp.State.Set(ctx, r.toModel(ctx, out, plan, &resp.Diagnostics))...) +} + +func (r *tcPriorityResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state tcPriorityModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + var out tcPriorityAPI + if err := r.client.get(ctx, "/api/v1/tc-priorities/"+strconv.FormatInt(state.ID.ValueInt64(), 10), &out); err != nil { + if isNotFound(err) { + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError("read tc_priority failed", err.Error()) + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, r.toModel(ctx, out, state, &resp.Diagnostics))...) +} + +func (r *tcPriorityResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state tcPriorityModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if err := r.client.del(ctx, "/api/v1/tc-priorities/"+strconv.FormatInt(state.ID.ValueInt64(), 10)); err != nil && !isNotFound(err) { + resp.Diagnostics.AddError("delete tc_priority failed", err.Error()) + } +} + +func (r *tcPriorityResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + id, err := strconv.ParseInt(req.ID, 10, 64) + if err != nil { + resp.Diagnostics.AddError("invalid import ID", "tc_priority id must be an integer") + return + } + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), id)...) +} + +func (r *tcPriorityResource) toModel(ctx context.Context, api tcPriorityAPI, prior tcPriorityModel, diags *diag.Diagnostics) tcPriorityModel { + return tcPriorityModel{ + ID: types.Int64Value(api.ID), Device: types.StringValue(api.Device), Band: types.Int64Value(int64(api.Band)), + Proto: optionalString(api.Proto, prior.Proto), DPort: optionalList(ctx, api.DPort, prior.DPort, diags), + SPort: optionalList(ctx, api.SPort, prior.SPort, diags), Address: optionalString(api.Address, prior.Address), + Interface: optionalString(api.Interface, prior.Interface), Helper: optionalString(api.Helper, prior.Helper), + Comment: optionalString(api.Comment, prior.Comment), + } +}