diff --git a/netbox/client.go b/netbox/client.go index a176247d..e496e967 100644 --- a/netbox/client.go +++ b/netbox/client.go @@ -19,6 +19,7 @@ type Config struct { Headers map[string]interface{} RequestTimeout int StripTrailingSlashesFromURL bool + JournalEntry string } // customHeaderTransport is a transport that adds the specified headers on @@ -81,6 +82,11 @@ func (cfg *Config) Client() (*netboxclient.NetBoxAPI, error) { transport.SetLogger(log.StandardLogger()) netboxClient := netboxclient.New(transport, nil) + if cfg.JournalEntry != "" { + jt := newJournalTransport(netboxClient.Transport, netboxClient, cfg.JournalEntry) + netboxClient.SetTransport(jt) + } + return netboxClient, nil } diff --git a/netbox/journal.go b/netbox/journal.go new file mode 100644 index 00000000..e85e0f51 --- /dev/null +++ b/netbox/journal.go @@ -0,0 +1,97 @@ +package netbox + +import ( + "fmt" + "net/http" + "reflect" + "strings" + + netboxclient "github.com/fbreckle/go-netbox/netbox/client" + netboxextras "github.com/fbreckle/go-netbox/netbox/client/extras" + "github.com/fbreckle/go-netbox/netbox/models" + "github.com/go-openapi/runtime" +) + +type journalAPITransport struct { + inner runtime.ClientTransport + client *netboxclient.NetBoxAPI + entry string +} + +func newJournalTransport(inner runtime.ClientTransport, client *netboxclient.NetBoxAPI, entry string) runtime.ClientTransport { + return &journalAPITransport{ + inner: inner, + client: client, + entry: entry, + } +} + +func (jt *journalAPITransport) Submit(op *runtime.ClientOperation) (interface{}, error) { + res, err := jt.inner.Submit(op) + if err != nil { + return res, err + } + + if op.ID == "extras_journal-entries_create" { + // avoid loops when writing journal + return res, nil + } + + // skip for some methods + if op.Method == http.MethodGet || op.Method == http.MethodDelete { + return res, nil + } + + // write journal + objectType, ok := getObjectType(op.ID) + if !ok { + return res, nil + } + + id, ok := getID(res) + if !ok { + return res, nil + } + + m := models.WritableJournalEntry{ + AssignedObjectType: &objectType, + AssignedObjectID: &id, + + Kind: models.WritableJournalEntryKindSuccess, + Comments: &jt.entry, + Tags: []*models.NestedTag{}, + } + p := netboxextras.NewExtrasJournalEntriesCreateParams().WithData(&m) + if _, err := jt.client.Extras.ExtrasJournalEntriesCreate(p, nil); err != nil { + return nil, fmt.Errorf("failed to create journal entry: %w", err) + } + + return res, nil +} + +func getObjectType(opID string) (string, bool) { + parts := strings.SplitN(opID, "_", 3) + if len(parts) < 3 { + return "", false + } + group := parts[0] + model := strings.TrimSuffix(parts[1], "s") + return group + "." + model, true +} + +func getID(res interface{}) (int64, bool) { + getter := reflect.ValueOf(res).MethodByName("GetPayload") + if getter.IsZero() { + return 0, false + } + pl := getter.Call(nil)[0] + if pl.IsNil() { + return 0, false + } + pl = pl.Elem() // deref pointer + id := pl.FieldByName("ID") + if id.IsZero() { + return 0, false + } + return id.Int(), true +} diff --git a/netbox/journal_test.go b/netbox/journal_test.go new file mode 100644 index 00000000..415ecfff --- /dev/null +++ b/netbox/journal_test.go @@ -0,0 +1,75 @@ +package netbox + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/fbreckle/go-netbox/netbox/client" + "github.com/fbreckle/go-netbox/netbox/client/extras" + "github.com/fbreckle/go-netbox/netbox/models" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccNetboxJournal(t *testing.T) { + cl, err := sharedClientForRegion("test") + if err != nil { + t.Fatal(err) + } + testClient := cl.(*client.NetBoxAPI) + + netboxProvider := *testAccProvider + origConfigure := netboxProvider.ConfigureContextFunc + netboxProvider.ConfigureContextFunc = func(ctx context.Context, rd *schema.ResourceData) (interface{}, diag.Diagnostics) { + rd.Set("journal_entry", "Test journal entry to be written") + return origConfigure(ctx, rd) + } + providers := map[string]*schema.Provider{ + "netbox": &netboxProvider, + } + + resource.ParallelTest(t, resource.TestCase{ + Providers: providers, + PreCheck: func() { testAccPreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: ` +resource "netbox_site" "test" { + name = "Test site for journal" +}`, + Check: func(s *terraform.State) error { + site, ok := s.RootModule().Resources["netbox_site.test"] + if !ok { + return errors.New("site resource not found in state") + } + + p := extras.NewExtrasJournalEntriesListParams() + p.AssignedObjectID = &site.Primary.ID + res, err := testClient.Extras.ExtrasJournalEntriesList(p, nil) + if err != nil { + return fmt.Errorf("failed to get journal from API: %w", err) + } + entries := res.GetPayload().Results + if len(entries) != 1 { + return fmt.Errorf("invalid number of journal entries: %d", len(entries)) + } + entry := entries[0] + if *entry.AssignedObjectType != "dcim.site" { + return fmt.Errorf("invalid object type on entry: %s", *entry.AssignedObjectType) + } + if *entry.Kind.Value != models.JournalEntryKindValueSuccess { + return fmt.Errorf("invalid kind: %v", *entry.Kind) + } + if *entry.Comments != "Test journal entry to be written" { + return fmt.Errorf("unexpected comment: %s", *entry.Comments) + } + return nil + }, + }, + }, + }) +} diff --git a/netbox/provider.go b/netbox/provider.go index 0a7a4dfd..4cac1725 100644 --- a/netbox/provider.go +++ b/netbox/provider.go @@ -234,6 +234,12 @@ func Provider() *schema.Provider { DefaultFunc: schema.EnvDefaultFunc("NETBOX_REQUEST_TIMEOUT", 10), Description: "Netbox API HTTP request timeout in seconds. Can be set via the `NETBOX_REQUEST_TIMEOUT` environment variable.", }, + "journal_entry": { + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("NETBOX_JOURNAL_ENTRY", ""), + Description: "Text for a journal entry to be written on a resource change (Markdown allowed). Journal is not written if this is empty.", + }, }, ConfigureContextFunc: providerConfigure, } @@ -249,6 +255,7 @@ func providerConfigure(ctx context.Context, data *schema.ResourceData) (interfac Headers: data.Get("headers").(map[string]interface{}), RequestTimeout: data.Get("request_timeout").(int), StripTrailingSlashesFromURL: data.Get("strip_trailing_slashes_from_url").(bool), + JournalEntry: data.Get("journal_entry").(string), } serverURL := data.Get("server_url").(string)