(intelligence_dir: Optional[str])
| 3655 | shared_data.save_config() |
| 3656 | logger.success(f"[mesh] joined tailnet as {hostname or socket.gethostname()}") |
| 3657 | else: |
| 3658 | logger.warning(f"[mesh] join failed: {message}") |
| 3659 | return jsonify({'success': ok, 'message': message}), (200 if ok else 400) |
| 3660 | |
| 3661 | |
| 3662 | @app.route('/api/mesh/leave', methods=['POST']) |
| 3663 | def mesh_leave(): |
| 3664 | """Log this node out of its tailnet.""" |
| 3665 | if not mesh_available: |
| 3666 | return jsonify({'success': False, 'error': 'mesh_manager unavailable'}), 503 |
| 3667 | ok, message = mesh_manager.leave() |
| 3668 | return jsonify({'success': ok, 'message': message}), (200 if ok else 400) |
| 3669 | |
| 3670 | |
| 3671 | @app.route('/api/mesh/diagnose', methods=['POST']) |
| 3672 | def mesh_diagnose(): |
| 3673 | """Probe a specific peer from this unit and classify why it does/doesn't |
| 3674 | answer. Operator action (POST, session-gated) — turns a red "did not answer" |
| 3675 | card into the actual cause (app down / wrong port / ACL / auth).""" |
| 3676 | if not mesh_available: |
| 3677 | return jsonify({'success': False, 'error': 'mesh_manager unavailable'}), 503 |
| 3678 | data = request.get_json(silent=True) or {} |
| 3679 | ip = (data.get('ip') or '').strip() |
| 3680 | if not ip: |
| 3681 | return jsonify({'success': False, 'message': 'No peer address to probe.'}), 400 |
| 3682 | try: |
| 3683 | port = int(data.get('port') or _mesh_node_port()) |
| 3684 | except (TypeError, ValueError): |
| 3685 | port = _mesh_node_port() |
| 3686 | try: |
| 3687 | timeout = max(2, int(shared_data.config.get('mesh_poll_timeout', 6))) |
| 3688 | except (TypeError, ValueError): |
| 3689 | timeout = 6 |
| 3690 | result = mesh_manager.diagnose_peer(ip, port=port, timeout=timeout, |
| 3691 | mesh_tag=_mesh_tag()) |
| 3692 | return jsonify({'success': True, 'result': result}) |
| 3693 | |
| 3694 | |
| 3695 | @app.route('/api/mesh/install', methods=['POST']) |
| 3696 | def mesh_install(): |
| 3697 | """Install the Tailscale client on this unit from the web UI. |
| 3698 | |
| 3699 | A stock Ragnar does not ship Tailscale — the mesh is opt-in — and `update` |
| 3700 | only installs it once a unit has opted in (auth key, boot config, or |
| 3701 | mesh_enabled). This route is the third onboarding path: a fresh unit whose |
| 3702 | operator opened the Mesh tab and wants to start here. Idempotent; the script |
| 3703 | no-ops if Tailscale is already present. |
| 3704 | """ |
| 3705 | global _mesh_installing |
| 3706 | if not mesh_available: |
| 3707 | return jsonify({'success': False, 'error': 'mesh_manager unavailable'}), 503 |
| 3708 | if mesh_manager.installed(): |
| 3709 | return jsonify({'success': True, 'message': 'Tailscale is already installed.', |
| 3710 | 'installing': False}) |
| 3711 | if not os.path.isfile(MESH_INSTALL_SCRIPT): |
| 3712 | return jsonify({'success': False, |
| 3713 | 'message': 'scripts/setup_mesh.sh is missing — reinstall or update Ragnar.'}), 500 |
no test coverage detected