feat: WMT client versioning, release management and force-update playbook

- api/wmt.py: add GET /api/wmt/client/version and GET /api/wmt/client/download endpoints; rewrite submit_update_request with dedup logic
- web/wmt.py: add releases, releases_upload, releases_delete, releases_build routes; build-from-folder excludes hidden/data/venv/pyc files
- web/main.py: admin per-device delete route; clear-device-logs route; pass devices list to admin template
- templates/wmt/releases.html: new release management page (current release info, upload form, build-from-folder card)
- templates/admin.html: replace nuclear clear-devices with clear-logs + per-device delete table
- templates/base.html: add Client Releases nav link in WMT sidebar section
- templates/ansible/execute.html: add Update WMT Code playbook card
- ansible/playbooks/update_wmt_code.yml: rsync WMT_project to clients excluding data/; backs up app.py; restarts wmt service
- ansible_service.py: register update_wmt_code description
- .gitignore: whitelist update_wmt_code.yml
This commit is contained in:
ske087
2026-05-13 16:36:17 +03:00
parent ccad5c1201
commit f1449285ba
10 changed files with 978 additions and 40 deletions
+306
View File
@@ -0,0 +1,306 @@
---
# Update WMT client code from the controller's WMT_project folder
# ──────────────────────────────────────────────────────────────────────────
# Use this for devices that have not yet received the HTTP auto-update,
# or whenever you need to force a code push from the server.
#
# What this playbook does:
# 1. Ensure WMT directory exists on the target
# 2. Back up the current app.py as app.py.bak.<version>
# 3. Copy everything from /home/pi/Desktop/WMT_project/ on the CONTROLLER
# → /home/pi/Desktop/WMT/ on the TARGET
# The data/ directory on the target is fully preserved (never touched)
# 4. Fix file ownership
# 5. Restart the wmt systemd service
#
# The data/ directory (config.txt, idmasa.txt, tag.txt, log.txt, device_info.txt)
# is intentionally excluded — device-specific settings stay intact.
#
# Run via: Ansible > Playbooks > "Update WMT Code"
# ──────────────────────────────────────────────────────────────────────────
- name: Update WMT client code from WMT_project folder
hosts: all
gather_facts: false
become: false
vars:
controller_src: /home/pi/Desktop/WMT_project/
wmt_dir: /home/pi/Desktop/WMT
tasks:
# ── 1. Ensure WMT directory exists ────────────────────────────────────
- name: Ensure WMT directory exists on target
file:
path: "{{ wmt_dir }}"
state: directory
owner: pi
group: pi
mode: '0755'
# ── 2. Back up current app.py ─────────────────────────────────────────
- name: Read first line of current app.py (for backup filename)
shell: head -1 {{ wmt_dir }}/app.py 2>/dev/null || echo "unknown"
register: local_first_line
changed_when: false
ignore_errors: true
- name: Extract local version number
set_fact:
local_version: >-
{{ local_first_line.stdout
| regex_search('version\s+([\d.]+)', '\1')
| first | default('old') }}
- name: Back up current app.py
copy:
src: "{{ wmt_dir }}/app.py"
dest: "{{ wmt_dir }}/app.py.bak.{{ local_version }}"
remote_src: true
owner: pi
group: pi
mode: preserve
ignore_errors: true
- name: Show backup info
debug:
msg: "Backed up app.py v{{ local_version }} → app.py.bak.{{ local_version }}"
# ── 3. Snapshot data/ before copy (audit) ────────────────────────────
- name: List current data/ files (audit)
shell: ls -1 {{ wmt_dir }}/data/ 2>/dev/null || echo "(empty or missing)"
register: data_files_before
changed_when: false
- name: Show data/ files that will be preserved
debug:
msg: "data/ contents (will NOT be changed): {{ data_files_before.stdout_lines }}"
# ── 4. Sync WMT_project → WMT on target, excluding data/ ─────────────
# synchronize uses rsync under the hood; delegate_to pushes
# from the controller to the target.
- name: Sync WMT_project to target (exclude data/ and junk files)
synchronize:
src: "{{ controller_src }}"
dest: "{{ wmt_dir }}/"
recursive: true
delete: false
checksum: true
rsync_opts:
- "--exclude=data/"
- "--exclude=.git/"
- "--exclude=.gitignore"
- "--exclude=__pycache__/"
- "--exclude=*.pyc"
- "--exclude=*.pyo"
- "--exclude=*.log"
- "--exclude=*.bak"
- "--exclude=venv/"
- "--exclude=.venv/"
- "--exclude=node_modules/"
- "--exclude=.*"
register: sync_result
- name: Show sync summary
debug:
msg: "Sync completed. Changed: {{ sync_result.changed }}"
# ── 5. Fix ownership ──────────────────────────────────────────────────
- name: Set correct ownership on WMT directory
become: true
file:
path: "{{ wmt_dir }}"
owner: pi
group: pi
recurse: true
# ── 6. Verify data/ is still intact ──────────────────────────────────
- name: List data/ files after update (verification)
shell: ls -1 {{ wmt_dir }}/data/ 2>/dev/null || echo "(empty)"
register: data_files_after
changed_when: false
- name: Show data/ contents after update (should match before)
debug:
msg: "{{ data_files_after.stdout_lines }}"
# ── 7. Restart WMT service ────────────────────────────────────────────
- name: Restart WMT systemd service
become: true
systemd:
name: wmt
state: restarted
enabled: true
register: service_result
ignore_errors: true
- name: Show service state
debug:
msg: "WMT service state: {{ service_result.status.ActiveState | default('unknown') }}"
when: service_result is not failed
- name: Warn if service restart failed
debug:
msg: "WARNING: wmt service restart failed the device may need a manual reboot."
when: service_result is failed
vars:
wmt_dir: /home/pi/Desktop/WMT
tmp_zip: /tmp/wmt_update.zip
# Controller address override on CLI with -e "server_url=http://..."
server_url: "http://{{ hostvars[inventory_hostname]['ansible_host'] | default(ansible_host) | regex_replace('\\d+\\.\\d+$', '10.76.157.1') }}"
tasks:
# ── 0. Resolve server URL ─────────────────────────────────────────────
# The monitoring server address is read from the device's own config.txt
# so we don't have to hard-code it here.
- name: Read server_host from WMT config.txt
shell: |
grep -E '^\s*server_host\s*=' {{ wmt_dir }}/data/config.txt 2>/dev/null \
| head -1 | awk -F'=' '{print $2}' | tr -d ' \r\n'
register: cfg_server_host
changed_when: false
ignore_errors: true
- name: Read server_port from WMT config.txt
shell: |
grep -E '^\s*server_port\s*=' {{ wmt_dir }}/data/config.txt 2>/dev/null \
| head -1 | awk -F'=' '{print $2}' | tr -d ' \r\n'
register: cfg_server_port
changed_when: false
ignore_errors: true
- name: Set monitoring server base URL
set_fact:
monitoring_base: "http://{{ cfg_server_host.stdout | default('rpi-ansible') }}:{{ cfg_server_port.stdout | default('5000') }}"
- name: Show resolved server URL
debug:
msg: "Monitoring server: {{ monitoring_base }}"
# ── 1. Check latest version on server ────────────────────────────────
- name: Query latest WMT version from monitoring server
uri:
url: "{{ monitoring_base }}/api/wmt/client/version"
method: GET
return_content: true
timeout: 15
register: version_response
ignore_errors: true
- name: Show server version info
debug:
msg: "Server release: v{{ version_response.json.version | default('unknown') }} ({{ version_response.json.filename | default('n/a') }})"
when: version_response is not failed
- name: Fail if server version endpoint unreachable
fail:
msg: "Cannot reach {{ monitoring_base }}/api/wmt/client/version is the server running?"
when: version_response is failed
# ── 2. Get current local version ─────────────────────────────────────
- name: Read first line of local app.py
shell: head -1 {{ wmt_dir }}/app.py 2>/dev/null || echo "unknown"
register: local_first_line
changed_when: false
- name: Extract local version number
set_fact:
local_version: "{{ local_first_line.stdout | regex_search('version\\s+([\\d.]+)', '\\1') | first | default('0') }}"
- name: Show local version
debug:
msg: "Local version: {{ local_version }} | Server version: {{ version_response.json.version }}"
# ── 3. Ensure WMT directory exists ───────────────────────────────────
- name: Ensure WMT directory exists
file:
path: "{{ wmt_dir }}"
state: directory
owner: pi
group: pi
mode: '0755'
# ── 4. Download release zip ───────────────────────────────────────────
- name: Download WMT release zip from monitoring server
get_url:
url: "{{ monitoring_base }}/api/wmt/client/download"
dest: "{{ tmp_zip }}"
force: true
timeout: 120
mode: '0644'
# ── 5. Back up current app.py ─────────────────────────────────────────
- name: Back up current app.py
copy:
src: "{{ wmt_dir }}/app.py"
dest: "{{ wmt_dir }}/app.py.bak.{{ local_version }}"
remote_src: true
owner: pi
group: pi
mode: preserve
ignore_errors: true
# ── 6. Extract zip skip data/ directory ─────────────────────────────
- name: Extract WMT release zip (preserving data/ directory)
shell: |
cd {{ wmt_dir }}
python3 - <<'EOF'
import zipfile, os, sys
zip_path = "{{ tmp_zip }}"
dest = "{{ wmt_dir }}"
skipped = 0
extracted = 0
with zipfile.ZipFile(zip_path, 'r') as zf:
for member in zf.infolist():
p = member.filename.replace('\\', '/')
if p.startswith('data/') or p == 'data':
skipped += 1
continue
zf.extract(member, dest)
extracted += 1
print(f"Extracted {extracted} files, skipped {skipped} data/ entries")
EOF
register: extract_result
changed_when: true
- name: Show extraction result
debug:
msg: "{{ extract_result.stdout }}"
# ── 7. Fix ownership ──────────────────────────────────────────────────
- name: Set correct ownership on WMT directory
become: true
file:
path: "{{ wmt_dir }}"
owner: pi
group: pi
recurse: true
# ── 8. Clean up temp zip ──────────────────────────────────────────────
- name: Remove temporary zip file
file:
path: "{{ tmp_zip }}"
state: absent
# ── 9. Restart WMT service ────────────────────────────────────────────
- name: Restart WMT systemd service
become: true
systemd:
name: wmt
state: restarted
enabled: true
register: service_result
ignore_errors: true
- name: Show service restart result
debug:
msg: "Service state: {{ service_result.status.ActiveState | default('unknown') }}"
when: service_result is not failed
- name: Warn if service restart failed
debug:
msg: "WARNING: wmt service restart failed the device may need a manual reboot."
when: service_result is failed