Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,34 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
before it writes anything, logs both zones as a critical error and reports the close unhealthy in
`/health/ready`. See `docs/operations/licensed-agent-ledger.md`. (#348)

- **Calls whose `Hangup` never reached Platform now end on the pool sweep (Verbara.Sdk `2.8.0`).**
Every 30 s each Asterisk server holding calls older than 60 s receives one AMI `Status`; a held call
Asterisk no longer lists is ended and wraps up like any other hang-up (Sdk #402, H142). Before, such a
call stayed open for the life of the process. The sweep needs, on **every** Asterisk Platform
connects to, an AMI user whose `write` includes `system`, `call` or `reporting` and that does **not**
filter out `Hangup` (`eventfilter`); otherwise the sweep fails on that server and those calls stay
open as before. Every Asterisk of a multi-server pool also needs its own `systemname` in
`asterisk.conf` `[options]`, or channel ids collide across servers (Sdk #403 now logs a Warning on a
collision). The signal of a missing permission is `[LIVE] Status refused: …` at Warning in the API's
**start-up** log, because every state load sends `Status`. Platform's Compose `manager.conf` template
already complies; the AMI user and its reasons are in the voice manual
(`docs/manuales/smb/06-canal-voz-sip.md`).
- **Helm: the Asterisk chart changed in a breaking way.** It now ships Platform's AMI user (the same
`read`/`write` classes as the Compose template, no `eventfilter`) and its ARI user, and serves ARI on
port 8088. It needs both passwords at install: pass `asterisk.ami.password` and
`asterisk.ari.password`, or create the Secrets named by `asterisk.ami.passwordSecret` and
`asterisk.ari.passwordSecret` first; without them the Asterisk pod does not start. Keep them equal to
the platform chart's `api.asterisk.ami.password` and `api.asterisk.ari.password`;
`scripts/k8s-apps.sh` now requires `VERBARA_AMI_PASSWORD` and `VERBARA_ARI_PASSWORD` and passes each
to both charts. The chart sets `systemname` to the pod name, runs **one** Asterisk
(`asterisk.replicas: 1`) and refuses to render with `replicas > 1`, because Platform registers one AMI
server. Its PodDisruptionBudget (`minAvailable: 1`) therefore blocks a drain of the only Asterisk on
purpose. To drain its node, plan a maintenance window: cordon the node; set the agents Away and pause
queues and campaigns; wait until `asterisk -rx 'core show channels count'` shows 0 active channels;
run `kubectl delete pod asterisk-0` (or `kubectl drain --disable-eviction`); uncordon. The pod stays
`Pending` until its node is back, because its volumes use `local-path` storage. A pod that is not
Ready can always be evicted (`unhealthyPodEvictionPolicy: AlwaysAllow`).

### Added

- **Licensed-agent ledger.** Every change that can alter who counts as a licensed agent — agent created
Expand Down Expand Up @@ -85,6 +113,27 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- **The PJSIP desired state enumerates every agent of a tenant.** It was capped at 1000 agents per
tenant, so agents beyond the first page were never provisioned. (#348)

### Dependencies

- **Verbara.Sdk `2.7.0` → `2.8.0`** and **Verbara.Sdk.Pro `2.17.1-pro` → `2.18.0-pro`** — all 29
consumed packages in one commit (7 `Verbara.Sdk.*` pins + 22 `Verbara.Sdk.Pro.*` pins). The
compatibility triple is **Verbara.Sdk `2.8.0` × Verbara.Sdk.Pro `2.18.0-pro` × Verbara.Platform (the
next version cut from `main`)**; per `Verbara.Sdk/ADR-0040` D3 the cascade cuts no version of its
own. **No floor moved:** `dotnet restore` reported no `NU1605`/`NU1109`, and the per-project
`project.assets.json` diff by package name moves only the Verbara packages; nothing entered or left
the graph.
- **The `OpenTelemetry.Exporter.Prometheus.AspNetCore` pin is removed.** Verbara.Sdk.OpenTelemetry
`2.8.0` declares the exporter at `1.19.1-beta.1` itself (Sdk #388), so the exporter resolves to the
same version without Platform's pin; `GET /metrics` is unchanged and its test stays.
- **`DeferredPrimaryAmiConnection` forwards `IAmiConnection.Subscribe(Func<ManagerEvent,
CancellationToken, ValueTask>)`**, the member Sdk `2.8.0` added with a default body (Sdk #391).
Without the forwarding, a handler subscribed through the wrapper got a token that is never
cancelled, so the primary connection's fast close never reached it.
- **Behaviour Platform takes on — the pool sweep (H142).** `AddVerbaraSessionsMultiServer()` now
registers the reconciliation sweep for every server, on by default; Platform keeps the Sdk defaults
(`ReconciliationInterval` 30 s, `DialingTimeout` 60 s). A call the sweep ends carries no hangup
cause, so it never triggers a callback-on-drop. Prerequisites and signal: see **Upgrading**.

---

## [2.26.1] - 2026-10-08
Expand Down
74 changes: 32 additions & 42 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@

<ItemGroup>
<PackageVersion Include="Npgsql" Version="10.0.3" />
<PackageVersion Include="Verbara.Sdk.Data.Npgsql" Version="2.7.0" />
<PackageVersion Include="Verbara.Sdk.Cluster.Primitives" Version="2.7.0" />
<PackageVersion Include="Verbara.Sdk.Cluster.Postgres" Version="2.7.0" />
<PackageVersion Include="Verbara.Sdk.Data.Npgsql" Version="2.8.0" />
<PackageVersion Include="Verbara.Sdk.Cluster.Primitives" Version="2.8.0" />
<PackageVersion Include="Verbara.Sdk.Cluster.Postgres" Version="2.8.0" />
</ItemGroup>

<!-- DataProtection consumed by Verbara.Platform.Identity (ADR-0003 + ADR-0022
Expand All @@ -40,45 +40,35 @@
</ItemGroup>

<ItemGroup>
<PackageVersion Include="Verbara.Sdk.Hosting" Version="2.7.0" />
<PackageVersion Include="Verbara.Sdk.Push" Version="2.7.0" />
<PackageVersion Include="Verbara.Sdk.Resilience" Version="2.7.0" />
<PackageVersion Include="Verbara.Sdk.OpenTelemetry" Version="2.7.0" />
</ItemGroup>

<!-- Transitive pin (CentralPackageTransitivePinningEnabled above): Verbara.Sdk.OpenTelemetry
2.7.0 raises OpenTelemetry to 1.19.1 but still pulls OpenTelemetry.Exporter.Prometheus.AspNetCore
1.15.2-beta.1, whose PrometheusSerializer calls OpenTelemetry.Internal.MathHelper (removed in
1.19.1) - GET /metrics answers 200 with an empty body (ILC: "will always throw"). Guarded by
PrometheusScrapingEndpointTests. Remove when the Sdk ships the aligned exporter (cascade C16 /
Verbara.Sdk 2.8.0). -->
<ItemGroup>
<PackageVersion Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.19.1-beta.1" />
</ItemGroup>

<ItemGroup>
<PackageVersion Include="Verbara.Sdk.Pro.EventStore" Version="2.17.1-pro" />
<PackageVersion Include="Verbara.Sdk.Pro.Analytics" Version="2.17.1-pro" />
<PackageVersion Include="Verbara.Sdk.Pro.CallAnalytics" Version="2.17.1-pro" />
<PackageVersion Include="Verbara.Sdk.Pro.Dialer" Version="2.17.1-pro" />
<PackageVersion Include="Verbara.Sdk.Pro.Dialer.Storage.Postgres" Version="2.17.1-pro" />
<PackageVersion Include="Verbara.Sdk.Pro.EventStore.Postgres" Version="2.17.1-pro" />
<PackageVersion Include="Verbara.Sdk.Pro.CallAnalytics.Storage.Postgres" Version="2.17.1-pro" />
<PackageVersion Include="Verbara.Sdk.Pro.Analytics.Storage.Postgres" Version="2.17.1-pro" />
<PackageVersion Include="Verbara.Sdk.Pro.Licensing" Version="2.17.1-pro" />
<PackageVersion Include="Verbara.Sdk.Pro.CsatRunner" Version="2.17.1-pro" />
<PackageVersion Include="Verbara.Sdk.Pro.AgentAssist" Version="2.17.1-pro" />
<PackageVersion Include="Verbara.Sdk.Pro.AgentAssist.Storage.Postgres" Version="2.17.1-pro" />
<PackageVersion Include="Verbara.Sdk.Pro.Routing" Version="2.17.1-pro" />
<PackageVersion Include="Verbara.Sdk.Pro.Realtime" Version="2.17.1-pro" />
<PackageVersion Include="Verbara.Sdk.Pro.Realtime.Storage.Postgres" Version="2.17.1-pro" />
<PackageVersion Include="Verbara.Sdk.Pro.Cluster" Version="2.17.1-pro" />
<PackageVersion Include="Verbara.Sdk.Pro.Cluster.Storage.Postgres" Version="2.17.1-pro" />
<PackageVersion Include="Verbara.Sdk.Pro.MultiTenant" Version="2.17.1-pro" />
<PackageVersion Include="Verbara.Sdk.Pro.Push" Version="2.17.1-pro" />
<PackageVersion Include="Verbara.Sdk.Pro.Push.SignalR" Version="2.17.1-pro" />
<PackageVersion Include="Verbara.Sdk.Pro.Storage.Common" Version="2.17.1-pro" />
<PackageVersion Include="Verbara.Sdk.Pro.OpenTelemetry" Version="2.17.1-pro" />
<PackageVersion Include="Verbara.Sdk.Hosting" Version="2.8.0" />
<PackageVersion Include="Verbara.Sdk.Push" Version="2.8.0" />
<PackageVersion Include="Verbara.Sdk.Resilience" Version="2.8.0" />
<PackageVersion Include="Verbara.Sdk.OpenTelemetry" Version="2.8.0" />
</ItemGroup>

<ItemGroup>
<PackageVersion Include="Verbara.Sdk.Pro.EventStore" Version="2.18.0-pro" />
<PackageVersion Include="Verbara.Sdk.Pro.Analytics" Version="2.18.0-pro" />
<PackageVersion Include="Verbara.Sdk.Pro.CallAnalytics" Version="2.18.0-pro" />
<PackageVersion Include="Verbara.Sdk.Pro.Dialer" Version="2.18.0-pro" />
<PackageVersion Include="Verbara.Sdk.Pro.Dialer.Storage.Postgres" Version="2.18.0-pro" />
<PackageVersion Include="Verbara.Sdk.Pro.EventStore.Postgres" Version="2.18.0-pro" />
<PackageVersion Include="Verbara.Sdk.Pro.CallAnalytics.Storage.Postgres" Version="2.18.0-pro" />
<PackageVersion Include="Verbara.Sdk.Pro.Analytics.Storage.Postgres" Version="2.18.0-pro" />
<PackageVersion Include="Verbara.Sdk.Pro.Licensing" Version="2.18.0-pro" />
<PackageVersion Include="Verbara.Sdk.Pro.CsatRunner" Version="2.18.0-pro" />
<PackageVersion Include="Verbara.Sdk.Pro.AgentAssist" Version="2.18.0-pro" />
<PackageVersion Include="Verbara.Sdk.Pro.AgentAssist.Storage.Postgres" Version="2.18.0-pro" />
<PackageVersion Include="Verbara.Sdk.Pro.Routing" Version="2.18.0-pro" />
<PackageVersion Include="Verbara.Sdk.Pro.Realtime" Version="2.18.0-pro" />
<PackageVersion Include="Verbara.Sdk.Pro.Realtime.Storage.Postgres" Version="2.18.0-pro" />
<PackageVersion Include="Verbara.Sdk.Pro.Cluster" Version="2.18.0-pro" />
<PackageVersion Include="Verbara.Sdk.Pro.Cluster.Storage.Postgres" Version="2.18.0-pro" />
<PackageVersion Include="Verbara.Sdk.Pro.MultiTenant" Version="2.18.0-pro" />
<PackageVersion Include="Verbara.Sdk.Pro.Push" Version="2.18.0-pro" />
<PackageVersion Include="Verbara.Sdk.Pro.Push.SignalR" Version="2.18.0-pro" />
<PackageVersion Include="Verbara.Sdk.Pro.Storage.Common" Version="2.18.0-pro" />
<PackageVersion Include="Verbara.Sdk.Pro.OpenTelemetry" Version="2.18.0-pro" />
</ItemGroup>

<ItemGroup>
Expand Down
54 changes: 54 additions & 0 deletions docs/manuales/smb/06-canal-voz-sip.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,60 @@ Después de cualquier cambio en trunks/colas/agentes, Asterisk los toma on-deman
$ docker exec verbara-asterisk asterisk -rx 'module reload res_pjsip_endpoint_identifier_ip.so'
```

### 0.1 Un Asterisk propio: usuario AMI y `systemname`

Si usás el Asterisk del stack de Verbara (Compose), esto ya está hecho: su `manager.conf` se genera en cada arranque desde `docker/asterisk-config/manager.conf.template`. Si conectás Verbara a **tu propio Asterisk** (o a varios), su usuario AMI tiene que ser **igual** a este:

```ini
; manager.conf
[general]
enabled = yes
bindaddr = 0.0.0.0
port = 5038

[platform]
secret = <tu-password-AMI>
read = system,call,agent,user,config,originate,reporting,command
write = system,call,agent,user,config,originate,command
deny = 0.0.0.0/0.0.0.0
permit = 0.0.0.0/0.0.0.0
```

El `secret` es el mismo valor que Verbara usa como `Asterisk__Ami__Password`. Restringí `permit` a la IP del servidor de Verbara si podés.

**Por qué cada clase de `write`** (cada acción AMI exige una clase; Asterisk rechaza la acción si el usuario no la tiene):

| Clase | Qué hace Verbara con ella |
|-------|---------------------------|
| `system` / `call` | `Status`: la carga del estado al arrancar (y en cada reconexión) y el barrido que cierra las llamadas cuyo `Hangup` se perdió (cada 30 s). `Hangup` y `ModuleLoad` (recarga de módulos) también la usan. |
| `call` | `Redirect` + `SetVar`: las transferencias de agente (`VoiceCallControlService`); `GetVar`; `BlindTransfer` hacia la encuesta CSAT por voz. |
| `agent` | `QueuePause`: el estado del agente (Disponible / Ausente) pausa o despausa su miembro de cola (`RealtimeStateBridge`); `QueueAdd`/`QueueRemove` al reconciliar miembros. |
| `originate` | `Originate`: llamadas salientes del agente y callbacks. |
| `command` | `Command`: `core show codecs` (lista de codecs instalados en la consola de administración) y la prueba de conectividad de trunks. |
| `user`, `config` | No encontramos ninguna acción de Verbara que las exija; se mantienen como en la plantilla. |

**Por qué cada clase de `read`** (qué eventos recibe Verbara): `call` (canales, puentes y cada `Hangup`: las sesiones de llamada), `agent` (`AgentConnect` y el estado de los miembros de cola: sin ella la llamada de cola no tiene dueño ni screen-pop) y `system` (`FullyBooted`: sabe cuándo Asterisk terminó de arrancar). Para `user`, `config`, `originate`, `reporting` y `command` no encontramos un evento que Verbara necesite; se mantienen como en la plantilla.

> ⚠️ **No filtres el evento `Hangup`.** No agregues un `eventfilter` que lo excluya (por ejemplo `eventfilter=!Event: Hangup`). Verbara cierra la llamada con ese evento; si no llega, sólo el barrido la cierra, y el barrido necesita `Status`.

**Varios Asterisk (un pool):** cada servidor necesita su propio nombre de sistema, o los ids de canal se repiten entre servidores y las llamadas se mezclan:

```ini
; asterisk.conf
[options]
systemname = <único por servidor> ; por ejemplo pbx-1, pbx-2
```

**Qué falla sin esto:** sin `write` con `system`, `call` o `reporting`, el `Status` se rechaza: las llamadas cuyo `Hangup` se perdió quedan abiertas (el agente sigue Ocupado). Sin `call` fallan las transferencias; sin `agent` el agente no se pausa ni despausa en la cola; sin `originate` no salen llamadas. Con dos servidores sin `systemname` distinto, Verbara puede mezclar llamadas de servidores distintos y sólo lo avisa con un Warning en el log.

**Cómo detectarlo:** el `Status` se manda en cada arranque del API, así que un permiso faltante aparece en el **log de arranque**:

```bash
$ dc logs platform-api | grep 'Status refused'
```

✓ Sin salida = el usuario AMI tiene el permiso. Si aparece `[LIVE] Status refused: …` (nivel Warning), corregí el `write` del usuario AMI, recargá el manager en Asterisk (`asterisk -rx 'manager reload'`) y reiniciá el API.

## 1. Pre-requisitos de red — checklist final

Si esto NO está verde, **revisá manual 01 §2 y §3** antes de seguir.
Expand Down
6 changes: 6 additions & 0 deletions docs/manuales/smb/checklist-validacion-cliente.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,12 @@ Test inbound (golden path — ✅ V1):
- ☐ Hangup limpio
- ☐ CDR aparece en `/admin/analytics/calls`

Usuario AMI (manual [06](06-canal-voz-sip.md) §0.1):
- ☐ Log de arranque del Api sin `[LIVE] Status refused`: tras reiniciar el API, `docker logs verbara-platform-api 2>&1 | grep 'Status refused'` no devuelve nada (si aparece, al usuario AMI le falta `write` con `system`, `call` o `reporting`)

Test transferencia (dos agentes):
- ☐ Una transferencia entre dos agentes completa: un cliente llama al DID y lo atiende el agente A; el agente A hace una **transferencia ciega** al agente B desde la consola (control de llamada); el teléfono o softphone del agente B suena, B atiende y el cliente queda hablando con B con audio en ambas direcciones; A queda libre. Si la transferencia falla o el cliente queda colgado, revisá que el usuario AMI tenga `write` con `call` (manual 06 §0.1)

Test concurrencia mínima (5 calls):
- ☐ 5 llamadas simultáneas al DID (SIPp o 5 teléfonos) → 5 entran a la cola
- ☐ 5 agentes distintos atienden (5 teléfonos SIP registrados)
Expand Down
4 changes: 2 additions & 2 deletions docs/operations/staging-environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ A fresh Phase 0L bring-up was the first scenario to exercise them.
| DB | CloudNativePG operator → Postgres 17 (3-instance HA) | CNPG 1.25.0 |
| DB pool | PgBouncer (CNPG Pooler CR, transaction mode) | (managed) |
| Cache | Redis 8 (StatefulSet, AOF persistence) | 8-alpine |
| PBX | Asterisk (StatefulSet, 2 replicas, anti-affinity) | 22 |
| PBX | Asterisk (StatefulSet, 1 replica — the chart refuses more: Platform registers one AMI server) | 22 |
| SBC | Kamailio (DaemonSet, hostNetwork) | 5.8.8 |
| Media | RTPEngine (DaemonSet, hostNetwork) | fonoster/latest |
| API | Platform.Api (Deployment, HPA 2→8) | 1.14.6 |
Expand Down Expand Up @@ -198,7 +198,7 @@ A fresh Phase 0L bring-up was the first scenario to exercise them.

| Feature | Scope | Detail |
|---------|-------|--------|
| PodDisruptionBudgets | Asterisk + Platform API | `minAvailable: 1` — protects during node drain/upgrades |
| PodDisruptionBudgets | Asterisk + Platform API | `minAvailable: 1`. Platform API: keeps one pod serving during a node drain. Asterisk (one replica): **blocks** the drain of its node on purpose, so live calls are not dropped — voice does not stay up through a drain; plan a maintenance window (procedure in the CHANGELOG upgrade note of the Sdk 2.8.0 cascade). A pod that is not Ready can always be evicted (`unhealthyPodEvictionPolicy: AlwaysAllow`) |
| NetworkPolicies | 17 policies, 4 namespaces | Default-deny ingress + service-level whitelists; standard `networking.k8s.io/v1` |
| ResourceQuotas | r55-data, r55-asterisk, r55-platform | Per-namespace CPU/memory/pod caps with headroom for HPA |
| SecurityContext | All workloads | `runAsNonRoot`, `seccompProfile: RuntimeDefault`, `allowPrivilegeEscalation: false` |
Expand Down
17 changes: 17 additions & 0 deletions infra/k8s/helm/asterisk/templates/asterisk-ami-secret.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{{- /*
Platform's AMI password. Rendered only when asterisk.ami.password is passed at
install; otherwise the Secret named by asterisk.ami.passwordSecret must already
exist. The repository holds no literal AMI password.
*/ -}}
{{- if .Values.asterisk.ami.password }}
apiVersion: v1
kind: Secret
metadata:
name: {{ .Values.asterisk.ami.passwordSecret }}
namespace: {{ .Values.namespace }}
labels:
{{- include "asterisk.labels" . | nindent 4 }}
type: Opaque
stringData:
{{ .Values.asterisk.ami.passwordKey }}: {{ .Values.asterisk.ami.password | quote }}
{{- end }}
17 changes: 17 additions & 0 deletions infra/k8s/helm/asterisk/templates/asterisk-ari-secret.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{{- /*
Platform's ARI password. Rendered only when asterisk.ari.password is passed at
install; otherwise the Secret named by asterisk.ari.passwordSecret must already
exist. The repository holds no literal ARI password.
*/ -}}
{{- if .Values.asterisk.ari.password }}
apiVersion: v1
kind: Secret
metadata:
name: {{ .Values.asterisk.ari.passwordSecret }}
namespace: {{ .Values.namespace }}
labels:
{{- include "asterisk.labels" . | nindent 4 }}
type: Opaque
stringData:
{{ .Values.asterisk.ari.passwordKey }}: {{ .Values.asterisk.ari.password | quote }}
{{- end }}
Loading
Loading