# Freepbx  отказоустойчивый кластер, бесплатно

Для реалезации отказоустойчевого кластера на **Freepbx 16.0.40** будем использовать **CARP** протокол. Устанавливаем два сервера Freepbx на которых будет настроен отказоустойчивый кластер.

`Freepbx01(Master) - 192.168.98.214`

`Freepbx02(Slave) - 192.168.98.215`

`HA(общий адрес) - 192.168.98.14`

### **Устанавливаем CARP на оба сервера:**

```plaintext
yum install ucarp
```

Редактируем файлы конфигурации:

`Freepbx01(Master) - 192.168.98.214`

`vi /etc/ucarp/vip-001.conf`

```plaintext
VIP_ADDRESS="192.168.98.14"
ID=001
PASSFILE=/etc/ucarp/vip-001.pwd
BIND_INTERFACE="eth0"
SOURCE_ADDRESS="192.168.98.214"
OPTIONS="-k 2 --shutdown --preempt"
```

в файле `/etc/ucarp/vip-001.pwd` указывыем свой пароль (любой)

в файле `vip-common.conf` все коментируем

*   Повторяем все тоже самое на втором сервере
    

`Freepbx02(Slave) - 192.168.98.215`

`vi /etc/ucarp/vip-001.conf`

```plaintext
VIP_ADDRESS="192.168.98.14"
ID=001
PASSFILE=/etc/ucarp/vip-001.pwd
BIND_INTERFACE="eth0"
SOURCE_ADDRESS="192.168.98.215"
OPTIONS="-k 10 --shutdown"
```

*   Запускаем службу и добавляем в автозагрузку:
    

`systemctl enable ucarp@001.service`

`systemctl start ucarp@001.service`

`systemctl daemon-reload`

*   Проверяем работоспособность Carp, вводим `ip a`
    

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1681300349664/4bd90548-362a-43df-b4a3-edd1ea05bd9b.png align="center")

**Carp** интерфейс появился 192.168.98.14. Также добавляю скрипт который будет мониторить состояние сервиса **asterisk**, и если по какой то причине он не работает, то интерфейс **CARP** будет отключен, соответственно все устройства переподключатся на резервный сервер, так как он станем Мастером.

*   `vi /home/script/sevrice_up_down.sh`
    

```plaintext
                  
          #!/bin/bash
          
          while true; do
              if ps -C asterisk >/dev/null 2>&1; then
                  sleep 10
              else
                  sleep 10
                  if ps -C asterisk >/dev/null 2>&1; then
                      systemctl start ucarp@001.service
                      sleep 10
                      fwconsole reload
                  else
                      systemctl stop ucarp@001.service
                  fi
              fi
          done
```

*У меня возникла проблема, при переходе на резервный сервер транк не всегда подымался. Решалось командой fwconsole reload. По этой причине добавил в скрипт.*

Также на одной из АТС у меня был  **SIP Trunk** от провайдера. Так как провайдер ограничил нас одной регестрацией, добавляю в скрипт отключение транка в зависимости от статуса CARP IP.

```plaintext
        #!/bin/bash
        
        # Переменная для хранения предыдущего статуса IP-адреса
        previous_ip_status="absent"
        
        while true; do
            # Проверка наличия процесса asterisk
            if ps -C asterisk >/dev/null 2>&1; then
                sleep 10
            else
                sleep 10
                # Если процесс asterisk не запущен, попытка его запуска
                if ps -C asterisk >/dev/null 2>&1; then
                    systemctl start ucarp@001.service
                    sleep 10
                    fwconsole reload
                else
                    systemctl stop ucarp@001.service
                fi
            fi
        
            # Проверка наличия IP-адреса 192.168.98.14
            if ip a | grep  '192.168.98.14'; then
                current_ip_status="present"
            else
                current_ip_status="absent"
            fi
        
            # Управление транком в зависимости от статуса IP
            if [ "$current_ip_status" = "present" ]; then
                fwconsole trunk --enable 1
            else
                fwconsole trunk --disable 1
            fi
            
            # Если статус IP изменился, выполнить reload
            if [ "$previous_ip_status" != "$current_ip_status" ]; then
                previous_ip_status="$current_ip_status"
                sleep 5
                fwconsole reload
            fi
        done
```

*   Делаем файл исполняемым:
    
    `chmod 777 /home/script/sevrice_up_down.sh`
    
*   Создаю службу автозапуска скрипта:
    
    `vi /etc/systemd/system/sevrice_up_down.service`
    

```plaintext
                  [Unit]
                  Description=sevrice_up_down
                  After=network.target
                  
                  [Service]
                  User=root
                  ExecStart=/home/script/sevrice_up_down.sh
                  Restart=on-failure
                  
                  [Install]
                  WantedBy=multi-user.target
```

`systemctl daemon-reload`

`sudo systemctl enable sevrice_up_down.service`

`sudo systemctl start sevrice_up_down.service`

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1681305210256/1be83860-9143-4d7a-a72f-4257544b991e.png align="center")

Сервис успешно запущен. Можно попробовать остановить Asterisk командой `fwconsole stop` , при этом ip carp должен перейти на ре зервный сервер. На этом с карпом мы закончили.

### **Настраеваем Freepbx для синхронизации между собой.**

Первым делом нужно настроить доступ SSH. Для этого переходим на сервер `Freepbx01(Master) - 192.168.98.214` и выполняем:

```plaintext
sudo -u asterisk ssh-copy-id -i /home/asterisk/.ssh/id_rsa.pub root@SecondaryServer
```

и введите пароль при появлении запроса.

Если эта команда выполнена без ошибок, вы готовы к тестированию:

В командной строке введите:

`ssh -i /home/asterisk/.ssh/id_rsa root@SecondaryServerIP`Если все прошло хорошо, вы должны войти на дополнительный сервер.

**Настройка модуля хранилища файлов**

После завершения настройки SSH нам нужно настроить модуль [Filestore](https://wiki.freepbx.org/display/FPG/Filestore)  для SSH.

Нажав «Добавить SSH-сервер», вы можете добавить свой SSH-сервер для хранения резервной копии. Всякий раз, когда вы запускаете резервное копирование с сервером SSH, установленным в качестве местоположения хранилища файлов, файл резервной копии будет передан на путь к серверам SSH, который вы добавили в хранилище файлов. Если резервное копирование по какой-то причине не удалось или не удалось подключиться к SSH-серверу, резервная копия будет сохранена в локальной системе (/var/spool/asterisk/backup/имя\_резервной\_копии/файл\_резервной копии).

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1681393199283/c40e9310-ac5c-4b36-aacd-9c11964aa68d.png align="center")

Переходим на `Freepbx02(Slave) - 192.168.98.215` и идем в раздел ***Connectivity - API,*** нам нужно сгенерировать ключи.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1681301046180/18d506b7-09e7-4f49-be65-5dbfe1181190.png align="center")

Нажимаем ***Machine-to-Machine app*, в окне вводим Your App Name, Description и Allowed Scopes он должен быть:**

`gql:backup:write`

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1681301263248/87f2ab3e-a733-491c-af26-2c6d9acdd9c0.png align="center")

**Генерируются ключи и сохраняем их в свой файлик \*.txt.**

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1681303019595/3f321293-7260-42dd-b6d3-63e630f71b01.png align="center")

Переходим на каждый из серверов **Freepbx** и идем в раздел **Settings-File-Local**

Добавляем новое месторасположения бекапов:

`_ASTSPOOLDIR_/backup`

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1681302105104/8fae5c58-1c4a-4cf4-84bf-ee2c9a35985d.png align="center")

*   Переходим на `Freepbx01(Master) - 192.168.98.214` и идем в раздел **Store** **Admin-Backup & Restore** там создаем новый бекап.
    

Даем имя бекапу, выбираем модули которые хотите бекапить. Важно отключить копирование двух модулей:

`Backup & Restore`

`System Admin`

В разделе **Custom Files** можно указать копирование дополнительных каталогов, например **tftpboot** если используется. Также настраивается уведомленя по почте. В разделе **Storage Location** указываем ранее созданое место для бекапов. Настраиваем **Schedule and Maintenance** по своим нуждам.

И теперь самы важный раздел, **Warm Spare** в котором мы должны ввести данные которые сохраняли в блокноте.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1681303034775/12769278-0fc5-404a-ab4b-854863d97b3d.png align="center")

Сохраняем и запускаем бекап. После успешного переноса бекапа на второй сервер, вы должны увидеть сообщение.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1681304105447/46363671-f74a-485b-96ca-07d1cfac9162.png align="center")

### Доработака Freepbx (Sip&IAX2)

1.  Так как у нас будет использоватся **Carp** ip адрес как основной нужно до настроить **Sip** а у кого используется **IAX2** то и его тоже.
    
    Переходим в **Settings- Asterisk SIP Settings** и указываем в качестве **External Address** адрес вашого **CARP.** В **Local Networks** указываем подсети с которых будет проводится регистрация  sip клиентов.
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1681304236469/b618cff8-db5f-4135-96a8-d1897fa5c777.png align="center")
    
    Для IAX2 переходим в транк и в [**Outgoing**](https://192.168.98.215/admin/config.php?display=trunks&tech=iax&extdisplay=OUT_1#sipoutset) нужно дописать\*\*:\*\*
    
    `sourceaddress=(Carp ip address) например sourceaddress=192.168.98.14`
    
    Теперь наш транк будет отбащатся через адрес **192.168.98.14**
    

Мы получили полностью отказоустойчивую АТС бесплатно.

######################

Обновление Freepbx

*yum upgrade -y*

*fwconsole ma upgradeall*

*fwconsole chown*

*fwconsole reload*

*fwconsole chown*

*fwconsole reload*

*fwconsole versionupgrade --check*

*fwconsole versionupgrade --upgrade*

*fwconsole chown*

*fwconsole reload*

*reboot or fwconsole restart*

## **Дополнительные настройки.**

## Zabbix

rpm -Uvh [https://repo.zabbix.com/zabbix/6.0/rhel/7/x86\_64/zabbix-release-6.0-4.el7.noarch.rpm](https://repo.zabbix.com/zabbix/6.0/rhel/7/x86_64/zabbix-release-6.0-4.el7.noarch.rpm)

yum install zabbix-agent2

systemctl restart zabbix-agent2  
systemctl enable zabbix-agent2

Скопировть конфиг с старого сервера

## Tftp

```plaintext
sudo vi /etc/selinux/config
SELINUX=disabled

sudo yum install tftp tftp-server xinetd
sudo systemctl enable xinetd
sudo systemctl enable tftp
sudo systemctl restart xinetd
```

## Vpn Up Down

Для выполнения (спец) скриптов от пользователя asterisk может понадобится интерпритатор.

```plaintext
yum install expect
проверяем
su - asterisk
```

Для некоторых скриптов добавляю права доступа в mcedit /etc/sudoers

```plaintext
root<-->ALL=(ALL) <---->ALL
pbx<------>ALL=(ALL)<----->ALL
zabbix<><------>ALL=NOPASSWD: /usr/sbin/asterisk
asterisk<------>ALL=(ALL) NOPASSWD: /home/script/disable_port12.sh, /home/script/shutdown_thinstantion.sh, /home/script/shutdown_thinstantion.sh_bk
```

## Проверяем каждый скрипт в ручную!!!

Для фиксации ip которые попадают в БАН настраиваем лог агента. Установка..

```plaintext
pwd
/usr/src
yum localinstall nxlog-ce-3.2.2329_rhel7.x86_64.rpm
systemctl enable nxlog
```

Копируем конфиги /etc/nxlog

```plaintext
systemctl restart nxlog
```

**На Astarisk добавляем мониторинг prometheus+grafana**

/etc/asterisk/prometheus.conf

```plaintext
[general]
enabled = yes
core_metrics_enabled = yes
uri = metrics
auth_username = prometheus
auth_password = pass
auth_realm = Asterisk Prometheus Metrics
```

```plaintext
asterisk -vvvvvvr
module reload res_prometheus.so
```

```plaintext
 asterisk -rx "module show like prometheus"
```

В /etc/prometheus/prometheus.yml на **prometheus** добавляем сервера

```plaintext
  - job_name: 'asterisk_vip'
    metrics_path: /metrics
    basic_auth:
      username: prometheus
      password: pass
    static_configs:
      - targets: ['192.168.98.14:8088']
        labels: {pair: 'idp'}
      - targets: ['192.168.121.14:8088']
        labels: {pair: 'pldpbx'}

  - job_name: 'asterisk_nodes'
    metrics_path: /metrics
    basic_auth:
      username: prometheus
      password: pass
    static_configs:
      - targets: ['192.168.98.214:8088']
        labels: {pair: 'idp', node: 'idppbx-214'}
      - targets: ['192.168.98.215:8088']
        labels: {pair: 'idp', node: 'idppbx-215'}
      - targets: ['192.168.121.214:8088']
        labels: {pair: 'pldpbx', node: 'pldpbx-214'}
      - targets: ['192.168.121.215:8088']
        labels: {pair: 'pldpbx', node: 'pldpbx-215'}
```

И делаем  JSON для Grafana

```plaintext
{
  "__inputs": [
    {
      "name": "DS_PROMETHEUS",
      "label": "Prometheus",
      "description": "Оберіть свій Prometheus datasource",
      "type": "datasource",
      "pluginId": "prometheus",
      "pluginName": "Prometheus"
    }
  ],
  "__requires": [],
  "annotations": {
    "list": [
      {
        "builtIn": 1,
        "datasource": {
          "type": "grafana",
          "uid": "-- Grafana --"
        },
        "enable": true,
        "hide": true,
        "iconColor": "rgba(0, 211, 255, 1)",
        "name": "Annotations & Alerts",
        "type": "dashboard"
      }
    ]
  },
  "editable": true,
  "fiscalYearStartMonth": 0,
  "graphTooltip": 0,
  "links": [],
  "liveNow": false,
  "templating": {
    "list": [
      {
        "current": {
          "text": "asterisk_vip",
          "value": "asterisk_vip"
        },
        "description": "job у prometheus.yml, що скрейпить CARP VIP",
        "hide": 0,
        "label": "VIP job",
        "name": "vip_job",
        "options": [
          {
            "text": "asterisk_vip",
            "value": "asterisk_vip",
            "selected": true
          }
        ],
        "query": "asterisk_vip",
        "skipUrlSync": false,
        "type": "textbox"
      },
      {
        "current": {
          "text": "asterisk_nodes",
          "value": "asterisk_nodes"
        },
        "description": "job, що скрейпить фізичні ноди",
        "hide": 0,
        "label": "Nodes job",
        "name": "nodes_job",
        "options": [
          {
            "text": "asterisk_nodes",
            "value": "asterisk_nodes",
            "selected": true
          }
        ],
        "query": "asterisk_nodes",
        "skipUrlSync": false,
        "type": "textbox"
      },
      {
        "allValue": null,
        "current": {
          "text": "All",
          "value": "$__all"
        },
        "datasource": {
          "type": "prometheus",
          "uid": "${DS_PROMETHEUS}"
        },
        "definition": "label_values(asterisk_core_uptime_seconds{job=\"$vip_job\"}, pair)",
        "description": "Список PBX береться автоматично з лейбла pair у prometheus.yml",
        "hide": 0,
        "includeAll": true,
        "label": "PBX (pair)",
        "multi": true,
        "name": "pair",
        "options": [],
        "query": {
          "qryType": 1,
          "query": "label_values(asterisk_core_uptime_seconds{job=\"$vip_job\"}, pair)",
          "refId": "PrometheusVariableQueryEditor-VariableQuery"
        },
        "refresh": 1,
        "regex": "",
        "skipUrlSync": false,
        "sort": 1,
        "type": "query"
      }
    ]
  },
  "panels": [
    {
      "collapsed": false,
      "gridPos": {
        "h": 1,
        "w": 24,
        "x": 0,
        "y": 0
      },
      "id": 100,
      "panels": [],
      "repeat": "pair",
      "repeatDirection": "v",
      "title": "━━━  PBX: $pair  ━━━",
      "type": "row"
    },
    {
      "datasource": {
        "type": "prometheus",
        "uid": "${DS_PROMETHEUS}"
      },
      "fieldConfig": {
        "defaults": {
          "mappings": [],
          "unit": "short",
          "color": {
            "mode": "fixed",
            "fixedColor": "blue"
          },
          "displayName": "${__field.labels.node}"
        },
        "overrides": []
      },
      "gridPos": {
        "h": 4,
        "w": 6,
        "x": 0,
        "y": 1
      },
      "id": 1,
      "options": {
        "colorMode": "value",
        "graphMode": "none",
        "justifyMode": "center",
        "orientation": "vertical",
        "reduceOptions": {
          "calcs": [
            "lastNotNull"
          ],
          "fields": "",
          "values": false
        },
        "textMode": "name"
      },
      "pluginVersion": "10.4.0",
      "targets": [
        {
          "datasource": {
            "type": "prometheus",
            "uid": "${DS_PROMETHEUS}"
          },
          "editorMode": "code",
          "expr": "(asterisk_core_uptime_seconds{job=\"$vip_job\", pair=\"$pair\"} > bool 0) * on(eid) group_left(node) (asterisk_core_uptime_seconds{job=\"$nodes_job\"} > bool 0)",
          "instant": true,
          "legendFormat": "{{node}}",
          "range": false,
          "refId": "A"
        }
      ],
      "title": "Поточний майстер",
      "description": "Хто зараз тримає CARP VIP. Визначається автоматично: eid з VIP-скрейпу зіставляється з лейблом node у job нод.",
      "type": "stat"
    },
    {
      "datasource": {
        "type": "prometheus",
        "uid": "${DS_PROMETHEUS}"
      },
      "fieldConfig": {
        "defaults": {
          "mappings": [],
          "unit": "short",
          "color": {
            "mode": "thresholds"
          },
          "thresholds": {
            "mode": "absolute",
            "steps": [
              {
                "color": "red",
                "value": null
              },
              {
                "color": "green",
                "value": 1
              }
            ]
          }
        },
        "overrides": []
      },
      "gridPos": {
        "h": 4,
        "w": 4,
        "x": 6,
        "y": 1
      },
      "id": 2,
      "options": {
        "colorMode": "background",
        "graphMode": "none",
        "justifyMode": "center",
        "orientation": "vertical",
        "reduceOptions": {
          "calcs": [
            "lastNotNull"
          ],
          "fields": "",
          "values": false
        },
        "textMode": "value"
      },
      "pluginVersion": "10.4.0",
      "targets": [
        {
          "datasource": {
            "type": "prometheus",
            "uid": "${DS_PROMETHEUS}"
          },
          "editorMode": "code",
          "expr": "count(asterisk_endpoints_state{job=\"$vip_job\", pair=\"$pair\", resource=~\"[0-9]+\"} == 2) or vector(0)",
          "instant": true,
          "legendFormat": "онлайн",
          "range": false,
          "refId": "A"
        }
      ],
      "title": "Екстеншени онлайн",
      "type": "stat"
    },
    {
      "datasource": {
        "type": "prometheus",
        "uid": "${DS_PROMETHEUS}"
      },
      "fieldConfig": {
        "defaults": {
          "mappings": [],
          "unit": "short",
          "color": {
            "mode": "fixed",
            "fixedColor": "text"
          }
        },
        "overrides": []
      },
      "gridPos": {
        "h": 4,
        "w": 4,
        "x": 10,
        "y": 1
      },
      "id": 3,
      "options": {
        "colorMode": "value",
        "graphMode": "none",
        "justifyMode": "center",
        "orientation": "vertical",
        "reduceOptions": {
          "calcs": [
            "lastNotNull"
          ],
          "fields": "",
          "values": false
        },
        "textMode": "value"
      },
      "pluginVersion": "10.4.0",
      "targets": [
        {
          "datasource": {
            "type": "prometheus",
            "uid": "${DS_PROMETHEUS}"
          },
          "editorMode": "code",
          "expr": "count(asterisk_endpoints_state{job=\"$vip_job\", pair=\"$pair\", resource=~\"[0-9]+\"}) or vector(0)",
          "instant": true,
          "legendFormat": "всього",
          "range": false,
          "refId": "A"
        }
      ],
      "title": "Екстеншени всього",
      "type": "stat"
    },
    {
      "datasource": {
        "type": "prometheus",
        "uid": "${DS_PROMETHEUS}"
      },
      "fieldConfig": {
        "defaults": {
          "mappings": [],
          "unit": "short",
          "color": {
            "mode": "fixed",
            "fixedColor": "purple"
          }
        },
        "overrides": []
      },
      "gridPos": {
        "h": 4,
        "w": 4,
        "x": 14,
        "y": 1
      },
      "id": 4,
      "options": {
        "colorMode": "value",
        "graphMode": "none",
        "justifyMode": "center",
        "orientation": "vertical",
        "reduceOptions": {
          "calcs": [
            "lastNotNull"
          ],
          "fields": "",
          "values": false
        },
        "textMode": "value"
      },
      "pluginVersion": "10.4.0",
      "targets": [
        {
          "datasource": {
            "type": "prometheus",
            "uid": "${DS_PROMETHEUS}"
          },
          "editorMode": "code",
          "expr": "count(asterisk_endpoints_channels_count{job=\"$vip_job\", pair=\"$pair\", resource=~\"[0-9]+\"} > 0) or vector(0)",
          "instant": true,
          "legendFormat": "розмов",
          "range": false,
          "refId": "A"
        }
      ],
      "title": "Активні розмови",
      "description": "Кількість екстеншенів, що зараз мають активні канали.",
      "type": "stat"
    },
    {
      "datasource": {
        "type": "prometheus",
        "uid": "${DS_PROMETHEUS}"
      },
      "fieldConfig": {
        "defaults": {
          "mappings": [
            {
              "type": "value",
              "options": {
                "0": {
                  "text": "DOWN",
                  "color": "red",
                  "index": 0
                },
                "1": {
                  "text": "UP",
                  "color": "green",
                  "index": 1
                }
              }
            }
          ],
          "unit": "short",
          "color": {
            "mode": "thresholds"
          },
          "thresholds": {
            "mode": "absolute",
            "steps": [
              {
                "color": "red",
                "value": null
              },
              {
                "color": "green",
                "value": 1
              }
            ]
          },
          "displayName": "${__field.labels.node}"
        },
        "overrides": []
      },
      "gridPos": {
        "h": 4,
        "w": 6,
        "x": 18,
        "y": 1
      },
      "id": 5,
      "options": {
        "colorMode": "background",
        "graphMode": "none",
        "justifyMode": "center",
        "orientation": "vertical",
        "reduceOptions": {
          "calcs": [
            "lastNotNull"
          ],
          "fields": "",
          "values": false
        },
        "textMode": "value_and_name"
      },
      "pluginVersion": "10.4.0",
      "targets": [
        {
          "datasource": {
            "type": "prometheus",
            "uid": "${DS_PROMETHEUS}"
          },
          "editorMode": "code",
          "expr": "up{job=\"$nodes_job\", pair=\"$pair\"}",
          "instant": true,
          "legendFormat": "{{node}}",
          "range": false,
          "refId": "A"
        }
      ],
      "title": "Ноди HA",
      "description": "Здоров'я кожної фізичної ноди окремо — ловить мертвий standby.",
      "type": "stat"
    },
    {
      "datasource": {
        "type": "prometheus",
        "uid": "${DS_PROMETHEUS}"
      },
      "fieldConfig": {
        "defaults": {
          "color": {
            "mode": "thresholds"
          },
          "custom": {
            "align": "auto",
            "cellOptions": {
              "type": "auto"
            },
            "filterable": true,
            "inspect": false
          },
          "mappings": [
            {
              "type": "value",
              "options": {
                "0": {
                  "text": "Unknown",
                  "color": "text",
                  "index": 0
                },
                "1": {
                  "text": "Offline",
                  "color": "red",
                  "index": 1
                },
                "2": {
                  "text": "Online",
                  "color": "green",
                  "index": 2
                }
              }
            }
          ],
          "thresholds": {
            "mode": "absolute",
            "steps": [
              {
                "color": "transparent",
                "value": null
              }
            ]
          }
        },
        "overrides": [
          {
            "matcher": {
              "id": "byName",
              "options": "Стан"
            },
            "properties": [
              {
                "id": "custom.cellOptions",
                "value": {
                  "type": "color-background",
                  "mode": "basic",
                  "applyToRow": true
                }
              }
            ]
          }
        ]
      },
      "gridPos": {
        "h": 9,
        "w": 12,
        "x": 0,
        "y": 5
      },
      "id": 6,
      "options": {
        "cellHeight": "sm",
        "footer": {
          "countRows": false,
          "fields": "",
          "reducer": [
            "count"
          ],
          "show": false
        },
        "showHeader": true,
        "sortBy": [
          {
            "displayName": "Стан",
            "desc": false
          }
        ]
      },
      "pluginVersion": "10.4.0",
      "targets": [
        {
          "datasource": {
            "type": "prometheus",
            "uid": "${DS_PROMETHEUS}"
          },
          "editorMode": "code",
          "expr": "asterisk_endpoints_state{job=\"$vip_job\", pair=\"$pair\", resource!~\"[0-9]+\"}",
          "instant": true,
          "legendFormat": "__auto",
          "range": false,
          "refId": "A",
          "format": "table"
        }
      ],
      "title": "Транки",
      "transformations": [
        {
          "id": "organize",
          "options": {
            "excludeByName": {
              "Time": true,
              "__name__": true,
              "job": true,
              "eid": true,
              "pair": true,
              "instance": true
            },
            "indexByName": {},
            "renameByName": {
              "resource": "Транк",
              "tech": "Тех",
              "Value": "Стан"
            }
          }
        }
      ],
      "type": "table"
    },
    {
      "datasource": {
        "type": "prometheus",
        "uid": "${DS_PROMETHEUS}"
      },
      "fieldConfig": {
        "defaults": {
          "color": {
            "mode": "thresholds"
          },
          "custom": {
            "align": "auto",
            "cellOptions": {
              "type": "auto"
            },
            "filterable": true,
            "inspect": false
          },
          "mappings": [
            {
              "type": "value",
              "options": {
                "0": {
                  "text": "Unknown",
                  "color": "text",
                  "index": 0
                },
                "1": {
                  "text": "Offline",
                  "color": "red",
                  "index": 1
                },
                "2": {
                  "text": "Online",
                  "color": "green",
                  "index": 2
                }
              }
            }
          ],
          "thresholds": {
            "mode": "absolute",
            "steps": [
              {
                "color": "transparent",
                "value": null
              }
            ]
          }
        },
        "overrides": [
          {
            "matcher": {
              "id": "byName",
              "options": "Стан"
            },
            "properties": [
              {
                "id": "custom.cellOptions",
                "value": {
                  "type": "color-background",
                  "mode": "basic",
                  "applyToRow": true
                }
              }
            ]
          }
        ]
      },
      "gridPos": {
        "h": 9,
        "w": 12,
        "x": 12,
        "y": 5
      },
      "id": 7,
      "options": {
        "cellHeight": "sm",
        "footer": {
          "countRows": true,
          "fields": "",
          "reducer": [
            "count"
          ],
          "show": true
        },
        "showHeader": true,
        "sortBy": [
          {
            "displayName": "Екстеншен",
            "desc": false
          }
        ]
      },
      "pluginVersion": "10.4.0",
      "targets": [
        {
          "datasource": {
            "type": "prometheus",
            "uid": "${DS_PROMETHEUS}"
          },
          "editorMode": "code",
          "expr": "asterisk_endpoints_state{job=\"$vip_job\", pair=\"$pair\", resource=~\"[0-9]+\"} == 1",
          "instant": true,
          "legendFormat": "__auto",
          "range": false,
          "refId": "A",
          "format": "table"
        }
      ],
      "title": "Екстеншени офлайн",
      "transformations": [
        {
          "id": "organize",
          "options": {
            "excludeByName": {
              "Time": true,
              "__name__": true,
              "job": true,
              "eid": true,
              "pair": true,
              "instance": true,
              "tech": true
            },
            "indexByName": {},
            "renameByName": {
              "resource": "Екстеншен",
              "Value": "Стан"
            }
          }
        }
      ],
      "type": "table",
      "description": "Сюди потрапляють і номери, створені у FreePBX без фізичного апарата — вони будуть офлайн постійно."
    },
    {
      "datasource": {
        "type": "prometheus",
        "uid": "${DS_PROMETHEUS}"
      },
      "fieldConfig": {
        "defaults": {
          "color": {
            "mode": "thresholds"
          },
          "custom": {
            "align": "auto",
            "cellOptions": {
              "type": "auto"
            },
            "filterable": true,
            "inspect": false
          },
          "mappings": [
            {
              "type": "range",
              "options": {
                "from": 1,
                "to": 99,
                "result": {
                  "color": "green",
                  "index": 0
                }
              }
            }
          ],
          "thresholds": {
            "mode": "absolute",
            "steps": [
              {
                "color": "transparent",
                "value": null
              }
            ]
          }
        },
        "overrides": [
          {
            "matcher": {
              "id": "byName",
              "options": "Каналів"
            },
            "properties": [
              {
                "id": "custom.cellOptions",
                "value": {
                  "type": "color-background",
                  "mode": "basic",
                  "applyToRow": true
                }
              }
            ]
          }
        ]
      },
      "gridPos": {
        "h": 8,
        "w": 24,
        "x": 0,
        "y": 14
      },
      "id": 8,
      "options": {
        "cellHeight": "sm",
        "footer": {
          "countRows": true,
          "fields": "",
          "reducer": [
            "count"
          ],
          "show": true
        },
        "showHeader": true,
        "sortBy": [
          {
            "displayName": "Номер",
            "desc": false
          }
        ]
      },
      "pluginVersion": "10.4.0",
      "targets": [
        {
          "datasource": {
            "type": "prometheus",
            "uid": "${DS_PROMETHEUS}"
          },
          "editorMode": "code",
          "expr": "asterisk_endpoints_channels_count{job=\"$vip_job\", pair=\"$pair\"} > 0",
          "instant": true,
          "legendFormat": "__auto",
          "range": false,
          "refId": "A",
          "format": "table"
        }
      ],
      "title": "Хто зараз розмовляє",
      "transformations": [
        {
          "id": "organize",
          "options": {
            "excludeByName": {
              "Time": true,
              "__name__": true,
              "job": true,
              "eid": true,
              "pair": true,
              "instance": true
            },
            "indexByName": {},
            "renameByName": {
              "resource": "Номер",
              "tech": "Тех",
              "Value": "Каналів"
            }
          }
        }
      ],
      "type": "table",
      "description": "Активні канали по ендпоінтах. Метрики res_prometheus не містять другої сторони дзвінка та тривалості — для цього потрібен AMI/ARI."
    }
  ],
  "refresh": "30s",
  "schemaVersion": 39,
  "tags": [
    "asterisk",
    "voip",
    "ha",
    "freepbx"
  ],
  "time": {
    "from": "now-6h",
    "to": "now"
  },
  "timepicker": {},
  "timezone": "",
  "title": "FreePBX / Asterisk — мультинодовий моніторинг",
  "uid": "asterisk-multi",
  "version": 1,
  "weekStart": ""
}
```
