├── .github └── workflows │ └── docker_build.yml ├── .gitignore ├── Dockerfile ├── README.md ├── device_status_checker.py ├── files ├── apply_macs.service ├── apply_macs.sh ├── gitignore ├── pushgit │ ├── pushgit.sh │ ├── pushgit_inotify.service │ ├── pushgit_inotify.sh │ ├── pushgit_inotify_special.service │ └── pushgit_run_on_start.timer ├── user_cmd.sh └── vestasync.js ├── mdns_search.py ├── miscellaneous ├── panel.png └── versions.jpeg ├── modbus_err_stats.py ├── requirements.txt └── vestasync.py /.github/workflows/docker_build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Publish Docker Image 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v3 15 | 16 | - name: Set up Docker Buildx 17 | uses: docker/setup-buildx-action@v1 18 | 19 | - name: Login to DockerHub 20 | uses: docker/login-action@v1 21 | with: 22 | username: ${{ secrets.DOCKERHUB_USERNAME }} 23 | password: ${{ secrets.DOCKERHUB_TOKEN }} 24 | 25 | - name: Build and push Docker image 26 | uses: docker/build-push-action@v4 27 | with: 28 | context: . 29 | push: true 30 | tags: vvzvlad/vestasync:latest 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.venv 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9 2 | 3 | WORKDIR /app 4 | COPY requirements.txt ./ 5 | RUN pip install --no-cache-dir -r requirements.txt 6 | COPY mdns_search.py ./ 7 | COPY vestasync.py ./ 8 | COPY files files/ 9 | CMD ["python", "vestasync.py"] 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VestaSync 2 | 3 | VestaSync - это ПО для бекапа и восстановления контроллеров Wirenboard. Оно решает две задачи: 4 | 5 | 1. Создание бекапа конфигурации автоматически и деплой ее на удаленный git-сервер (поддерживается Gitea, для поддержки других сервисов необходимо дописать соответствующую функцию создания репозитория) по расписанию (раз в день) и по изменению файлов. 6 | 7 | ![Пример версионности конфигурации](miscellaneous/versions.jpeg) 8 | 9 | 2. Восстановление бекапа одной командой: после подключения нового контроллера достаточно ввести его IP и имя хоста предыдущего контроллера, чтобы Vestasync автоматически восстановила бекап вплоть до MAC-адресов сетевых интерфейсов, чтобы не было нужды менять настройки на DHCP-сервере. После перезагрузки контроллер вернется в сеть с IP старого контроллера. 10 | 11 | VestaSync — это набор скриптов, которые выполняют следующие функции: 12 | 13 | 1. При первоначальной установке на контроллер: 14 | 15 | - Создают в /mnt/data/etc/ git-репозитарий 16 | - Сохраняют текущие MAC-адреса в /mnt/data/etc/vestasync/macs/eth0(1...) 17 | - Сохраняют hostname в /mnt/data/etc/vestasync/hostname 18 | 19 | 2. При изменении конфигурационных файлов 20 | 21 | - Создают коммиты на каждое изменение файлов 22 | - Загружают эти коммиты на сервер 23 | 24 | 3. При восстановлении из бекапа 25 | 26 | - Копируют репозитарий 27 | - Восстанавливают конфиги, hostname и mac-адрес 28 | 29 | 30 | 31 | 32 | ## Установка VestaSync на локальную машину или запуск докер-контейнера 33 | 34 | **Эти команды выполняются не на контроллере**, а на локальной машине или на сервере, с которых есть доступ к контроллеру. Система изначально писалась для инсталляций с множеством контроллеров, поэтому она работает по модели ansible — при запуска на локальной машине сама заходит на пустой контроллер и настраивает его. 35 | Плюс этого подхода в том, что для настройки десяти контроллеров надо просто запустить скрипт локально (подробнее см. Разное-Множественный запуск в этом файле) с разными ```device_ip```, а не заходить на каждый контроллер вручную. 36 | 37 | ```bash 38 | apt update 39 | apt install python3 python3-pip python3-venv git 40 | git clone https://github.com/vvzvlad/vestasync 41 | cd vestasync 42 | python3 -m venv .venv 43 | source .venv/bin/activate 44 | pip3 install -r requirements.txt 45 | ``` 46 | 47 | Можно не устанавливать локально, а запустить докер (см. справку по командам ниже): 48 | 49 | ```bash 50 | docker run --rm -it --name vestasync vvzvlad/vestasync:latest \ 51 | ./vestasync.py --cmd install \ 52 | --device_ip 192.168.1.58 \ 53 | --gitea_address http://192.168.1.38:3001/ \ 54 | --device_new_name WB_1 \ 55 | --gitea_token de8a2eaee0d2f27746157c2fd563815f932d671c 56 | ``` 57 | 58 | ## Команды 59 | 60 | У Vestasync есть всего две команды: `install` и `restore`. 61 | 62 | ### install — установка на контроллер 63 | 64 | Команда `install` выполняет подготовительные действия — устанавливает ПО, создает гит-репозитарий, устанавливает службы (подробнее в разделе "Службы"). 65 | Эту команду надо выполнять, указывая в ```device_ip``` исходный контроллер (который будет стоять в проде инсталляции) перед началом эксплуатации (если выполнять ее еще до настройки, то бонусом получим сохранение конфигов и wb-rules в гите во время разработки и ПНР). Эта команда выполняется на контроллере один раз. 66 | 67 | 68 | Пример запуска (запускается на локальной машине, адрес контроллера указывается в ```device_ip```): 69 | 70 | ```bash 71 | ./vestasync.py \ 72 | --cmd install \ 73 | --device_ip 192.168.1.85 \ 74 | --gitea_address http://192.168.1.101:3001/ \ 75 | --device_new_name WB2 \ 76 | --gitea_token de8a2eaee0d2f27746157c2fd563815f932d671c \ 77 | --user_cmd user_cmd.sh 78 | ``` 79 | 80 | 81 | ```--cmd install``` означает, что надо установить Vestasync на контроллер и подготовить его к созданию бекапа 82 | ```--device_ip``` IP-адрес контроллера 83 | ```--gitea_address``` адрес Gitea-сервера в виде "http://192.168.1.101:3001/", куда будет загружаться бекапы конфигов 84 | ```--device_new_name``` имя контроллера, из которого вместе с SN будет сформировано название контроллера, которое запишется в хостнейм и будет служить именем репозитария с конфигами 85 | ```--gitea_token``` токен для авторизации на Gitea-сервере (получается в интерфейсе Gitea) 86 | ```--user_cmd``` файл sh с командами, которые надо выполняить на контроллере для его настройки под ваши задачи (указывать необязательно). В нем можно описать любые команды, которыми вам надо конфигурировать контроллер: например, установка ключа SSH, установка таймзоны и локали, и так далее. 87 | Пример файла — ```files/user_cmd.sh```: 88 | 89 | ``` 90 | #!/usr/bin/env sh 91 | timedatectl set-timezone Asia/Krasnoyarsk 92 | localectl set-locale LANG=en_GB.UTF-8 93 | service ntp stop 94 | ntpdate pool.ntp.org 95 | service ntp start 96 | hwclock --systohc --localtime 97 | ``` 98 | 99 | 100 | ### restore — восстановление из бекапа 101 | 102 | Команда `restore` выполняет восстановление существующего бекапа на контроллере. 103 | Эту команду надо выполнять на подменном контроллере из ЗИП-а в случае замены основного контроллера подменным. Эта команда выполянется один раз, после чего контроллер становится копией старого контроллера и продолжает сохранять свои изменения в конфигах в тот же репозитарий. 104 | 105 | Пример запуска (запускается на локальной машине, адрес контроллера указывается в ```device_ip```): 106 | 107 | ```bash 108 | ./vestasync.py \ 109 | --cmd restore \ 110 | --device_ip 192.168.1.85 \ 111 | --gitea_address http://192.168.1.101:3001/ \ 112 | --gitea_token de8a2eaee0d2f27746157c2fd563815f932d671c \ 113 | --source_hostname WB2-A3TBJXLS \ 114 | --reinstall_packages yes 115 | ``` 116 | 117 | Используются те же аргументы, что и в ```install```, но дополнительно еще нужен аругмент ```source_hostname```, который определяет имя контроллера, с которого выполняется бекап. ```device_new_name``` не используется, в качестве имени будет взято имя старого контроллера. Опциональный аргумент ```reinstall_packages``` определяет, надо ли устанавливать пакеты, которые были установлены на старом контроллере. 118 | 119 | **Обратите внимание, что после восстановления бекапа на новом контроллере старый контроллер не должен включаться в сеть, иначе произойдет конфликт адресов.** 120 | 121 | ## Службы 122 | 123 | Службы, которые будут запущенны на контроллере при установке: 124 | 125 | ### Восстановление MAC-адресов (apply_macs) 126 | 127 | Служба apply_macs отвечает за применение MAC-адресов к сетевым интерфейсам при загрузке системы. 128 | Эта служба считывает MAC-адреса из файлов, расположенных в каталоге /mnt/data/etc/vestasync/macs/, если они есть, и присваивает их соответствующим интерфейсам, таким как eth0, eth1, wlan0 и т. д. Это используется, если на контроллер был восстанновлен созданный бекап, чтобы сохранять MAC-адреса старого контроллера, и соотвественно, адрес, выданный DHCP. 129 | Для изменения MAC-адресов на изначальные надо просто удалить все файлы и перезагрузиться: 130 | 131 | ``` 132 | rm -rf /mnt/data/etc/vestasync/macs/* 133 | reboot 134 | ``` 135 | 136 | Или, если надо сделать это временно, остановить службу: ```systemctl stop apply_macs.service``` 137 | Обратно запустить: ```systemctl start apply_macs.service``` 138 | Узнать статус: ```systemctl status apply_macs.service``` 139 | 140 | ### Автоматическое версионирование и деплой конфигов (pushgit) 141 | 142 | Службы ```pushgit``` (и таймер ```pushgit.timer```) и ```pushgit_inotify``` обеспечивают автоматическое сохранение конфигов в репозиторий Git на удаленном сервере. 143 | Это позволяет сохранять изменения в файлах и версионировать их, что упрощает управление конфигурационными файлами и предотвращает потерю данных при их случайном изменении или удалении. 144 | Данные сохраняются при каждом сохранении файлов или каждый день, если отключен или не сработал мониторинг сохранения. 145 | Чтобы отключить сохранение каждый день, надо остановить службу: ```systemctl stop pushgit.timer```. Запустить обратно — ```systemctl start pushgit.timer```. 146 | Чтобы отключить сохранение по изменению файлов, надо остановить службу: ```systemctl stop pushgit_inotify.service```. Запустить обратно — ```systemctl start pushgit_inotify.service```. 147 | 148 | Для принудительной загрузки конфигов надо выполнить в консоли контроллера ```systemctl start pushgit.service``` 149 | 150 | Все эти действия можно так же сделать из виджета, скрипт для которого устанавливается на контроллер автоматически. 151 | ![Внешний вид виджета](miscellaneous/panel.png) 152 | 153 | ## Разное 154 | 155 | ### Обновление скриптов 156 | 157 | При повторном запуске команда ```install``` перезапишет файлы скриптов и сервисов для обновления скриптов на существующих контроллерах, если вышла новая версия VestaSync. 158 | В этом случае в ```--device_ip``` можно передать несколько IP-адресов, разделенных пробелами: 159 | 160 | ```bash 161 | ./vestasync.py --cmd install \ 162 | --device_ip 192.168.98.92 192.168.98.85 \ 163 | --gitea_address http://192.168.98.101:3001/ \ 164 | --device_new_name WB1 \ 165 | --gitea_token de8a2eaee0d2f27746157c2fd563815f932d670c 166 | ``` 167 | 168 | Обратите внимание, что устанавливать Vestasync на несколько контроллеров лучше с помощью скрипта ниже из раздела "Множественный запуск", потому что при указании набора из нескольких адресов ```device_ip``` с командой ```install``` у них будет одинаковые имена хостов (```--device_new_name WB1```), отличающееся только серийным номером: WB1-AFYATAO7, WB1-A3TBJXLS и так далее. 169 | 170 | ### Множественный запуск 171 | 172 | Если вам надо запустить скрипт сразу на множестве контроллеров, это можно сделать так: 173 | 174 | ```bash 175 | #!/bin/bash 176 | 177 | GITEA_ADDRESS="http://192.168.1.101:3001/" 178 | GITEA_TOKEN="de8a2eaee0d2f27746157c2fd563815f932d671c" 179 | DEVICES=( 180 | "192.168.1.1 WB1" 181 | "192.168.1.2 WB2" 182 | "192.168.1.3 WB3" 183 | "192.168.1.4 WB4" 184 | ) 185 | 186 | for DEVICE_INFO in "${DEVICES[@]}"; do 187 | IP=$(echo "$DEVICE_INFO" | cut -d ' ' -f1) 188 | DEVICE_NAME=$(echo "$DEVICE_INFO" | cut -d ' ' -f2) 189 | 190 | echo "Run on $IP/$DEVICE_NAME" 191 | ./vestasync.py --cmd install --device_ip "$IP" --gitea_address "$GITEA_ADDRESS" --device_new_name "$DEVICE_NAME" --gitea_token "$GITEA_TOKEN" 192 | done 193 | ``` 194 | 195 | ### Gitea 196 | 197 | В качестве git-сервера используется gitea. Предполагается, что она работает локально, но можно использовать и публичные инсталляции. Устанавливать ее можно любым удобным способом, например с помощью такого docker-compose: 198 | 199 | ``` 200 | version: "3" 201 | 202 | networks: 203 | gitea: 204 | external: false 205 | 206 | services: 207 | server: 208 | image: gitea/gitea:1.19.0 209 | container_name: gitea 210 | environment: 211 | - USER_UID=1000 212 | - USER_GID=1000 213 | - GITEA__database__DB_TYPE=postgres 214 | - GITEA__database__HOST=gitea_pg_db:5432 215 | - GITEA__database__NAME=gitea 216 | - GITEA__database__USER=gitea 217 | - GITEA__database__PASSWD=gitea 218 | restart: always 219 | networks: 220 | - gitea 221 | volumes: 222 | - /root/gitea/data:/data 223 | - /etc/timezone:/etc/timezone:ro 224 | - /etc/localtime:/etc/localtime:ro 225 | ports: 226 | - "3001:3000" 227 | - "222:22" 228 | depends_on: 229 | - db 230 | 231 | db: 232 | image: postgres:14 233 | restart: always 234 | container_name: gitea_pg_db 235 | environment: 236 | - POSTGRES_USER=gitea 237 | - POSTGRES_PASSWORD=gitea 238 | - POSTGRES_DB=gitea 239 | networks: 240 | - gitea 241 | volumes: 242 | - /root/gitea/pg-data:/var/lib/postgresql/data 243 | ``` 244 | 245 | После запуска контейнера, надо перейти в веб-панель Gitea, создать там пользователя "vestasync", после чего получить в его настройках токен доступа, установив все галочки. В дальнейшем этот токен указывается в ```gitea_token```. 246 | -------------------------------------------------------------------------------- /device_status_checker.py: -------------------------------------------------------------------------------- 1 | import paho.mqtt.client as mqtt 2 | import time 3 | import os 4 | from collections import Counter 5 | import argparse 6 | 7 | parser = argparse.ArgumentParser(description="MQTT Device Error Status") 8 | parser.add_argument("-a", "--wb", type=str, required=True, help="WB address") 9 | args = parser.parse_args() 10 | 11 | def get_modbus_devices(): 12 | devices = {} 13 | def on_connect(client, userdata, flags, rc): 14 | client.subscribe("/devices/+/meta/driver") 15 | 16 | def on_message(client, userdata, msg): 17 | if msg.payload.decode() == "wb-modbus": 18 | device_name = msg.topic.split('/')[2] 19 | devices[device_name] = {} 20 | 21 | client = mqtt.Client() 22 | client.on_connect = on_connect 23 | client.on_message = on_message 24 | client.connect(args.wb, 1883, 60) 25 | client.loop_start() 26 | time.sleep(3) 27 | client.loop_stop() 28 | client.unsubscribe("/devices/+/meta/driver") 29 | client.disconnect() 30 | return devices 31 | 32 | def get_all_controls(devices): 33 | def on_connect(client, userdata, flags, rc): 34 | for device in devices.keys(): 35 | client.subscribe(f"/devices/{device}/controls/+") 36 | 37 | def on_message(client, userdata, msg): 38 | device = msg.topic.split('/')[2] 39 | control = msg.topic.split('/')[-1] 40 | devices[device][control] = "noerror" 41 | 42 | client = mqtt.Client() 43 | client.on_connect = on_connect 44 | client.on_message = on_message 45 | client.connect(args.wb, 1883, 60) 46 | client.loop_start() 47 | time.sleep(3) 48 | client.loop_stop() 49 | client.disconnect() 50 | return devices 51 | 52 | def get_all_controls_errors(devices): 53 | def on_connect(client, userdata, flags, rc): 54 | for device, controls in devices.items(): 55 | for control in controls.keys(): 56 | client.subscribe(f"/devices/{device}/controls/{control}/meta/error") 57 | 58 | def on_message(client, userdata, msg): 59 | error = msg.payload.decode() 60 | device = msg.topic.split('/')[2] 61 | control = msg.topic.split('/')[4] 62 | if any(char in error for char in ['r', 'w']): 63 | devices[device][control] = "readwriteerror" 64 | elif error == "p": 65 | devices[device][control] = "perioderror" 66 | else: 67 | devices[device][control] = "noerror" 68 | 69 | client = mqtt.Client() 70 | client.on_connect = on_connect 71 | client.on_message = on_message 72 | client.connect(args.wb, 1883, 60) 73 | client.loop_start() 74 | 75 | def sort_devices(devices): 76 | sorted_devices = sorted(devices.items(), key=lambda item: item[0]) 77 | sorted_devices = sorted(sorted_devices, key=lambda item: sum(value == "readwriteerror" for value in item[1].values()), reverse=True) 78 | return sorted_devices 79 | 80 | 81 | print("Get devices...") 82 | devices = get_modbus_devices() 83 | print("Get controls...") 84 | devices = get_all_controls(devices) 85 | print("Subscribe errors...") 86 | get_all_controls_errors(devices) 87 | 88 | lines_printed = 0 89 | while True: 90 | time.sleep(1) 91 | # Move the cursor up and clear the line for each line printed in the last iteration 92 | for _ in range(lines_printed): 93 | print("\033[F\033[K", end="") 94 | lines_printed = 0 95 | 96 | print("Device error status:") 97 | lines_printed += 1 98 | sorted_devices = sort_devices(devices) 99 | max_device_name_length = max(len(device) for device, _ in sorted_devices) 100 | print(f"+{'-'*(max_device_name_length+2)}+{'-'*7}+{'-'*7}+{'-'*8}+{'-'*8}+") 101 | print(f"| {'Device':<{max_device_name_length}} | {'All':<5} | {'R/W':<5} | {'Period':<5} | {'Normal':<5} |") 102 | print(f"+{'-'*(max_device_name_length+2)}+{'-'*7}+{'-'*7}+{'-'*8}+{'-'*8}+") 103 | lines_printed += 3 104 | for device, controls in sorted_devices: 105 | error_counter = Counter(controls.values()) 106 | normal = len(controls) - error_counter['readwriteerror'] 107 | if normal != len(controls): 108 | if normal == 0: 109 | print("\033[31m", end="") # Start red color 110 | print(f"| {device:<{max_device_name_length}} | {len(controls):<5} | {error_counter['readwriteerror']:<5} | {error_counter['perioderror']:<6} | {normal:<6} |") 111 | lines_printed += 1 112 | if normal == 0: 113 | print("\033[0m", end="") # Reset color 114 | 115 | print(f"+{'-'*(max_device_name_length+2)}+{'-'*7}+{'-'*7}+{'-'*8}+{'-'*8}+") 116 | lines_printed += 1 117 | 118 | -------------------------------------------------------------------------------- /files/apply_macs.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Apply MAC addresses to network interfaces 3 | [Service] 4 | Type=oneshot 5 | ExecStart=/usr/local/bin/apply_macs.sh 6 | [Install] 7 | WantedBy=multi-user.target 8 | -------------------------------------------------------------------------------- /files/apply_macs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | macs_dir="/mnt/data/etc/vestasync/macs" 3 | 4 | for mac_file in "$macs_dir"/*; do 5 | ifname=$(basename "$mac_file") 6 | if [ -f "$mac_file" ]; then 7 | mac_address=$(cat "$mac_file") 8 | ip link set "$ifname" address "$mac_address" 9 | fi 10 | done 11 | -------------------------------------------------------------------------------- /files/gitignore: -------------------------------------------------------------------------------- 1 | wb-mqtt-mbgate.conf 2 | wb-mqtt-opcua.conf 3 | wb-hardware.conf 4 | resolv.conf 5 | nginx/ 6 | NetworkManager/ 7 | -------------------------------------------------------------------------------- /files/pushgit/pushgit.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | apt-mark showmanual > /mnt/data/etc/vestasync/packages 3 | echo $(hostname) > /mnt/data/etc/vestasync/hostname 4 | cd /mnt/data/etc/ 5 | git add . 6 | git commit -m "$(date +"%Y-%m-%d %H:%M:%S %z %Z")" 7 | git push -u origin master 8 | -------------------------------------------------------------------------------- /files/pushgit/pushgit_inotify.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Main pushgit 3 | 4 | [Service] 5 | Type=simple 6 | ExecStart=/usr/local/bin/pushgit_inotify.sh 7 | 8 | [Install] 9 | WantedBy=multi-user.target 10 | -------------------------------------------------------------------------------- /files/pushgit/pushgit_inotify.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | export GIT_AUTHOR_NAME="vestasync_wb_$(hostname)" 3 | export GIT_COMMITTER_NAME="vestasync_wb_$(hostname)" 4 | export LC_TIME=en_GB.UTF-8 5 | EXCLUDE_PATTERN='(^|\/)(\.git|packages|hostname|wb-mqtt-mbgate\.conf|resolv\.conf)($|\/)' 6 | inotifywait -m -r -e close_write,move,create,delete --exclude "$EXCLUDE_PATTERN" --format '%w%f' /mnt/data/etc | while read FILE 7 | do 8 | /usr/local/bin/pushgit.sh 9 | done 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /files/pushgit/pushgit_inotify_special.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Script triggered by file changes in a folder (including subfolders) 3 | 4 | [Service] 5 | ExecStartPre=systemctl stop pushgit_inotify.service 6 | ExecStart=systemctl start pushgit_inotify.service 7 | ExecStop=systemctl stop pushgit_inotify.service 8 | Type=oneshot 9 | RemainAfterExit=true 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | -------------------------------------------------------------------------------- /files/pushgit/pushgit_run_on_start.timer: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Run pushgit.service on start 3 | 4 | [Timer] 5 | OnBootSec=180 6 | Unit=pushgit_inotify_special.service 7 | 8 | [Install] 9 | WantedBy=timers.target 10 | -------------------------------------------------------------------------------- /files/user_cmd.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | cd && mkdir .ssh ; echo "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC/mtlYUEoWutGWNhjGZ8XEV2G6Plt6o96uMRUYwnyHjGrNoz1oEfEWAFXExAp1ovPXI+m2Wm3VUgfDYiURUuqU8r8mRUvIml6lOljXtHVVKtHwMJOS3f3RCbWxGsTiQBIDUcNz8EtIqS5vAWwcj7P+Tsk8S/e/0ge5VdbR1wOTmWEnWc+JemVEMYTUxB5idnaQiB3M7dMguYc5u/7GdGOLyT/f70DABZAw/WCPIsA99/tQqPqp0T3I/r/c8ZpZOvZA9jB8+dXMMFJucoFimzNXmXBqNVIUmzkAUnpM91OUUKp3/mi5cdKdot/s80Tdar/SCszEYfA9j4vZffjfS34h vvzvlad@MBP.local" >> .ssh/authorized_keys 3 | 4 | timedatectl set-timezone Asia/Krasnoyarsk 5 | localectl set-locale LANG=en_GB.UTF-8 6 | 7 | service ntp stop 8 | ntpdate pool.ntp.org 9 | service ntp start 10 | hwclock --systohc --localtime 11 | 12 | apt install serial-tool mc wb-mb-explorer -y 13 | 14 | -------------------------------------------------------------------------------- /files/vestasync.js: -------------------------------------------------------------------------------- 1 | defineVirtualDevice("vestasync", { 2 | title: "Vestasync", 3 | cells: { 4 | last_push: { 5 | type: "text", 6 | value: "Update...", 7 | title: "Last push" 8 | }, 9 | last_commit: { 10 | type: "text", 11 | value: "Update...", 12 | title: "Last commit hash" 13 | }, 14 | autopush_inotify: { 15 | type: "switch", 16 | value: false, 17 | title: "Auto push on files changed" 18 | }, 19 | push_now: { 20 | type: "pushbutton", 21 | value: false, 22 | title: "Commit and push now" 23 | }, 24 | hostname: { 25 | type: "text", 26 | value: "", 27 | title: "Hostname" 28 | } 29 | } 30 | }); 31 | 32 | 33 | function _update_vestasync() { 34 | runShellCommand("git -C /mnt/data/etc log -1 --format=%ct", { 35 | captureOutput: true, 36 | exitCallback: function (exitCode, capturedOutput) { 37 | if (exitCode === 0) { 38 | var last_push_time = parseInt(capturedOutput); 39 | var now = new Date(); 40 | var diff_in_seconds = Math.floor((now.getTime() / 1000) - last_push_time); 41 | var diff_in_minutes = Math.floor(diff_in_seconds / 60); 42 | var diff_in_hours = Math.floor(diff_in_minutes / 60); 43 | var diff_in_days = Math.floor(diff_in_hours / 24); 44 | var human_readable_time = ""; 45 | if (diff_in_days > 0) { 46 | human_readable_time = diff_in_days + " days ago"; 47 | } else if (diff_in_hours > 0) { 48 | human_readable_time = diff_in_hours + " hours ago"; 49 | } else if (diff_in_minutes > 0) { 50 | human_readable_time = diff_in_minutes + " minutes ago"; 51 | } else { 52 | human_readable_time = "just now"; 53 | } 54 | dev.vestasync.last_push = human_readable_time; 55 | } 56 | } 57 | }); 58 | runShellCommand("git -C /mnt/data/etc log -1 --format=%H", { 59 | captureOutput: true, 60 | exitCallback: function (exitCode, capturedOutput) { 61 | if (exitCode === 0) { 62 | var shortenedCommit = capturedOutput.trim().substring(0, 10); 63 | dev.vestasync.last_commit = shortenedCommit; 64 | } 65 | } 66 | }); 67 | 68 | runShellCommand("systemctl is-active pushgit_inotify_special.service", { 69 | captureOutput: true, 70 | exitCallback: function (exitCode, capturedOutput) { 71 | var isEnabled = capturedOutput.trim() === "active"; 72 | getControl("vestasync/autopush_inotify").setValue({ value: isEnabled, notify: false }) 73 | } 74 | }); 75 | 76 | runShellCommand("hostname", { 77 | captureOutput: true, 78 | exitCallback: function (exitCode, capturedOutput) { 79 | if (exitCode === 0) { 80 | var hostname = capturedOutput.trim(); 81 | dev.vestasync.hostname = hostname; 82 | } else { 83 | console.error("Error checking hostname:", capturedOutput.trim()); 84 | } 85 | } 86 | }); 87 | 88 | }; 89 | 90 | 91 | 92 | defineRule("_vestasync_autopush_inotify", { 93 | whenChanged: "vestasync/autopush_inotify", 94 | then: function (newValue, devName, cellName) { 95 | if (dev.vestasync.autopush_inotify) { 96 | runShellCommand("systemctl start pushgit_inotify_special.service"); 97 | _update_vestasync(); 98 | } else { 99 | runShellCommand("systemctl stop pushgit_inotify_special.service"); 100 | _update_vestasync(); 101 | } 102 | } 103 | }); 104 | 105 | 106 | 107 | defineRule("_vestasync_push", { 108 | whenChanged: "vestasync/push_now", 109 | then: function (newValue, devName, cellName) { 110 | if (dev.vestasync.push_now) { 111 | runShellCommand("/usr/local/bin/pushgit.sh"); 112 | } 113 | } 114 | }); 115 | 116 | 117 | _update_vestasync(); 118 | setInterval(_update_vestasync, 60000); 119 | 120 | 121 | -------------------------------------------------------------------------------- /mdns_search.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | import time 4 | from zeroconf import ServiceBrowser, Zeroconf 5 | from threading import Event 6 | 7 | class MyListener: 8 | 9 | def __init__(self): 10 | self.found_services = [] 11 | 12 | def remove_service(self, zeroconf, type, name): 13 | pass 14 | 15 | def add_service(self, zeroconf, type, name): 16 | info = zeroconf.get_service_info(type, name) 17 | self.found_services.append(info) 18 | print(f"Name: {info.name}") 19 | print(f"IP: {info.parsed_addresses()[0]}") 20 | print("") 21 | 22 | def update_service(self, zeroconf, type, name): 23 | pass 24 | 25 | def main(): 26 | zeroconf = Zeroconf() 27 | listener = MyListener() 28 | browser = ServiceBrowser(zeroconf, "_workstation._tcp.local.", listener) 29 | 30 | try: 31 | while True: 32 | time.sleep(0.1) 33 | except KeyboardInterrupt: 34 | pass 35 | finally: 36 | browser.cancel() 37 | zeroconf.close() 38 | 39 | if __name__ == "__main__": 40 | main() 41 | -------------------------------------------------------------------------------- /miscellaneous/panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzvlad/vestasync/e898e4e21e0fe0f29811416c29ff91bba7813172/miscellaneous/panel.png -------------------------------------------------------------------------------- /miscellaneous/versions.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzvlad/vestasync/e898e4e21e0fe0f29811416c29ff91bba7813172/miscellaneous/versions.jpeg -------------------------------------------------------------------------------- /modbus_err_stats.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import subprocess 3 | import re 4 | import os 5 | import json 6 | import argparse 7 | 8 | def restart_service(skip_restart=False, history=False): 9 | if not skip_restart and not history: 10 | subprocess.Popen(["systemctl", "restart", "wb-mqtt-serial"], stdout=subprocess.PIPE) 11 | 12 | def parse_config_file(filename): 13 | with open(filename, "r") as file: 14 | config_data = json.load(file) 15 | 16 | device_to_port = {} 17 | device_stats = {} 18 | for port in config_data["ports"]: 19 | for device in port["devices"]: 20 | device_to_port[device["slave_id"]] = port["path"] 21 | device_stats[device["slave_id"]] = {"type": device.get("device_type", "Unknown type"), "errors": 0, "disconnects": 0, "write_failures": 0, "invalid_crc_errors": 0} # New line 22 | 23 | return device_to_port, device_stats 24 | 25 | 26 | def parse_journal(device_to_port, device_stats, skip_lines=10, history=False): 27 | cmd = ["journalctl", "-f", "-u", "wb-mqtt-serial"] if not history else ["journalctl", "-u", "wb-mqtt-serial"] 28 | p = subprocess.Popen(cmd, stdout=subprocess.PIPE) 29 | last_log_line = None 30 | line_counter = 0 31 | 32 | for line in iter(p.stdout.readline, b''): 33 | line_counter += 1 34 | if line_counter <= skip_lines: 35 | continue 36 | 37 | line = line.decode('utf-8') # convert bytes to string 38 | match_error = re.search(r'modbus:(\d+): Serial protocol error: request timed out', line) 39 | match_disconnect = re.search(r'INFO: \[serial device\] device modbus:(\d+) is disconnected', line) 40 | match_write_failure = re.search(r'WARNING: \[modbus\] failed to write: http|https)://(?P[^:]+):(?P\d+)(/.*|)$' 44 | match = re.match(pattern, address) 45 | if match: 46 | return match.group('protocol'), match.group('host'), match.group('port') 47 | else: 48 | raise ValueError("Invalid address format") 49 | 50 | 51 | def get_short_sn(c): 52 | global device_short_sn 53 | device_short_sn = c.run('wb-gen-serial -s', hide=True).stdout.strip() 54 | if device_short_sn is None: 55 | raise ValueError("Both device_new_name and device_short_sn must be provided") 56 | 57 | def set_hostname(c): 58 | c.run(f'hostnamectl set-hostname {args.device_new_name}-{device_short_sn}') 59 | hostname = c.run('hostname', hide=True).stdout.strip() 60 | return hostname 61 | 62 | def save_hostname(c): 63 | c.run(f'echo $(hostname) > /mnt/data/etc/vestasync/hostname') 64 | hostname = c.run('hostname', hide=True).stdout.strip() 65 | return hostname 66 | 67 | def restore_hostname(c): 68 | c.run(f'hostnamectl set-hostname $(cat /mnt/data/etc/vestasync/hostname)') 69 | 70 | 71 | def prepare_packages_wb(c): 72 | c.run('apt-get update') 73 | c.run('apt-get install git apt-transport-https ca-certificates htop sudo mc wget curl jq zip gzip tar -y') 74 | c.run('apt-get -y autoremove') 75 | c.run('apt-get -y clean') 76 | c.run('apt-get -y autoclean ') 77 | 78 | 79 | def configure_git(c): 80 | c.run(f'git config --global user.name vestasync_wb_$(hostname)_manual') 81 | c.run(f'git config --global user.email "vestasync@fake.mail"') 82 | c.run(f'git config --global init.defaultBranch "master"') 83 | 84 | def create_repo(c): 85 | hostname = c.run('hostname', hide=True).stdout.strip() 86 | headers = {'Authorization': f'token {args.gitea_token}', 'Content-Type': 'application/json'} 87 | data = {"name": hostname, "private": False} 88 | response = requests.post(f'{args.vestasync_gitea_protocol}://{args.vestasync_gitea_host}:{args.vestasync_gitea_port}/api/v1/user/repos', headers=headers, json=data) 89 | if response.status_code == 201: # 201 - Created, ожидаемый код успешного создания репозитория 90 | print("[VestaSync] Repository created successfully.") 91 | elif response.status_code == 409: # 409 - Conflict, репозиторий уже существует 92 | print("[VestaSync] Error: Repository already exists.") 93 | print("[VestaSync] Exiting...") 94 | sys.exit(1) 95 | else: 96 | print(f"[VestaSync] Create repo error: Unexpected HTTP status code {response.status_code}") 97 | print("[VestaSync] Exiting...") 98 | sys.exit(1) 99 | 100 | 101 | def init_repo(c): 102 | hostname = c.run('hostname', hide=True).stdout.strip() 103 | c.run('cd /mnt/data/etc/ && git init') 104 | c.run('echo "wb-mqtt-mbgate.conf" > /mnt/data/etc/.gitignore') 105 | c.run('echo "wb-mqtt-opcua.conf" >> /mnt/data/etc/.gitignore') 106 | c.run(f'cd /mnt/data/etc/ && git remote add origin {args.vestasync_gitea_protocol}://{gitea_user}:{args.gitea_token}@{args.vestasync_gitea_host}:{args.vestasync_gitea_port}/{gitea_user}/{hostname}.git') 107 | 108 | 109 | def copy_wb_rule(c): 110 | c.put("./files/vestasync.js", "/mnt/data/etc/wb-rules/vestasync.js") 111 | 112 | def create_automac_systemd(c): 113 | #disable 114 | for service in ['apply_macs.service']: 115 | c.run(f'systemctl stop {service}', hide=True, warn=True) 116 | c.run(f'systemctl disable {service}', hide=True, warn=True) 117 | 118 | file_paths = { #local path: remote path 119 | './files/apply_macs.sh': '/usr/local/bin/apply_macs.sh', 120 | './files/apply_macs.service': '/etc/systemd/system/apply_macs.service', 121 | } 122 | 123 | for local_path, remote_path in file_paths.items(): 124 | c.put(local_path, remote_path) 125 | c.run(f"chmod +x {remote_path}") 126 | 127 | #reload 128 | c.run("systemctl daemon-reload") 129 | 130 | #enable and start 131 | for service in ['apply_macs.service']: 132 | c.run(f'systemctl enable {service}', hide=True, warn=True) 133 | #c.run(f'systemctl start {service}') 134 | 135 | 136 | #check statuses 137 | for service in ['apply_macs.service']: 138 | active = c.run(f'systemctl is-active {service}', hide=True, warn=True).stdout.strip() 139 | enabled = c.run(f'systemctl is-enabled {service}', hide=True, warn=True).stdout.strip() 140 | print(f"[VestaSync] Service {service}: {active}, {enabled}") 141 | 142 | 143 | 144 | 145 | def create_autogit_systemd(c): 146 | #disable and remove 147 | print("[VestaSync] Autogit: stop and disable services") 148 | for service in ['pushgit.timer', 149 | 'pushgit_inotify_special.service', 150 | 'pushgit_inotify.service', 151 | 'pushgit_run_on_start.timer' ]: 152 | c.run(f'systemctl stop {service}', hide=True, warn=True) 153 | c.run(f'systemctl disable {service}', hide=True, warn=True) 154 | 155 | print("[VestaSync] Autogit: Remove old files") 156 | c.run(f'rm /etc/systemd/system/pushgit*', hide=True, warn=True) 157 | c.run(f'rm /usr/local/bin/pushgit*', hide=True, warn=True) 158 | 159 | 160 | print("[VestaSync] Autogit: copy new files, chmod +x") 161 | file_paths = { #local path: remote path 162 | './files/pushgit/pushgit.sh': '/usr/local/bin/pushgit.sh', 163 | './files/pushgit/pushgit_inotify.sh': '/usr/local/bin/pushgit_inotify.sh', 164 | './files/pushgit/pushgit_inotify.service': '/etc/systemd/system/pushgit_inotify.service', 165 | './files/pushgit/pushgit_run_on_start.timer': '/etc/systemd/system/pushgit_run_on_start.timer', 166 | './files/pushgit/pushgit_inotify_special.service': '/etc/systemd/system/pushgit_inotify_special.service', 167 | } 168 | 169 | for local_path, remote_path in file_paths.items(): 170 | c.put(local_path, remote_path) 171 | c.run(f"chmod +x {remote_path}") 172 | 173 | print("[VestaSync] Autogit: reload configs") 174 | c.run("systemctl daemon-reload", hide=True, warn=True) 175 | 176 | #enable and start 177 | print("[VestaSync] Autogit: enable run on start") 178 | for service in ['pushgit_run_on_start.timer']: 179 | c.run(f'systemctl enable {service}', hide=True, warn=True) 180 | 181 | print("[VestaSync] Autogit: start inotify") 182 | for service in ['pushgit_inotify_special.service']: 183 | c.run(f'systemctl start {service}', hide=True, warn=True) 184 | 185 | 186 | #check statuses 187 | for service in ['pushgit_run_on_start.timer', 'pushgit_inotify.service', 'pushgit_inotify_special.service']: 188 | active = c.run(f'systemctl is-active {service} || true', hide=True).stdout.strip() 189 | enabled = c.run(f'systemctl is-enabled {service} || true', hide=True).stdout.strip() 190 | print(f"[VestaSync] Service {service}: {active}, {enabled}") 191 | 192 | def mark_original_restored(c, mark): 193 | if mark == "original": 194 | c.run("rm /mnt/data/etc/vestasync/restored", warn=True, hide=True) 195 | c.run("touch /mnt/data/etc/vestasync/original", warn=True, hide=True) 196 | if mark == "restored": 197 | c.run("touch /mnt/data/etc/vestasync/restored", warn=True, hide=True) 198 | c.run("rm /mnt/data/etc/vestasync/original", warn=True, hide=True) 199 | 200 | def reboot(c): 201 | c.run("reboot > /dev/null 2>&1", warn=True) 202 | 203 | def git_remove_remote(c): 204 | hostname = c.run('hostname', hide=True).stdout.strip() 205 | c.run(f'cd /mnt/data/etc/ && git remote | xargs -L1 git remote remove', warn=True, hide=True) 206 | 207 | 208 | def git_clone(c): 209 | c.run(f'rm -rf /mnt/data/{args.source_hostname}_etc ', warn=True) 210 | c.run(f'mkdir -p /mnt/data/{args.source_hostname}_etc ', hide=True) 211 | c.run(f'git clone {args.vestasync_gitea_protocol}://{gitea_user}:{args.gitea_token}@{args.vestasync_gitea_host}:{args.vestasync_gitea_port}/{gitea_user}/{args.source_hostname}.git /mnt/data/{args.source_hostname}_etc') 212 | 213 | def copy_etc(c): 214 | current_date = datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S") 215 | 216 | archive_name = f"backup_of_vestasync_restore_{current_date}.tar.gz" 217 | print(f"[VestaSync] Remove old .git...") 218 | c.run(f"rm -rf /mnt/data/etc/.git", warn=True, hide=True) 219 | print(f"[VestaSync] Create backup: /mnt/data/{archive_name}") 220 | c.run(f"tar -czvf /mnt/data/{archive_name} -C /mnt/data etc", hide=True) 221 | 222 | files_and_folders = c.run(f"find /mnt/data/{args.source_hostname}_etc", hide=True).stdout.strip().split('\n') 223 | files_and_folders = [item for item in files_and_folders if ".git" not in item] 224 | 225 | for item in files_and_folders: 226 | dest_item = item.replace(f"/{args.source_hostname}_etc/", "/etc/") 227 | if c.run(f"test -f {item}", hide=True, warn=True).ok: 228 | c.run(f"cat {item} > {dest_item}") 229 | elif c.run(f"test -d {item}", hide=True, warn=True).ok: 230 | c.run(f"mkdir -p {dest_item}") 231 | print(f"Restore: {item} -> {dest_item}") 232 | 233 | print(f"[VestaSync] Copy source .git...") 234 | c.run(f"cp -R /mnt/data/{args.source_hostname}_etc/.git /mnt/data/etc/.git") 235 | 236 | print(f"[VestaSync] Remove source etc...") 237 | c.run(f"rm -rf /mnt/data/{args.source_hostname}_etc") 238 | 239 | print(f"[VestaSync] Restore completed") 240 | 241 | def ppush_the_repo(c): 242 | c.run('cd /mnt/data/etc/ && git add .', hide=True) 243 | try: 244 | c.run('GIT_AUTHOR_NAME="vestasync_wb_$(hostname)_update" GIT_COMMITTER_NAME=$GIT_AUTHOR_NAME cd /mnt/data/etc/ && git commit -m "$(date)"', hide=True) 245 | except UnexpectedExit as e: 246 | if 'nothing to commit' in e.result.stdout: 247 | print("Nothing to commit, exit") 248 | else: 249 | print(f"Error: {e.result.stderr}") 250 | c.run('cd /mnt/data/etc/ && git push --force --set-upstream -u origin master', hide=True) 251 | 252 | def run_user_cmd(c, file): 253 | user_cmd_file = "/tmp/user_cmd.sh" 254 | c.put(file, user_cmd_file) 255 | c.run(f"bash {user_cmd_file}") 256 | c.run(f"rm {user_cmd_file}") 257 | 258 | def save_mac_in_cfg(c): 259 | hostname = c.run('hostname', hide=True).stdout.strip() 260 | interfaces_info = c.run("ip -j a", hide=True).stdout.strip() 261 | interfaces_data = json.loads(interfaces_info) 262 | c.run("mkdir -p /mnt/data/etc/vestasync/macs") 263 | for interface in interfaces_data: 264 | ifname = interface["ifname"] 265 | if re.match(r'^(eth|wlan)', ifname): 266 | mac_address = interface["address"] 267 | c.run(f"echo {mac_address} > /mnt/data/etc/vestasync/macs/{ifname}") 268 | 269 | 270 | def save_packages(c): 271 | c.run("apt-mark showmanual > /mnt/data/etc/vestasync/packages") 272 | 273 | def install_packages(c): 274 | c.run("xargs -a user_installed_packages.txt apt-get install -y", warn=True) 275 | 276 | def check_vestasync_installed(c): 277 | vestasync_path = "/mnt/data/etc/vestasync" 278 | result = c.run(f"test -d {vestasync_path}", warn=True) 279 | return result.ok 280 | 281 | def device_update(c): 282 | print("[VestaSync] Found vestasync! Update...") 283 | c.run(f'systemctl disable pushgit_inotify.service', warn=True) 284 | print("[VestaSync] Install new wb rule, automac/autogit...") 285 | copy_wb_rule(c) 286 | create_automac_systemd(c) 287 | create_autogit_systemd(c) 288 | print("[VestaSync] Pushing updated cfg's...") 289 | ppush_the_repo(c) 290 | print("[VestaSync] Update vestasync complete\n") 291 | 292 | def device_install(c): 293 | print("[VestaSync] Not found vestasync! Install...") 294 | print("[VestaSync] Update and install packages...") 295 | prepare_packages_wb(c) 296 | 297 | print("[VestaSync] Configuring git...") 298 | configure_git(c) 299 | 300 | print("[VestaSync] Setting hostname...") 301 | get_short_sn(c) 302 | set_hostname(c) 303 | 304 | if args.user_cmd is not None: 305 | print("[VestaSync] Run users cmd's...") 306 | run_user_cmd(c, args.user_cmd) 307 | 308 | print("[VestaSync] Initializing local repo and add remote...") 309 | init_repo(c) 310 | 311 | print("[VestaSync] Creating repo on gitea...") 312 | create_repo(c) 313 | 314 | print("[VestaSync] Pushing raw cfg's...") 315 | ppush_the_repo(c) 316 | 317 | print("[VestaSync] Saving mac, packages and hostname in cfg...") 318 | save_mac_in_cfg(c) 319 | save_packages(c) 320 | hostname = save_hostname(c) 321 | 322 | print("[VestaSync] Install wb rule, automac/autogit...") 323 | copy_wb_rule(c) 324 | create_automac_systemd(c) 325 | create_autogit_systemd(c) 326 | 327 | print("[VestaSync] Pushing updated cfg's...") 328 | ppush_the_repo(c) 329 | 330 | print("[VestaSync] Marking controller as original...") 331 | mark_original_restored(c, "original") 332 | 333 | print("[VestaSync] Rebooting...") 334 | reboot(c) 335 | 336 | print(f"[VestaSync] Install vestasync complete (hostname {hostname}), rebooting target device..\n") 337 | 338 | 339 | def device_install_or_update(): 340 | print(f"[VestaSync] Install/update command on host(s) {', '.join(args.device_ip)}") 341 | for device_ip in args.device_ip: 342 | with Connection(host=device_ip, port=args.device_port, user=device_user, connect_kwargs={"password": "wirenboard"}) as c: 343 | print(f"\n[VestaSync] Connect to {device_ip} as {device_user}..") 344 | try: 345 | if not check_vestasync_installed(c): 346 | device_install(c) 347 | else: 348 | device_update(c) 349 | except socket.timeout: 350 | print(f"Failed to connect to the host {device_ip}") 351 | 352 | 353 | def device_restore(): 354 | for device_ip in args.device_ip: 355 | with Connection(host=device_ip, user=device_user, connect_kwargs={"password": "wirenboard"}) as c: 356 | print(f"\n[VestaSync] Connect to {device_ip} as {device_user}..") 357 | try: 358 | if not check_vestasync_installed(c): 359 | print("[VestaSync] Not found vestasync! Install...") 360 | prepare_packages_wb(c) 361 | configure_git(c) 362 | print(f"[VestaSync] Restore to {device_ip} backup from {args.source_hostname}") 363 | git_clone(c) 364 | copy_etc(c) 365 | restore_hostname(c) 366 | if args.reinstall_packages is not None: 367 | install_packages(c) 368 | #ppush_the_repo(c) #TODO: не работает! 369 | create_autogit_systemd(c) 370 | create_automac_systemd(c) 371 | mark_original_restored(c, "restored") 372 | if args.user_cmd is not None: 373 | run_user_cmd(c, args.user_cmd) 374 | #ppush_the_repo(c) 375 | reboot(c) 376 | print(f"[VestaSync] Restore backup complete (hostname {args.source_hostname}), rebooting target device..\n") 377 | except socket.timeout: 378 | print(f"[VestaSync] Failed to connect to the host {device_ip}") 379 | 380 | 381 | 382 | 383 | 384 | 385 | if __name__ == '__main__': 386 | try: 387 | args.vestasync_gitea_protocol, args.vestasync_gitea_host, args.vestasync_gitea_port = parse_address(args.gitea_address) 388 | del args.gitea_address 389 | except ValueError as e: 390 | print(e) 391 | exit(1) 392 | 393 | if cmd_args.cmd == "install": 394 | device_install_or_update() 395 | if cmd_args.cmd == "restore": 396 | device_restore() 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | --------------------------------------------------------------------------------