diff --git a/.changes/issue-629.md b/.changes/issue-629.md index a5607ea5..cd7361ee 100644 --- a/.changes/issue-629.md +++ b/.changes/issue-629.md @@ -1,6 +1,8 @@ FEATURES: +* add **junos_system_tacplus_server** resource (Fix [#629](https://github.com/jeremmfr/terraform-provider-junos/issues/629)) + ENHANCEMENTS: * **resource/junos_system_radius_server**: resource now use new [terraform-plugin-framework](https://github.com/hashicorp/terraform-plugin-framework) diff --git a/docs/resources/system_tacplus_server.md b/docs/resources/system_tacplus_server.md new file mode 100644 index 00000000..b93ce121 --- /dev/null +++ b/docs/resources/system_tacplus_server.md @@ -0,0 +1,50 @@ +--- +page_title: "Junos: junos_system_tacplus_server" +--- + +# junos_system_tacplus_server + +Configure a system tacplus-server. + +## Example Usage + +```hcl +# Add a system tacplus-server +resource "junos_system_tacplus_server" "demo_tacplus_server" { + address = "192.0.2.1" +} +``` + +## Argument Reference + +The following arguments are supported: + +- **address** (Required, String, Forces new resource) + TACACS+ authentication server address. +- **port** (Optional, Number) + TACACS+ authentication server port number (1..65535). +- **routing_instance** (Optional, String) + Routing instance. +- **secret** (Optional, String, Sensitive) + Shared secret with the authentication server. +- **single_connection** (Optional, Boolean) + Optimize TCP connection attempts. +- **source_address** (Optional, String) + Use specified address as source address. +- **timeout** (Optional, Number) + Request timeout period (1..90 seconds). + +## Attributes Reference + +The following attributes are exported: + +- **id** (String) + An identifier for the resource with format `
`. + +## Import + +Junos system tacplus-server can be imported using an id made up of `
`, e.g. + +```shell +$ terraform import junos_system_tacplus_server.demo_tacplus_server 192.0.2.1 +``` diff --git a/internal/providerfwk/provider.go b/internal/providerfwk/provider.go index 279331e6..65dd8ad3 100644 --- a/internal/providerfwk/provider.go +++ b/internal/providerfwk/provider.go @@ -292,6 +292,7 @@ func (p *junosProvider) Resources(_ context.Context) []func() resource.Resource newSystemSyslogFileResource, newSystemSyslogHostResource, newSystemSyslogUserResource, + newSystemTacplusServerResource, } } diff --git a/internal/providerfwk/resource_system_tacplus_server.go b/internal/providerfwk/resource_system_tacplus_server.go new file mode 100644 index 00000000..d4ff972d --- /dev/null +++ b/internal/providerfwk/resource_system_tacplus_server.go @@ -0,0 +1,410 @@ +package providerfwk + +import ( + "context" + "strings" + + "github.com/jeremmfr/terraform-provider-junos/internal/junos" + "github.com/jeremmfr/terraform-provider-junos/internal/tfdata" + "github.com/jeremmfr/terraform-provider-junos/internal/tfdiag" + "github.com/jeremmfr/terraform-provider-junos/internal/tfvalidator" + "github.com/jeremmfr/terraform-provider-junos/internal/utils" + + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "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/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + balt "github.com/jeremmfr/go-utils/basicalter" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &systemTacplusServer{} + _ resource.ResourceWithConfigure = &systemTacplusServer{} + _ resource.ResourceWithImportState = &systemTacplusServer{} +) + +type systemTacplusServer struct { + client *junos.Client +} + +func newSystemTacplusServerResource() resource.Resource { + return &systemTacplusServer{} +} + +func (rsc *systemTacplusServer) typeName() string { + return providerName + "_system_tacplus_server" +} + +func (rsc *systemTacplusServer) junosName() string { + return "system tacplus-server" +} + +func (rsc *systemTacplusServer) junosClient() *junos.Client { + return rsc.client +} + +func (rsc *systemTacplusServer) Metadata( + _ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse, +) { + resp.TypeName = rsc.typeName() +} + +func (rsc *systemTacplusServer) Configure( + ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse, +) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*junos.Client) + if !ok { + unexpectedResourceConfigureType(ctx, req, resp) + + return + } + rsc.client = client +} + +func (rsc *systemTacplusServer) Schema( + _ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse, +) { + resp.Schema = schema.Schema{ + Description: defaultResourceSchemaDescription(rsc), + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + Description: "An identifier for the resource with format `
`.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "address": schema.StringAttribute{ + Required: true, + Description: "TACACS+ authentication server address.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + tfvalidator.StringIPAddress(), + }, + }, + "port": schema.Int64Attribute{ + Optional: true, + Description: "TACACS+ authentication server port number.", + Validators: []validator.Int64{ + int64validator.Between(1, 65535), + }, + }, + "routing_instance": schema.StringAttribute{ + Optional: true, + Description: "Routing instance.", + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 63), + tfvalidator.StringFormat(tfvalidator.DefaultFormat), + }, + }, + "secret": schema.StringAttribute{ + Optional: true, + Sensitive: true, + Description: "Shared secret with the authentication server.", + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + tfvalidator.StringDoubleQuoteExclusion(), + }, + }, + "single_connection": schema.BoolAttribute{ + Optional: true, + Description: "Optimize TCP connection attempts.", + Validators: []validator.Bool{ + tfvalidator.BoolTrue(), + }, + }, + "source_address": schema.StringAttribute{ + Optional: true, + Description: "Use specified address as source address.", + Validators: []validator.String{ + tfvalidator.StringIPAddress(), + }, + }, + "timeout": schema.Int64Attribute{ + Optional: true, + Description: "Request timeout period.", + Validators: []validator.Int64{ + int64validator.Between(1, 90), + }, + }, + }, + } +} + +type systemTacplusServerData struct { + SingleConnection types.Bool `tfsdk:"single_connection"` + ID types.String `tfsdk:"id"` + Address types.String `tfsdk:"address"` + Port types.Int64 `tfsdk:"port"` + RoutingInstance types.String `tfsdk:"routing_instance"` + Secret types.String `tfsdk:"secret"` + SourceAddress types.String `tfsdk:"source_address"` + Timeout types.Int64 `tfsdk:"timeout"` +} + +func (rsc *systemTacplusServer) Create( + ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse, +) { + var plan systemTacplusServerData + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if plan.Address.ValueString() == "" { + resp.Diagnostics.AddAttributeError( + path.Root("address"), + "Empty Address", + defaultResourceCouldNotCreateWithEmptyMessage(rsc, "address"), + ) + + return + } + + defaultResourceCreate( + ctx, + rsc, + func(fnCtx context.Context, junSess *junos.Session) bool { + serverExists, err := checkSystemTacplusServerExists(fnCtx, plan.Address.ValueString(), junSess) + if err != nil { + resp.Diagnostics.AddError(tfdiag.PreCheckErrSummary, err.Error()) + + return false + } + if serverExists { + resp.Diagnostics.AddError( + tfdiag.DuplicateConfigErrSummary, + defaultResourceAlreadyExistsMessage(rsc, plan.Address), + ) + + return false + } + + return true + }, + func(fnCtx context.Context, junSess *junos.Session) bool { + serverExists, err := checkSystemTacplusServerExists(fnCtx, plan.Address.ValueString(), junSess) + if err != nil { + resp.Diagnostics.AddError(tfdiag.PostCheckErrSummary, err.Error()) + + return false + } + if !serverExists { + resp.Diagnostics.AddError( + tfdiag.NotFoundErrSummary, + defaultResourceDoesNotExistsAfterCommitMessage(rsc, plan.Address), + ) + + return false + } + + return true + }, + &plan, + resp, + ) +} + +func (rsc *systemTacplusServer) Read( + ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse, +) { + var state, data systemTacplusServerData + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + var _ resourceDataReadFrom1String = &data + defaultResourceRead( + ctx, + rsc, + []string{ + state.Address.ValueString(), + }, + &data, + nil, + resp, + ) +} + +func (rsc *systemTacplusServer) Update( + ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse, +) { + var plan, state systemTacplusServerData + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + defaultResourceUpdate( + ctx, + rsc, + &state, + &plan, + resp, + ) +} + +func (rsc *systemTacplusServer) Delete( + ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse, +) { + var state systemTacplusServerData + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + defaultResourceDelete( + ctx, + rsc, + &state, + resp, + ) +} + +func (rsc *systemTacplusServer) ImportState( + ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse, +) { + var data systemTacplusServerData + + var _ resourceDataReadFrom1String = &data + defaultResourceImportState( + ctx, + rsc, + &data, + req, + resp, + defaultResourceImportDontFindIDStrMessage(rsc, req.ID, "address"), + ) +} + +func checkSystemTacplusServerExists( + _ context.Context, address string, junSess *junos.Session, +) ( + _ bool, err error, +) { + showConfig, err := junSess.Command(junos.CmdShowConfig + + "system tacplus-server " + address + junos.PipeDisplaySet) + if err != nil { + return false, err + } + if showConfig == junos.EmptyW { + return false, nil + } + + return true, nil +} + +func (rscData *systemTacplusServerData) fillID() { + rscData.ID = types.StringValue(rscData.Address.ValueString()) +} + +func (rscData *systemTacplusServerData) nullID() bool { + return rscData.ID.IsNull() +} + +func (rscData *systemTacplusServerData) set( + _ context.Context, junSess *junos.Session, +) ( + path.Path, error, +) { + setPrefix := "set system tacplus-server " + rscData.Address.ValueString() + " " + configSet := []string{ + setPrefix, + } + + if !rscData.Port.IsNull() { + configSet = append(configSet, setPrefix+"port "+ + utils.ConvI64toa(rscData.Port.ValueInt64())) + } + if v := rscData.RoutingInstance.ValueString(); v != "" { + configSet = append(configSet, setPrefix+"routing-instance "+v) + } + if v := rscData.Secret.ValueString(); v != "" { + configSet = append(configSet, setPrefix+"secret \""+v+"\"") + } + if rscData.SingleConnection.ValueBool() { + configSet = append(configSet, setPrefix+"single-connection") + } + if v := rscData.SourceAddress.ValueString(); v != "" { + configSet = append(configSet, setPrefix+"source-address "+v) + } + if !rscData.Timeout.IsNull() { + configSet = append(configSet, setPrefix+"timeout "+ + utils.ConvI64toa(rscData.Timeout.ValueInt64())) + } + + return path.Empty(), junSess.ConfigSet(configSet) +} + +func (rscData *systemTacplusServerData) read( + _ context.Context, address string, junSess *junos.Session, +) ( + err error, +) { + showConfig, err := junSess.Command(junos.CmdShowConfig + + "system tacplus-server " + address + junos.PipeDisplaySetRelative) + if err != nil { + return err + } + if showConfig != junos.EmptyW { + rscData.Address = types.StringValue(address) + rscData.fillID() + for _, item := range strings.Split(showConfig, "\n") { + if strings.Contains(item, junos.XMLStartTagConfigOut) { + continue + } + if strings.Contains(item, junos.XMLEndTagConfigOut) { + break + } + itemTrim := strings.TrimPrefix(item, junos.SetLS) + switch { + case balt.CutPrefixInString(&itemTrim, "port "): + rscData.Port, err = tfdata.ConvAtoi64Value(itemTrim) + if err != nil { + return err + } + case balt.CutPrefixInString(&itemTrim, "routing-instance "): + rscData.RoutingInstance = types.StringValue(itemTrim) + case balt.CutPrefixInString(&itemTrim, "secret "): + rscData.Secret, err = tfdata.JunosDecode(strings.Trim(itemTrim, "\""), "secret") + if err != nil { + return err + } + case itemTrim == "single-connection": + rscData.SingleConnection = types.BoolValue(true) + case balt.CutPrefixInString(&itemTrim, "source-address "): + rscData.SourceAddress = types.StringValue(itemTrim) + case balt.CutPrefixInString(&itemTrim, "timeout "): + rscData.Timeout, err = tfdata.ConvAtoi64Value(itemTrim) + if err != nil { + return err + } + } + } + } + + return nil +} + +func (rscData *systemTacplusServerData) del( + _ context.Context, junSess *junos.Session, +) error { + configSet := []string{ + "delete system tacplus-server " + rscData.Address.ValueString(), + } + + return junSess.ConfigSet(configSet) +} diff --git a/internal/providerfwk/resource_system_tacplus_server_test.go b/internal/providerfwk/resource_system_tacplus_server_test.go new file mode 100644 index 00000000..8aec08af --- /dev/null +++ b/internal/providerfwk/resource_system_tacplus_server_test.go @@ -0,0 +1,31 @@ +package providerfwk_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/config" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccResourceSystemTacplusServer_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccProtoV5ProviderFactories, + Steps: []resource.TestStep{ + { + ConfigDirectory: config.TestStepDirectory(), + }, + { + ConfigDirectory: config.TestStepDirectory(), + }, + { + ResourceName: "junos_system_tacplus_server.testacc_tacplusServer", + ImportState: true, + ImportStateVerify: true, + }, + { + ConfigDirectory: config.TestStepDirectory(), + }, + }, + }) +} diff --git a/internal/providerfwk/testdata/TestAccResourceSystemTacplusServer_basic/1/main.tf b/internal/providerfwk/testdata/TestAccResourceSystemTacplusServer_basic/1/main.tf new file mode 100644 index 00000000..b2f0700f --- /dev/null +++ b/internal/providerfwk/testdata/TestAccResourceSystemTacplusServer_basic/1/main.tf @@ -0,0 +1,3 @@ +resource "junos_system_tacplus_server" "testacc_tacplusServer" { + address = "192.0.2.1" +} diff --git a/internal/providerfwk/testdata/TestAccResourceSystemTacplusServer_basic/2/main.tf b/internal/providerfwk/testdata/TestAccResourceSystemTacplusServer_basic/2/main.tf new file mode 100644 index 00000000..5504f708 --- /dev/null +++ b/internal/providerfwk/testdata/TestAccResourceSystemTacplusServer_basic/2/main.tf @@ -0,0 +1,12 @@ +resource "junos_routing_instance" "testacc_tacplusServer" { + name = "testacc_tacplusServer" +} +resource "junos_system_tacplus_server" "testacc_tacplusServer" { + address = "192.0.2.1" + secret = "password" + source_address = "192.0.2.2" + port = 49 + timeout = 10 + single_connection = true + routing_instance = junos_routing_instance.testacc_tacplusServer.name +} diff --git a/internal/providerfwk/testdata/TestAccResourceSystemTacplusServer_basic/4/main.tf b/internal/providerfwk/testdata/TestAccResourceSystemTacplusServer_basic/4/main.tf new file mode 100644 index 00000000..f82bef41 --- /dev/null +++ b/internal/providerfwk/testdata/TestAccResourceSystemTacplusServer_basic/4/main.tf @@ -0,0 +1,7 @@ +resource "junos_routing_instance" "testacc_tacplusServer" { + name = "testacc_tacplusServer" +} +resource "junos_system_tacplus_server" "testacc_tacplusServer" { + address = "192.0.2.1" + port = 49 +}