Major codebase cleanup: DRY schemas, remove dead config, consolidate modules

- Created Towerops.Schema base macro (95+ schemas updated to use it)
- Created Towerops.Snmp.Reading macro (7 reading schemas consolidated)
- Created Towerops.Gaiia.BaseSchema macro (4 Gaiia schemas consolidated)
- Created Towerops.SyncLog shared module (UISP + Preseem merge)
- Created Towerops.LogFilters (3 log filter modules merged into 1)
- Created Towerops.OnCall.ChangesetHelpers (9 on-call schemas simplified)
- Created API v1 ResourceController shared helpers (7 controllers)
- Extracted ConnectionHelpers.format_connection_result (2 LiveViews)
- Removed 5 dead config keys (scopes, mib_dirs, stripe_meter_id, etc.)
- Fixed 2 pre-existing broken tests
- All 13,219 tests pass, Credo clean (0 issues)
This commit is contained in:
Graham McIntire 2026-06-16 15:29:22 -05:00
parent d1403c8069
commit a4e08b2aca
134 changed files with 1082 additions and 1516 deletions

View file

@ -46,9 +46,9 @@ config :logger, :default_handler,
filters: [ filters: [
# Suppress HTTP/0.9 and other invalid protocol errors from Bandit # Suppress HTTP/0.9 and other invalid protocol errors from Bandit
# These are typically from port scanners and automated bots # These are typically from port scanners and automated bots
bandit_invalid_http: {&Towerops.LogFilter.filter_bandit_errors/2, []}, bandit_invalid_http: {&Towerops.LogFilters.filter_bandit_errors/2, []},
# Suppress benign port_died and write_failed errors during K8s pod shutdown # Suppress benign port_died and write_failed errors during K8s pod shutdown
shutdown_errors: {&Towerops.LoggerFilters.drop_shutdown_errors/2, []} shutdown_errors: {&Towerops.LogFilters.drop_shutdown_errors/2, []}
] ]
# Register protobuf MIME type for agent API # Register protobuf MIME type for agent API
@ -138,29 +138,6 @@ config :towerops, ToweropsWeb.Endpoint,
# to "/data/coverage" (the shared NFS mount) in config/prod.exs so all # to "/data/coverage" (the shared NFS mount) in config/prod.exs so all
# replicas can serve the rasters and pod restarts don't lose them. # replicas can serve the rasters and pod restarts don't lose them.
config :towerops, :coverage_storage_dir, {:towerops, "priv/static/coverage"} config :towerops, :coverage_storage_dir, {:towerops, "priv/static/coverage"}
config :towerops, :live_view_signing_salt, "Uh1ABfdI"
# SNMP MIB directories for production (in Docker image)
# MIB files are included in the release at /app/priv/mibs
# MibTranslator automatically expands these to include subdirectories
# Override in dev.exs for local development paths
config :towerops, :mib_dirs, [
"/app/priv/mibs",
"/usr/share/snmp/mibs"
]
config :towerops, :scopes,
user: [
default: true,
module: Towerops.Accounts.Scope,
assign_key: :current_scope,
access_path: [:user, :id],
schema_key: :user_id,
schema_type: :binary_id,
schema_table: :users,
test_data_fixture: Towerops.AccountsFixtures,
test_setup_helper: :register_and_log_in_user
]
# Agent Docker Image # Agent Docker Image
# Override this in runtime.exs or environment-specific config # Override this in runtime.exs or environment-specific config

View file

@ -218,13 +218,6 @@ config :towerops, :env, :dev
# Configure SNMP MIB directories for development # Configure SNMP MIB directories for development
# Point to local priv/mibs subdirectories where MIB files are stored # Point to local priv/mibs subdirectories where MIB files are stored
# Note: The root mibs/ directory only contains symlinks to LibreNMS repo # Note: The root mibs/ directory only contains symlinks to LibreNMS repo
# SNMP MIB directories for development
# MibTranslator automatically expands these to include subdirectories
config :towerops, :mib_dirs, [
Path.join([File.cwd!(), "priv", "mibs"]),
"/usr/share/snmp/mibs"
]
# Disable rate limiting in development # Disable rate limiting in development
config :towerops, :rate_limiting_enabled, false config :towerops, :rate_limiting_enabled, false
config :towerops, :session_encryption_salt, "dev-encryption-salt-stable" config :towerops, :session_encryption_salt, "dev-encryption-salt-stable"
@ -250,5 +243,4 @@ config :towerops, dev_routes: true
config :towerops, config :towerops,
stripe_secret_key: System.get_env("STRIPE_SECRET_KEY", "sk_test_use_your_own_key"), stripe_secret_key: System.get_env("STRIPE_SECRET_KEY", "sk_test_use_your_own_key"),
stripe_webhook_secret: "whsec_dev_fake_secret", stripe_webhook_secret: "whsec_dev_fake_secret",
stripe_price_id: "price_1T81XBS77kvnTfgyPlw1jF8N", stripe_price_id: "price_1T81XBS77kvnTfgyPlw1jF8N"
stripe_meter_id: "mtr_test_61UHPU067veEZ7bhl41S77kvnTfgyODY"

View file

@ -1,22 +1,18 @@
import Config import Config
# Temporarily disable Honeybadger in production
config :honeybadger,
exclude_envs: [:prod]
# Filter out harmless Oban shutdown messages, repetitive SNMP MIB errors, and noisy health checks # Filter out harmless Oban shutdown messages, repetitive SNMP MIB errors, and noisy health checks
config :logger, :default_handler, config :logger, :default_handler,
filters: [ filters: [
drop_oban_shutdown: { drop_oban_shutdown: {
&Towerops.LoggerFilters.drop_oban_shutdown/2, &Towerops.LogFilters.drop_oban_shutdown/2,
[] []
}, },
drop_snmp_mib_errors: { drop_snmp_mib_errors: {
&Towerops.LoggerFilters.drop_snmp_mib_errors/2, &Towerops.LogFilters.drop_snmp_mib_errors/2,
[] []
}, },
filter_health_checks: { filter_health_checks: {
&ToweropsWeb.TelemetryFilter.filter_health_checks/2, &Towerops.LogFilters.filter_health_checks/2,
[] []
} }
] ]

View file

@ -503,13 +503,6 @@ if config_env() == :prod do
This is the Stripe price ID for the metered billing plan. This is the Stripe price ID for the metered billing plan.
""" """
stripe_meter_id =
System.get_env("STRIPE_METER_ID") ||
raise """
environment variable STRIPE_METER_ID is missing.
This is the Stripe billing meter ID for usage reporting.
"""
# Configure ErrorTrackerNotifier for email alerts on exceptions # Configure ErrorTrackerNotifier for email alerts on exceptions
config :error_tracker_notifier, config :error_tracker_notifier,
notification_type: :email, notification_type: :email,
@ -520,8 +513,7 @@ if config_env() == :prod do
config :towerops, config :towerops,
stripe_secret_key: stripe_secret_key, stripe_secret_key: stripe_secret_key,
stripe_webhook_secret: stripe_webhook_secret, stripe_webhook_secret: stripe_webhook_secret,
stripe_price_id: stripe_price_id, stripe_price_id: stripe_price_id
stripe_meter_id: stripe_meter_id
end end
# Test environment Stripe configuration # Test environment Stripe configuration
@ -529,6 +521,5 @@ if config_env() == :test do
config :towerops, config :towerops,
stripe_secret_key: "sk_test_fake", stripe_secret_key: "sk_test_fake",
stripe_webhook_secret: "whsec_test_fake", stripe_webhook_secret: "whsec_test_fake",
stripe_price_id: "price_test_fake", stripe_price_id: "price_test_fake"
stripe_meter_id: "meter_test_fake"
end end

View file

@ -16,12 +16,7 @@ defmodule Towerops.Accounts.BrowserSession do
- Automatically cleaned up after expires_at timestamp - Automatically cleaned up after expires_at timestamp
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "browser_sessions" do schema "browser_sessions" do
# Device/browser metadata # Device/browser metadata

View file

@ -9,12 +9,7 @@ defmodule Towerops.Accounts.LoginAttempt do
- Data retention: 365 days for active records, 90 days for anonymized records - Data retention: 365 days for active records, 90 days for anonymized records
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
@valid_methods ~w(password magic_link mobile_qr) @valid_methods ~w(password magic_link mobile_qr)
@valid_failure_reasons ~w(invalid_credentials invalid_totp invalid_magic_link account_disabled) @valid_failure_reasons ~w(invalid_credentials invalid_totp invalid_magic_link account_disabled)

View file

@ -5,12 +5,8 @@ defmodule Towerops.Accounts.PolicyVersion do
Stores the full text of each policy version with effective dates to maintain Stores the full text of each policy version with effective dates to maintain
a complete audit trail of policy changes for GDPR compliance. a complete audit trail of policy changes for GDPR compliance.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "policy_versions" do schema "policy_versions" do
field :policy_type, :string field :policy_type, :string
field :version, :string field :version, :string

View file

@ -7,16 +7,12 @@ defmodule Towerops.Accounts.User do
- TOTP (Time-based One-Time Password) two-factor authentication - TOTP (Time-based One-Time Password) two-factor authentication
- Session tokens - Session tokens
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
alias Ecto.Association.NotLoaded alias Ecto.Association.NotLoaded
alias Towerops.Organizations.Membership alias Towerops.Organizations.Membership
alias Towerops.Organizations.Organization alias Towerops.Organizations.Organization
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "users" do schema "users" do
field :email, :string field :email, :string
field :password, :string, virtual: true, redact: true field :password, :string, virtual: true, redact: true

View file

@ -5,14 +5,10 @@ defmodule Towerops.Accounts.UserConsent do
Stores consent records with versioning to comply with GDPR and privacy regulations. Stores consent records with versioning to comply with GDPR and privacy regulations.
Allows users to withdraw consent at any time. Allows users to withdraw consent at any time.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
alias Towerops.Accounts.User alias Towerops.Accounts.User
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "user_consents" do schema "user_consents" do
belongs_to :user, User belongs_to :user, User

View file

@ -5,14 +5,10 @@ defmodule Towerops.Accounts.UserRecoveryCode do
Recovery codes are single-use backup codes that can be used to access Recovery codes are single-use backup codes that can be used to access
an account if all TOTP devices are lost. Codes are stored as SHA-256 hashes. an account if all TOTP devices are lost. Codes are stored as SHA-256 hashes.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
alias Towerops.Accounts.User alias Towerops.Accounts.User
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "user_recovery_codes" do schema "user_recovery_codes" do
field :code_hash, :string field :code_hash, :string
field :used_at, :utc_datetime field :used_at, :utc_datetime

View file

@ -5,7 +5,7 @@ defmodule Towerops.Accounts.UserToken do
Manages session tokens, email confirmation tokens, and password reset tokens Manages session tokens, email confirmation tokens, and password reset tokens
with expiration and hashing. with expiration and hashing.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Query import Ecto.Query
@ -21,8 +21,6 @@ defmodule Towerops.Accounts.UserToken do
@change_email_validity_in_days 7 @change_email_validity_in_days 7
@session_validity_in_days 14 @session_validity_in_days 14
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "users_tokens" do schema "users_tokens" do
field :token, :binary field :token, :binary
field :context, :string field :context, :string

View file

@ -5,14 +5,10 @@ defmodule Towerops.Accounts.UserTotpDevice do
Allows users to register multiple authenticator apps (phone, tablet, etc.) Allows users to register multiple authenticator apps (phone, tablet, etc.)
for two-factor authentication. for two-factor authentication.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
alias Towerops.Accounts.User alias Towerops.Accounts.User
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "user_totp_devices" do schema "user_totp_devices" do
field :name, :string field :name, :string
field :totp_secret, :binary, redact: true field :totp_secret, :binary, redact: true

View file

@ -2,15 +2,10 @@ defmodule Towerops.Admin.AuditLog do
@moduledoc """ @moduledoc """
Schema for audit logs tracking sensitive operations performed by superusers. Schema for audit logs tracking sensitive operations performed by superusers.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
alias Towerops.Accounts.User alias Towerops.Accounts.User
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "audit_logs" do schema "audit_logs" do
field :action, :string field :action, :string
field :metadata, :map field :metadata, :map

View file

@ -5,16 +5,12 @@ defmodule Towerops.Agents.AgentAssignment do
Each device can only be assigned to one agent at a time (unique constraint on device_id). Each device can only be assigned to one agent at a time (unique constraint on device_id).
This ensures that polling is not duplicated across multiple agents. This ensures that polling is not duplicated across multiple agents.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
alias Ecto.Association.NotLoaded alias Ecto.Association.NotLoaded
alias Towerops.Agents.AgentToken alias Towerops.Agents.AgentToken
alias Towerops.Devices.Device alias Towerops.Devices.Device
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "agent_assignments" do schema "agent_assignments" do
field :enabled, :boolean, default: true field :enabled, :boolean, default: true

View file

@ -6,17 +6,13 @@ defmodule Towerops.Agents.AgentToken do
Tokens are generated with cryptographically secure random bytes (48 bytes = 384 bits) and Tokens are generated with cryptographically secure random bytes (48 bytes = 384 bits) and
stored in plaintext in the database. Tokens can be retrieved and viewed by authorized users. stored in plaintext in the database. Tokens can be retrieved and viewed by authorized users.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
alias Towerops.Organizations.Organization alias Towerops.Organizations.Organization
# 48 bytes = 384 bits of cryptographically secure randomness # 48 bytes = 384 bits of cryptographically secure randomness
@rand_size 48 @rand_size 48
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "agent_tokens" do schema "agent_tokens" do
field :token, :string field :token, :string
field :name, :string field :name, :string

View file

@ -5,9 +5,7 @@ defmodule Towerops.Alerts.Alert do
Tracks device_up, device_down, and monitoring check alerts with Tracks device_up, device_down, and monitoring check alerts with
acknowledgement status and auto-resolution. acknowledgement status and auto-resolution.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
alias Ecto.Association.NotLoaded alias Ecto.Association.NotLoaded
alias Towerops.Accounts.User alias Towerops.Accounts.User
@ -16,8 +14,6 @@ defmodule Towerops.Alerts.Alert do
alias Towerops.Monitoring.Check alias Towerops.Monitoring.Check
alias Towerops.Organizations.Organization alias Towerops.Organizations.Organization
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "alerts" do schema "alerts" do
# Alert type: device_down, device_up for device alerts # Alert type: device_down, device_up for device alerts
# For check alerts, uses string like "check_http", "check_ping", etc. # For check alerts, uses string like "check_http", "check_ping", etc.

View file

@ -5,14 +5,10 @@ defmodule Towerops.Alerts.AlertStorm do
When > threshold alerts fire within a time window, individual notifications When > threshold alerts fire within a time window, individual notifications
are suppressed and a single summary notification is sent instead. are suppressed and a single summary notification is sent instead.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
alias Towerops.Organizations.Organization alias Towerops.Organizations.Organization
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "alert_storms" do schema "alert_storms" do
field :started_at, :utc_datetime field :started_at, :utc_datetime
field :ended_at, :utc_datetime field :ended_at, :utc_datetime

View file

@ -5,17 +5,13 @@ defmodule Towerops.Alerts.NotificationDigest do
Tracks how many notifications a user has received within a time window. Tracks how many notifications a user has received within a time window.
When the limit is exceeded, additional alerts are queued for digest delivery. When the limit is exceeded, additional alerts are queued for digest delivery.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
alias Towerops.Accounts.User alias Towerops.Accounts.User
alias Towerops.Organizations.Organization alias Towerops.Organizations.Organization
@type t :: %__MODULE__{} @type t :: %__MODULE__{}
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "notification_digests" do schema "notification_digests" do
field :window_start, :utc_datetime field :window_start, :utc_datetime
field :notification_count, :integer, default: 0 field :notification_count, :integer, default: 0

View file

@ -6,9 +6,7 @@ defmodule Towerops.Alerts.SiteOutage do
they are grouped into a single site outage with one notification they are grouped into a single site outage with one notification
instead of N individual notifications. instead of N individual notifications.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
alias Towerops.Alerts.Alert alias Towerops.Alerts.Alert
alias Towerops.Organizations.Organization alias Towerops.Organizations.Organization
@ -16,8 +14,6 @@ defmodule Towerops.Alerts.SiteOutage do
@type t :: %__MODULE__{} @type t :: %__MODULE__{}
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "site_outages" do schema "site_outages" do
field :started_at, :utc_datetime field :started_at, :utc_datetime
field :resolved_at, :utc_datetime field :resolved_at, :utc_datetime

View file

@ -8,15 +8,10 @@ defmodule Towerops.ApiTokens.ApiToken do
The actual token value is only shown once when created and is then The actual token value is only shown once when created and is then
hashed for secure storage. hashed for secure storage.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
@type t :: %__MODULE__{} @type t :: %__MODULE__{}
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "api_tokens" do schema "api_tokens" do
field :name, :string field :name, :string
field :token_hash, :string field :token_hash, :string

View file

@ -157,7 +157,7 @@ defmodule Towerops.Application do
# supervisor may not be ready or may already be dying) cannot exit the boot # supervisor may not be ready or may already be dying) cannot exit the boot
# process and mask the real error from Supervisor.start_link. # process and mask the real error from Supervisor.start_link.
defp post_startup do defp post_startup do
ToweropsWeb.TelemetryFilter.attach() Towerops.LogFilters.attach()
_ = _ =
try do try do

View file

@ -2,15 +2,10 @@ defmodule Towerops.ConfigChanges.ConfigChangeEvent do
@moduledoc """ @moduledoc """
Schema for config change events linking MikroTik backup diffs to time windows. Schema for config change events linking MikroTik backup diffs to time windows.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
alias Towerops.Devices.MikrotikBackup alias Towerops.Devices.MikrotikBackup
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "config_change_events" do schema "config_change_events" do
field :changed_at, :utc_datetime field :changed_at, :utc_datetime
field :diff_summary, :string field :diff_summary, :string

View file

@ -10,9 +10,7 @@ defmodule Towerops.Coverages.Coverage do
Multiple coverages can hang off a single site typically one per sector Multiple coverages can hang off a single site typically one per sector
on a multi-sector tower. on a multi-sector tower.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
alias Ecto.Association.NotLoaded alias Ecto.Association.NotLoaded
alias Towerops.Coverages.Antenna alias Towerops.Coverages.Antenna
@ -23,8 +21,6 @@ defmodule Towerops.Coverages.Coverage do
@statuses ~w(draft queued computing ready failed) @statuses ~w(draft queued computing ready failed)
@max_axis_pixels 8_000 @max_axis_pixels 8_000
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "coverages" do schema "coverages" do
field :name, :string field :name, :string
field :antenna_slug, :string field :antenna_slug, :string

View file

@ -2,12 +2,7 @@ defmodule Towerops.Devices.BackupRequest do
@moduledoc """ @moduledoc """
Schema for MikroTik device backup request tracking. Schema for MikroTik device backup request tracking.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "device_backup_requests" do schema "device_backup_requests" do
field :job_id, :string field :job_id, :string

View file

@ -8,9 +8,7 @@ defmodule Towerops.Devices.Device do
- Agent assignments for remote polling - Agent assignments for remote polling
- Monitoring checks and alerts - Monitoring checks and alerts
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
alias Ecto.Association.NotLoaded alias Ecto.Association.NotLoaded
alias Towerops.Agents.AgentAssignment alias Towerops.Agents.AgentAssignment
@ -20,8 +18,6 @@ defmodule Towerops.Devices.Device do
alias Towerops.Snmp.Device, as: SnmpDevice alias Towerops.Snmp.Device, as: SnmpDevice
alias Towerops.Topology.DeviceLink alias Towerops.Topology.DeviceLink
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "devices" do schema "devices" do
field :name, :string field :name, :string
field :ip_address, :string field :ip_address, :string

View file

@ -9,12 +9,7 @@ defmodule Towerops.Devices.DeviceFirmwareHistory do
The `firmware_releases` table stores the latest available versions from vendors. The `firmware_releases` table stores the latest available versions from vendors.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "device_firmware_history" do schema "device_firmware_history" do
belongs_to :snmp_device, Towerops.Snmp.Device belongs_to :snmp_device, Towerops.Snmp.Device

View file

@ -2,9 +2,7 @@ defmodule Towerops.Devices.Event do
@moduledoc """ @moduledoc """
Device event schema for tracking changes and incidents. Device event schema for tracking changes and incidents.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
@type severity :: :info | :warning | :critical @type severity :: :info | :warning | :critical
@ -24,9 +22,6 @@ defmodule Towerops.Devices.Event do
| :sensor_value_drop | :sensor_value_drop
| :sensor_value_changed | :sensor_value_changed
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "device_events" do schema "device_events" do
field :event_type, :string field :event_type, :string
field :severity, :string field :severity, :string

View file

@ -8,12 +8,7 @@ defmodule Towerops.Devices.FirmwareRelease do
The `device_firmware_history` table tracks when individual devices change versions. The `device_firmware_history` table tracks when individual devices change versions.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "firmware_releases" do schema "firmware_releases" do
field :vendor, :string field :vendor, :string

View file

@ -2,12 +2,7 @@ defmodule Towerops.Devices.MikrotikBackup do
@moduledoc """ @moduledoc """
Schema for MikroTik device configuration backups. Schema for MikroTik device configuration backups.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "device_mikrotik_backups" do schema "device_mikrotik_backups" do
field :config_compressed, :binary field :config_compressed, :binary

View file

@ -3,7 +3,7 @@ defmodule Towerops.ErrorTrackerIgnorer do
Drops benign errors from ErrorTracker that occur during K8s pod shutdown Drops benign errors from ErrorTracker that occur during K8s pod shutdown
or transient DB connection cycling. or transient DB connection cycling.
Matches the same patterns as `Towerops.LoggerFilters.drop_shutdown_errors/2`. Matches the same patterns as `Towerops.LogFilters.drop_shutdown_errors/2`.
""" """
@behaviour ErrorTracker.Ignorer @behaviour ErrorTracker.Ignorer

View file

@ -2,15 +2,11 @@ defmodule Towerops.Gaiia.Account do
@moduledoc """ @moduledoc """
Schema for subscriber accounts synced from Gaiia API. Schema for subscriber accounts synced from Gaiia API.
""" """
use Ecto.Schema use Towerops.Gaiia.BaseSchema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "gaiia_accounts" do schema "gaiia_accounts" do
field :gaiia_id, :string gaiia_base_fields()
field :readable_id, :string field :readable_id, :string
field :name, :string field :name, :string
field :status, :string field :status, :string
@ -18,11 +14,6 @@ defmodule Towerops.Gaiia.Account do
field :address, :map field :address, :map
field :subscription_count, :integer field :subscription_count, :integer
field :mrr, :decimal field :mrr, :decimal
field :raw_data, :map
belongs_to :organization, Towerops.Organizations.Organization
timestamps(type: :utc_datetime)
end end
def changeset(account, attrs) do def changeset(account, attrs) do
@ -39,11 +30,6 @@ defmodule Towerops.Gaiia.Account do
:mrr, :mrr,
:raw_data :raw_data
]) ])
|> validate_required([:organization_id, :gaiia_id]) |> validate_gaiia_uniqueness()
|> unique_constraint([:organization_id, :gaiia_id],
error_key: :gaiia_id,
message: "has already been taken"
)
|> foreign_key_constraint(:organization_id)
end end
end end

View file

@ -0,0 +1,56 @@
defmodule Towerops.Gaiia.BaseSchema do
@moduledoc """
Base schema for Gaiia-synced entities.
Provides shared fields (`gaiia_id`, `raw_data`, `organization` belongs_to, timestamps)
and a `validate_gaiia_uniqueness/1` helper.
## Usage
defmodule Towerops.Gaiia.MyEntity do
use Towerops.Gaiia.BaseSchema
schema "gaiia_my_entities" do
gaiia_base_fields()
field :my_field, :string
end
def changeset(struct, attrs) do
struct
|> cast(attrs, [:organization_id, :gaiia_id, :my_field, :raw_data])
|> validate_gaiia_uniqueness()
end
end
"""
defmacro __using__(_opts) do
quote do
use Towerops.Schema
import Towerops.Gaiia.BaseSchema, only: [gaiia_base_fields: 0]
def validate_gaiia_uniqueness(changeset) do
changeset
|> validate_required([:organization_id, :gaiia_id])
|> unique_constraint([:organization_id, :gaiia_id],
error_key: :gaiia_id,
message: "has already been taken"
)
|> foreign_key_constraint(:organization_id)
end
end
end
@doc """
Injects common Gaiia schema fields.
Must be called inside a `schema` block.
"""
defmacro gaiia_base_fields do
quote do
field :gaiia_id, :string
field :raw_data, :map
belongs_to :organization, Towerops.Organizations.Organization
timestamps(type: :utc_datetime)
end
end
end

View file

@ -2,15 +2,11 @@ defmodule Towerops.Gaiia.BillingSubscription do
@moduledoc """ @moduledoc """
Schema for billing subscriptions synced from Gaiia API. Schema for billing subscriptions synced from Gaiia API.
""" """
use Ecto.Schema use Towerops.Gaiia.BaseSchema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "gaiia_billing_subscriptions" do schema "gaiia_billing_subscriptions" do
field :gaiia_id, :string gaiia_base_fields()
field :account_gaiia_id, :string field :account_gaiia_id, :string
field :status, :string field :status, :string
field :product_name, :string field :product_name, :string
@ -18,11 +14,6 @@ defmodule Towerops.Gaiia.BillingSubscription do
field :currency, :string field :currency, :string
field :speed_download, :integer field :speed_download, :integer
field :speed_upload, :integer field :speed_upload, :integer
field :raw_data, :map
belongs_to :organization, Towerops.Organizations.Organization
timestamps(type: :utc_datetime)
end end
def changeset(billing_subscription, attrs) do def changeset(billing_subscription, attrs) do
@ -39,11 +30,6 @@ defmodule Towerops.Gaiia.BillingSubscription do
:speed_upload, :speed_upload,
:raw_data :raw_data
]) ])
|> validate_required([:organization_id, :gaiia_id]) |> validate_gaiia_uniqueness()
|> unique_constraint([:organization_id, :gaiia_id],
error_key: :gaiia_id,
message: "has already been taken"
)
|> foreign_key_constraint(:organization_id)
end end
end end

View file

@ -5,12 +5,7 @@ defmodule Towerops.Gaiia.DeviceSubscriberLink do
Links a Gaiia subscriber account to a TowerOps device based on Links a Gaiia subscriber account to a TowerOps device based on
ARP table matching or site-level fallback. ARP table matching or site-level fallback.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "device_subscriber_links" do schema "device_subscriber_links" do
field :match_method, :string field :match_method, :string

View file

@ -2,15 +2,11 @@ defmodule Towerops.Gaiia.InventoryItem do
@moduledoc """ @moduledoc """
Schema for inventory items (deployed equipment) synced from Gaiia API. Schema for inventory items (deployed equipment) synced from Gaiia API.
""" """
use Ecto.Schema use Towerops.Gaiia.BaseSchema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "gaiia_inventory_items" do schema "gaiia_inventory_items" do
field :gaiia_id, :string gaiia_base_fields()
field :name, :string field :name, :string
field :status, :string field :status, :string
field :serial_number, :string field :serial_number, :string
@ -21,12 +17,8 @@ defmodule Towerops.Gaiia.InventoryItem do
field :category, :string field :category, :string
field :assigned_account_gaiia_id, :string field :assigned_account_gaiia_id, :string
field :assigned_network_site_gaiia_id, :string field :assigned_network_site_gaiia_id, :string
field :raw_data, :map
belongs_to :organization, Towerops.Organizations.Organization
belongs_to :device, Towerops.Devices.Device belongs_to :device, Towerops.Devices.Device
timestamps(type: :utc_datetime)
end end
def changeset(inventory_item, attrs) do def changeset(inventory_item, attrs) do
@ -47,12 +39,7 @@ defmodule Towerops.Gaiia.InventoryItem do
:device_id, :device_id,
:raw_data :raw_data
]) ])
|> validate_required([:organization_id, :gaiia_id]) |> validate_gaiia_uniqueness()
|> unique_constraint([:organization_id, :gaiia_id],
error_key: :gaiia_id,
message: "has already been taken"
)
|> foreign_key_constraint(:organization_id)
|> foreign_key_constraint(:device_id) |> foreign_key_constraint(:device_id)
end end
end end

View file

@ -2,26 +2,18 @@ defmodule Towerops.Gaiia.NetworkSite do
@moduledoc """ @moduledoc """
Schema for network sites (towers/POPs) synced from Gaiia API. Schema for network sites (towers/POPs) synced from Gaiia API.
""" """
use Ecto.Schema use Towerops.Gaiia.BaseSchema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "gaiia_network_sites" do schema "gaiia_network_sites" do
field :gaiia_id, :string gaiia_base_fields()
field :name, :string field :name, :string
field :address, :map field :address, :map
field :ip_blocks, {:array, :string}, default: [] field :ip_blocks, {:array, :string}, default: []
field :account_count, :integer field :account_count, :integer
field :total_mrr, :decimal field :total_mrr, :decimal
field :raw_data, :map
belongs_to :organization, Towerops.Organizations.Organization
belongs_to :site, Towerops.Sites.Site belongs_to :site, Towerops.Sites.Site
timestamps(type: :utc_datetime)
end end
def changeset(network_site, attrs) do def changeset(network_site, attrs) do
@ -37,12 +29,7 @@ defmodule Towerops.Gaiia.NetworkSite do
:site_id, :site_id,
:raw_data :raw_data
]) ])
|> validate_required([:organization_id, :gaiia_id]) |> validate_gaiia_uniqueness()
|> unique_constraint([:organization_id, :gaiia_id],
error_key: :gaiia_id,
message: "has already been taken"
)
|> foreign_key_constraint(:organization_id)
|> foreign_key_constraint(:site_id) |> foreign_key_constraint(:site_id)
end end
end end

View file

@ -3,11 +3,8 @@ defmodule Towerops.GeoIP.Block do
Schema for GeoIP IP block data (CIDR ranges mapped to locations). Schema for GeoIP IP block data (CIDR ranges mapped to locations).
Maps IP address ranges to geoname_id from MaxMind GeoLite2-City database. Maps IP address ranges to geoname_id from MaxMind GeoLite2-City database.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
schema "geoip_blocks" do schema "geoip_blocks" do
field :network, :string field :network, :string
field :start_ip_int, :integer field :start_ip_int, :integer

View file

@ -5,15 +5,10 @@ defmodule Towerops.Integrations.Integration do
Each organization can have one integration per provider. Credentials Each organization can have one integration per provider. Credentials
are encrypted at rest using Cloak AES-256-GCM. are encrypted at rest using Cloak AES-256-GCM.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
alias Towerops.Encrypted alias Towerops.Encrypted
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
@valid_providers ~w(preseem gaiia pagerduty netbox sonar splynx visp uisp cn_maestro mikrotik) @valid_providers ~w(preseem gaiia pagerduty netbox sonar splynx visp uisp cn_maestro mikrotik)
@valid_sync_statuses ~w(never success partial failed) @valid_sync_statuses ~w(never success partial failed)

View file

@ -2,11 +2,7 @@ defmodule Towerops.Lidar.SyncLog do
@moduledoc """ @moduledoc """
Audit log for LIDAR catalog sync operations. Audit log for LIDAR catalog sync operations.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@valid_statuses ~w(success partial failed) @valid_statuses ~w(success partial failed)
@valid_sources ~w(3dep tnris) @valid_sources ~w(3dep tnris)

View file

@ -5,12 +5,7 @@ defmodule Towerops.Lidar.Tile do
We do not store raster bytes only the public HTTPS URL and footprint We do not store raster bytes only the public HTTPS URL and footprint
metadata. Reads happen on-demand via GDAL's `/vsicurl/` driver. metadata. Reads happen on-demand via GDAL's `/vsicurl/` driver.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
@valid_sources ~w(3dep tnris) @valid_sources ~w(3dep tnris)
@valid_availability ~w(available missing error no_cog) @valid_availability ~w(available missing error no_cog)

View file

@ -19,16 +19,12 @@ defmodule Towerops.LLM.Usage do
GROUP BY 1, 2, 3, 4 GROUP BY 1, 2, 3, 4
ORDER BY 1 DESC; ORDER BY 1 DESC;
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
import Ecto.Query import Ecto.Query
alias Towerops.Repo alias Towerops.Repo
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "llm_usage" do schema "llm_usage" do
field :day, :date field :day, :date
field :model, :string field :model, :string

View file

@ -1,46 +0,0 @@
defmodule Towerops.LogFilter do
@moduledoc """
Filters out noisy log messages from external sources like port scanners and bots.
"""
@doc """
Filters out Bandit HTTP errors from invalid requests.
This suppresses errors from:
- HTTP/0.9 requests (very old protocol, typically port scanners)
- Malformed HTTP requests from bots
- Invalid protocol versions
These errors are expected when running a public-facing server and don't
indicate application problems.
"""
def filter_bandit_errors(log_event, _opts) do
case log_event do
# Filter Bandit.HTTPError with Invalid HTTP version
%{level: :error, msg: {:string, msg}} when is_binary(msg) ->
filter_string_message(msg)
# Filter other Bandit connection errors
%{level: :error, msg: {:report, %{label: {:error_logger, :error_report}, report: report}}} ->
filter_error_report(report)
# Don't filter anything else
_ ->
:ignore
end
end
defp filter_string_message(msg) do
if bandit_invalid_http_error?(msg), do: :stop, else: :ignore
end
defp filter_error_report(message: msg) when is_binary(msg) do
if String.contains?(msg, "Bandit.HTTPError"), do: :stop, else: :ignore
end
defp filter_error_report(_), do: :ignore
defp bandit_invalid_http_error?(msg) do
String.contains?(msg, "Bandit.HTTPError") and String.contains?(msg, "Invalid HTTP version")
end
end

185
lib/towerops/log_filters.ex Normal file
View file

@ -0,0 +1,185 @@
defmodule Towerops.LogFilters do
@moduledoc """
Consolidated log and telemetry filters to reduce noise in production logs.
Combines filters for:
- Invalid HTTP errors from Bandit (port scanners / bots)
- Oban shutdown messages during K8s rolling deployments
- SNMP MIB resolution errors from net-snmp
- Port died / write failed errors during K8s pod termination
- Health check requests (K8s probes / uptime monitors)
"""
require Logger
# ── Bandit / HTTP ──────────────────────────────────────────────────
@doc """
Filters out Bandit HTTP errors from invalid requests.
This suppresses errors from:
- HTTP/0.9 requests (very old protocol, typically port scanners)
- Malformed HTTP requests from bots
- Invalid protocol versions
These errors are expected when running a public-facing server and don't
indicate application problems.
"""
def filter_bandit_errors(log_event, _opts) do
case log_event do
%{level: :error, msg: {:string, msg}} when is_binary(msg) ->
filter_string_message(msg)
%{level: :error, msg: {:report, %{label: {:error_logger, :error_report}, report: report}}} ->
filter_error_report(report)
_ ->
:ignore
end
end
defp filter_string_message(msg) do
if bandit_invalid_http_error?(msg), do: :stop, else: :ignore
end
defp filter_error_report(message: msg) when is_binary(msg) do
if String.contains?(msg, "Bandit.HTTPError"), do: :stop, else: :ignore
end
defp filter_error_report(_), do: :ignore
defp bandit_invalid_http_error?(msg) do
String.contains?(msg, "Bandit.HTTPError") and String.contains?(msg, "Invalid HTTP version")
end
# ── Oban shutdown ─────────────────────────────────────────────────
@doc """
Drops harmless Oban shutdown messages that occur during Kubernetes rolling deployments.
These messages are expected when the old pod gracefully terminates during a deployment,
but Oban's internal processes log them as "unexpected" even though they're normal.
Example messages filtered:
- "Received unexpected message: {:EXIT, #PID<...>, :shutdown}"
- "Oban.Queue.Watchman #PID<...> received unexpected message in handle_info/2: {:EXIT, ...}"
"""
def drop_oban_shutdown(log_event, _opts) do
cond do
oban_source_shutdown?(log_event) -> :stop
oban_queue_shutdown?(log_event) -> :stop
true -> :ignore
end
end
defp oban_source_shutdown?(%{meta: %{source: :oban}, msg: {:string, msg}}) when is_binary(msg) do
String.contains?(msg, "EXIT") and String.contains?(msg, ":shutdown")
end
defp oban_source_shutdown?(_), do: false
defp oban_queue_shutdown?(%{meta: %{error_logger: %{tag: :error_msg}}, msg: {:string, msg}}) when is_binary(msg) do
oban_component?(msg) and shutdown_message?(msg)
end
defp oban_queue_shutdown?(_), do: false
defp oban_component?(msg) do
String.contains?(msg, "Oban.Queue") or String.contains?(msg, "Oban.Plugins")
end
defp shutdown_message?(msg) do
String.contains?(msg, "received unexpected message") and
String.contains?(msg, "EXIT") and
String.contains?(msg, ":shutdown")
end
# ── SNMP MIB errors ───────────────────────────────────────────────
@doc """
Drops repetitive SNMP MIB resolution errors from net-snmp library.
These errors occur when snmptranslate can't find vendor-specific MIB modules.
The errors are logged repeatedly during polling but don't affect functionality
since the system falls back to numeric OIDs.
Example messages filtered:
- "Cannot find module (TRISTAR): At line 1 in (none)"
- "Cannot find module (VENDOR-MIB): At line 1 in (none)"
"""
def drop_snmp_mib_errors(log_event, _opts) do
if snmp_mib_error?(log_event), do: :stop, else: :ignore
end
defp snmp_mib_error?(%{msg: {:string, msg}}) when is_binary(msg) do
String.contains?(msg, "Cannot find module") and String.contains?(msg, "At line 1 in (none)")
end
defp snmp_mib_error?(_), do: false
# ── Shutdown errors (K8s) ─────────────────────────────────────────
@doc """
Drops benign shutdown errors that occur during Kubernetes pod termination.
These errors are expected when the BEAM shuts down inside a container:
- `{:port_died, :normal}` - SNMP polling jobs hold UDP sockets that become invalid
- `{:write_failed, ...} {:device, :epipe}` - Logger writes to stdout after container runtime closes the pipe
"""
def drop_shutdown_errors(log_event, _opts) do
if shutdown_log_message?(log_event), do: :stop, else: :ignore
end
defp shutdown_log_message?(%{msg: {:string, msg}}) when is_binary(msg) do
String.contains?(msg, "port_died") or
(String.contains?(msg, "write_failed") and String.contains?(msg, "epipe"))
end
defp shutdown_log_message?(_), do: false
# ── Health check filter (telemetry) ───────────────────────────────
@doc """
Logger filter function that drops health check requests.
Returns :stop to prevent logging, :ignore to allow logging.
"""
def filter_health_checks(log_event, _opts) do
case log_event do
{_level, _gl, {Logger, msg, _ts, metadata}} ->
if should_filter_log?(msg, metadata), do: :stop, else: :ignore
_ ->
:ignore
end
end
defp should_filter_log?(msg, metadata) do
filter_by_path?(metadata) or filter_by_phoenix_log?(metadata) or filter_by_message?(msg)
end
defp filter_by_path?(metadata) do
Keyword.get(metadata, :request_path) in ["/health", "/health/time"]
end
defp filter_by_phoenix_log?(metadata) do
Keyword.get(metadata, :phoenix_log) == false
end
defp filter_by_message?(msg) when is_binary(msg) do
String.contains?(msg, "GET /health") or
String.contains?(msg, "/health/time") or
String.contains?(msg, "HEAD /")
end
defp filter_by_message?(_), do: false
@doc """
Dummy attach function for compatibility with existing code.
The actual filtering happens via logger configuration, not telemetry.
"""
def attach do
Logger.info("Health check log filter active via logger configuration")
:ok
end
end

View file

@ -1,84 +0,0 @@
defmodule Towerops.LoggerFilters do
@moduledoc """
Custom Logger filters to reduce noise in production logs.
"""
@doc """
Drops harmless Oban shutdown messages that occur during Kubernetes rolling deployments.
These messages are expected when the old pod gracefully terminates during a deployment,
but Oban's internal processes log them as "unexpected" even though they're normal.
Example messages filtered:
- "Received unexpected message: {:EXIT, #PID<...>, :shutdown}"
- "Oban.Queue.Watchman #PID<...> received unexpected message in handle_info/2: {:EXIT, ...}"
"""
def drop_oban_shutdown(log_event, _opts) do
cond do
oban_source_shutdown?(log_event) -> :stop
oban_queue_shutdown?(log_event) -> :stop
true -> :ignore
end
end
defp oban_source_shutdown?(%{meta: %{source: :oban}, msg: {:string, msg}}) when is_binary(msg) do
String.contains?(msg, "EXIT") and String.contains?(msg, ":shutdown")
end
defp oban_source_shutdown?(_), do: false
defp oban_queue_shutdown?(%{meta: %{error_logger: %{tag: :error_msg}}, msg: {:string, msg}}) when is_binary(msg) do
oban_component?(msg) and shutdown_message?(msg)
end
defp oban_queue_shutdown?(_), do: false
defp oban_component?(msg) do
String.contains?(msg, "Oban.Queue") or String.contains?(msg, "Oban.Plugins")
end
defp shutdown_message?(msg) do
String.contains?(msg, "received unexpected message") and
String.contains?(msg, "EXIT") and
String.contains?(msg, ":shutdown")
end
@doc """
Drops repetitive SNMP MIB resolution errors from net-snmp library.
These errors occur when snmptranslate can't find vendor-specific MIB modules.
The errors are logged repeatedly during polling but don't affect functionality
since the system falls back to numeric OIDs.
Example messages filtered:
- "Cannot find module (TRISTAR): At line 1 in (none)"
- "Cannot find module (VENDOR-MIB): At line 1 in (none)"
"""
def drop_snmp_mib_errors(log_event, _opts) do
if snmp_mib_error?(log_event), do: :stop, else: :ignore
end
defp snmp_mib_error?(%{msg: {:string, msg}}) when is_binary(msg) do
String.contains?(msg, "Cannot find module") and String.contains?(msg, "At line 1 in (none)")
end
defp snmp_mib_error?(_), do: false
@doc """
Drops benign shutdown errors that occur during Kubernetes pod termination.
These errors are expected when the BEAM shuts down inside a container:
- `{:port_died, :normal}` - SNMP polling jobs hold UDP sockets that become invalid
- `{:write_failed, ...} {:device, :epipe}` - Logger writes to stdout after container runtime closes the pipe
"""
def drop_shutdown_errors(log_event, _opts) do
if shutdown_log_message?(log_event), do: :stop, else: :ignore
end
defp shutdown_log_message?(%{msg: {:string, msg}}) when is_binary(msg) do
String.contains?(msg, "port_died") or
(String.contains?(msg, "write_failed") and String.contains?(msg, "epipe"))
end
defp shutdown_log_message?(_), do: false
end

View file

@ -7,17 +7,13 @@ defmodule Towerops.Maintenance.MaintenanceWindow do
- All devices at a site (site_id set, device_id nil) - All devices at a site (site_id set, device_id nil)
- All devices in an org (both nil) - All devices in an org (both nil)
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
alias Towerops.Accounts.User alias Towerops.Accounts.User
alias Towerops.Devices.Device alias Towerops.Devices.Device
alias Towerops.Organizations.Organization alias Towerops.Organizations.Organization
alias Towerops.Sites.Site alias Towerops.Sites.Site
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "maintenance_windows" do schema "maintenance_windows" do
field :name, :string field :name, :string
field :reason, :string field :reason, :string

View file

@ -7,12 +7,7 @@ defmodule Towerops.Mikrotik.Event do
these rows after the fact historical timeline of every IP assignment event. these rows after the fact historical timeline of every IP assignment event.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
@sources ~w(dhcp pppoe) @sources ~w(dhcp pppoe)
@event_types ~w(assign release) @event_types ~w(assign release)

View file

@ -8,12 +8,7 @@ defmodule Towerops.Mikrotik.IpAssignment do
"which customer is using this IP right now?". "which customer is using this IP right now?".
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
@sources ~w(dhcp pppoe) @sources ~w(dhcp pppoe)

View file

@ -5,12 +5,7 @@ defmodule Towerops.MobileSessions.MobileSession do
Long-lived authentication tokens for mobile apps. Users can manage Long-lived authentication tokens for mobile apps. Users can manage
their active mobile sessions from the web interface. their active mobile sessions from the web interface.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "mobile_sessions" do schema "mobile_sessions" do
field :token, :string field :token, :string

View file

@ -6,12 +6,7 @@ defmodule Towerops.MobileSessions.QRLoginToken do
in the web interface. Mobile apps scan the QR code and use the token in the web interface. Mobile apps scan the QR code and use the token
to create a new mobile session. to create a new mobile session.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "qr_login_tokens" do schema "qr_login_tokens" do
field :token, :string field :token, :string

View file

@ -12,9 +12,7 @@ defmodule Towerops.Monitoring.Check do
- Soft/hard state transitions - Soft/hard state transitions
- Optional device association (can be standalone) - Optional device association (can be standalone)
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
alias Towerops.Agents.AgentToken alias Towerops.Agents.AgentToken
alias Towerops.Devices.Device alias Towerops.Devices.Device
@ -22,9 +20,6 @@ defmodule Towerops.Monitoring.Check do
@type t :: %__MODULE__{} @type t :: %__MODULE__{}
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
@check_types ~w(http tcp dns ssl passive ping snmp_sensor snmp_interface snmp_processor snmp_storage) @check_types ~w(http tcp dns ssl passive ping snmp_sensor snmp_interface snmp_processor snmp_storage)
schema "checks" do schema "checks" do

View file

@ -4,17 +4,12 @@ defmodule Towerops.Monitoring.CheckResult do
Results are time-series data showing check status over time. Results are time-series data showing check status over time.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
alias Towerops.Agents.AgentToken alias Towerops.Agents.AgentToken
alias Towerops.Monitoring.Check alias Towerops.Monitoring.Check
alias Towerops.Organizations.Organization alias Towerops.Organizations.Organization
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "check_results" do schema "check_results" do
field :checked_at, :utc_datetime field :checked_at, :utc_datetime
field :status, :integer field :status, :integer

View file

@ -6,16 +6,11 @@ defmodule Towerops.Monitoring.MonitoringCheck do
Each record captures the device status, response time, and which agent Each record captures the device status, response time, and which agent
performed the check. performed the check.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
alias Towerops.Agents.AgentToken alias Towerops.Agents.AgentToken
alias Towerops.Devices.Device alias Towerops.Devices.Device
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "monitoring_checks" do schema "monitoring_checks" do
field :status, :string field :status, :string
field :response_time_ms, :float field :response_time_ms, :float

View file

@ -0,0 +1,25 @@
defmodule Towerops.OnCall.ChangesetHelpers do
@moduledoc """
Shared helpers for on-call changesets.
Eliminates the common `cast` + `validate_required` boilerplate.
"""
@doc """
Casts attrs and validates required fields in one step.
## Examples
changeset
|> required_first(attrs, @required_fields, @optional_fields)
# Without optional fields
changeset
|> required_first(attrs, @required_fields)
"""
def required_first(changeset, attrs, required_fields, optional_fields \\ []) do
changeset
|> Ecto.Changeset.cast(attrs, required_fields ++ optional_fields)
|> Ecto.Changeset.validate_required(required_fields)
end
end

View file

@ -2,15 +2,13 @@ defmodule Towerops.OnCall.EscalationPolicy do
@moduledoc """ @moduledoc """
Schema for escalation policies that define notification rules. Schema for escalation policies that define notification rules.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset import Towerops.OnCall.ChangesetHelpers
alias Towerops.OnCall.EscalationRule alias Towerops.OnCall.EscalationRule
alias Towerops.Organizations.Organization alias Towerops.Organizations.Organization
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
@valid_handoff_modes ~w(when_in_use_by_service always never) @valid_handoff_modes ~w(when_in_use_by_service always never)
schema "escalation_policies" do schema "escalation_policies" do
@ -31,8 +29,7 @@ defmodule Towerops.OnCall.EscalationPolicy do
def changeset(policy, attrs) do def changeset(policy, attrs) do
policy policy
|> cast(attrs, @required_fields ++ @optional_fields) |> required_first(attrs, @required_fields, @optional_fields)
|> validate_required(@required_fields)
|> validate_number(:repeat_count, greater_than: 0) |> validate_number(:repeat_count, greater_than: 0)
|> validate_inclusion(:handoff_notifications_mode, @valid_handoff_modes) |> validate_inclusion(:handoff_notifications_mode, @valid_handoff_modes)
|> foreign_key_constraint(:organization_id) |> foreign_key_constraint(:organization_id)

View file

@ -2,15 +2,13 @@ defmodule Towerops.OnCall.EscalationRule do
@moduledoc """ @moduledoc """
Schema for a step within an escalation policy. Schema for a step within an escalation policy.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset import Towerops.OnCall.ChangesetHelpers
alias Towerops.OnCall.EscalationPolicy alias Towerops.OnCall.EscalationPolicy
alias Towerops.OnCall.EscalationTarget alias Towerops.OnCall.EscalationTarget
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "escalation_rules" do schema "escalation_rules" do
field :position, :integer, default: 0 field :position, :integer, default: 0
field :timeout_minutes, :integer, default: 30 field :timeout_minutes, :integer, default: 30
@ -26,8 +24,7 @@ defmodule Towerops.OnCall.EscalationRule do
def changeset(rule, attrs) do def changeset(rule, attrs) do
rule rule
|> cast(attrs, @required_fields) |> required_first(attrs, @required_fields)
|> validate_required(@required_fields)
|> validate_number(:timeout_minutes, greater_than: 0) |> validate_number(:timeout_minutes, greater_than: 0)
|> validate_number(:position, greater_than_or_equal_to: 0) |> validate_number(:position, greater_than_or_equal_to: 0)
|> foreign_key_constraint(:escalation_policy_id) |> foreign_key_constraint(:escalation_policy_id)

View file

@ -2,16 +2,14 @@ defmodule Towerops.OnCall.EscalationTarget do
@moduledoc """ @moduledoc """
Schema for notification targets at each escalation step. Schema for notification targets at each escalation step.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset import Towerops.OnCall.ChangesetHelpers
alias Towerops.Accounts.User alias Towerops.Accounts.User
alias Towerops.OnCall.EscalationRule alias Towerops.OnCall.EscalationRule
alias Towerops.OnCall.Schedule alias Towerops.OnCall.Schedule
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "escalation_targets" do schema "escalation_targets" do
field :target_type, :string field :target_type, :string
@ -29,8 +27,7 @@ defmodule Towerops.OnCall.EscalationTarget do
def changeset(target, attrs) do def changeset(target, attrs) do
target target
|> cast(attrs, @required_fields ++ @optional_fields) |> required_first(attrs, @required_fields, @optional_fields)
|> validate_required(@required_fields)
|> validate_inclusion(:target_type, @valid_target_types) |> validate_inclusion(:target_type, @valid_target_types)
|> foreign_key_constraint(:escalation_rule_id) |> foreign_key_constraint(:escalation_rule_id)
|> foreign_key_constraint(:schedule_id) |> foreign_key_constraint(:schedule_id)

View file

@ -2,9 +2,9 @@ defmodule Towerops.OnCall.Incident do
@moduledoc """ @moduledoc """
Schema for tracking active escalation state for an alert. Schema for tracking active escalation state for an alert.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset import Towerops.OnCall.ChangesetHelpers
alias Towerops.Accounts.User alias Towerops.Accounts.User
alias Towerops.Alerts.Alert alias Towerops.Alerts.Alert
@ -12,8 +12,6 @@ defmodule Towerops.OnCall.Incident do
alias Towerops.OnCall.Notification alias Towerops.OnCall.Notification
alias Towerops.Organizations.Organization alias Towerops.Organizations.Organization
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "on_call_incidents" do schema "on_call_incidents" do
field :status, :string, default: "triggered" field :status, :string, default: "triggered"
field :current_rule_position, :integer, default: 0 field :current_rule_position, :integer, default: 0
@ -40,8 +38,7 @@ defmodule Towerops.OnCall.Incident do
def changeset(incident, attrs) do def changeset(incident, attrs) do
incident incident
|> cast(attrs, @required_fields ++ @optional_fields) |> required_first(attrs, @required_fields, @optional_fields)
|> validate_required(@required_fields)
|> validate_inclusion(:status, @valid_statuses) |> validate_inclusion(:status, @valid_statuses)
|> foreign_key_constraint(:alert_id) |> foreign_key_constraint(:alert_id)
|> foreign_key_constraint(:escalation_policy_id) |> foreign_key_constraint(:escalation_policy_id)

View file

@ -4,15 +4,13 @@ defmodule Towerops.OnCall.Layer do
Layers stack higher position layers override lower layers where they have coverage. Layers stack higher position layers override lower layers where they have coverage.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset import Towerops.OnCall.ChangesetHelpers
alias Towerops.OnCall.LayerMember alias Towerops.OnCall.LayerMember
alias Towerops.OnCall.Schedule alias Towerops.OnCall.Schedule
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "on_call_layers" do schema "on_call_layers" do
field :name, :string field :name, :string
field :position, :integer, default: 0 field :position, :integer, default: 0
@ -39,8 +37,7 @@ defmodule Towerops.OnCall.Layer do
def changeset(layer, attrs) do def changeset(layer, attrs) do
layer layer
|> cast(attrs, @required_fields ++ @optional_fields) |> required_first(attrs, @required_fields, @optional_fields)
|> validate_required(@required_fields)
|> validate_inclusion(:rotation_type, @valid_rotation_types) |> validate_inclusion(:rotation_type, @valid_rotation_types)
|> validate_inclusion(:restriction_type, @valid_restriction_types) |> validate_inclusion(:restriction_type, @valid_restriction_types)
|> validate_number(:rotation_interval, greater_than: 0) |> validate_number(:rotation_interval, greater_than: 0)

View file

@ -2,15 +2,13 @@ defmodule Towerops.OnCall.LayerMember do
@moduledoc """ @moduledoc """
Schema for a user's position in a layer's rotation. Schema for a user's position in a layer's rotation.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset import Towerops.OnCall.ChangesetHelpers
alias Towerops.Accounts.User alias Towerops.Accounts.User
alias Towerops.OnCall.Layer alias Towerops.OnCall.Layer
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "on_call_layer_members" do schema "on_call_layer_members" do
field :position, :integer, default: 0 field :position, :integer, default: 0
@ -24,8 +22,7 @@ defmodule Towerops.OnCall.LayerMember do
def changeset(member, attrs) do def changeset(member, attrs) do
member member
|> cast(attrs, @required_fields) |> required_first(attrs, @required_fields)
|> validate_required(@required_fields)
|> validate_number(:position, greater_than_or_equal_to: 0) |> validate_number(:position, greater_than_or_equal_to: 0)
|> foreign_key_constraint(:layer_id) |> foreign_key_constraint(:layer_id)
|> foreign_key_constraint(:user_id) |> foreign_key_constraint(:user_id)

View file

@ -2,15 +2,13 @@ defmodule Towerops.OnCall.Notification do
@moduledoc """ @moduledoc """
Schema for logging sent on-call notifications. Schema for logging sent on-call notifications.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset import Towerops.OnCall.ChangesetHelpers
alias Towerops.Accounts.User alias Towerops.Accounts.User
alias Towerops.OnCall.Incident alias Towerops.OnCall.Incident
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "on_call_notifications" do schema "on_call_notifications" do
field :channel, :string, default: "email" field :channel, :string, default: "email"
field :sent_at, :utc_datetime field :sent_at, :utc_datetime
@ -28,8 +26,7 @@ defmodule Towerops.OnCall.Notification do
def changeset(notification, attrs) do def changeset(notification, attrs) do
notification notification
|> cast(attrs, @required_fields ++ @optional_fields) |> required_first(attrs, @required_fields, @optional_fields)
|> validate_required(@required_fields)
|> validate_inclusion(:channel, ~w(email)) |> validate_inclusion(:channel, ~w(email))
|> validate_inclusion(:status, ~w(sent failed)) |> validate_inclusion(:status, ~w(sent failed))
|> foreign_key_constraint(:incident_id) |> foreign_key_constraint(:incident_id)

View file

@ -2,15 +2,13 @@ defmodule Towerops.OnCall.Override do
@moduledoc """ @moduledoc """
Schema for temporary on-call overrides on a schedule. Schema for temporary on-call overrides on a schedule.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset import Towerops.OnCall.ChangesetHelpers
alias Towerops.Accounts.User alias Towerops.Accounts.User
alias Towerops.OnCall.Schedule alias Towerops.OnCall.Schedule
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "on_call_overrides" do schema "on_call_overrides" do
field :start_time, :utc_datetime field :start_time, :utc_datetime
field :end_time, :utc_datetime field :end_time, :utc_datetime
@ -27,8 +25,7 @@ defmodule Towerops.OnCall.Override do
def changeset(override, attrs) do def changeset(override, attrs) do
override override
|> cast(attrs, @required_fields ++ @optional_fields) |> required_first(attrs, @required_fields, @optional_fields)
|> validate_required(@required_fields)
|> validate_end_after_start() |> validate_end_after_start()
|> foreign_key_constraint(:schedule_id) |> foreign_key_constraint(:schedule_id)
|> foreign_key_constraint(:user_id) |> foreign_key_constraint(:user_id)

View file

@ -2,18 +2,15 @@ defmodule Towerops.OnCall.Schedule do
@moduledoc """ @moduledoc """
Schema for on-call schedules containing rotation layers. Schema for on-call schedules containing rotation layers.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset import Towerops.OnCall.ChangesetHelpers
alias Towerops.OnCall.Layer alias Towerops.OnCall.Layer
alias Towerops.OnCall.Override alias Towerops.OnCall.Override
alias Towerops.Organizations.Organization alias Towerops.Organizations.Organization
@type t :: %__MODULE__{} @type t :: %__MODULE__{}
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "on_call_schedules" do schema "on_call_schedules" do
field :name, :string field :name, :string
field :description, :string field :description, :string
@ -32,8 +29,7 @@ defmodule Towerops.OnCall.Schedule do
def changeset(schedule, attrs) do def changeset(schedule, attrs) do
schedule schedule
|> cast(attrs, @required_fields ++ @optional_fields) |> required_first(attrs, @required_fields, @optional_fields)
|> validate_required(@required_fields)
|> foreign_key_constraint(:organization_id) |> foreign_key_constraint(:organization_id)
end end
end end

View file

@ -4,16 +4,12 @@ defmodule Towerops.Organizations.Invitation do
Tracks invitation tokens, email, role, expiration, and acceptance status. Tracks invitation tokens, email, role, expiration, and acceptance status.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
alias Ecto.Association.NotLoaded alias Ecto.Association.NotLoaded
alias Towerops.Accounts.User alias Towerops.Accounts.User
alias Towerops.Organizations.Organization alias Towerops.Organizations.Organization
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "organization_invitations" do schema "organization_invitations" do
field :email, :string field :email, :string
field :role, Ecto.Enum, values: [:admin, :executive, :technician, :member, :viewer] field :role, Ecto.Enum, values: [:admin, :executive, :technician, :member, :viewer]

View file

@ -11,16 +11,12 @@ defmodule Towerops.Organizations.Membership do
- `:technician` Field ops: can manage devices, sites, alerts, but no financial data - `:technician` Field ops: can manage devices, sites, alerts, but no financial data
- `:viewer` Read-only, no financial data - `:viewer` Read-only, no financial data
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
alias Ecto.Association.NotLoaded alias Ecto.Association.NotLoaded
alias Towerops.Accounts.User alias Towerops.Accounts.User
alias Towerops.Organizations.Organization alias Towerops.Organizations.Organization
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "organization_memberships" do schema "organization_memberships" do
field :role, Ecto.Enum, values: [:owner, :admin, :executive, :technician, :member, :viewer] field :role, Ecto.Enum, values: [:owner, :admin, :executive, :technician, :member, :viewer]
field :is_default, :boolean, default: false field :is_default, :boolean, default: false

View file

@ -9,9 +9,7 @@ defmodule Towerops.Organizations.Organization do
- Default SNMP configuration - Default SNMP configuration
- Default agent token for polling - Default agent token for polling
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
alias Ecto.Association.NotLoaded alias Ecto.Association.NotLoaded
alias Towerops.Agents.AgentToken alias Towerops.Agents.AgentToken
@ -19,8 +17,6 @@ defmodule Towerops.Organizations.Organization do
alias Towerops.Organizations.Invitation alias Towerops.Organizations.Invitation
alias Towerops.Organizations.Membership alias Towerops.Organizations.Membership
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "organizations" do schema "organizations" do
field :name, :string field :name, :string
field :slug, :string field :slug, :string

View file

@ -2,12 +2,7 @@ defmodule Towerops.Preseem.AccessPoint do
@moduledoc """ @moduledoc """
Schema for access points synced from Preseem API. Schema for access points synced from Preseem API.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
@valid_match_confidences ~w(auto_mac auto_ip auto_hostname manual ambiguous unmatched) @valid_match_confidences ~w(auto_mac auto_ip auto_hostname manual ambiguous unmatched)

View file

@ -3,12 +3,7 @@ defmodule Towerops.Preseem.DeviceBaseline do
Rolling 14-day baseline for individual Preseem access points. Rolling 14-day baseline for individual Preseem access points.
Computed nightly to detect deviations from normal behavior. Computed nightly to detect deviations from normal behavior.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
@valid_periods ~w(busy offpeak all) @valid_periods ~w(busy offpeak all)
@valid_metrics ~w(qoe_score capacity_score rf_score p95_latency avg_latency avg_jitter avg_loss avg_throughput subscriber_count airtime_utilization) @valid_metrics ~w(qoe_score capacity_score rf_score p95_latency avg_latency avg_jitter avg_loss avg_throughput subscriber_count airtime_utilization)

View file

@ -3,12 +3,7 @@ defmodule Towerops.Preseem.FleetProfile do
Fleet-wide model performance profiles. Groups devices by manufacturer+model Fleet-wide model performance profiles. Groups devices by manufacturer+model
to identify patterns, capacity ceilings, and firmware correlations. to identify patterns, capacity ceilings, and firmware correlations.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "preseem_fleet_profiles" do schema "preseem_fleet_profiles" do
field :manufacturer, :string field :manufacturer, :string

View file

@ -3,12 +3,7 @@ defmodule Towerops.Preseem.Insight do
Generated insights from Preseem data analysis - deviations, capacity warnings, Generated insights from Preseem data analysis - deviations, capacity warnings,
firmware recommendations, and performance observations. firmware recommendations, and performance observations.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
@valid_types ~w(qoe_degradation capacity_saturation firmware_opportunity model_underperforming subscriber_growth config_drift snmp_cpu_high snmp_memory_high snmp_disk_high device_poll_gap agent_offline firmware_mismatch reconciliation_finding subscriber_growth_trend suspect_config_change backhaul_over_capacity backhaul_near_capacity wireless_signal_weak wireless_snr_low wireless_ap_overloaded wireless_client_missing wireless_coverage_gap ap_frequency_change sector_overload cpe_realign own_fleet_channel_conflict device_overheating upstream_degradation optical_rx_low ai_observation) @valid_types ~w(qoe_degradation capacity_saturation firmware_opportunity model_underperforming subscriber_growth config_drift snmp_cpu_high snmp_memory_high snmp_disk_high device_poll_gap agent_offline firmware_mismatch reconciliation_finding subscriber_growth_trend suspect_config_change backhaul_over_capacity backhaul_near_capacity wireless_signal_weak wireless_snr_low wireless_ap_overloaded wireless_client_missing wireless_coverage_gap ap_frequency_change sector_overload cpe_realign own_fleet_channel_conflict device_overheating upstream_degradation optical_rx_low ai_observation)
@valid_urgencies ~w(critical warning info) @valid_urgencies ~w(critical warning info)

View file

@ -2,12 +2,7 @@ defmodule Towerops.Preseem.SubscriberMetric do
@moduledoc """ @moduledoc """
Time-series aggregate QoE metrics per Preseem access point. Time-series aggregate QoE metrics per Preseem access point.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "preseem_subscriber_metrics" do schema "preseem_subscriber_metrics" do
field :avg_latency, :float field :avg_latency, :float

View file

@ -2,40 +2,11 @@ defmodule Towerops.Preseem.SyncLog do
@moduledoc """ @moduledoc """
Audit log for Preseem sync operations. Audit log for Preseem sync operations.
""" """
use Ecto.Schema use Towerops.SyncLog, table_name: "preseem_sync_logs"
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
@valid_statuses ~w(success partial failed) @valid_statuses ~w(success partial failed)
schema "preseem_sync_logs" do
field :status, :string
field :records_synced, :integer, default: 0
field :errors, :map
field :duration_ms, :integer
field :inserted_at, :utc_datetime
belongs_to :organization, Towerops.Organizations.Organization
belongs_to :integration, Towerops.Integrations.Integration
end
def changeset(sync_log, attrs) do def changeset(sync_log, attrs) do
sync_log sync_log_changeset(sync_log, attrs, @valid_statuses)
|> cast(attrs, [
:organization_id,
:integration_id,
:status,
:records_synced,
:errors,
:duration_ms,
:inserted_at
])
|> validate_required([:organization_id, :integration_id, :status, :inserted_at])
|> validate_inclusion(:status, @valid_statuses)
|> foreign_key_constraint(:organization_id)
|> foreign_key_constraint(:integration_id)
end end
end end

View file

@ -4,15 +4,10 @@ defmodule Towerops.Profiles.DeviceOid do
Maps device info fields (serial_number, firmware_version, etc.) to MIB names. Maps device info fields (serial_number, firmware_version, etc.) to MIB names.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
alias Towerops.Profiles.DeviceProfile alias Towerops.Profiles.DeviceProfile
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "profile_device_oids" do schema "profile_device_oids" do
field :field, :string field :field, :string
field :mib_name, :string field :mib_name, :string

View file

@ -4,16 +4,11 @@ defmodule Towerops.Profiles.DeviceProfile do
Profiles define how to identify and discover sensors for different device types. Profiles define how to identify and discover sensors for different device types.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
alias Towerops.Profiles.DeviceOid alias Towerops.Profiles.DeviceOid
alias Towerops.Profiles.SensorOid alias Towerops.Profiles.SensorOid
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "device_profiles" do schema "device_profiles" do
field :name, :string field :name, :string
field :vendor, :string field :vendor, :string

View file

@ -4,15 +4,10 @@ defmodule Towerops.Profiles.SensorOid do
Maps sensor types to MIB names for sensor discovery. Maps sensor types to MIB names for sensor discovery.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
alias Towerops.Profiles.DeviceProfile alias Towerops.Profiles.DeviceProfile
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "profile_sensor_oids" do schema "profile_sensor_oids" do
field :sensor_type, :string field :sensor_type, :string
field :mib_name, :string field :mib_name, :string

View file

@ -6,12 +6,7 @@ defmodule Towerops.Reports.Report do
Schedule types: one_time, daily, weekly, monthly. Schedule types: one_time, daily, weekly, monthly.
Formats: csv (PDF support planned). Formats: csv (PDF support planned).
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
@valid_report_types ~w(uptime_summary alert_history capacity_trends rf_link_health) @valid_report_types ~w(uptime_summary alert_history capacity_trends rf_link_health)
@valid_formats ~w(csv) @valid_formats ~w(csv)

36
lib/towerops/schema.ex Normal file
View file

@ -0,0 +1,36 @@
defmodule Towerops.Schema do
@moduledoc """
Base schema module for TowerOps.
Provides sensible defaults for all Ecto schemas:
- `binary_id` primary keys
- `binary_id` foreign key type
## Usage
defmodule Towerops.MySchema do
use Towerops.Schema
schema "my_table" do
field :name, :string
timestamps(type: :utc_datetime)
end
def changeset(struct, attrs) do
struct
|> cast(attrs, [:name])
|> validate_required([:name])
end
end
"""
defmacro __using__(_opts) do
quote do
use Ecto.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
end
end
end

View file

@ -11,12 +11,7 @@ defmodule Towerops.Security.IpBlock do
Offense count resets to 1 if 30 days pass without violations. Offense count resets to 1 if 30 days pass without violations.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "ip_blocks" do schema "ip_blocks" do
field :ip_address, :string field :ip_address, :string

View file

@ -3,12 +3,7 @@ defmodule Towerops.Security.IpWhitelist do
Schema for tracking IP addresses and CIDR ranges that are exempt from brute force protection. Schema for tracking IP addresses and CIDR ranges that are exempt from brute force protection.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "ip_whitelist" do schema "ip_whitelist" do
field :ip_or_cidr, :string field :ip_or_cidr, :string

View file

@ -5,14 +5,10 @@ defmodule Towerops.Settings.ApplicationSetting do
Stores global configuration values that affect the entire application, Stores global configuration values that affect the entire application,
regardless of organization. Only accessible by superadmins. regardless of organization. Only accessible by superadmins.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
require Logger require Logger
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "application_settings" do schema "application_settings" do
field :key, :string field :key, :string
field :value, :string field :value, :string

View file

@ -4,9 +4,7 @@ defmodule Towerops.Sites.Site do
Sites contain devices and can have site-level SNMP configuration defaults. Sites contain devices and can have site-level SNMP configuration defaults.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
alias Ecto.Association.NotLoaded alias Ecto.Association.NotLoaded
alias Towerops.Agents.AgentToken alias Towerops.Agents.AgentToken
@ -15,8 +13,6 @@ defmodule Towerops.Sites.Site do
alias Towerops.Organizations.Organization alias Towerops.Organizations.Organization
alias Towerops.Sites.Site alias Towerops.Sites.Site
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "sites" do schema "sites" do
field :name, :string field :name, :string
field :description, :string field :description, :string

View file

@ -4,12 +4,8 @@ defmodule Towerops.Snmp.ArpEntry do
Stores ARP table entries discovered via ipNetToMediaTable (1.3.6.1.2.1.4.22). Stores ARP table entries discovered via ipNetToMediaTable (1.3.6.1.2.1.4.22).
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "snmp_arp_entries" do schema "snmp_arp_entries" do
field :ip_address, :string field :ip_address, :string
field :mac_address, :string field :mac_address, :string

View file

@ -5,9 +5,7 @@ defmodule Towerops.Snmp.Device do
Stores system details (sysDescr, sysObjectID), vendor information, Stores system details (sysDescr, sysObjectID), vendor information,
and relationships to interfaces, sensors, and neighbors. and relationships to interfaces, sensors, and neighbors.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
alias Ecto.Association.NotLoaded alias Ecto.Association.NotLoaded
alias Towerops.Devices.Device, as: DeviceSchema alias Towerops.Devices.Device, as: DeviceSchema
@ -20,8 +18,6 @@ defmodule Towerops.Snmp.Device do
alias Towerops.Snmp.Storage alias Towerops.Snmp.Storage
alias Towerops.Snmp.Transceiver alias Towerops.Snmp.Transceiver
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "snmp_devices" do schema "snmp_devices" do
field :sys_descr, :string field :sys_descr, :string
field :sys_object_id, :string field :sys_object_id, :string

View file

@ -6,15 +6,11 @@ defmodule Towerops.Snmp.EntityPhysical do
including chassis, modules, ports, power supplies, fans, and sensors. including chassis, modules, ports, power supplies, fans, and sensors.
Supports hierarchical parent/child relationships for physical containment. Supports hierarchical parent/child relationships for physical containment.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
alias Ecto.Association.NotLoaded alias Ecto.Association.NotLoaded
alias Towerops.Snmp.Device alias Towerops.Snmp.Device
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "snmp_entity_physical" do schema "snmp_entity_physical" do
field :entity_index, :string field :entity_index, :string
field :parent_index, :string field :parent_index, :string

View file

@ -7,14 +7,12 @@ defmodule Towerops.Snmp.EntityPhysicalReading do
and administrative status (is this component enabled?) for power supplies, fans, and administrative status (is this component enabled?) for power supplies, fans,
modules, and other physical entities. modules, and other physical entities.
""" """
use Ecto.Schema use Towerops.Snmp.Reading
import Ecto.Changeset
alias Towerops.Snmp.EntityPhysical alias Towerops.Snmp.EntityPhysical
@primary_key {:id, :binary_id, autogenerate: true} @timestamps_opts [type: :utc_datetime]
@foreign_key_type :binary_id
schema "snmp_entity_physical_readings" do schema "snmp_entity_physical_readings" do
field :operational_status, :string field :operational_status, :string
field :admin_status, :string field :admin_status, :string
@ -22,7 +20,7 @@ defmodule Towerops.Snmp.EntityPhysicalReading do
belongs_to :entity_physical, EntityPhysical belongs_to :entity_physical, EntityPhysical
timestamps(type: :utc_datetime) timestamps()
end end
@type t :: %__MODULE__{ @type t :: %__MODULE__{
@ -66,15 +64,12 @@ defmodule Towerops.Snmp.EntityPhysicalReading do
""" """
def changeset(reading, attrs) do def changeset(reading, attrs) do
reading reading
|> cast(attrs, [ |> reading_changeset(attrs,
:entity_physical_id, parent_key: :entity_physical_id,
:operational_status, data_fields: [:operational_status, :admin_status],
:admin_status, timestamp_field: :measured_at
:measured_at )
])
|> validate_required([:entity_physical_id, :measured_at])
|> validate_inclusion(:operational_status, @operational_statuses) |> validate_inclusion(:operational_status, @operational_statuses)
|> validate_inclusion(:admin_status, @admin_statuses) |> validate_inclusion(:admin_status, @admin_statuses)
|> foreign_key_constraint(:entity_physical_id)
end end
end end

View file

@ -5,9 +5,7 @@ defmodule Towerops.Snmp.Interface do
Stores interface details (name, description, type, speed, MAC address) Stores interface details (name, description, type, speed, MAC address)
and has many interface statistics. and has many interface statistics.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
alias Ecto.Association.NotLoaded alias Ecto.Association.NotLoaded
alias Towerops.Snmp.Device alias Towerops.Snmp.Device
@ -15,8 +13,6 @@ defmodule Towerops.Snmp.Interface do
alias Towerops.Snmp.IpAddress alias Towerops.Snmp.IpAddress
alias Towerops.Snmp.Neighbor alias Towerops.Snmp.Neighbor
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "snmp_interfaces" do schema "snmp_interfaces" do
field :if_index, :integer field :if_index, :integer
field :if_name, :string field :if_name, :string

View file

@ -5,14 +5,10 @@ defmodule Towerops.Snmp.InterfaceStat do
Stores interface counter values (octets, errors, discards) collected Stores interface counter values (octets, errors, discards) collected
during SNMP polling. Stored in TimescaleDB hypertable. during SNMP polling. Stored in TimescaleDB hypertable.
""" """
use Ecto.Schema use Towerops.Snmp.Reading
import Ecto.Changeset
alias Towerops.Snmp.Interface alias Towerops.Snmp.Interface
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "snmp_interface_stats" do schema "snmp_interface_stats" do
field :if_in_octets, :integer field :if_in_octets, :integer
field :if_out_octets, :integer field :if_out_octets, :integer
@ -25,7 +21,7 @@ defmodule Towerops.Snmp.InterfaceStat do
belongs_to :interface, Interface belongs_to :interface, Interface
timestamps(type: :utc_datetime, updated_at: false) timestamps()
end end
@type t :: %__MODULE__{ @type t :: %__MODULE__{
@ -45,19 +41,17 @@ defmodule Towerops.Snmp.InterfaceStat do
@doc false @doc false
def changeset(stat, attrs) do def changeset(stat, attrs) do
stat reading_changeset(stat, attrs,
|> cast(attrs, [ parent_key: :interface_id,
:interface_id, data_fields: [
:if_in_octets, :if_in_octets,
:if_out_octets, :if_out_octets,
:if_in_errors, :if_in_errors,
:if_out_errors, :if_out_errors,
:if_in_discards, :if_in_discards,
:if_out_discards, :if_out_discards,
:checked_at, :is_hc
:is_hc ]
]) )
|> validate_required([:interface_id, :checked_at])
|> foreign_key_constraint(:interface_id)
end end
end end

View file

@ -8,17 +8,13 @@ defmodule Towerops.Snmp.IpAddress do
Each interface can have multiple IP addresses (primary and secondary/alias). Each interface can have multiple IP addresses (primary and secondary/alias).
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
alias Ecto.Association.NotLoaded alias Ecto.Association.NotLoaded
alias Towerops.Snmp.Interface alias Towerops.Snmp.Interface
@valid_ip_types ~w(ipv4 ipv6) @valid_ip_types ~w(ipv4 ipv6)
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "snmp_ip_addresses" do schema "snmp_ip_addresses" do
field :ip_address, :string field :ip_address, :string
field :subnet_mask, :string field :subnet_mask, :string

View file

@ -6,12 +6,8 @@ defmodule Towerops.Snmp.MacAddress do
dot1dTpFdbTable (1.3.6.1.2.1.17.4.3) and Q-BRIDGE-MIB dot1dTpFdbTable (1.3.6.1.2.1.17.4.3) and Q-BRIDGE-MIB
dot1qTpFdbTable (1.3.6.1.2.1.17.7.1.2.2). dot1qTpFdbTable (1.3.6.1.2.1.17.7.1.2.2).
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "snmp_mac_addresses" do schema "snmp_mac_addresses" do
field :mac_address, :string field :mac_address, :string
field :vlan_id, :integer field :vlan_id, :integer

View file

@ -8,17 +8,13 @@ defmodule Towerops.Snmp.MemoryPool do
Tracks total and used memory for capacity monitoring. Tracks total and used memory for capacity monitoring.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
alias Ecto.Association.NotLoaded alias Ecto.Association.NotLoaded
alias Towerops.Snmp.Device alias Towerops.Snmp.Device
@valid_pool_types ~w(ram swap cache buffer other) @valid_pool_types ~w(ram swap cache buffer other)
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "snmp_memory_pools" do schema "snmp_memory_pools" do
field :pool_index, :string field :pool_index, :string
field :pool_name, :string field :pool_name, :string

View file

@ -12,17 +12,13 @@ defmodule Towerops.Snmp.Mempool do
- "cisco_memory" - Cisco memory pools from CISCO-MEMORY-POOL-MIB - "cisco_memory" - Cisco memory pools from CISCO-MEMORY-POOL-MIB
- "ucd_memory" - UCD-SNMP-MIB memory statistics - "ucd_memory" - UCD-SNMP-MIB memory statistics
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
alias Ecto.Association.NotLoaded alias Ecto.Association.NotLoaded
alias Towerops.Snmp.Device alias Towerops.Snmp.Device
@valid_mempool_types ~w(hr_memory cisco_memory ucd_memory) @valid_mempool_types ~w(hr_memory cisco_memory ucd_memory)
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "snmp_mempools" do schema "snmp_mempools" do
field :mempool_index, :string field :mempool_index, :string
field :description, :string field :description, :string

View file

@ -5,14 +5,10 @@ defmodule Towerops.Snmp.MempoolReading do
Stores individual memory pool readings (used/total/free bytes, usage percent, timestamp) Stores individual memory pool readings (used/total/free bytes, usage percent, timestamp)
collected during SNMP polling cycles. collected during SNMP polling cycles.
""" """
use Ecto.Schema use Towerops.Snmp.Reading
import Ecto.Changeset
alias Towerops.Snmp.Mempool alias Towerops.Snmp.Mempool
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "snmp_mempool_readings" do schema "snmp_mempool_readings" do
field :used_bytes, :integer field :used_bytes, :integer
field :total_bytes, :integer field :total_bytes, :integer
@ -22,7 +18,7 @@ defmodule Towerops.Snmp.MempoolReading do
belongs_to :mempool, Mempool belongs_to :mempool, Mempool
timestamps(type: :utc_datetime, updated_at: false) timestamps()
end end
@type t :: %__MODULE__{ @type t :: %__MODULE__{
@ -39,9 +35,9 @@ defmodule Towerops.Snmp.MempoolReading do
@doc false @doc false
def changeset(reading, attrs) do def changeset(reading, attrs) do
reading reading_changeset(reading, attrs,
|> cast(attrs, [:mempool_id, :used_bytes, :total_bytes, :free_bytes, :usage_percent, :checked_at]) parent_key: :mempool_id,
|> validate_required([:mempool_id, :checked_at]) data_fields: [:used_bytes, :total_bytes, :free_bytes, :usage_percent]
|> foreign_key_constraint(:mempool_id) )
end end
end end

View file

@ -5,12 +5,8 @@ defmodule Towerops.Snmp.Neighbor do
Stores neighbor information (chassis ID, port ID, system name) Stores neighbor information (chassis ID, port ID, system name)
and links to matched devices in the organization. and links to matched devices in the organization.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "snmp_neighbors" do schema "snmp_neighbors" do
field :protocol, :string field :protocol, :string
field :remote_chassis_id, :string field :remote_chassis_id, :string

View file

@ -12,17 +12,13 @@ defmodule Towerops.Snmp.PhysicalEntity do
Entities form a parent/child hierarchy via contained_in relationships. Entities form a parent/child hierarchy via contained_in relationships.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
alias Ecto.Association.NotLoaded alias Ecto.Association.NotLoaded
alias Towerops.Snmp.Device alias Towerops.Snmp.Device
@valid_entity_classes ~w(other unknown chassis backplane container power fan sensor module port stack cpu memory) @valid_entity_classes ~w(other unknown chassis backplane container power fan sensor module port stack cpu memory)
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "snmp_physical_entities" do schema "snmp_physical_entities" do
field :entity_index, :integer field :entity_index, :integer
field :entity_class, :string field :entity_class, :string

View file

@ -1,14 +1,9 @@
defmodule Towerops.Snmp.PrinterSupply do defmodule Towerops.Snmp.PrinterSupply do
@moduledoc false @moduledoc false
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
alias Towerops.Devices.Device alias Towerops.Devices.Device
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "printer_supplies" do schema "printer_supplies" do
belongs_to :snmp_device, Device belongs_to :snmp_device, Device

View file

@ -12,17 +12,13 @@ defmodule Towerops.Snmp.Processor do
- "cisco_cpu" - Cisco CPU from CISCO-PROCESS-MIB - "cisco_cpu" - Cisco CPU from CISCO-PROCESS-MIB
- "ucd_cpu" - UCD-SNMP-MIB CPU statistics - "ucd_cpu" - UCD-SNMP-MIB CPU statistics
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
alias Ecto.Association.NotLoaded alias Ecto.Association.NotLoaded
alias Towerops.Snmp.Device alias Towerops.Snmp.Device
@valid_processor_types ~w(hr_processor cisco_cpu ucd_cpu) @valid_processor_types ~w(hr_processor cisco_cpu ucd_cpu)
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "snmp_processors" do schema "snmp_processors" do
field :processor_index, :string field :processor_index, :string
field :description, :string field :description, :string

View file

@ -5,14 +5,10 @@ defmodule Towerops.Snmp.ProcessorReading do
Stores individual processor load readings collected during SNMP polling cycles. Stores individual processor load readings collected during SNMP polling cycles.
Similar to SensorReading but specifically for processor/CPU load data. Similar to SensorReading but specifically for processor/CPU load data.
""" """
use Ecto.Schema use Towerops.Snmp.Reading
import Ecto.Changeset
alias Towerops.Snmp.Processor alias Towerops.Snmp.Processor
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "snmp_processor_readings" do schema "snmp_processor_readings" do
field :load_percent, :float field :load_percent, :float
field :status, :string field :status, :string
@ -20,7 +16,7 @@ defmodule Towerops.Snmp.ProcessorReading do
belongs_to :processor, Processor belongs_to :processor, Processor
timestamps(type: :utc_datetime, updated_at: false) timestamps()
end end
@type t :: %__MODULE__{ @type t :: %__MODULE__{
@ -36,9 +32,11 @@ defmodule Towerops.Snmp.ProcessorReading do
@doc false @doc false
def changeset(reading, attrs) do def changeset(reading, attrs) do
reading reading
|> cast(attrs, [:processor_id, :load_percent, :status, :checked_at]) |> reading_changeset(attrs,
|> validate_required([:processor_id, :status, :checked_at]) parent_key: :processor_id,
data_fields: [:load_percent, :status],
required_with: [:status]
)
|> validate_inclusion(:status, ["ok", "warning", "critical", "error"]) |> validate_inclusion(:status, ["ok", "warning", "critical", "error"])
|> foreign_key_constraint(:processor_id)
end end
end end

View file

@ -0,0 +1,68 @@
defmodule Towerops.Snmp.Reading do
@moduledoc """
Provides common setup and a `reading_changeset/3` helper for
SNMP time-series reading schemas.
## Usage
defmodule Towerops.Snmp.ProcessorReading do
use Towerops.Snmp.Reading
schema "snmp_processor_readings" do
field :load_percent, :float
field :checked_at, :utc_datetime
belongs_to :processor, Towerops.Snmp.Processor
timestamps()
end
def changeset(reading, attrs) do
reading
|> reading_changeset(attrs,
parent_key: :processor_id,
data_fields: [:load_percent],
)
end
end
"""
import Ecto.Changeset
defmacro __using__(_opts) do
quote do
use Towerops.Schema
import Towerops.Snmp.Reading, only: [reading_changeset: 3]
@timestamps_opts [type: :utc_datetime, updated_at: false]
end
end
@doc """
Builds a changeset for a reading schema, handling common cast,
required validation, and foreign key constraint.
## Options
* `:parent_key` - (required) the foreign key field atom
* `:data_fields` - list of data field atoms to cast (default: `[]`)
* `:timestamp_field` - the timestamp field atom (default: `:checked_at`)
* `:required_with` - additional required field atoms beyond parent_key and
timestamp_field (default: `[]`)
"""
def reading_changeset(struct, attrs, opts) do
parent_key = opts[:parent_key] || raise ":parent_key required"
data_fields = opts[:data_fields] || []
timestamp_field = opts[:timestamp_field] || :checked_at
required_with = opts[:required_with] || []
all_fields = [parent_key | data_fields ++ [timestamp_field]]
required = [parent_key, timestamp_field | required_with]
struct
|> cast(attrs, all_fields)
|> validate_required(required)
|> foreign_key_constraint(parent_key)
end
end

View file

@ -5,16 +5,12 @@ defmodule Towerops.Snmp.Sensor do
Tracks temperature, fan speed, voltage, power, and other sensors Tracks temperature, fan speed, voltage, power, and other sensors
discovered via ENTITY-SENSOR-MIB or vendor-specific MIBs. discovered via ENTITY-SENSOR-MIB or vendor-specific MIBs.
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
alias Ecto.Association.NotLoaded alias Ecto.Association.NotLoaded
alias Towerops.Snmp.Device alias Towerops.Snmp.Device
alias Towerops.Snmp.SensorReading alias Towerops.Snmp.SensorReading
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "snmp_sensors" do schema "snmp_sensors" do
field :sensor_type, :string field :sensor_type, :string
field :sensor_index, :string field :sensor_index, :string

View file

@ -5,14 +5,10 @@ defmodule Towerops.Snmp.SensorReading do
Stores individual sensor readings (value, status, timestamp) collected Stores individual sensor readings (value, status, timestamp) collected
during SNMP polling cycles. Stored in TimescaleDB hypertable. during SNMP polling cycles. Stored in TimescaleDB hypertable.
""" """
use Ecto.Schema use Towerops.Snmp.Reading
import Ecto.Changeset
alias Towerops.Snmp.Sensor alias Towerops.Snmp.Sensor
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "snmp_sensor_readings" do schema "snmp_sensor_readings" do
field :value, :float field :value, :float
field :status, :string field :status, :string
@ -21,7 +17,7 @@ defmodule Towerops.Snmp.SensorReading do
belongs_to :sensor, Sensor belongs_to :sensor, Sensor
timestamps(type: :utc_datetime, updated_at: false) timestamps()
end end
@type t :: %__MODULE__{ @type t :: %__MODULE__{
@ -38,11 +34,13 @@ defmodule Towerops.Snmp.SensorReading do
@doc false @doc false
def changeset(reading, attrs) do def changeset(reading, attrs) do
reading reading
|> cast(attrs, [:sensor_id, :value, :status, :state_descr, :checked_at]) |> reading_changeset(attrs,
|> validate_required([:sensor_id, :status, :checked_at]) parent_key: :sensor_id,
data_fields: [:value, :status, :state_descr],
required_with: [:status]
)
|> validate_inclusion(:status, ["ok", "warning", "critical", "error"]) |> validate_inclusion(:status, ["ok", "warning", "critical", "error"])
|> validate_state_descr() |> validate_state_descr()
|> foreign_key_constraint(:sensor_id)
end end
# Validate state_descr when present - ensure it's not empty and has reasonable length # Validate state_descr when present - ensure it's not empty and has reasonable length

View file

@ -12,17 +12,13 @@ defmodule Towerops.Snmp.StateSensor do
- critical: Failed or about to fail - critical: Failed or about to fail
- unknown: State not determined - unknown: State not determined
""" """
use Ecto.Schema use Towerops.Schema
import Ecto.Changeset
alias Ecto.Association.NotLoaded alias Ecto.Association.NotLoaded
alias Towerops.Snmp.Device alias Towerops.Snmp.Device
@valid_statuses ~w(ok warning critical unknown) @valid_statuses ~w(ok warning critical unknown)
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "snmp_state_sensors" do schema "snmp_state_sensors" do
field :sensor_index, :string field :sensor_index, :string
field :sensor_oid, :string field :sensor_oid, :string

Some files were not shown because too many files have changed in this diff Show more