package provider import ( "bytes" "encoding/json" "errors" "fmt" "io" "net/http" "time" ) // ErrNotFound is returned when a resource is not found (404). var ErrNotFound = errors.New("resource not found") const defaultBaseURL = "https://towerops.net" // Client is the TowerOps API client. type Client struct { BaseURL string Token string HTTPClient *http.Client } // NewClient creates a new TowerOps API client. func NewClient(token, baseURL string) *Client { if baseURL == "" { baseURL = defaultBaseURL } return &Client{ BaseURL: baseURL, Token: token, HTTPClient: &http.Client{ Timeout: 30 * time.Second, }, } } // Site represents a TowerOps site. type Site struct { ID string `json:"id,omitempty"` Name string `json:"name"` Location *string `json:"location,omitempty"` Address *string `json:"address,omitempty"` Latitude *float64 `json:"latitude,omitempty"` Longitude *float64 `json:"longitude,omitempty"` SNMPCommunity *string `json:"snmp_community,omitempty"` InsertedAt string `json:"inserted_at,omitempty"` } // Device represents a TowerOps device. type Device struct { ID string `json:"id,omitempty"` SiteID *string `json:"site_id,omitempty"` OrganizationID *string `json:"organization_id,omitempty"` Name *string `json:"name,omitempty"` IPAddress string `json:"ip_address"` Description *string `json:"description,omitempty"` MonitoringEnabled *bool `json:"monitoring_enabled,omitempty"` SNMPEnabled *bool `json:"snmp_enabled,omitempty"` SNMPVersion *string `json:"snmp_version,omitempty"` SNMPPort *int `json:"snmp_port,omitempty"` CheckIntervalSeconds *int `json:"check_interval_seconds,omitempty"` DeviceRole *string `json:"device_role,omitempty"` // SNMPv3 fields SNMPv3SecurityLevel *string `json:"snmpv3_security_level,omitempty"` SNMPv3Username *string `json:"snmpv3_username,omitempty"` SNMPv3AuthProtocol *string `json:"snmpv3_auth_protocol,omitempty"` SNMPv3AuthPassword *string `json:"snmpv3_auth_password,omitempty"` SNMPv3PrivProtocol *string `json:"snmpv3_priv_protocol,omitempty"` SNMPv3PrivPassword *string `json:"snmpv3_priv_password,omitempty"` InsertedAt string `json:"inserted_at,omitempty"` } // Organization represents a TowerOps organization. type Organization struct { ID string `json:"id,omitempty"` Name string `json:"name,omitempty"` Slug string `json:"slug,omitempty"` UseSites bool `json:"use_sites"` SnmpCommunity string `json:"snmp_community,omitempty"` } // organizationResponse wraps the API response for organization endpoints. type organizationResponse struct { Data Organization `json:"data"` } // APIError represents an error response from the API. type APIError struct { Error string `json:"error,omitempty"` Errors map[string]string `json:"errors,omitempty"` } func (c *Client) doRequest(method, path string, body interface{}) ([]byte, error) { var reqBody io.Reader if body != nil { jsonBody, err := json.Marshal(body) if err != nil { return nil, fmt.Errorf("failed to marshal request body: %w", err) } reqBody = bytes.NewBuffer(jsonBody) } req, err := http.NewRequest(method, c.BaseURL+path, reqBody) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } req.Header.Set("Authorization", "Bearer "+c.Token) req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") resp, err := c.HTTPClient.Do(req) if err != nil { return nil, fmt.Errorf("request failed: %w", err) } defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } if resp.StatusCode >= 400 { if resp.StatusCode == http.StatusNotFound { return nil, ErrNotFound } var apiErr APIError if err := json.Unmarshal(respBody, &apiErr); err == nil { if apiErr.Error != "" { return nil, fmt.Errorf("API error (%d): %s", resp.StatusCode, apiErr.Error) } if len(apiErr.Errors) > 0 { return nil, fmt.Errorf("API validation error (%d): %v", resp.StatusCode, apiErr.Errors) } } return nil, fmt.Errorf("API error (%d): %s", resp.StatusCode, string(respBody)) } return respBody, nil } // CreateSite creates a new site. func (c *Client) CreateSite(site Site) (*Site, error) { body := map[string]Site{"site": site} respBody, err := c.doRequest(http.MethodPost, "/api/v1/sites", body) if err != nil { return nil, err } var result Site if err := json.Unmarshal(respBody, &result); err != nil { return nil, fmt.Errorf("failed to unmarshal response: %w", err) } return &result, nil } // GetSite retrieves a site by ID. func (c *Client) GetSite(id string) (*Site, error) { respBody, err := c.doRequest(http.MethodGet, "/api/v1/sites/"+id, nil) if err != nil { return nil, err } var result Site if err := json.Unmarshal(respBody, &result); err != nil { return nil, fmt.Errorf("failed to unmarshal response: %w", err) } return &result, nil } // UpdateSite updates an existing site. func (c *Client) UpdateSite(id string, site Site) (*Site, error) { body := map[string]Site{"site": site} respBody, err := c.doRequest(http.MethodPatch, "/api/v1/sites/"+id, body) if err != nil { return nil, err } var result Site if err := json.Unmarshal(respBody, &result); err != nil { return nil, fmt.Errorf("failed to unmarshal response: %w", err) } return &result, nil } // DeleteSite deletes a site. func (c *Client) DeleteSite(id string) error { _, err := c.doRequest(http.MethodDelete, "/api/v1/sites/"+id, nil) return err } // CreateDevice creates a new device. func (c *Client) CreateDevice(device Device) (*Device, error) { body := map[string]Device{"device": device} respBody, err := c.doRequest(http.MethodPost, "/api/v1/devices", body) if err != nil { return nil, err } var result Device if err := json.Unmarshal(respBody, &result); err != nil { return nil, fmt.Errorf("failed to unmarshal response: %w", err) } return &result, nil } // GetDevice retrieves a device by ID. func (c *Client) GetDevice(id string) (*Device, error) { respBody, err := c.doRequest(http.MethodGet, "/api/v1/devices/"+id, nil) if err != nil { return nil, err } var result Device if err := json.Unmarshal(respBody, &result); err != nil { return nil, fmt.Errorf("failed to unmarshal response: %w", err) } return &result, nil } // UpdateDevice updates an existing device. func (c *Client) UpdateDevice(id string, device Device) (*Device, error) { body := map[string]Device{"device": device} respBody, err := c.doRequest(http.MethodPatch, "/api/v1/devices/"+id, body) if err != nil { return nil, err } var result Device if err := json.Unmarshal(respBody, &result); err != nil { return nil, fmt.Errorf("failed to unmarshal response: %w", err) } return &result, nil } // DeleteDevice deletes a device. func (c *Client) DeleteDevice(id string) error { _, err := c.doRequest(http.MethodDelete, "/api/v1/devices/"+id, nil) return err } // OnCallSchedule represents a TowerOps on-call schedule. type OnCallSchedule struct { ID string `json:"id,omitempty"` Name string `json:"name"` Description *string `json:"description,omitempty"` Timezone string `json:"timezone"` InsertedAt string `json:"inserted_at,omitempty"` } // CreateSchedule creates a new on-call schedule. func (c *Client) CreateSchedule(schedule OnCallSchedule) (*OnCallSchedule, error) { body := map[string]OnCallSchedule{"schedule": schedule} respBody, err := c.doRequest(http.MethodPost, "/api/v1/schedules", body) if err != nil { return nil, err } var result OnCallSchedule if err := json.Unmarshal(respBody, &result); err != nil { return nil, fmt.Errorf("failed to unmarshal response: %w", err) } return &result, nil } // GetSchedule retrieves an on-call schedule by ID. func (c *Client) GetSchedule(id string) (*OnCallSchedule, error) { respBody, err := c.doRequest(http.MethodGet, "/api/v1/schedules/"+id, nil) if err != nil { return nil, err } var result OnCallSchedule if err := json.Unmarshal(respBody, &result); err != nil { return nil, fmt.Errorf("failed to unmarshal response: %w", err) } return &result, nil } // UpdateSchedule updates an existing on-call schedule. func (c *Client) UpdateSchedule(id string, schedule OnCallSchedule) (*OnCallSchedule, error) { body := map[string]OnCallSchedule{"schedule": schedule} respBody, err := c.doRequest(http.MethodPatch, "/api/v1/schedules/"+id, body) if err != nil { return nil, err } var result OnCallSchedule if err := json.Unmarshal(respBody, &result); err != nil { return nil, fmt.Errorf("failed to unmarshal response: %w", err) } return &result, nil } // DeleteSchedule deletes an on-call schedule. func (c *Client) DeleteSchedule(id string) error { _, err := c.doRequest(http.MethodDelete, "/api/v1/schedules/"+id, nil) return err } // EscalationPolicyAPI represents a TowerOps escalation policy. type EscalationPolicyAPI struct { ID string `json:"id,omitempty"` Name string `json:"name"` Description *string `json:"description,omitempty"` RepeatCount *int `json:"repeat_count,omitempty"` InsertedAt string `json:"inserted_at,omitempty"` } // CreateEscalationPolicy creates a new escalation policy. func (c *Client) CreateEscalationPolicy(policy EscalationPolicyAPI) (*EscalationPolicyAPI, error) { body := map[string]EscalationPolicyAPI{"escalation_policy": policy} respBody, err := c.doRequest(http.MethodPost, "/api/v1/escalation_policies", body) if err != nil { return nil, err } var result EscalationPolicyAPI if err := json.Unmarshal(respBody, &result); err != nil { return nil, fmt.Errorf("failed to unmarshal response: %w", err) } return &result, nil } // GetEscalationPolicy retrieves an escalation policy by ID. func (c *Client) GetEscalationPolicy(id string) (*EscalationPolicyAPI, error) { respBody, err := c.doRequest(http.MethodGet, "/api/v1/escalation_policies/"+id, nil) if err != nil { return nil, err } var result EscalationPolicyAPI if err := json.Unmarshal(respBody, &result); err != nil { return nil, fmt.Errorf("failed to unmarshal response: %w", err) } return &result, nil } // UpdateEscalationPolicy updates an existing escalation policy. func (c *Client) UpdateEscalationPolicy(id string, policy EscalationPolicyAPI) (*EscalationPolicyAPI, error) { body := map[string]EscalationPolicyAPI{"escalation_policy": policy} respBody, err := c.doRequest(http.MethodPatch, "/api/v1/escalation_policies/"+id, body) if err != nil { return nil, err } var result EscalationPolicyAPI if err := json.Unmarshal(respBody, &result); err != nil { return nil, fmt.Errorf("failed to unmarshal response: %w", err) } return &result, nil } // DeleteEscalationPolicy deletes an escalation policy. func (c *Client) DeleteEscalationPolicy(id string) error { _, err := c.doRequest(http.MethodDelete, "/api/v1/escalation_policies/"+id, nil) return err } // Agent represents a TowerOps agent token. type Agent struct { ID string `json:"id,omitempty"` Name string `json:"name"` Enabled *bool `json:"enabled,omitempty"` LastSeenAt *string `json:"last_seen_at,omitempty"` InsertedAt string `json:"inserted_at,omitempty"` Token string `json:"token,omitempty"` } // agentCreateResponse wraps the create response which includes token. type agentCreateResponse struct { Agent } // CreateAgent creates a new agent token. func (c *Client) CreateAgent(agent Agent) (*Agent, error) { body := map[string]Agent{"agent": agent} respBody, err := c.doRequest(http.MethodPost, "/api/v1/agents", body) if err != nil { return nil, err } var result Agent if err := json.Unmarshal(respBody, &result); err != nil { return nil, fmt.Errorf("failed to unmarshal response: %w", err) } return &result, nil } // GetAgent retrieves an agent by ID. func (c *Client) GetAgent(id string) (*Agent, error) { respBody, err := c.doRequest(http.MethodGet, "/api/v1/agents/"+id, nil) if err != nil { return nil, err } var result Agent if err := json.Unmarshal(respBody, &result); err != nil { return nil, fmt.Errorf("failed to unmarshal response: %w", err) } return &result, nil } // DeleteAgent deletes an agent. func (c *Client) DeleteAgent(id string) error { _, err := c.doRequest(http.MethodDelete, "/api/v1/agents/"+id, nil) return err } // Integration represents a TowerOps integration. type Integration struct { ID string `json:"id,omitempty"` Provider string `json:"provider"` Enabled *bool `json:"enabled,omitempty"` SyncIntervalMinutes *int `json:"sync_interval_minutes,omitempty"` InsertedAt string `json:"inserted_at,omitempty"` UpdatedAt string `json:"updated_at,omitempty"` } // integrationWithCredentials is used for create/update requests that include credentials. type integrationWithCredentials struct { Provider string `json:"provider"` Enabled *bool `json:"enabled,omitempty"` Credentials map[string]interface{} `json:"credentials,omitempty"` SyncIntervalMinutes *int `json:"sync_interval_minutes,omitempty"` } // CreateIntegration creates a new integration. func (c *Client) CreateIntegration(integration integrationWithCredentials) (*Integration, error) { body := map[string]integrationWithCredentials{"integration": integration} respBody, err := c.doRequest(http.MethodPost, "/api/v1/integrations", body) if err != nil { return nil, err } var result Integration if err := json.Unmarshal(respBody, &result); err != nil { return nil, fmt.Errorf("failed to unmarshal response: %w", err) } return &result, nil } // GetIntegration retrieves an integration by ID. func (c *Client) GetIntegration(id string) (*Integration, error) { respBody, err := c.doRequest(http.MethodGet, "/api/v1/integrations/"+id, nil) if err != nil { return nil, err } var result Integration if err := json.Unmarshal(respBody, &result); err != nil { return nil, fmt.Errorf("failed to unmarshal response: %w", err) } return &result, nil } // UpdateIntegration updates an existing integration. func (c *Client) UpdateIntegration(id string, integration integrationWithCredentials) (*Integration, error) { body := map[string]integrationWithCredentials{"integration": integration} respBody, err := c.doRequest(http.MethodPatch, "/api/v1/integrations/"+id, body) if err != nil { return nil, err } var result Integration if err := json.Unmarshal(respBody, &result); err != nil { return nil, fmt.Errorf("failed to unmarshal response: %w", err) } return &result, nil } // DeleteIntegration deletes an integration. func (c *Client) DeleteIntegration(id string) error { _, err := c.doRequest(http.MethodDelete, "/api/v1/integrations/"+id, nil) return err } // MaintenanceWindowAPI represents a TowerOps maintenance window. type MaintenanceWindowAPI struct { ID string `json:"id,omitempty"` Name string `json:"name"` Reason *string `json:"reason,omitempty"` StartsAt string `json:"starts_at"` EndsAt string `json:"ends_at"` SuppressAlerts *bool `json:"suppress_alerts,omitempty"` SiteID *string `json:"site_id,omitempty"` DeviceID *string `json:"device_id,omitempty"` InsertedAt string `json:"inserted_at,omitempty"` } // CreateMaintenanceWindow creates a new maintenance window. func (c *Client) CreateMaintenanceWindow(window MaintenanceWindowAPI) (*MaintenanceWindowAPI, error) { body := map[string]MaintenanceWindowAPI{"maintenance_window": window} respBody, err := c.doRequest(http.MethodPost, "/api/v1/maintenance_windows", body) if err != nil { return nil, err } var result MaintenanceWindowAPI if err := json.Unmarshal(respBody, &result); err != nil { return nil, fmt.Errorf("failed to unmarshal response: %w", err) } return &result, nil } // GetMaintenanceWindow retrieves a maintenance window by ID. func (c *Client) GetMaintenanceWindow(id string) (*MaintenanceWindowAPI, error) { respBody, err := c.doRequest(http.MethodGet, "/api/v1/maintenance_windows/"+id, nil) if err != nil { return nil, err } var result MaintenanceWindowAPI if err := json.Unmarshal(respBody, &result); err != nil { return nil, fmt.Errorf("failed to unmarshal response: %w", err) } return &result, nil } // UpdateMaintenanceWindow updates an existing maintenance window. func (c *Client) UpdateMaintenanceWindow(id string, window MaintenanceWindowAPI) (*MaintenanceWindowAPI, error) { body := map[string]MaintenanceWindowAPI{"maintenance_window": window} respBody, err := c.doRequest(http.MethodPatch, "/api/v1/maintenance_windows/"+id, body) if err != nil { return nil, err } var result MaintenanceWindowAPI if err := json.Unmarshal(respBody, &result); err != nil { return nil, fmt.Errorf("failed to unmarshal response: %w", err) } return &result, nil } // DeleteMaintenanceWindow deletes a maintenance window. func (c *Client) DeleteMaintenanceWindow(id string) error { _, err := c.doRequest(http.MethodDelete, "/api/v1/maintenance_windows/"+id, nil) return err } // Check represents a TowerOps service check. type Check struct { ID string `json:"id,omitempty"` Name string `json:"name"` CheckType string `json:"check_type"` Description *string `json:"description,omitempty"` Enabled *bool `json:"enabled,omitempty"` DeviceID *string `json:"device_id,omitempty"` AgentTokenID *string `json:"agent_token_id,omitempty"` IntervalSeconds *int `json:"interval_seconds,omitempty"` TimeoutMs *int `json:"timeout_ms,omitempty"` RetryIntervalSeconds *int `json:"retry_interval_seconds,omitempty"` MaxCheckAttempts *int `json:"max_check_attempts,omitempty"` Config map[string]any `json:"config"` CurrentState *int `json:"current_state,omitempty"` CurrentStateType *string `json:"current_state_type,omitempty"` LastCheckAt *string `json:"last_check_at,omitempty"` InsertedAt string `json:"inserted_at,omitempty"` } // CreateCheck creates a new service check. func (c *Client) CreateCheck(check Check) (*Check, error) { body := map[string]Check{"check": check} respBody, err := c.doRequest(http.MethodPost, "/api/v1/checks", body) if err != nil { return nil, err } var result Check if err := json.Unmarshal(respBody, &result); err != nil { return nil, fmt.Errorf("failed to unmarshal response: %w", err) } return &result, nil } // GetCheck retrieves a check by ID. func (c *Client) GetCheck(id string) (*Check, error) { respBody, err := c.doRequest(http.MethodGet, "/api/v1/checks/"+id, nil) if err != nil { return nil, err } var result Check if err := json.Unmarshal(respBody, &result); err != nil { return nil, fmt.Errorf("failed to unmarshal response: %w", err) } return &result, nil } // UpdateCheck updates an existing check. func (c *Client) UpdateCheck(id string, check Check) (*Check, error) { body := map[string]Check{"check": check} respBody, err := c.doRequest(http.MethodPatch, "/api/v1/checks/"+id, body) if err != nil { return nil, err } var result Check if err := json.Unmarshal(respBody, &result); err != nil { return nil, fmt.Errorf("failed to unmarshal response: %w", err) } return &result, nil } // DeleteCheck deletes a check. func (c *Client) DeleteCheck(id string) error { _, err := c.doRequest(http.MethodDelete, "/api/v1/checks/"+id, nil) return err } // GetOrganization retrieves the current organization settings. func (c *Client) GetOrganization() (*Organization, error) { respBody, err := c.doRequest(http.MethodGet, "/api/v1/organization", nil) if err != nil { return nil, err } var result organizationResponse if err := json.Unmarshal(respBody, &result); err != nil { return nil, fmt.Errorf("failed to unmarshal response: %w", err) } return &result.Data, nil } // UpdateOrganization updates the current organization settings. func (c *Client) UpdateOrganization(org Organization) (*Organization, error) { body := map[string]Organization{"organization": org} respBody, err := c.doRequest(http.MethodPatch, "/api/v1/organization", body) if err != nil { return nil, err } var result organizationResponse if err := json.Unmarshal(respBody, &result); err != nil { return nil, fmt.Errorf("failed to unmarshal response: %w", err) } return &result.Data, nil }