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:
parent
d1403c8069
commit
a4e08b2aca
134 changed files with 1082 additions and 1516 deletions
|
|
@ -46,9 +46,9 @@ config :logger, :default_handler,
|
|||
filters: [
|
||||
# Suppress HTTP/0.9 and other invalid protocol errors from Bandit
|
||||
# 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
|
||||
shutdown_errors: {&Towerops.LoggerFilters.drop_shutdown_errors/2, []}
|
||||
shutdown_errors: {&Towerops.LogFilters.drop_shutdown_errors/2, []}
|
||||
]
|
||||
|
||||
# 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
|
||||
# replicas can serve the rasters and pod restarts don't lose them.
|
||||
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
|
||||
# Override this in runtime.exs or environment-specific config
|
||||
|
|
|
|||
|
|
@ -218,13 +218,6 @@ config :towerops, :env, :dev
|
|||
# Configure SNMP MIB directories for development
|
||||
# Point to local priv/mibs subdirectories where MIB files are stored
|
||||
# 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
|
||||
config :towerops, :rate_limiting_enabled, false
|
||||
config :towerops, :session_encryption_salt, "dev-encryption-salt-stable"
|
||||
|
|
@ -250,5 +243,4 @@ config :towerops, dev_routes: true
|
|||
config :towerops,
|
||||
stripe_secret_key: System.get_env("STRIPE_SECRET_KEY", "sk_test_use_your_own_key"),
|
||||
stripe_webhook_secret: "whsec_dev_fake_secret",
|
||||
stripe_price_id: "price_1T81XBS77kvnTfgyPlw1jF8N",
|
||||
stripe_meter_id: "mtr_test_61UHPU067veEZ7bhl41S77kvnTfgyODY"
|
||||
stripe_price_id: "price_1T81XBS77kvnTfgyPlw1jF8N"
|
||||
|
|
|
|||
|
|
@ -1,22 +1,18 @@
|
|||
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
|
||||
config :logger, :default_handler,
|
||||
filters: [
|
||||
drop_oban_shutdown: {
|
||||
&Towerops.LoggerFilters.drop_oban_shutdown/2,
|
||||
&Towerops.LogFilters.drop_oban_shutdown/2,
|
||||
[]
|
||||
},
|
||||
drop_snmp_mib_errors: {
|
||||
&Towerops.LoggerFilters.drop_snmp_mib_errors/2,
|
||||
&Towerops.LogFilters.drop_snmp_mib_errors/2,
|
||||
[]
|
||||
},
|
||||
filter_health_checks: {
|
||||
&ToweropsWeb.TelemetryFilter.filter_health_checks/2,
|
||||
&Towerops.LogFilters.filter_health_checks/2,
|
||||
[]
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -503,13 +503,6 @@ if config_env() == :prod do
|
|||
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
|
||||
config :error_tracker_notifier,
|
||||
notification_type: :email,
|
||||
|
|
@ -520,8 +513,7 @@ if config_env() == :prod do
|
|||
config :towerops,
|
||||
stripe_secret_key: stripe_secret_key,
|
||||
stripe_webhook_secret: stripe_webhook_secret,
|
||||
stripe_price_id: stripe_price_id,
|
||||
stripe_meter_id: stripe_meter_id
|
||||
stripe_price_id: stripe_price_id
|
||||
end
|
||||
|
||||
# Test environment Stripe configuration
|
||||
|
|
@ -529,6 +521,5 @@ if config_env() == :test do
|
|||
config :towerops,
|
||||
stripe_secret_key: "sk_test_fake",
|
||||
stripe_webhook_secret: "whsec_test_fake",
|
||||
stripe_price_id: "price_test_fake",
|
||||
stripe_meter_id: "meter_test_fake"
|
||||
stripe_price_id: "price_test_fake"
|
||||
end
|
||||
|
|
|
|||
|
|
@ -16,12 +16,7 @@ defmodule Towerops.Accounts.BrowserSession do
|
|||
- Automatically cleaned up after expires_at timestamp
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
use Towerops.Schema
|
||||
|
||||
schema "browser_sessions" do
|
||||
# Device/browser metadata
|
||||
|
|
|
|||
|
|
@ -9,12 +9,7 @@ defmodule Towerops.Accounts.LoginAttempt do
|
|||
- Data retention: 365 days for active records, 90 days for anonymized records
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
use Towerops.Schema
|
||||
|
||||
@valid_methods ~w(password magic_link mobile_qr)
|
||||
@valid_failure_reasons ~w(invalid_credentials invalid_totp invalid_magic_link account_disabled)
|
||||
|
|
|
|||
|
|
@ -5,12 +5,8 @@ defmodule Towerops.Accounts.PolicyVersion do
|
|||
Stores the full text of each policy version with effective dates to maintain
|
||||
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
|
||||
field :policy_type, :string
|
||||
field :version, :string
|
||||
|
|
|
|||
|
|
@ -7,16 +7,12 @@ defmodule Towerops.Accounts.User do
|
|||
- TOTP (Time-based One-Time Password) two-factor authentication
|
||||
- Session tokens
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
use Towerops.Schema
|
||||
|
||||
alias Ecto.Association.NotLoaded
|
||||
alias Towerops.Organizations.Membership
|
||||
alias Towerops.Organizations.Organization
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "users" do
|
||||
field :email, :string
|
||||
field :password, :string, virtual: true, redact: true
|
||||
|
|
|
|||
|
|
@ -5,14 +5,10 @@ defmodule Towerops.Accounts.UserConsent do
|
|||
Stores consent records with versioning to comply with GDPR and privacy regulations.
|
||||
Allows users to withdraw consent at any time.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
use Towerops.Schema
|
||||
|
||||
alias Towerops.Accounts.User
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "user_consents" do
|
||||
belongs_to :user, User
|
||||
|
||||
|
|
|
|||
|
|
@ -5,14 +5,10 @@ defmodule Towerops.Accounts.UserRecoveryCode do
|
|||
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.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
use Towerops.Schema
|
||||
|
||||
alias Towerops.Accounts.User
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "user_recovery_codes" do
|
||||
field :code_hash, :string
|
||||
field :used_at, :utc_datetime
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ defmodule Towerops.Accounts.UserToken do
|
|||
Manages session tokens, email confirmation tokens, and password reset tokens
|
||||
with expiration and hashing.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
use Towerops.Schema
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
|
|
@ -21,8 +21,6 @@ defmodule Towerops.Accounts.UserToken do
|
|||
@change_email_validity_in_days 7
|
||||
@session_validity_in_days 14
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "users_tokens" do
|
||||
field :token, :binary
|
||||
field :context, :string
|
||||
|
|
|
|||
|
|
@ -5,14 +5,10 @@ defmodule Towerops.Accounts.UserTotpDevice do
|
|||
Allows users to register multiple authenticator apps (phone, tablet, etc.)
|
||||
for two-factor authentication.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
use Towerops.Schema
|
||||
|
||||
alias Towerops.Accounts.User
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "user_totp_devices" do
|
||||
field :name, :string
|
||||
field :totp_secret, :binary, redact: true
|
||||
|
|
|
|||
|
|
@ -2,15 +2,10 @@ defmodule Towerops.Admin.AuditLog do
|
|||
@moduledoc """
|
||||
Schema for audit logs tracking sensitive operations performed by superusers.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
use Towerops.Schema
|
||||
|
||||
alias Towerops.Accounts.User
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
|
||||
schema "audit_logs" do
|
||||
field :action, :string
|
||||
field :metadata, :map
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
This ensures that polling is not duplicated across multiple agents.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
use Towerops.Schema
|
||||
|
||||
alias Ecto.Association.NotLoaded
|
||||
alias Towerops.Agents.AgentToken
|
||||
alias Towerops.Devices.Device
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "agent_assignments" do
|
||||
field :enabled, :boolean, default: true
|
||||
|
||||
|
|
|
|||
|
|
@ -6,17 +6,13 @@ defmodule Towerops.Agents.AgentToken do
|
|||
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.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
use Towerops.Schema
|
||||
|
||||
alias Towerops.Organizations.Organization
|
||||
|
||||
# 48 bytes = 384 bits of cryptographically secure randomness
|
||||
@rand_size 48
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "agent_tokens" do
|
||||
field :token, :string
|
||||
field :name, :string
|
||||
|
|
|
|||
|
|
@ -5,9 +5,7 @@ defmodule Towerops.Alerts.Alert do
|
|||
Tracks device_up, device_down, and monitoring check alerts with
|
||||
acknowledgement status and auto-resolution.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
use Towerops.Schema
|
||||
|
||||
alias Ecto.Association.NotLoaded
|
||||
alias Towerops.Accounts.User
|
||||
|
|
@ -16,8 +14,6 @@ defmodule Towerops.Alerts.Alert do
|
|||
alias Towerops.Monitoring.Check
|
||||
alias Towerops.Organizations.Organization
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "alerts" do
|
||||
# Alert type: device_down, device_up for device alerts
|
||||
# For check alerts, uses string like "check_http", "check_ping", etc.
|
||||
|
|
|
|||
|
|
@ -5,14 +5,10 @@ defmodule Towerops.Alerts.AlertStorm do
|
|||
When > threshold alerts fire within a time window, individual notifications
|
||||
are suppressed and a single summary notification is sent instead.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
use Towerops.Schema
|
||||
|
||||
alias Towerops.Organizations.Organization
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "alert_storms" do
|
||||
field :started_at, :utc_datetime
|
||||
field :ended_at, :utc_datetime
|
||||
|
|
|
|||
|
|
@ -5,17 +5,13 @@ defmodule Towerops.Alerts.NotificationDigest do
|
|||
Tracks how many notifications a user has received within a time window.
|
||||
When the limit is exceeded, additional alerts are queued for digest delivery.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
use Towerops.Schema
|
||||
|
||||
alias Towerops.Accounts.User
|
||||
alias Towerops.Organizations.Organization
|
||||
|
||||
@type t :: %__MODULE__{}
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "notification_digests" do
|
||||
field :window_start, :utc_datetime
|
||||
field :notification_count, :integer, default: 0
|
||||
|
|
|
|||
|
|
@ -6,9 +6,7 @@ defmodule Towerops.Alerts.SiteOutage do
|
|||
they are grouped into a single site outage with one notification
|
||||
instead of N individual notifications.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
use Towerops.Schema
|
||||
|
||||
alias Towerops.Alerts.Alert
|
||||
alias Towerops.Organizations.Organization
|
||||
|
|
@ -16,8 +14,6 @@ defmodule Towerops.Alerts.SiteOutage do
|
|||
|
||||
@type t :: %__MODULE__{}
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "site_outages" do
|
||||
field :started_at, :utc_datetime
|
||||
field :resolved_at, :utc_datetime
|
||||
|
|
|
|||
|
|
@ -8,15 +8,10 @@ defmodule Towerops.ApiTokens.ApiToken do
|
|||
The actual token value is only shown once when created and is then
|
||||
hashed for secure storage.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
use Towerops.Schema
|
||||
|
||||
@type t :: %__MODULE__{}
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
|
||||
schema "api_tokens" do
|
||||
field :name, :string
|
||||
field :token_hash, :string
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ defmodule Towerops.Application do
|
|||
# supervisor may not be ready or may already be dying) cannot exit the boot
|
||||
# process and mask the real error from Supervisor.start_link.
|
||||
defp post_startup do
|
||||
ToweropsWeb.TelemetryFilter.attach()
|
||||
Towerops.LogFilters.attach()
|
||||
|
||||
_ =
|
||||
try do
|
||||
|
|
|
|||
|
|
@ -2,15 +2,10 @@ defmodule Towerops.ConfigChanges.ConfigChangeEvent do
|
|||
@moduledoc """
|
||||
Schema for config change events linking MikroTik backup diffs to time windows.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
use Towerops.Schema
|
||||
|
||||
alias Towerops.Devices.MikrotikBackup
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
|
||||
schema "config_change_events" do
|
||||
field :changed_at, :utc_datetime
|
||||
field :diff_summary, :string
|
||||
|
|
|
|||
|
|
@ -10,9 +10,7 @@ defmodule Towerops.Coverages.Coverage do
|
|||
Multiple coverages can hang off a single site — typically one per sector
|
||||
on a multi-sector tower.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
use Towerops.Schema
|
||||
|
||||
alias Ecto.Association.NotLoaded
|
||||
alias Towerops.Coverages.Antenna
|
||||
|
|
@ -23,8 +21,6 @@ defmodule Towerops.Coverages.Coverage do
|
|||
@statuses ~w(draft queued computing ready failed)
|
||||
@max_axis_pixels 8_000
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "coverages" do
|
||||
field :name, :string
|
||||
field :antenna_slug, :string
|
||||
|
|
|
|||
|
|
@ -2,12 +2,7 @@ defmodule Towerops.Devices.BackupRequest do
|
|||
@moduledoc """
|
||||
Schema for MikroTik device backup request tracking.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
use Towerops.Schema
|
||||
|
||||
schema "device_backup_requests" do
|
||||
field :job_id, :string
|
||||
|
|
|
|||
|
|
@ -8,9 +8,7 @@ defmodule Towerops.Devices.Device do
|
|||
- Agent assignments for remote polling
|
||||
- Monitoring checks and alerts
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
use Towerops.Schema
|
||||
|
||||
alias Ecto.Association.NotLoaded
|
||||
alias Towerops.Agents.AgentAssignment
|
||||
|
|
@ -20,8 +18,6 @@ defmodule Towerops.Devices.Device do
|
|||
alias Towerops.Snmp.Device, as: SnmpDevice
|
||||
alias Towerops.Topology.DeviceLink
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "devices" do
|
||||
field :name, :string
|
||||
field :ip_address, :string
|
||||
|
|
|
|||
|
|
@ -9,12 +9,7 @@ defmodule Towerops.Devices.DeviceFirmwareHistory do
|
|||
The `firmware_releases` table stores the latest available versions from vendors.
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
use Towerops.Schema
|
||||
|
||||
schema "device_firmware_history" do
|
||||
belongs_to :snmp_device, Towerops.Snmp.Device
|
||||
|
|
|
|||
|
|
@ -2,9 +2,7 @@ defmodule Towerops.Devices.Event do
|
|||
@moduledoc """
|
||||
Device event schema for tracking changes and incidents.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
use Towerops.Schema
|
||||
|
||||
@type severity :: :info | :warning | :critical
|
||||
|
||||
|
|
@ -24,9 +22,6 @@ defmodule Towerops.Devices.Event do
|
|||
| :sensor_value_drop
|
||||
| :sensor_value_changed
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
|
||||
schema "device_events" do
|
||||
field :event_type, :string
|
||||
field :severity, :string
|
||||
|
|
|
|||
|
|
@ -8,12 +8,7 @@ defmodule Towerops.Devices.FirmwareRelease do
|
|||
The `device_firmware_history` table tracks when individual devices change versions.
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
use Towerops.Schema
|
||||
|
||||
schema "firmware_releases" do
|
||||
field :vendor, :string
|
||||
|
|
|
|||
|
|
@ -2,12 +2,7 @@ defmodule Towerops.Devices.MikrotikBackup do
|
|||
@moduledoc """
|
||||
Schema for MikroTik device configuration backups.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
use Towerops.Schema
|
||||
|
||||
schema "device_mikrotik_backups" do
|
||||
field :config_compressed, :binary
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ defmodule Towerops.ErrorTrackerIgnorer do
|
|||
Drops benign errors from ErrorTracker that occur during K8s pod shutdown
|
||||
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
|
||||
|
|
|
|||
|
|
@ -2,15 +2,11 @@ defmodule Towerops.Gaiia.Account do
|
|||
@moduledoc """
|
||||
Schema for subscriber accounts synced from Gaiia API.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
use Towerops.Gaiia.BaseSchema
|
||||
|
||||
schema "gaiia_accounts" do
|
||||
field :gaiia_id, :string
|
||||
gaiia_base_fields()
|
||||
|
||||
field :readable_id, :string
|
||||
field :name, :string
|
||||
field :status, :string
|
||||
|
|
@ -18,11 +14,6 @@ defmodule Towerops.Gaiia.Account do
|
|||
field :address, :map
|
||||
field :subscription_count, :integer
|
||||
field :mrr, :decimal
|
||||
field :raw_data, :map
|
||||
|
||||
belongs_to :organization, Towerops.Organizations.Organization
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
|
||||
def changeset(account, attrs) do
|
||||
|
|
@ -39,11 +30,6 @@ defmodule Towerops.Gaiia.Account do
|
|||
:mrr,
|
||||
:raw_data
|
||||
])
|
||||
|> 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)
|
||||
|> validate_gaiia_uniqueness()
|
||||
end
|
||||
end
|
||||
|
|
|
|||
56
lib/towerops/gaiia/base_schema.ex
Normal file
56
lib/towerops/gaiia/base_schema.ex
Normal 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
|
||||
|
|
@ -2,15 +2,11 @@ defmodule Towerops.Gaiia.BillingSubscription do
|
|||
@moduledoc """
|
||||
Schema for billing subscriptions synced from Gaiia API.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
use Towerops.Gaiia.BaseSchema
|
||||
|
||||
schema "gaiia_billing_subscriptions" do
|
||||
field :gaiia_id, :string
|
||||
gaiia_base_fields()
|
||||
|
||||
field :account_gaiia_id, :string
|
||||
field :status, :string
|
||||
field :product_name, :string
|
||||
|
|
@ -18,11 +14,6 @@ defmodule Towerops.Gaiia.BillingSubscription do
|
|||
field :currency, :string
|
||||
field :speed_download, :integer
|
||||
field :speed_upload, :integer
|
||||
field :raw_data, :map
|
||||
|
||||
belongs_to :organization, Towerops.Organizations.Organization
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
|
||||
def changeset(billing_subscription, attrs) do
|
||||
|
|
@ -39,11 +30,6 @@ defmodule Towerops.Gaiia.BillingSubscription do
|
|||
:speed_upload,
|
||||
:raw_data
|
||||
])
|
||||
|> 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)
|
||||
|> validate_gaiia_uniqueness()
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,12 +5,7 @@ defmodule Towerops.Gaiia.DeviceSubscriberLink do
|
|||
Links a Gaiia subscriber account to a TowerOps device based on
|
||||
ARP table matching or site-level fallback.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
use Towerops.Schema
|
||||
|
||||
schema "device_subscriber_links" do
|
||||
field :match_method, :string
|
||||
|
|
|
|||
|
|
@ -2,15 +2,11 @@ defmodule Towerops.Gaiia.InventoryItem do
|
|||
@moduledoc """
|
||||
Schema for inventory items (deployed equipment) synced from Gaiia API.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
use Towerops.Gaiia.BaseSchema
|
||||
|
||||
schema "gaiia_inventory_items" do
|
||||
field :gaiia_id, :string
|
||||
gaiia_base_fields()
|
||||
|
||||
field :name, :string
|
||||
field :status, :string
|
||||
field :serial_number, :string
|
||||
|
|
@ -21,12 +17,8 @@ defmodule Towerops.Gaiia.InventoryItem do
|
|||
field :category, :string
|
||||
field :assigned_account_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
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
|
||||
def changeset(inventory_item, attrs) do
|
||||
|
|
@ -47,12 +39,7 @@ defmodule Towerops.Gaiia.InventoryItem do
|
|||
:device_id,
|
||||
:raw_data
|
||||
])
|
||||
|> 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)
|
||||
|> validate_gaiia_uniqueness()
|
||||
|> foreign_key_constraint(:device_id)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,26 +2,18 @@ defmodule Towerops.Gaiia.NetworkSite do
|
|||
@moduledoc """
|
||||
Schema for network sites (towers/POPs) synced from Gaiia API.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
use Towerops.Gaiia.BaseSchema
|
||||
|
||||
schema "gaiia_network_sites" do
|
||||
field :gaiia_id, :string
|
||||
gaiia_base_fields()
|
||||
|
||||
field :name, :string
|
||||
field :address, :map
|
||||
field :ip_blocks, {:array, :string}, default: []
|
||||
field :account_count, :integer
|
||||
field :total_mrr, :decimal
|
||||
field :raw_data, :map
|
||||
|
||||
belongs_to :organization, Towerops.Organizations.Organization
|
||||
belongs_to :site, Towerops.Sites.Site
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
|
||||
def changeset(network_site, attrs) do
|
||||
|
|
@ -37,12 +29,7 @@ defmodule Towerops.Gaiia.NetworkSite do
|
|||
:site_id,
|
||||
:raw_data
|
||||
])
|
||||
|> 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)
|
||||
|> validate_gaiia_uniqueness()
|
||||
|> foreign_key_constraint(:site_id)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,11 +3,8 @@ defmodule Towerops.GeoIP.Block do
|
|||
Schema for GeoIP IP block data (CIDR ranges mapped to locations).
|
||||
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
|
||||
field :network, :string
|
||||
field :start_ip_int, :integer
|
||||
|
|
|
|||
|
|
@ -5,15 +5,10 @@ defmodule Towerops.Integrations.Integration do
|
|||
Each organization can have one integration per provider. Credentials
|
||||
are encrypted at rest using Cloak AES-256-GCM.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
use Towerops.Schema
|
||||
|
||||
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_sync_statuses ~w(never success partial failed)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,7 @@ defmodule Towerops.Lidar.SyncLog do
|
|||
@moduledoc """
|
||||
Audit log for LIDAR catalog sync operations.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
use Towerops.Schema
|
||||
|
||||
@valid_statuses ~w(success partial failed)
|
||||
@valid_sources ~w(3dep tnris)
|
||||
|
|
|
|||
|
|
@ -5,12 +5,7 @@ defmodule Towerops.Lidar.Tile do
|
|||
We do not store raster bytes — only the public HTTPS URL and footprint
|
||||
metadata. Reads happen on-demand via GDAL's `/vsicurl/` driver.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
use Towerops.Schema
|
||||
|
||||
@valid_sources ~w(3dep tnris)
|
||||
@valid_availability ~w(available missing error no_cog)
|
||||
|
|
|
|||
|
|
@ -19,16 +19,12 @@ defmodule Towerops.LLM.Usage do
|
|||
GROUP BY 1, 2, 3, 4
|
||||
ORDER BY 1 DESC;
|
||||
"""
|
||||
use Ecto.Schema
|
||||
use Towerops.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
import Ecto.Query
|
||||
|
||||
alias Towerops.Repo
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
|
||||
schema "llm_usage" do
|
||||
field :day, :date
|
||||
field :model, :string
|
||||
|
|
|
|||
|
|
@ -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
185
lib/towerops/log_filters.ex
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
@ -7,17 +7,13 @@ defmodule Towerops.Maintenance.MaintenanceWindow do
|
|||
- All devices at a site (site_id set, device_id nil)
|
||||
- All devices in an org (both nil)
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
use Towerops.Schema
|
||||
|
||||
alias Towerops.Accounts.User
|
||||
alias Towerops.Devices.Device
|
||||
alias Towerops.Organizations.Organization
|
||||
alias Towerops.Sites.Site
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "maintenance_windows" do
|
||||
field :name, :string
|
||||
field :reason, :string
|
||||
|
|
|
|||
|
|
@ -7,12 +7,7 @@ defmodule Towerops.Mikrotik.Event do
|
|||
these rows after the fact — historical timeline of every IP assignment event.
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
use Towerops.Schema
|
||||
|
||||
@sources ~w(dhcp pppoe)
|
||||
@event_types ~w(assign release)
|
||||
|
|
|
|||
|
|
@ -8,12 +8,7 @@ defmodule Towerops.Mikrotik.IpAssignment do
|
|||
"which customer is using this IP right now?".
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
use Towerops.Schema
|
||||
|
||||
@sources ~w(dhcp pppoe)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,12 +5,7 @@ defmodule Towerops.MobileSessions.MobileSession do
|
|||
Long-lived authentication tokens for mobile apps. Users can manage
|
||||
their active mobile sessions from the web interface.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
use Towerops.Schema
|
||||
|
||||
schema "mobile_sessions" do
|
||||
field :token, :string
|
||||
|
|
|
|||
|
|
@ -6,12 +6,7 @@ defmodule Towerops.MobileSessions.QRLoginToken do
|
|||
in the web interface. Mobile apps scan the QR code and use the token
|
||||
to create a new mobile session.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
use Towerops.Schema
|
||||
|
||||
schema "qr_login_tokens" do
|
||||
field :token, :string
|
||||
|
|
|
|||
|
|
@ -12,9 +12,7 @@ defmodule Towerops.Monitoring.Check do
|
|||
- Soft/hard state transitions
|
||||
- Optional device association (can be standalone)
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
use Towerops.Schema
|
||||
|
||||
alias Towerops.Agents.AgentToken
|
||||
alias Towerops.Devices.Device
|
||||
|
|
@ -22,9 +20,6 @@ defmodule Towerops.Monitoring.Check do
|
|||
|
||||
@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)
|
||||
|
||||
schema "checks" do
|
||||
|
|
|
|||
|
|
@ -4,17 +4,12 @@ defmodule Towerops.Monitoring.CheckResult do
|
|||
|
||||
Results are time-series data showing check status over time.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
use Towerops.Schema
|
||||
|
||||
alias Towerops.Agents.AgentToken
|
||||
alias Towerops.Monitoring.Check
|
||||
alias Towerops.Organizations.Organization
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
|
||||
schema "check_results" do
|
||||
field :checked_at, :utc_datetime
|
||||
field :status, :integer
|
||||
|
|
|
|||
|
|
@ -6,16 +6,11 @@ defmodule Towerops.Monitoring.MonitoringCheck do
|
|||
Each record captures the device status, response time, and which agent
|
||||
performed the check.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
use Towerops.Schema
|
||||
|
||||
alias Towerops.Agents.AgentToken
|
||||
alias Towerops.Devices.Device
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
|
||||
schema "monitoring_checks" do
|
||||
field :status, :string
|
||||
field :response_time_ms, :float
|
||||
|
|
|
|||
25
lib/towerops/on_call/changeset_helpers.ex
Normal file
25
lib/towerops/on_call/changeset_helpers.ex
Normal 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
|
||||
|
|
@ -2,15 +2,13 @@ defmodule Towerops.OnCall.EscalationPolicy do
|
|||
@moduledoc """
|
||||
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.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)
|
||||
|
||||
schema "escalation_policies" do
|
||||
|
|
@ -31,8 +29,7 @@ defmodule Towerops.OnCall.EscalationPolicy do
|
|||
|
||||
def changeset(policy, attrs) do
|
||||
policy
|
||||
|> cast(attrs, @required_fields ++ @optional_fields)
|
||||
|> validate_required(@required_fields)
|
||||
|> required_first(attrs, @required_fields, @optional_fields)
|
||||
|> validate_number(:repeat_count, greater_than: 0)
|
||||
|> validate_inclusion(:handoff_notifications_mode, @valid_handoff_modes)
|
||||
|> foreign_key_constraint(:organization_id)
|
||||
|
|
|
|||
|
|
@ -2,15 +2,13 @@ defmodule Towerops.OnCall.EscalationRule do
|
|||
@moduledoc """
|
||||
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.EscalationTarget
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "escalation_rules" do
|
||||
field :position, :integer, default: 0
|
||||
field :timeout_minutes, :integer, default: 30
|
||||
|
|
@ -26,8 +24,7 @@ defmodule Towerops.OnCall.EscalationRule do
|
|||
|
||||
def changeset(rule, attrs) do
|
||||
rule
|
||||
|> cast(attrs, @required_fields)
|
||||
|> validate_required(@required_fields)
|
||||
|> required_first(attrs, @required_fields)
|
||||
|> validate_number(:timeout_minutes, greater_than: 0)
|
||||
|> validate_number(:position, greater_than_or_equal_to: 0)
|
||||
|> foreign_key_constraint(:escalation_policy_id)
|
||||
|
|
|
|||
|
|
@ -2,16 +2,14 @@ defmodule Towerops.OnCall.EscalationTarget do
|
|||
@moduledoc """
|
||||
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.OnCall.EscalationRule
|
||||
alias Towerops.OnCall.Schedule
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "escalation_targets" do
|
||||
field :target_type, :string
|
||||
|
||||
|
|
@ -29,8 +27,7 @@ defmodule Towerops.OnCall.EscalationTarget do
|
|||
|
||||
def changeset(target, attrs) do
|
||||
target
|
||||
|> cast(attrs, @required_fields ++ @optional_fields)
|
||||
|> validate_required(@required_fields)
|
||||
|> required_first(attrs, @required_fields, @optional_fields)
|
||||
|> validate_inclusion(:target_type, @valid_target_types)
|
||||
|> foreign_key_constraint(:escalation_rule_id)
|
||||
|> foreign_key_constraint(:schedule_id)
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ defmodule Towerops.OnCall.Incident do
|
|||
@moduledoc """
|
||||
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.Alerts.Alert
|
||||
|
|
@ -12,8 +12,6 @@ defmodule Towerops.OnCall.Incident do
|
|||
alias Towerops.OnCall.Notification
|
||||
alias Towerops.Organizations.Organization
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "on_call_incidents" do
|
||||
field :status, :string, default: "triggered"
|
||||
field :current_rule_position, :integer, default: 0
|
||||
|
|
@ -40,8 +38,7 @@ defmodule Towerops.OnCall.Incident do
|
|||
|
||||
def changeset(incident, attrs) do
|
||||
incident
|
||||
|> cast(attrs, @required_fields ++ @optional_fields)
|
||||
|> validate_required(@required_fields)
|
||||
|> required_first(attrs, @required_fields, @optional_fields)
|
||||
|> validate_inclusion(:status, @valid_statuses)
|
||||
|> foreign_key_constraint(:alert_id)
|
||||
|> foreign_key_constraint(:escalation_policy_id)
|
||||
|
|
|
|||
|
|
@ -4,15 +4,13 @@ defmodule Towerops.OnCall.Layer do
|
|||
|
||||
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.Schedule
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "on_call_layers" do
|
||||
field :name, :string
|
||||
field :position, :integer, default: 0
|
||||
|
|
@ -39,8 +37,7 @@ defmodule Towerops.OnCall.Layer do
|
|||
|
||||
def changeset(layer, attrs) do
|
||||
layer
|
||||
|> cast(attrs, @required_fields ++ @optional_fields)
|
||||
|> validate_required(@required_fields)
|
||||
|> required_first(attrs, @required_fields, @optional_fields)
|
||||
|> validate_inclusion(:rotation_type, @valid_rotation_types)
|
||||
|> validate_inclusion(:restriction_type, @valid_restriction_types)
|
||||
|> validate_number(:rotation_interval, greater_than: 0)
|
||||
|
|
|
|||
|
|
@ -2,15 +2,13 @@ defmodule Towerops.OnCall.LayerMember do
|
|||
@moduledoc """
|
||||
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.OnCall.Layer
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "on_call_layer_members" do
|
||||
field :position, :integer, default: 0
|
||||
|
||||
|
|
@ -24,8 +22,7 @@ defmodule Towerops.OnCall.LayerMember do
|
|||
|
||||
def changeset(member, attrs) do
|
||||
member
|
||||
|> cast(attrs, @required_fields)
|
||||
|> validate_required(@required_fields)
|
||||
|> required_first(attrs, @required_fields)
|
||||
|> validate_number(:position, greater_than_or_equal_to: 0)
|
||||
|> foreign_key_constraint(:layer_id)
|
||||
|> foreign_key_constraint(:user_id)
|
||||
|
|
|
|||
|
|
@ -2,15 +2,13 @@ defmodule Towerops.OnCall.Notification do
|
|||
@moduledoc """
|
||||
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.OnCall.Incident
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "on_call_notifications" do
|
||||
field :channel, :string, default: "email"
|
||||
field :sent_at, :utc_datetime
|
||||
|
|
@ -28,8 +26,7 @@ defmodule Towerops.OnCall.Notification do
|
|||
|
||||
def changeset(notification, attrs) do
|
||||
notification
|
||||
|> cast(attrs, @required_fields ++ @optional_fields)
|
||||
|> validate_required(@required_fields)
|
||||
|> required_first(attrs, @required_fields, @optional_fields)
|
||||
|> validate_inclusion(:channel, ~w(email))
|
||||
|> validate_inclusion(:status, ~w(sent failed))
|
||||
|> foreign_key_constraint(:incident_id)
|
||||
|
|
|
|||
|
|
@ -2,15 +2,13 @@ defmodule Towerops.OnCall.Override do
|
|||
@moduledoc """
|
||||
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.OnCall.Schedule
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "on_call_overrides" do
|
||||
field :start_time, :utc_datetime
|
||||
field :end_time, :utc_datetime
|
||||
|
|
@ -27,8 +25,7 @@ defmodule Towerops.OnCall.Override do
|
|||
|
||||
def changeset(override, attrs) do
|
||||
override
|
||||
|> cast(attrs, @required_fields ++ @optional_fields)
|
||||
|> validate_required(@required_fields)
|
||||
|> required_first(attrs, @required_fields, @optional_fields)
|
||||
|> validate_end_after_start()
|
||||
|> foreign_key_constraint(:schedule_id)
|
||||
|> foreign_key_constraint(:user_id)
|
||||
|
|
|
|||
|
|
@ -2,18 +2,15 @@ defmodule Towerops.OnCall.Schedule do
|
|||
@moduledoc """
|
||||
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.Override
|
||||
alias Towerops.Organizations.Organization
|
||||
|
||||
@type t :: %__MODULE__{}
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "on_call_schedules" do
|
||||
field :name, :string
|
||||
field :description, :string
|
||||
|
|
@ -32,8 +29,7 @@ defmodule Towerops.OnCall.Schedule do
|
|||
|
||||
def changeset(schedule, attrs) do
|
||||
schedule
|
||||
|> cast(attrs, @required_fields ++ @optional_fields)
|
||||
|> validate_required(@required_fields)
|
||||
|> required_first(attrs, @required_fields, @optional_fields)
|
||||
|> foreign_key_constraint(:organization_id)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,16 +4,12 @@ defmodule Towerops.Organizations.Invitation do
|
|||
|
||||
Tracks invitation tokens, email, role, expiration, and acceptance status.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
use Towerops.Schema
|
||||
|
||||
alias Ecto.Association.NotLoaded
|
||||
alias Towerops.Accounts.User
|
||||
alias Towerops.Organizations.Organization
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "organization_invitations" do
|
||||
field :email, :string
|
||||
field :role, Ecto.Enum, values: [:admin, :executive, :technician, :member, :viewer]
|
||||
|
|
|
|||
|
|
@ -11,16 +11,12 @@ defmodule Towerops.Organizations.Membership do
|
|||
- `:technician` — Field ops: can manage devices, sites, alerts, but no financial data
|
||||
- `:viewer` — Read-only, no financial data
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
use Towerops.Schema
|
||||
|
||||
alias Ecto.Association.NotLoaded
|
||||
alias Towerops.Accounts.User
|
||||
alias Towerops.Organizations.Organization
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "organization_memberships" do
|
||||
field :role, Ecto.Enum, values: [:owner, :admin, :executive, :technician, :member, :viewer]
|
||||
field :is_default, :boolean, default: false
|
||||
|
|
|
|||
|
|
@ -9,9 +9,7 @@ defmodule Towerops.Organizations.Organization do
|
|||
- Default SNMP configuration
|
||||
- Default agent token for polling
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
use Towerops.Schema
|
||||
|
||||
alias Ecto.Association.NotLoaded
|
||||
alias Towerops.Agents.AgentToken
|
||||
|
|
@ -19,8 +17,6 @@ defmodule Towerops.Organizations.Organization do
|
|||
alias Towerops.Organizations.Invitation
|
||||
alias Towerops.Organizations.Membership
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "organizations" do
|
||||
field :name, :string
|
||||
field :slug, :string
|
||||
|
|
|
|||
|
|
@ -2,12 +2,7 @@ defmodule Towerops.Preseem.AccessPoint do
|
|||
@moduledoc """
|
||||
Schema for access points synced from Preseem API.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
use Towerops.Schema
|
||||
|
||||
@valid_match_confidences ~w(auto_mac auto_ip auto_hostname manual ambiguous unmatched)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,12 +3,7 @@ defmodule Towerops.Preseem.DeviceBaseline do
|
|||
Rolling 14-day baseline for individual Preseem access points.
|
||||
Computed nightly to detect deviations from normal behavior.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
use Towerops.Schema
|
||||
|
||||
@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)
|
||||
|
|
|
|||
|
|
@ -3,12 +3,7 @@ defmodule Towerops.Preseem.FleetProfile do
|
|||
Fleet-wide model performance profiles. Groups devices by manufacturer+model
|
||||
to identify patterns, capacity ceilings, and firmware correlations.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
use Towerops.Schema
|
||||
|
||||
schema "preseem_fleet_profiles" do
|
||||
field :manufacturer, :string
|
||||
|
|
|
|||
|
|
@ -3,12 +3,7 @@ defmodule Towerops.Preseem.Insight do
|
|||
Generated insights from Preseem data analysis - deviations, capacity warnings,
|
||||
firmware recommendations, and performance observations.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
use Towerops.Schema
|
||||
|
||||
@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)
|
||||
|
|
|
|||
|
|
@ -2,12 +2,7 @@ defmodule Towerops.Preseem.SubscriberMetric do
|
|||
@moduledoc """
|
||||
Time-series aggregate QoE metrics per Preseem access point.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
use Towerops.Schema
|
||||
|
||||
schema "preseem_subscriber_metrics" do
|
||||
field :avg_latency, :float
|
||||
|
|
|
|||
|
|
@ -2,40 +2,11 @@ defmodule Towerops.Preseem.SyncLog do
|
|||
@moduledoc """
|
||||
Audit log for Preseem sync operations.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
use Towerops.SyncLog, table_name: "preseem_sync_logs"
|
||||
|
||||
@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
|
||||
sync_log
|
||||
|> 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)
|
||||
sync_log_changeset(sync_log, attrs, @valid_statuses)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,15 +4,10 @@ defmodule Towerops.Profiles.DeviceOid do
|
|||
Maps device info fields (serial_number, firmware_version, etc.) to MIB names.
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
use Towerops.Schema
|
||||
|
||||
alias Towerops.Profiles.DeviceProfile
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
|
||||
schema "profile_device_oids" do
|
||||
field :field, :string
|
||||
field :mib_name, :string
|
||||
|
|
|
|||
|
|
@ -4,16 +4,11 @@ defmodule Towerops.Profiles.DeviceProfile do
|
|||
Profiles define how to identify and discover sensors for different device types.
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
use Towerops.Schema
|
||||
|
||||
alias Towerops.Profiles.DeviceOid
|
||||
alias Towerops.Profiles.SensorOid
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
|
||||
schema "device_profiles" do
|
||||
field :name, :string
|
||||
field :vendor, :string
|
||||
|
|
|
|||
|
|
@ -4,15 +4,10 @@ defmodule Towerops.Profiles.SensorOid do
|
|||
Maps sensor types to MIB names for sensor discovery.
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
use Towerops.Schema
|
||||
|
||||
alias Towerops.Profiles.DeviceProfile
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
|
||||
schema "profile_sensor_oids" do
|
||||
field :sensor_type, :string
|
||||
field :mib_name, :string
|
||||
|
|
|
|||
|
|
@ -6,12 +6,7 @@ defmodule Towerops.Reports.Report do
|
|||
Schedule types: one_time, daily, weekly, monthly.
|
||||
Formats: csv (PDF support planned).
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
use Towerops.Schema
|
||||
|
||||
@valid_report_types ~w(uptime_summary alert_history capacity_trends rf_link_health)
|
||||
@valid_formats ~w(csv)
|
||||
|
|
|
|||
36
lib/towerops/schema.ex
Normal file
36
lib/towerops/schema.ex
Normal 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
|
||||
|
|
@ -11,12 +11,7 @@ defmodule Towerops.Security.IpBlock do
|
|||
Offense count resets to 1 if 30 days pass without violations.
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
use Towerops.Schema
|
||||
|
||||
schema "ip_blocks" do
|
||||
field :ip_address, :string
|
||||
|
|
|
|||
|
|
@ -3,12 +3,7 @@ defmodule Towerops.Security.IpWhitelist do
|
|||
Schema for tracking IP addresses and CIDR ranges that are exempt from brute force protection.
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
use Towerops.Schema
|
||||
|
||||
schema "ip_whitelist" do
|
||||
field :ip_or_cidr, :string
|
||||
|
|
|
|||
|
|
@ -5,14 +5,10 @@ defmodule Towerops.Settings.ApplicationSetting do
|
|||
Stores global configuration values that affect the entire application,
|
||||
regardless of organization. Only accessible by superadmins.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
use Towerops.Schema
|
||||
|
||||
require Logger
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "application_settings" do
|
||||
field :key, :string
|
||||
field :value, :string
|
||||
|
|
|
|||
|
|
@ -4,9 +4,7 @@ defmodule Towerops.Sites.Site do
|
|||
|
||||
Sites contain devices and can have site-level SNMP configuration defaults.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
use Towerops.Schema
|
||||
|
||||
alias Ecto.Association.NotLoaded
|
||||
alias Towerops.Agents.AgentToken
|
||||
|
|
@ -15,8 +13,6 @@ defmodule Towerops.Sites.Site do
|
|||
alias Towerops.Organizations.Organization
|
||||
alias Towerops.Sites.Site
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "sites" do
|
||||
field :name, :string
|
||||
field :description, :string
|
||||
|
|
|
|||
|
|
@ -4,12 +4,8 @@ defmodule Towerops.Snmp.ArpEntry do
|
|||
|
||||
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
|
||||
field :ip_address, :string
|
||||
field :mac_address, :string
|
||||
|
|
|
|||
|
|
@ -5,9 +5,7 @@ defmodule Towerops.Snmp.Device do
|
|||
Stores system details (sysDescr, sysObjectID), vendor information,
|
||||
and relationships to interfaces, sensors, and neighbors.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
use Towerops.Schema
|
||||
|
||||
alias Ecto.Association.NotLoaded
|
||||
alias Towerops.Devices.Device, as: DeviceSchema
|
||||
|
|
@ -20,8 +18,6 @@ defmodule Towerops.Snmp.Device do
|
|||
alias Towerops.Snmp.Storage
|
||||
alias Towerops.Snmp.Transceiver
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "snmp_devices" do
|
||||
field :sys_descr, :string
|
||||
field :sys_object_id, :string
|
||||
|
|
|
|||
|
|
@ -6,15 +6,11 @@ defmodule Towerops.Snmp.EntityPhysical do
|
|||
including chassis, modules, ports, power supplies, fans, and sensors.
|
||||
Supports hierarchical parent/child relationships for physical containment.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
use Towerops.Schema
|
||||
|
||||
alias Ecto.Association.NotLoaded
|
||||
alias Towerops.Snmp.Device
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "snmp_entity_physical" do
|
||||
field :entity_index, :string
|
||||
field :parent_index, :string
|
||||
|
|
|
|||
|
|
@ -7,14 +7,12 @@ defmodule Towerops.Snmp.EntityPhysicalReading do
|
|||
and administrative status (is this component enabled?) for power supplies, fans,
|
||||
modules, and other physical entities.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
use Towerops.Snmp.Reading
|
||||
|
||||
alias Towerops.Snmp.EntityPhysical
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
@timestamps_opts [type: :utc_datetime]
|
||||
|
||||
schema "snmp_entity_physical_readings" do
|
||||
field :operational_status, :string
|
||||
field :admin_status, :string
|
||||
|
|
@ -22,7 +20,7 @@ defmodule Towerops.Snmp.EntityPhysicalReading do
|
|||
|
||||
belongs_to :entity_physical, EntityPhysical
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
|
|
@ -66,15 +64,12 @@ defmodule Towerops.Snmp.EntityPhysicalReading do
|
|||
"""
|
||||
def changeset(reading, attrs) do
|
||||
reading
|
||||
|> cast(attrs, [
|
||||
:entity_physical_id,
|
||||
:operational_status,
|
||||
:admin_status,
|
||||
:measured_at
|
||||
])
|
||||
|> validate_required([:entity_physical_id, :measured_at])
|
||||
|> reading_changeset(attrs,
|
||||
parent_key: :entity_physical_id,
|
||||
data_fields: [:operational_status, :admin_status],
|
||||
timestamp_field: :measured_at
|
||||
)
|
||||
|> validate_inclusion(:operational_status, @operational_statuses)
|
||||
|> validate_inclusion(:admin_status, @admin_statuses)
|
||||
|> foreign_key_constraint(:entity_physical_id)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,9 +5,7 @@ defmodule Towerops.Snmp.Interface do
|
|||
Stores interface details (name, description, type, speed, MAC address)
|
||||
and has many interface statistics.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
use Towerops.Schema
|
||||
|
||||
alias Ecto.Association.NotLoaded
|
||||
alias Towerops.Snmp.Device
|
||||
|
|
@ -15,8 +13,6 @@ defmodule Towerops.Snmp.Interface do
|
|||
alias Towerops.Snmp.IpAddress
|
||||
alias Towerops.Snmp.Neighbor
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "snmp_interfaces" do
|
||||
field :if_index, :integer
|
||||
field :if_name, :string
|
||||
|
|
|
|||
|
|
@ -5,14 +5,10 @@ defmodule Towerops.Snmp.InterfaceStat do
|
|||
Stores interface counter values (octets, errors, discards) collected
|
||||
during SNMP polling. Stored in TimescaleDB hypertable.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
use Towerops.Snmp.Reading
|
||||
|
||||
alias Towerops.Snmp.Interface
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "snmp_interface_stats" do
|
||||
field :if_in_octets, :integer
|
||||
field :if_out_octets, :integer
|
||||
|
|
@ -25,7 +21,7 @@ defmodule Towerops.Snmp.InterfaceStat do
|
|||
|
||||
belongs_to :interface, Interface
|
||||
|
||||
timestamps(type: :utc_datetime, updated_at: false)
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
|
|
@ -45,19 +41,17 @@ defmodule Towerops.Snmp.InterfaceStat do
|
|||
|
||||
@doc false
|
||||
def changeset(stat, attrs) do
|
||||
stat
|
||||
|> cast(attrs, [
|
||||
:interface_id,
|
||||
:if_in_octets,
|
||||
:if_out_octets,
|
||||
:if_in_errors,
|
||||
:if_out_errors,
|
||||
:if_in_discards,
|
||||
:if_out_discards,
|
||||
:checked_at,
|
||||
:is_hc
|
||||
])
|
||||
|> validate_required([:interface_id, :checked_at])
|
||||
|> foreign_key_constraint(:interface_id)
|
||||
reading_changeset(stat, attrs,
|
||||
parent_key: :interface_id,
|
||||
data_fields: [
|
||||
:if_in_octets,
|
||||
:if_out_octets,
|
||||
:if_in_errors,
|
||||
:if_out_errors,
|
||||
:if_in_discards,
|
||||
:if_out_discards,
|
||||
:is_hc
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -8,17 +8,13 @@ defmodule Towerops.Snmp.IpAddress do
|
|||
|
||||
Each interface can have multiple IP addresses (primary and secondary/alias).
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
use Towerops.Schema
|
||||
|
||||
alias Ecto.Association.NotLoaded
|
||||
alias Towerops.Snmp.Interface
|
||||
|
||||
@valid_ip_types ~w(ipv4 ipv6)
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "snmp_ip_addresses" do
|
||||
field :ip_address, :string
|
||||
field :subnet_mask, :string
|
||||
|
|
|
|||
|
|
@ -6,12 +6,8 @@ defmodule Towerops.Snmp.MacAddress do
|
|||
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).
|
||||
"""
|
||||
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
|
||||
field :mac_address, :string
|
||||
field :vlan_id, :integer
|
||||
|
|
|
|||
|
|
@ -8,17 +8,13 @@ defmodule Towerops.Snmp.MemoryPool do
|
|||
|
||||
Tracks total and used memory for capacity monitoring.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
use Towerops.Schema
|
||||
|
||||
alias Ecto.Association.NotLoaded
|
||||
alias Towerops.Snmp.Device
|
||||
|
||||
@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
|
||||
field :pool_index, :string
|
||||
field :pool_name, :string
|
||||
|
|
|
|||
|
|
@ -12,17 +12,13 @@ defmodule Towerops.Snmp.Mempool do
|
|||
- "cisco_memory" - Cisco memory pools from CISCO-MEMORY-POOL-MIB
|
||||
- "ucd_memory" - UCD-SNMP-MIB memory statistics
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
use Towerops.Schema
|
||||
|
||||
alias Ecto.Association.NotLoaded
|
||||
alias Towerops.Snmp.Device
|
||||
|
||||
@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
|
||||
field :mempool_index, :string
|
||||
field :description, :string
|
||||
|
|
|
|||
|
|
@ -5,14 +5,10 @@ defmodule Towerops.Snmp.MempoolReading do
|
|||
Stores individual memory pool readings (used/total/free bytes, usage percent, timestamp)
|
||||
collected during SNMP polling cycles.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
use Towerops.Snmp.Reading
|
||||
|
||||
alias Towerops.Snmp.Mempool
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "snmp_mempool_readings" do
|
||||
field :used_bytes, :integer
|
||||
field :total_bytes, :integer
|
||||
|
|
@ -22,7 +18,7 @@ defmodule Towerops.Snmp.MempoolReading do
|
|||
|
||||
belongs_to :mempool, Mempool
|
||||
|
||||
timestamps(type: :utc_datetime, updated_at: false)
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
|
|
@ -39,9 +35,9 @@ defmodule Towerops.Snmp.MempoolReading do
|
|||
|
||||
@doc false
|
||||
def changeset(reading, attrs) do
|
||||
reading
|
||||
|> cast(attrs, [:mempool_id, :used_bytes, :total_bytes, :free_bytes, :usage_percent, :checked_at])
|
||||
|> validate_required([:mempool_id, :checked_at])
|
||||
|> foreign_key_constraint(:mempool_id)
|
||||
reading_changeset(reading, attrs,
|
||||
parent_key: :mempool_id,
|
||||
data_fields: [:used_bytes, :total_bytes, :free_bytes, :usage_percent]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,12 +5,8 @@ defmodule Towerops.Snmp.Neighbor do
|
|||
Stores neighbor information (chassis ID, port ID, system name)
|
||||
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
|
||||
field :protocol, :string
|
||||
field :remote_chassis_id, :string
|
||||
|
|
|
|||
|
|
@ -12,17 +12,13 @@ defmodule Towerops.Snmp.PhysicalEntity do
|
|||
|
||||
Entities form a parent/child hierarchy via contained_in relationships.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
use Towerops.Schema
|
||||
|
||||
alias Ecto.Association.NotLoaded
|
||||
alias Towerops.Snmp.Device
|
||||
|
||||
@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
|
||||
field :entity_index, :integer
|
||||
field :entity_class, :string
|
||||
|
|
|
|||
|
|
@ -1,14 +1,9 @@
|
|||
defmodule Towerops.Snmp.PrinterSupply do
|
||||
@moduledoc false
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
use Towerops.Schema
|
||||
|
||||
alias Towerops.Devices.Device
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
|
||||
schema "printer_supplies" do
|
||||
belongs_to :snmp_device, Device
|
||||
|
||||
|
|
|
|||
|
|
@ -12,17 +12,13 @@ defmodule Towerops.Snmp.Processor do
|
|||
- "cisco_cpu" - Cisco CPU from CISCO-PROCESS-MIB
|
||||
- "ucd_cpu" - UCD-SNMP-MIB CPU statistics
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
use Towerops.Schema
|
||||
|
||||
alias Ecto.Association.NotLoaded
|
||||
alias Towerops.Snmp.Device
|
||||
|
||||
@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
|
||||
field :processor_index, :string
|
||||
field :description, :string
|
||||
|
|
|
|||
|
|
@ -5,14 +5,10 @@ defmodule Towerops.Snmp.ProcessorReading do
|
|||
Stores individual processor load readings collected during SNMP polling cycles.
|
||||
Similar to SensorReading but specifically for processor/CPU load data.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
use Towerops.Snmp.Reading
|
||||
|
||||
alias Towerops.Snmp.Processor
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "snmp_processor_readings" do
|
||||
field :load_percent, :float
|
||||
field :status, :string
|
||||
|
|
@ -20,7 +16,7 @@ defmodule Towerops.Snmp.ProcessorReading do
|
|||
|
||||
belongs_to :processor, Processor
|
||||
|
||||
timestamps(type: :utc_datetime, updated_at: false)
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
|
|
@ -36,9 +32,11 @@ defmodule Towerops.Snmp.ProcessorReading do
|
|||
@doc false
|
||||
def changeset(reading, attrs) do
|
||||
reading
|
||||
|> cast(attrs, [:processor_id, :load_percent, :status, :checked_at])
|
||||
|> validate_required([:processor_id, :status, :checked_at])
|
||||
|> reading_changeset(attrs,
|
||||
parent_key: :processor_id,
|
||||
data_fields: [:load_percent, :status],
|
||||
required_with: [:status]
|
||||
)
|
||||
|> validate_inclusion(:status, ["ok", "warning", "critical", "error"])
|
||||
|> foreign_key_constraint(:processor_id)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
68
lib/towerops/snmp/reading.ex
Normal file
68
lib/towerops/snmp/reading.ex
Normal 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
|
||||
|
|
@ -5,16 +5,12 @@ defmodule Towerops.Snmp.Sensor do
|
|||
Tracks temperature, fan speed, voltage, power, and other sensors
|
||||
discovered via ENTITY-SENSOR-MIB or vendor-specific MIBs.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
use Towerops.Schema
|
||||
|
||||
alias Ecto.Association.NotLoaded
|
||||
alias Towerops.Snmp.Device
|
||||
alias Towerops.Snmp.SensorReading
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "snmp_sensors" do
|
||||
field :sensor_type, :string
|
||||
field :sensor_index, :string
|
||||
|
|
|
|||
|
|
@ -5,14 +5,10 @@ defmodule Towerops.Snmp.SensorReading do
|
|||
Stores individual sensor readings (value, status, timestamp) collected
|
||||
during SNMP polling cycles. Stored in TimescaleDB hypertable.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
use Towerops.Snmp.Reading
|
||||
|
||||
alias Towerops.Snmp.Sensor
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "snmp_sensor_readings" do
|
||||
field :value, :float
|
||||
field :status, :string
|
||||
|
|
@ -21,7 +17,7 @@ defmodule Towerops.Snmp.SensorReading do
|
|||
|
||||
belongs_to :sensor, Sensor
|
||||
|
||||
timestamps(type: :utc_datetime, updated_at: false)
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
|
|
@ -38,11 +34,13 @@ defmodule Towerops.Snmp.SensorReading do
|
|||
@doc false
|
||||
def changeset(reading, attrs) do
|
||||
reading
|
||||
|> cast(attrs, [:sensor_id, :value, :status, :state_descr, :checked_at])
|
||||
|> validate_required([:sensor_id, :status, :checked_at])
|
||||
|> reading_changeset(attrs,
|
||||
parent_key: :sensor_id,
|
||||
data_fields: [:value, :status, :state_descr],
|
||||
required_with: [:status]
|
||||
)
|
||||
|> validate_inclusion(:status, ["ok", "warning", "critical", "error"])
|
||||
|> validate_state_descr()
|
||||
|> foreign_key_constraint(:sensor_id)
|
||||
end
|
||||
|
||||
# Validate state_descr when present - ensure it's not empty and has reasonable length
|
||||
|
|
|
|||
|
|
@ -12,17 +12,13 @@ defmodule Towerops.Snmp.StateSensor do
|
|||
- critical: Failed or about to fail
|
||||
- unknown: State not determined
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
use Towerops.Schema
|
||||
|
||||
alias Ecto.Association.NotLoaded
|
||||
alias Towerops.Snmp.Device
|
||||
|
||||
@valid_statuses ~w(ok warning critical unknown)
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "snmp_state_sensors" do
|
||||
field :sensor_index, :string
|
||||
field :sensor_oid, :string
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue