From 897c6cb9ae42b558c9a929dbc8bf18732d318c67 Mon Sep 17 00:00:00 2001 From: MegaportPhilipBrowne Date: Mon, 23 Jun 2025 10:26:00 -0400 Subject: [PATCH 1/2] feat: maintenance and outage events support --- client.go | 3 + errors.go | 22 ++++++ events.go | 210 +++++++++++++++++++++++++++++++++++++++++++++++++ events_test.go | 120 ++++++++++++++++++++++++++++ 4 files changed, 355 insertions(+) create mode 100644 events.go create mode 100644 events_test.go diff --git a/client.go b/client.go index 9850fa0..5dcfefd 100644 --- a/client.go +++ b/client.go @@ -74,6 +74,8 @@ type Client struct { ManagedAccountService ManagedAccountService // IXService provides methods for interacting with the IX API IXService IXService + // EventsService provides methods for interacting with the Events API + EventsService EventsService accessToken string // Access Token for client tokenExpiry time.Time // Token Expiration @@ -169,6 +171,7 @@ func NewClient(httpClient *http.Client, base *url.URL) *Client { c.PartnerService = NewPartnerService(c) c.ServiceKeyService = NewServiceKeyService(c) c.ManagedAccountService = NewManagedAccountService(c) + c.EventsService = NewEventsService(c) c.headers = make(map[string]string) diff --git a/errors.go b/errors.go index 951ca96..5372d9e 100644 --- a/errors.go +++ b/errors.go @@ -83,3 +83,25 @@ var ErrInvalidVXCAEndPartnerConfig = errors.New("invalid vxc a-end partner confi // ErrInvalidVXCBEndPartnerConfig is returned when an invalid VXC B-End partner config is provided var ErrInvalidVXCBEndPartnerConfig = errors.New("invalid vxc b-end partner config") + +// maintenanceStatesToString converts a slice of MaintenanceState to a slice of string +func maintenanceStatesToString(states []MaintenanceState) []string { + strs := make([]string, len(states)) + for i, v := range states { + strs[i] = string(v) + } + return strs +} + +var ErrInvalidMaintenanceState = fmt.Errorf("invalid maintenance state, valid states are %s", strings.Join(maintenanceStatesToString(VALID_MAINTENANCE_STATES), ", ")) + +// outageStatesToString converts a slice of OutageState to a slice of string +func outageStatesToString(states []OutageState) []string { + strs := make([]string, len(states)) + for i, v := range states { + strs[i] = string(v) + } + return strs +} + +var ErrInvalidOutageState = fmt.Errorf("invalid outage state, valid states are %s", strings.Join(outageStatesToString(VALID_OUTAGE_STATES), ", ")) diff --git a/events.go b/events.go new file mode 100644 index 0000000..96baa7e --- /dev/null +++ b/events.go @@ -0,0 +1,210 @@ +package megaport + +import ( + "encoding/json" + "net/http" + "strings" +) + +type EventsService interface { + // GetMaintenanceEvents returns details about maintenance events, filtered by the specified state value. + GetMaintenanceEvents(state string) ([]MaintenanceEvent, error) + // GetOutageEvents returns details about outage events, filtered by the specified state value. + GetOutageEvents(state string) ([]OutageEvent, error) +} + +type EventsServiceOp struct { + client *Client +} + +func NewEventsService(client *Client) EventsService { + return &EventsServiceOp{ + client: client, + } +} + +type MaintenanceState string +type OutageState string + +var ( + VALID_MAINTENANCE_STATES = []MaintenanceState{ + MAINTENANCE_STATE_COMPLETED, + MAINTENANCE_STATE_SCHEDULED, + MAINTENANCE_STATE_CANCELLED, + MAINTENANCE_STATE_RUNNING, + } + VALID_OUTAGE_STATES = []OutageState{ + OUTAGE_STATE_ONGOING, + OUTAGE_STATE_RESOLVED, + } +) + +const ( + MAINTENANCE_STATE_COMPLETED = MaintenanceState("Completed") + MAINTENANCE_STATE_SCHEDULED = MaintenanceState("Scheduled") + MAINTENANCE_STATE_CANCELLED = MaintenanceState("Cancelled") + MAINTENANCE_STATE_RUNNING = MaintenanceState("Running") + OUTAGE_STATE_ONGOING = OutageState("Ongoing") + OUTAGE_STATE_RESOLVED = OutageState("Resolved") +) + +// MaintenanceEvent represents a maintenance event returned by the Events API. +// +// Returns details about maintenance events, filtered by the specified state value. +// +// The following information is returned for maintenance events in the response, with some fields being optional and only included under certain conditions. +type MaintenanceEvent struct { + // EventID is the ticket number against which a particular event is created. + EventID string `json:"eventId"` + + // State is the current state of the event. + State string `json:"state"` + + // StartTime is the event start time in ISO 8601 UTC format (yyyy-MM-dd'T'HH:mm:ss.SSSX). + StartTime string `json:"startTime"` + + // EndTime is the event end time in ISO 8601 UTC format (yyyy-MM-dd'T'HH:mm:ss.SSSX). + EndTime string `json:"endTime"` + + // Impact is the impact of the event on the services, if any. + Impact string `json:"impact"` + + // Purpose is the reason why this event is created. + Purpose string `json:"purpose"` + + // CancelReason is returned if the event is canceled, stating the cancellation reason. + CancelReason string `json:"cancelReason"` + + // EventType is "Emergency" if the event is created on short notice; otherwise, it is a "Planned" event. + EventType string `json:"eventType"` + + // ServiceIDs is the list of services affected by the event, containing the short UUIDs of the services. + ServiceIDs []string `json:"services"` +} + +// OutageEvent represents an outage event returned by the Events API. +// +// The following information is returned for outage events in the response, with some fields being optional and only included under certain conditions. +type OutageEvent struct { + // OutageID is a unique identifier for each outage event. + OutageID string `json:"outageId"` + + // EventID is the ticket number against which a particular event is created. + EventID string `json:"eventId"` + + // State is the current state of the event. + State string `json:"state"` + + // StartTime is the event start time in ISO 8601 UTC format (yyyy-MM-dd'T'HH:mm:ss.SSSX). + StartTime string `json:"startTime"` + + // EndTime is the event end time in ISO 8601 UTC format (yyyy-MM-dd'T'HH:mm:ss.SSSX). + EndTime string `json:"endTime"` + + // Purpose is the reason why this event is created. + Purpose string `json:"purpose"` + + // Services is the list of services affected by the event, containing the short UUIDs of the services. + Services []string `json:"services"` + + // RootCause is the reason explaining why an outage happened. This field is present only when an outage is resolved. + RootCause string `json:"rootCause"` + + // Resolution explains the solution taken to resolve the outage. Present when an outage is resolved. + Resolution string `json:"resolution"` + + // MitigationActions explains the steps taken to avoid such outages in the future. Present for resolved outages. + MitigationActions string `json:"mitigationActions"` + + // CreatedBy is the user who created the outage. + CreatedBy string `json:"createdBy"` + + // CreatedDate is the date and time when an outage event is created, in ISO 8601 UTC format (yyyy-MM-dd'T'HH:mm:ss.SSSX). + CreatedDate string `json:"createdDate"` + + // UpdatedDate is the date and time when an outage event is updated, in ISO 8601 UTC format (yyyy-MM-dd'T'HH:mm:ss.SSSX). + UpdatedDate string `json:"updatedDate"` + + // Notices is the list of notices sent as an update for an ongoing outage. + Notices []string `json:"notices"` +} + +// GetMaintenanceEvents retrieves maintenance events from the Megaport API, filtered by the specified state. +// It validates the state against the valid maintenance states and returns an error if invalid. +func (s *EventsServiceOp) GetMaintenanceEvents(state string) ([]MaintenanceEvent, error) { + // Validate state + valid := false + for _, st := range VALID_MAINTENANCE_STATES { + if strings.EqualFold(string(st), state) { + valid = true + break + } + } + if !valid { + return nil, ErrInvalidMaintenanceState + } + + // Build URL + url := s.client.BaseURL.JoinPath("/ens/v1/status/maintenance").String() + "?state=" + state + + // Create HTTP request + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, err + } + + // Perform request + resp, err := s.client.HTTPClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + // Decode response as an array of MaintenanceEvent + var events []MaintenanceEvent + if err := json.NewDecoder(resp.Body).Decode(&events); err != nil { + return nil, err + } + + return events, nil +} + +// GetOutageEvents retrieves outage events from the Megaport API, filtered by the specified state. +// It validates the state against the valid outage states and returns an error if invalid. +func (s *EventsServiceOp) GetOutageEvents(state string) ([]OutageEvent, error) { + // Validate state + valid := false + for _, st := range VALID_OUTAGE_STATES { + if strings.EqualFold(string(st), state) { + valid = true + break + } + } + if !valid { + return nil, ErrInvalidOutageState + } + + // Build URL + url := s.client.BaseURL.JoinPath("/ens/v1/status/outage").String() + "?state=" + state + + // Create HTTP request + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, err + } + + // Perform request + resp, err := s.client.HTTPClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + // Decode response as an array of OutageEvent + var events []OutageEvent + if err := json.NewDecoder(resp.Body).Decode(&events); err != nil { + return nil, err + } + + return events, nil +} diff --git a/events_test.go b/events_test.go new file mode 100644 index 0000000..807cb06 --- /dev/null +++ b/events_test.go @@ -0,0 +1,120 @@ +package megaport + +import ( + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/stretchr/testify/suite" +) + +// EventsTestSuite tests the Events service. +type EventsTestSuite struct { + ClientTestSuite +} + +func TestEventsTestSuite(t *testing.T) { + t.Parallel() + suite.Run(t, new(EventsTestSuite)) +} + +func (suite *EventsTestSuite) SetupTest() { + suite.mux = http.NewServeMux() + suite.server = httptest.NewServer(suite.mux) + + suite.client = NewClient(nil, nil) + url, _ := url.Parse(suite.server.URL) + suite.client.BaseURL = url +} + +func (suite *EventsTestSuite) TearDownTest() { + suite.server.Close() +} + +func (suite *EventsTestSuite) TestGetMaintenanceEvents() { + sampleJSON := `[ + { + "eventId": "CSS-1234", + "state": "Scheduled", + "startTime": "2024-05-24T09:12:00.000Z", + "endTime": "2024-05-24T09:42:00.000Z", + "impact": "There will be minor impact on services.", + "purpose": "Services will become more effective", + "eventType": "Emergency", + "services": [ + "f06c80bc", + "0746e9a3" + ] + }, + { + "eventId": "CSS-1235", + "state": "Cancelled", + "startTime": "2024-05-24T09:12:00.000Z", + "endTime": "2024-05-24T09:42:00.000Z", + "impact": "There will be minor impact on services.", + "purpose": "Services will become more effective", + "cancelReason": "Not Needed", + "eventType": "Emergency", + "services": [ + "f06c80bc", + "0746e9a3" + ] + } + ]` + + suite.mux.HandleFunc("/ens/v1/status/maintenance", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(sampleJSON)) + }) + + svc := &EventsServiceOp{client: suite.client} + events, err := svc.GetMaintenanceEvents("Scheduled") + suite.NoError(err) + suite.Len(events, 2) + suite.Equal("CSS-1234", events[0].EventID) + suite.Equal("Not Needed", events[1].CancelReason) +} + +func (suite *EventsTestSuite) TestGetOutageEvents() { + sampleJSON := `[ + { + "outageId": "c2037361-eb5b-48a3-9c73-fb4efbf2c886", + "state": "Ongoing", + "eventId": "CSS-1234", + "purpose": "Due to high CPU Usage, service outage occurred", + "startTime": "2024-05-22T09:08:00.000Z", + "createdBy": "john.cena@fibre.com", + "createdDate": "2024-05-22T13:39:32.468Z", + "updatedDate": "2024-05-22T13:39:32.468Z", + "services": [], + "notices": [] + }, + { + "outageId": "ce0dd76b-655c-425f-923f-af5ae896756f", + "state": "Ongoing", + "eventId": "CSS-12345", + "purpose": "This happened because something broke", + "startTime": "2024-05-23T08:32:00.000Z", + "createdBy": "john.cena@fibre.com", + "createdDate": "2024-05-23T13:02:30.968Z", + "updatedDate": "2024-05-23T13:02:30.968Z", + "services": [], + "notices": [] + } + ]` + + suite.mux.HandleFunc("/ens/v1/status/outage", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(sampleJSON)) + }) + + svc := &EventsServiceOp{client: suite.client} + events, err := svc.GetOutageEvents("Ongoing") + suite.NoError(err) + suite.Len(events, 2) + suite.Equal("c2037361-eb5b-48a3-9c73-fb4efbf2c886", events[0].OutageID) + suite.Equal("CSS-12345", events[1].EventID) +} From 753c08e110212beec04ca1ba9f25cf0a2dd0f607 Mon Sep 17 00:00:00 2001 From: MegaportPhilipBrowne Date: Mon, 23 Jun 2025 15:44:25 -0400 Subject: [PATCH 2/2] fix: error response --- events_test.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/events_test.go b/events_test.go index 807cb06..7332784 100644 --- a/events_test.go +++ b/events_test.go @@ -66,7 +66,10 @@ func (suite *EventsTestSuite) TestGetMaintenanceEvents() { suite.mux.HandleFunc("/ens/v1/status/maintenance", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - w.Write([]byte(sampleJSON)) + _, err := w.Write([]byte(sampleJSON)) + if err != nil { + suite.FailNowf("Failed to write response", "Error: %v", err) + } }) svc := &EventsServiceOp{client: suite.client} @@ -108,7 +111,10 @@ func (suite *EventsTestSuite) TestGetOutageEvents() { suite.mux.HandleFunc("/ens/v1/status/outage", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - w.Write([]byte(sampleJSON)) + _, err := w.Write([]byte(sampleJSON)) + if err != nil { + suite.FailNowf("Failed to write response", "Error: %v", err) + } }) svc := &EventsServiceOp{client: suite.client}