fix: resolve credo warnings — refactor decoder_macros complexity and remove unused assign indirection
- Refactored build_field_handler_ast to use pattern-matching function heads instead of a large cond block, reducing cyclomatic complexity from 30 to ≤9 - Extracted classify_type_spec and per-type AST builder helpers into separate private functions for maintainability - Removed :previous_interface_stats initialization from show.ex (only polling.ex reads it); prev_stats now passed explicitly as a parameter to polling functions instead of reaching through socket.assigns - Updated Polling.poll_sensors, poll_traffic, and poll_traffic_with_state to accept prev_stats as an explicit argument
This commit is contained in:
parent
012a8ece95
commit
b5512f2579
19 changed files with 229 additions and 153 deletions
|
|
@ -293,8 +293,8 @@ Towerops uses Forgejo CI/CD with two trigger paths (see `.forgejo/workflows/`):
|
|||
- Push to `main` triggers `production.yaml`
|
||||
- **Test gate:** ExUnit suite must pass before any image is built
|
||||
- After tests pass:
|
||||
- Builds Docker image from `k8s/Dockerfile` — `FROM codeberg.org/gmcintire/towerops-base:latest`
|
||||
- Pushes to `codeberg.org/gmcintire/towerops` (tagged `main-<ts>-<sha>` and `production`)
|
||||
- Builds Docker image from `k8s/Dockerfile` — `FROM git.mcintire.me/gmcintire/towerops-base:latest`
|
||||
- Pushes to `git.mcintire.me/gmcintire/towerops` (tagged `main-<ts>-<sha>` and `production`)
|
||||
- `argocd-image-updater` picks up the new tag (~2 min), writes it to the
|
||||
ArgoCD Application's `spec.source.kustomize.images`, and ArgoCD rolls the
|
||||
Deployment. No commit to `k8s/deployment.yaml`.
|
||||
|
|
@ -306,7 +306,7 @@ ship to prod once tests pass.
|
|||
|
||||
The slow runtime apt installs (`gdal-bin`, `snmp`, `libsnmp40`, `locales`,
|
||||
BEAM runtime libs) live in a prebuilt base image —
|
||||
`codeberg.org/gmcintire/towerops-base:latest` — so day-to-day app builds
|
||||
`git.mcintire.me/gmcintire/towerops-base:latest` — so day-to-day app builds
|
||||
skip ~500 MB of GIS dep installation.
|
||||
|
||||
- **Source**: `k8s/Dockerfile.base`
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ config :towerops, :coverage_storage_dir, {:towerops, "priv/static/coverage"}
|
|||
# Agent Docker Image
|
||||
# Override this in runtime.exs or environment-specific config
|
||||
config :towerops,
|
||||
agent_docker_image: "codeberg.org/towerops-agent/towerops-agent:latest"
|
||||
agent_docker_image: "git.mcintire.me/towerops-agent/towerops-agent:latest"
|
||||
|
||||
# Import environment specific config. This must remain at the bottom
|
||||
# of this file so it overrides the configuration defined above.
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use Bearer-token auth scoped to a single organization.
|
|||
|
||||
> Looking for an Ansible-native interface? The official Towerops Ansible
|
||||
> collection wraps these endpoints with idempotent modules:
|
||||
> <https://codeberg.org/towerops/ansible>.
|
||||
> <https://git.mcintire.me/towerops/ansible>.
|
||||
|
||||
## Authentication
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
# This file is based on these images:
|
||||
#
|
||||
# - https://hub.docker.com/r/hexpm/elixir/tags - for the build image
|
||||
# - codeberg.org/gmcintire/towerops-base - prebuilt runtime base (see Dockerfile.base)
|
||||
# - git.mcintire.me/gmcintire/towerops-base - prebuilt runtime base (see Dockerfile.base)
|
||||
# - Ex: docker.io/hexpm/elixir:1.20.2-erlang-29.0.3-debian-trixie-20260713-slim
|
||||
#
|
||||
ARG ELIXIR_VERSION=1.20.2
|
||||
|
|
@ -20,7 +20,7 @@ ARG BUILDER_IMAGE="docker.io/hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION
|
|||
# Built by .forgejo/workflows/build-base.yaml only when Dockerfile.base
|
||||
# changes, so this Dockerfile pulls a ready-made layer instead of
|
||||
# reinstalling gdal (~500 MB) on every push.
|
||||
ARG BASE_IMAGE="codeberg.org/gmcintire/towerops-base:latest"
|
||||
ARG BASE_IMAGE="git.mcintire.me/gmcintire/towerops-base:latest"
|
||||
|
||||
FROM ${BUILDER_IMAGE} AS builder
|
||||
ENV MIX_OS_DEPS_COMPILE_PARTITION_COUNT=6
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
#
|
||||
# Built by `.forgejo/workflows/build-base.yaml` only when this file or
|
||||
# the workflow changes. The app `Dockerfile` does
|
||||
# `FROM codeberg.org/gmcintire/towerops-base:latest` so everyday code
|
||||
# `FROM git.mcintire.me/gmcintire/towerops-base:latest` so everyday code
|
||||
# pushes skip the gdal + snmp apt install entirely.
|
||||
#
|
||||
# Bumping the Debian release: change the ARG below and push — the base
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ defmodule Towerops.Agents.ReleaseChecker do
|
|||
end
|
||||
|
||||
defp fetch_and_cache do
|
||||
url = "https://codeberg.org/api/v1/repos/#{@codeberg_repo}/releases/latest"
|
||||
url = "https://git.mcintire.me/api/v1/repos/#{@codeberg_repo}/releases/latest"
|
||||
|
||||
case req_get(url, headers: [{"accept", "application/json"}], retry: false) do
|
||||
{:ok, %Req.Response{status: 200, body: body}} ->
|
||||
|
|
|
|||
|
|
@ -218,154 +218,227 @@ defmodule Towerops.Proto.DecoderMacros do
|
|||
end
|
||||
end
|
||||
|
||||
# Generate a field handler clause for a single field
|
||||
defp build_field_handler_ast(fun, field_num, field_name, type_spec) do
|
||||
# Generate a field handler clause for a single field (dispatch by type_spec shape)
|
||||
|
||||
defp build_field_handler_ast(fun, field_num, field_name, :string) do
|
||||
fields_fn = :"#{fun}_fields"
|
||||
build_scalar_handler_ast(fun, field_num, field_name, fields_fn, :string)
|
||||
end
|
||||
|
||||
cond do
|
||||
type_spec == :string ->
|
||||
quote do
|
||||
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 2, rest}}) do
|
||||
with {:ok, {value, rest2}} <- Wire.decode_bytes(rest) do
|
||||
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => value})
|
||||
end
|
||||
end
|
||||
defp build_field_handler_ast(fun, field_num, field_name, :uint) do
|
||||
fields_fn = :"#{fun}_fields"
|
||||
build_scalar_handler_ast(fun, field_num, field_name, fields_fn, :uint)
|
||||
end
|
||||
|
||||
defp build_field_handler_ast(fun, field_num, field_name, :int64) do
|
||||
fields_fn = :"#{fun}_fields"
|
||||
build_scalar_handler_ast(fun, field_num, field_name, fields_fn, :int64)
|
||||
end
|
||||
|
||||
defp build_field_handler_ast(fun, field_num, field_name, :double) do
|
||||
fields_fn = :"#{fun}_fields"
|
||||
build_scalar_handler_ast(fun, field_num, field_name, fields_fn, :double)
|
||||
end
|
||||
|
||||
defp build_field_handler_ast(fun, field_num, field_name, :bool) do
|
||||
fields_fn = :"#{fun}_fields"
|
||||
build_scalar_handler_ast(fun, field_num, field_name, fields_fn, :bool)
|
||||
end
|
||||
|
||||
defp build_field_handler_ast(fun, field_num, field_name, {:{}, _, [:enum, conv_fn]}) do
|
||||
fields_fn = :"#{fun}_fields"
|
||||
build_enum_handler_ast(fun, field_num, field_name, fields_fn, conv_fn)
|
||||
end
|
||||
|
||||
defp build_field_handler_ast(fun, field_num, field_name, {:{}, _, [:message, decode_fn]}) do
|
||||
fields_fn = :"#{fun}_fields"
|
||||
build_message_handler_ast(fun, field_num, field_name, fields_fn, decode_fn)
|
||||
end
|
||||
|
||||
defp build_field_handler_ast(fun, field_num, field_name, {:{}, _, [:message, decode_fn, validate_fn]}) do
|
||||
fields_fn = :"#{fun}_fields"
|
||||
build_validated_message_handler_ast(fun, field_num, field_name, fields_fn, decode_fn, validate_fn)
|
||||
end
|
||||
|
||||
defp build_field_handler_ast(fun, field_num, field_name, {:{}, _, [:repeated_message, decode_fn]}) do
|
||||
fields_fn = :"#{fun}_fields"
|
||||
build_repeated_message_handler_ast(fun, field_num, field_name, fields_fn, decode_fn)
|
||||
end
|
||||
|
||||
defp build_field_handler_ast(fun, field_num, field_name, {:{}, _, [:repeated_message, decode_fn, validate_fn]}) do
|
||||
fields_fn = :"#{fun}_fields"
|
||||
build_validated_repeated_message_handler_ast(fun, field_num, field_name, fields_fn, decode_fn, validate_fn)
|
||||
end
|
||||
|
||||
defp build_field_handler_ast(fun, field_num, field_name, {:{}, _, [:repeated, :string]}) do
|
||||
fields_fn = :"#{fun}_fields"
|
||||
build_repeated_string_handler_ast(fun, field_num, field_name, fields_fn)
|
||||
end
|
||||
|
||||
defp build_field_handler_ast(fun, field_num, field_name, {:{}, _, [:map, :string, :string]}) do
|
||||
fields_fn = :"#{fun}_fields"
|
||||
build_map_handler_ast(fun, field_num, field_name, fields_fn)
|
||||
end
|
||||
|
||||
defp build_field_handler_ast(fun, field_num, field_name, {:{}, _, [:oneof, decode_fn, tag]}) do
|
||||
fields_fn = :"#{fun}_fields"
|
||||
build_oneof_handler_ast(fun, field_num, field_name, fields_fn, decode_fn, tag)
|
||||
end
|
||||
|
||||
defp build_field_handler_ast(_fun, _field_num, _field_name, type_spec) do
|
||||
raise "Unknown field type spec: #{inspect(type_spec)}"
|
||||
end
|
||||
|
||||
# -- Scalar helpers --
|
||||
|
||||
defp build_scalar_handler_ast(fun, field_num, field_name, fields_fn, :string) do
|
||||
quote do
|
||||
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 2, rest}}) do
|
||||
with {:ok, {value, rest2}} <- Wire.decode_bytes(rest) do
|
||||
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => value})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
type_spec == :uint ->
|
||||
quote do
|
||||
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 0, rest}}) do
|
||||
with {:ok, {value, rest2}} <- Wire.decode_varint(rest) do
|
||||
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => value})
|
||||
end
|
||||
end
|
||||
defp build_scalar_handler_ast(fun, field_num, field_name, fields_fn, :uint) do
|
||||
quote do
|
||||
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 0, rest}}) do
|
||||
with {:ok, {value, rest2}} <- Wire.decode_varint(rest) do
|
||||
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => value})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
type_spec == :int64 ->
|
||||
quote do
|
||||
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 0, rest}}) do
|
||||
with {:ok, {raw, rest2}} <- Wire.decode_varint(rest) do
|
||||
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => Wire.decode_int64_value(raw)})
|
||||
end
|
||||
end
|
||||
defp build_scalar_handler_ast(fun, field_num, field_name, fields_fn, :int64) do
|
||||
quote do
|
||||
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 0, rest}}) do
|
||||
with {:ok, {raw, rest2}} <- Wire.decode_varint(rest) do
|
||||
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => Wire.decode_int64_value(raw)})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
type_spec == :double ->
|
||||
quote do
|
||||
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 1, rest}}) do
|
||||
with {:ok, {value, rest2}} <- Wire.decode_double(rest) do
|
||||
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => value})
|
||||
end
|
||||
end
|
||||
defp build_scalar_handler_ast(fun, field_num, field_name, fields_fn, :double) do
|
||||
quote do
|
||||
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 1, rest}}) do
|
||||
with {:ok, {value, rest2}} <- Wire.decode_double(rest) do
|
||||
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => value})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
type_spec == :bool ->
|
||||
quote do
|
||||
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 0, rest}}) do
|
||||
with {:ok, {value, rest2}} <- Wire.decode_varint(rest) do
|
||||
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => value != 0})
|
||||
end
|
||||
end
|
||||
defp build_scalar_handler_ast(fun, field_num, field_name, fields_fn, :bool) do
|
||||
quote do
|
||||
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 0, rest}}) do
|
||||
with {:ok, {value, rest2}} <- Wire.decode_varint(rest) do
|
||||
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => value != 0})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Tuple type specs arrive as {:{}, meta, [tag | args]}
|
||||
match?({:{}, _, [:enum, _]}, type_spec) ->
|
||||
{:{}, _, [_, conv_fn]} = type_spec
|
||||
# -- Enum handler --
|
||||
|
||||
quote do
|
||||
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 0, rest}}) do
|
||||
with {:ok, {value, rest2}} <- Wire.decode_varint(rest),
|
||||
{:ok, converted} <- unquote(conv_fn).(value) do
|
||||
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => converted})
|
||||
end
|
||||
end
|
||||
defp build_enum_handler_ast(fun, field_num, field_name, fields_fn, conv_fn) do
|
||||
quote do
|
||||
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 0, rest}}) do
|
||||
with {:ok, {value, rest2}} <- Wire.decode_varint(rest),
|
||||
{:ok, converted} <- unquote(conv_fn).(value) do
|
||||
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => converted})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
match?({:{}, _, [:message, _]}, type_spec) and
|
||||
not match?({:{}, _, [:message, _, _]}, type_spec) ->
|
||||
{:{}, _, [_, decode_fn]} = type_spec
|
||||
# -- Message handlers --
|
||||
|
||||
quote do
|
||||
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 2, rest}}) do
|
||||
with {:ok, {msg_data, rest2}} <- Wire.decode_bytes(rest),
|
||||
{:ok, msg} <- unquote(decode_fn)(msg_data) do
|
||||
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => msg})
|
||||
end
|
||||
end
|
||||
defp build_message_handler_ast(fun, field_num, field_name, fields_fn, decode_fn) do
|
||||
quote do
|
||||
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 2, rest}}) do
|
||||
with {:ok, {msg_data, rest2}} <- Wire.decode_bytes(rest),
|
||||
{:ok, msg} <- unquote(decode_fn)(msg_data) do
|
||||
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => msg})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
match?({:{}, _, [:message, _, _]}, type_spec) ->
|
||||
{:{}, _, [_, decode_fn, validate_fn]} = type_spec
|
||||
|
||||
quote do
|
||||
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 2, rest}}) do
|
||||
with {:ok, {msg_data, rest2}} <- Wire.decode_bytes(rest),
|
||||
{:ok, msg} <- unquote(decode_fn)(msg_data),
|
||||
{:ok, msg} <- unquote(validate_fn)(msg) do
|
||||
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => msg})
|
||||
end
|
||||
end
|
||||
defp build_validated_message_handler_ast(fun, field_num, field_name, fields_fn, decode_fn, validate_fn) do
|
||||
quote do
|
||||
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 2, rest}}) do
|
||||
with {:ok, {msg_data, rest2}} <- Wire.decode_bytes(rest),
|
||||
{:ok, msg} <- unquote(decode_fn)(msg_data),
|
||||
{:ok, msg} <- unquote(validate_fn)(msg) do
|
||||
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => msg})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
match?({:{}, _, [:repeated_message, _]}, type_spec) and
|
||||
not match?({:{}, _, [:repeated_message, _, _]}, type_spec) ->
|
||||
{:{}, _, [_, decode_fn]} = type_spec
|
||||
# -- Repeated message handlers --
|
||||
|
||||
quote do
|
||||
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 2, rest}}) do
|
||||
with {:ok, {msg_data, rest2}} <- Wire.decode_bytes(rest),
|
||||
{:ok, msg} <- unquote(decode_fn)(msg_data) do
|
||||
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => [msg | acc.unquote(field_name)]})
|
||||
end
|
||||
end
|
||||
defp build_repeated_message_handler_ast(fun, field_num, field_name, fields_fn, decode_fn) do
|
||||
quote do
|
||||
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 2, rest}}) do
|
||||
with {:ok, {msg_data, rest2}} <- Wire.decode_bytes(rest),
|
||||
{:ok, msg} <- unquote(decode_fn)(msg_data) do
|
||||
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => [msg | acc.unquote(field_name)]})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
match?({:{}, _, [:repeated_message, _, _]}, type_spec) ->
|
||||
{:{}, _, [_, decode_fn, validate_fn]} = type_spec
|
||||
|
||||
quote do
|
||||
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 2, rest}}) do
|
||||
with {:ok, {msg_data, rest2}} <- Wire.decode_bytes(rest),
|
||||
{:ok, msg} <- unquote(decode_fn)(msg_data),
|
||||
{:ok, msg} <- unquote(validate_fn)(msg) do
|
||||
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => [msg | acc.unquote(field_name)]})
|
||||
end
|
||||
end
|
||||
defp build_validated_repeated_message_handler_ast(fun, field_num, field_name, fields_fn, decode_fn, validate_fn) do
|
||||
quote do
|
||||
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 2, rest}}) do
|
||||
with {:ok, {msg_data, rest2}} <- Wire.decode_bytes(rest),
|
||||
{:ok, msg} <- unquote(decode_fn)(msg_data),
|
||||
{:ok, msg} <- unquote(validate_fn)(msg) do
|
||||
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => [msg | acc.unquote(field_name)]})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
match?({:{}, _, [:repeated, :string]}, type_spec) ->
|
||||
quote do
|
||||
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 2, rest}}) do
|
||||
with {:ok, {value, rest2}} <- Wire.decode_bytes(rest) do
|
||||
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => [value | acc.unquote(field_name)]})
|
||||
end
|
||||
end
|
||||
# -- Repeated string handler --
|
||||
|
||||
defp build_repeated_string_handler_ast(fun, field_num, field_name, fields_fn) do
|
||||
quote do
|
||||
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 2, rest}}) do
|
||||
with {:ok, {value, rest2}} <- Wire.decode_bytes(rest) do
|
||||
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => [value | acc.unquote(field_name)]})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
match?({:{}, _, [:map, :string, :string]}, type_spec) ->
|
||||
quote do
|
||||
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 2, rest}}) do
|
||||
with {:ok, {entry_data, rest2}} <- Wire.decode_bytes(rest),
|
||||
{:ok, {key, val}} <- decode_map_entry(entry_data) do
|
||||
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => Map.put(acc.unquote(field_name), key, val)})
|
||||
end
|
||||
end
|
||||
# -- Map handler --
|
||||
|
||||
defp build_map_handler_ast(fun, field_num, field_name, fields_fn) do
|
||||
quote do
|
||||
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 2, rest}}) do
|
||||
with {:ok, {entry_data, rest2}} <- Wire.decode_bytes(rest),
|
||||
{:ok, {key, val}} <- decode_map_entry(entry_data) do
|
||||
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => Map.put(acc.unquote(field_name), key, val)})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
match?({:{}, _, [:oneof, _, _]}, type_spec) ->
|
||||
{:{}, _, [_, decode_fn, tag]} = type_spec
|
||||
# -- Oneof handler --
|
||||
|
||||
quote do
|
||||
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 2, rest}}) do
|
||||
with {:ok, {msg_data, rest2}} <- Wire.decode_bytes(rest),
|
||||
{:ok, msg} <- unquote(decode_fn)(msg_data) do
|
||||
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => {unquote(tag), msg}})
|
||||
end
|
||||
end
|
||||
defp build_oneof_handler_ast(fun, field_num, field_name, fields_fn, decode_fn, tag) do
|
||||
quote do
|
||||
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 2, rest}}) do
|
||||
with {:ok, {msg_data, rest2}} <- Wire.decode_bytes(rest),
|
||||
{:ok, msg} <- unquote(decode_fn)(msg_data) do
|
||||
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => {unquote(tag), msg}})
|
||||
end
|
||||
|
||||
true ->
|
||||
raise "Unknown field type spec: #{inspect(type_spec)}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -229,6 +229,7 @@ defmodule Towerops.Snmp.Discovery do
|
|||
{:ok, transceivers} = with_timeout(client_opts, fn -> Base.discover_transceivers(client_opts) end, timeouts)
|
||||
|
||||
Logger.info("Discovering printer supplies...", device_id: device.id)
|
||||
|
||||
{:ok, printer_supplies} =
|
||||
with_timeout(client_opts, fn -> Base.discover_printer_supplies(client_opts) end, timeouts)
|
||||
|
||||
|
|
@ -316,7 +317,8 @@ defmodule Towerops.Snmp.Discovery do
|
|||
timeout: timeouts[:slow],
|
||||
default: {:error, :interface_discovery_timeout}
|
||||
) do
|
||||
{:ok, _ifaces} = ok -> ok
|
||||
{:ok, _ifaces} = ok ->
|
||||
ok
|
||||
|
||||
{:error, :interface_discovery_timeout} ->
|
||||
Logger.error("Interface discovery timed out - aborting to prevent data loss", device_id: device_id)
|
||||
|
|
@ -334,7 +336,8 @@ defmodule Towerops.Snmp.Discovery do
|
|||
timeout: timeouts[:slow],
|
||||
default: {:error, :sensor_discovery_timeout}
|
||||
) do
|
||||
{:ok, _sensors} = ok -> ok
|
||||
{:ok, _sensors} = ok ->
|
||||
ok
|
||||
|
||||
{:error, :sensor_discovery_timeout} ->
|
||||
Logger.error("Sensor discovery timed out - aborting to prevent data loss", device_id: device_id)
|
||||
|
|
@ -1604,8 +1607,7 @@ defmodule Towerops.Snmp.Discovery do
|
|||
|
||||
@spec update_device_name_from_snmp(DeviceSchema.t(), system_info()) ::
|
||||
{:ok, DeviceSchema.t()} | {:error, term()}
|
||||
defp update_device_name_from_snmp(%{name: name} = device, _system_info)
|
||||
when is_binary(name) and name != "" do
|
||||
defp update_device_name_from_snmp(%{name: name} = device, _system_info) when is_binary(name) and name != "" do
|
||||
Logger.info("Device name already set or no sysName, skipping update", device_id: device.id)
|
||||
{:ok, device}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -243,9 +243,9 @@
|
|||
</h3>
|
||||
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
Drive Towerops as code. The Ansible collection wraps every resource on this page with idempotent modules: <a
|
||||
href="https://codeberg.org/towerops/ansible"
|
||||
href="https://git.mcintire.me/towerops/ansible"
|
||||
class="font-medium text-emerald-600 hover:underline dark:text-emerald-400"
|
||||
>codeberg.org/towerops/ansible</a>.
|
||||
>git.mcintire.me/towerops/ansible</a>.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -3922,7 +3922,7 @@ curl -G https://towerops.net/api/v1/activity \\
|
|||
and progresses through <code>computing</code>
|
||||
to <code>ready</code>
|
||||
or <code>failed</code>. The status field is present on every response so polling-based clients (Terraform, the <a
|
||||
href="https://codeberg.org/towerops/ansible"
|
||||
href="https://git.mcintire.me/towerops/ansible"
|
||||
class="font-medium text-emerald-600 hover:underline dark:text-emerald-400"
|
||||
>Ansible collection</a>, scripts) can wait for a healthy resource. Outputs include a coloured PNG heatmap, a Float32 GeoTIFF raster, and a downloadable KMZ bundle for Google Earth.
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -326,9 +326,9 @@
|
|||
</h3>
|
||||
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
Prefer infrastructure-as-code? The Towerops Ansible collection covers every resource exposed here: <a
|
||||
href="https://codeberg.org/towerops/ansible"
|
||||
href="https://git.mcintire.me/towerops/ansible"
|
||||
class="font-medium text-emerald-600 hover:underline dark:text-emerald-400"
|
||||
>codeberg.org/towerops/ansible</a>.
|
||||
>git.mcintire.me/towerops/ansible</a>.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -199,7 +199,7 @@
|
|||
and starts collecting metrics. No firewall changes, no VPN, no open ports.
|
||||
</p>
|
||||
<div class="mt-6 rounded-lg bg-slate-900 dark:bg-black/50 dark:ring-1 dark:ring-white/10 p-4 font-mono text-sm text-green-400">
|
||||
$ docker run -d codeberg.org/towerops-agent/towerops-agent
|
||||
$ docker run -d git.mcintire.me/towerops-agent/towerops-agent
|
||||
</div>
|
||||
</div>
|
||||
<!-- Step 2 -->
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ defmodule ToweropsWeb.AgentLive.Index do
|
|||
Application.get_env(
|
||||
:towerops,
|
||||
:agent_docker_image,
|
||||
"codeberg.org/towerops-agent/towerops-agent:latest"
|
||||
"git.mcintire.me/towerops-agent/towerops-agent:latest"
|
||||
)
|
||||
|
||||
{:ok,
|
||||
|
|
|
|||
|
|
@ -31,9 +31,9 @@ defmodule ToweropsWeb.GraphLive.Polling do
|
|||
@doc """
|
||||
Poll sensors for live mode, dispatching based on sensor type.
|
||||
"""
|
||||
def poll_sensors(assigns, client_opts, timestamp_ms) do
|
||||
def poll_sensors(assigns, client_opts, timestamp_ms, prev_stats) do
|
||||
if assigns.sensor_type == "traffic" do
|
||||
poll_traffic(assigns, client_opts, timestamp_ms)
|
||||
poll_traffic(assigns, client_opts, timestamp_ms, prev_stats)
|
||||
else
|
||||
poll_regular_sensors(assigns, client_opts, timestamp_ms)
|
||||
end
|
||||
|
|
@ -62,7 +62,7 @@ defmodule ToweropsWeb.GraphLive.Polling do
|
|||
Poll traffic stats (used from non-state-updating path).
|
||||
Requires two readings to calculate BPS; uses `previous_interface_stats` from assigns.
|
||||
"""
|
||||
def poll_traffic(assigns, client_opts, timestamp_ms) do
|
||||
def poll_traffic(assigns, client_opts, timestamp_ms, prev_stats) do
|
||||
device = Snmp.get_device_with_associations(assigns.device_id)
|
||||
|
||||
if is_nil(device) || Enum.empty?(device.interfaces) do
|
||||
|
|
@ -70,9 +70,9 @@ defmodule ToweropsWeb.GraphLive.Polling do
|
|||
else
|
||||
current_stats = poll_interface_stats_parallel(device.interfaces, client_opts)
|
||||
|
||||
if assigns.previous_interface_stats do
|
||||
if prev_stats do
|
||||
calculate_traffic_bps_from_stats(
|
||||
assigns.previous_interface_stats,
|
||||
prev_stats,
|
||||
current_stats,
|
||||
timestamp_ms
|
||||
)
|
||||
|
|
@ -86,7 +86,7 @@ defmodule ToweropsWeb.GraphLive.Polling do
|
|||
Poll traffic with socket state update.
|
||||
Returns `{data_points, updated_stats}` for the caller to assign back.
|
||||
"""
|
||||
def poll_traffic_with_state(assigns, client_opts, timestamp_ms) do
|
||||
def poll_traffic_with_state(assigns, client_opts, timestamp_ms, prev_stats) do
|
||||
device = Snmp.get_device_with_associations(assigns.device_id)
|
||||
|
||||
if is_nil(device) || Enum.empty?(device.interfaces) do
|
||||
|
|
@ -95,9 +95,9 @@ defmodule ToweropsWeb.GraphLive.Polling do
|
|||
current_stats = poll_interface_stats_parallel(device.interfaces, client_opts)
|
||||
|
||||
data_points =
|
||||
if assigns.previous_interface_stats do
|
||||
if prev_stats do
|
||||
calculate_traffic_bps_from_stats(
|
||||
assigns.previous_interface_stats,
|
||||
prev_stats,
|
||||
current_stats,
|
||||
timestamp_ms
|
||||
)
|
||||
|
|
|
|||
|
|
@ -353,7 +353,6 @@ defmodule ToweropsWeb.GraphLive.Show do
|
|||
|> assign(:show_zero_line, sensor_type == "traffic")
|
||||
|> assign(:max_value, nil)
|
||||
|> assign(:min_value, nil)
|
||||
|> assign(:previous_interface_stats, nil)
|
||||
end
|
||||
|
||||
defp get_title_suffix(assigns) do
|
||||
|
|
@ -1070,10 +1069,12 @@ defmodule ToweropsWeb.GraphLive.Show do
|
|||
client_opts = Polling.build_snmp_client_opts(device)
|
||||
|
||||
if socket.assigns.sensor_type == "traffic" do
|
||||
{points, stats} = Polling.poll_traffic_with_state(socket.assigns, client_opts, timestamp_ms)
|
||||
prev_stats = Map.get(socket.assigns, :previous_interface_stats)
|
||||
{points, stats} = Polling.poll_traffic_with_state(socket.assigns, client_opts, timestamp_ms, prev_stats)
|
||||
{points, stats}
|
||||
else
|
||||
points = Polling.poll_sensors(socket.assigns, client_opts, timestamp_ms)
|
||||
prev_stats = Map.get(socket.assigns, :previous_interface_stats)
|
||||
points = Polling.poll_sensors(socket.assigns, client_opts, timestamp_ms, prev_stats)
|
||||
{points, nil}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ defmodule ToweropsWeb.HelpLive.Sections.About do
|
|||
<li>No data collection or sharing.</li>
|
||||
<li>
|
||||
Our remote agent is
|
||||
<a href="https://codeberg.org/towerops-agent/towerops-agent">
|
||||
<a href="https://git.mcintire.me/towerops-agent/towerops-agent">
|
||||
fully open source
|
||||
</a>
|
||||
and ONLY collects monitoring jobs from the server and WILL NEVER allow us to access any part of your internal network.
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ defmodule ToweropsWeb.HelpLive.Sections.Agents do
|
|||
<div class="mt-3 p-4 bg-black rounded-lg not-prose overflow-x-auto">
|
||||
<pre class="text-xs text-green-400 font-mono"><code><%= raw(~S[services:
|
||||
towerops-agent:
|
||||
image: codeberg.org/towerops-agent/towerops-agent:latest
|
||||
image: git.mcintire.me/towerops-agent/towerops-agent:latest
|
||||
container_name: towerops-agent
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
|
|
|
|||
|
|
@ -347,7 +347,7 @@
|
|||
-e TOWEROPS_TOKEN=<%= if @agent_token_value, do: @agent_token_value, else: "<your-token>" %> \
|
||||
-e TOWEROPS_SERVER=<%= ToweropsWeb.Endpoint.url() %> \
|
||||
--network host \
|
||||
codeberg.org/towerops-agent/towerops-agent:latest</code></pre>
|
||||
git.mcintire.me/towerops-agent/towerops-agent:latest</code></pre>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ syntax = "proto3";
|
|||
|
||||
package towerops.agent;
|
||||
|
||||
option go_package = "codeberg.org/towerops-agent/towerops-agent/pb";
|
||||
option go_package = "git.mcintire.me/towerops-agent/towerops-agent/pb";
|
||||
|
||||
// Configuration received from the API
|
||||
message AgentConfig {
|
||||
|
|
|
|||
|
|
@ -1980,7 +1980,7 @@ defmodule Towerops.AgentsTest do
|
|||
Phoenix.PubSub.subscribe(Towerops.PubSub, "agent:#{agent_token.id}:lifecycle")
|
||||
|
||||
url =
|
||||
"https://codeberg.org/towerops-agent/towerops-agent/releases/download/v0.2.0/towerops-agent-linux-amd64"
|
||||
"https://git.mcintire.me/towerops-agent/towerops-agent/releases/download/v0.2.0/towerops-agent-linux-amd64"
|
||||
|
||||
checksum = "abc123def456"
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue