Coverage for integrations / social / api_provision.py: 62.0%
92 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-12 04:49 +0000
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-12 04:49 +0000
1"""
2HART OS Provisioning API — REST endpoints for network provisioning.
4Blueprint mounted at /api/provision/
6Endpoints:
7 POST /api/provision/deploy — Trigger remote provisioning
8 GET /api/provision/nodes — List provisioned nodes
9 GET /api/provision/nodes/<id> — Get node detail
10 POST /api/provision/scan — Scan network for targets
11 POST /api/provision/update/<id> — Update remote node
12 DELETE /api/provision/nodes/<id> — Decommission node
13 POST /api/provision/preflight — Run preflight checks only
14"""
16import logging
17from datetime import datetime
18from flask import Blueprint, request, jsonify
20logger = logging.getLogger('hevolve_provision_api')
22provision_bp = Blueprint('provision', __name__, url_prefix='/api/provision')
25@provision_bp.route('/deploy', methods=['POST'])
26def deploy():
27 """Trigger remote HART OS provisioning via SSH."""
28 data = request.get_json() or {}
30 target_host = data.get('target_host')
31 if not target_host:
32 return jsonify({'error': 'target_host is required'}), 400
34 ssh_user = data.get('ssh_user', 'root')
35 ssh_key_path = data.get('ssh_key_path')
36 join_peer = data.get('join_peer')
37 backend_port = data.get('backend_port', 6777)
38 no_vision = data.get('no_vision', False)
39 no_llm = data.get('no_llm', False)
41 try:
42 from integrations.agent_engine.network_provisioner import NetworkProvisioner
44 result = NetworkProvisioner.provision_remote(
45 target_host=target_host,
46 ssh_user=ssh_user,
47 ssh_key_path=ssh_key_path,
48 join_peer=join_peer,
49 backend_port=backend_port,
50 no_vision=no_vision,
51 no_llm=no_llm,
52 provisioned_by=data.get('provisioned_by', 'api'),
53 )
55 status_code = 200 if result.get('success') else 500
56 return jsonify(result), status_code
58 except Exception as e:
59 logger.error("Provisioning error: %s", e)
60 return jsonify({'error': str(e)}), 500
63@provision_bp.route('/nodes', methods=['GET'])
64def list_nodes():
65 """List all provisioned HART OS nodes."""
66 try:
67 from integrations.social.models import get_db, ProvisionedNode
68 db = get_db()
69 try:
70 nodes = db.query(ProvisionedNode).all()
71 return jsonify({
72 'count': len(nodes),
73 'nodes': [_node_to_dict(n) for n in nodes],
74 })
75 finally:
76 db.close()
77 except Exception as e:
78 logger.error("List nodes error: %s", e)
79 return jsonify({'error': str(e)}), 500
82@provision_bp.route('/nodes/<int:node_id>', methods=['GET'])
83def get_node(node_id):
84 """Get details of a specific provisioned node."""
85 try:
86 from integrations.social.models import get_db, ProvisionedNode
87 db = get_db()
88 try:
89 node = db.query(ProvisionedNode).filter_by(id=node_id).first()
90 if not node:
91 return jsonify({'error': 'Node not found'}), 404
92 return jsonify(_node_to_dict(node))
93 finally:
94 db.close()
95 except Exception as e:
96 return jsonify({'error': str(e)}), 500
99@provision_bp.route('/scan', methods=['POST'])
100def scan_network():
101 """Scan network for provisionable machines."""
102 data = request.get_json() or {}
103 subnet = data.get('subnet')
105 try:
106 from integrations.agent_engine.network_provisioner import NetworkProvisioner
107 targets = NetworkProvisioner.discover_network_targets(subnet=subnet)
108 return jsonify({
109 'count': len(targets),
110 'targets': targets,
111 'subnet': subnet or 'auto-detected',
112 })
113 except Exception as e:
114 return jsonify({'error': str(e)}), 500
117@provision_bp.route('/update/<int:node_id>', methods=['POST'])
118def update_node(node_id):
119 """Update HART OS on a provisioned node."""
120 try:
121 from integrations.social.models import get_db, ProvisionedNode
122 from integrations.agent_engine.network_provisioner import NetworkProvisioner
124 db = get_db()
125 try:
126 node = db.query(ProvisionedNode).filter_by(id=node_id).first()
127 if not node:
128 return jsonify({'error': 'Node not found'}), 404
130 result = NetworkProvisioner.update_remote(
131 target_host=node.target_host,
132 ssh_user=node.ssh_user,
133 )
135 if result.get('success'):
136 node.last_health_check = datetime.utcnow()
137 node.status = 'active'
138 db.commit()
140 return jsonify(result)
141 finally:
142 db.close()
143 except Exception as e:
144 return jsonify({'error': str(e)}), 500
147@provision_bp.route('/nodes/<int:node_id>', methods=['DELETE'])
148def decommission_node(node_id):
149 """Decommission a provisioned node (marks as offline, does NOT uninstall)."""
150 try:
151 from integrations.social.models import get_db, ProvisionedNode
152 db = get_db()
153 try:
154 node = db.query(ProvisionedNode).filter_by(id=node_id).first()
155 if not node:
156 return jsonify({'error': 'Node not found'}), 404
158 node.status = 'decommissioned'
159 db.commit()
161 return jsonify({
162 'success': True,
163 'message': f'Node {node.target_host} marked as decommissioned',
164 })
165 finally:
166 db.close()
167 except Exception as e:
168 return jsonify({'error': str(e)}), 500
171@provision_bp.route('/preflight', methods=['POST'])
172def preflight():
173 """Run preflight checks on a target machine without installing."""
174 data = request.get_json() or {}
175 target_host = data.get('target_host')
176 if not target_host:
177 return jsonify({'error': 'target_host is required'}), 400
179 try:
180 from integrations.agent_engine.network_provisioner import NetworkProvisioner
181 result = NetworkProvisioner.preflight_check(
182 target_host=target_host,
183 ssh_user=data.get('ssh_user', 'root'),
184 ssh_key_path=data.get('ssh_key_path'),
185 )
186 return jsonify(result)
187 except Exception as e:
188 return jsonify({'error': str(e)}), 500
191def _node_to_dict(node) -> dict:
192 """Convert ProvisionedNode ORM object to dict."""
193 return {
194 'id': node.id,
195 'target_host': node.target_host,
196 'ssh_user': node.ssh_user,
197 'node_id': node.node_id,
198 'capability_tier': node.capability_tier,
199 'status': node.status,
200 'installed_version': node.installed_version,
201 'provisioned_at': node.provisioned_at.isoformat() if node.provisioned_at else None,
202 'last_health_check': node.last_health_check.isoformat() if node.last_health_check else None,
203 'provisioned_by': node.provisioned_by,
204 'error_message': node.error_message,
205 }