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

1""" 

2HART OS Provisioning API — REST endpoints for network provisioning. 

3 

4Blueprint mounted at /api/provision/ 

5 

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""" 

15 

16import logging 

17from datetime import datetime 

18from flask import Blueprint, request, jsonify 

19 

20logger = logging.getLogger('hevolve_provision_api') 

21 

22provision_bp = Blueprint('provision', __name__, url_prefix='/api/provision') 

23 

24 

25@provision_bp.route('/deploy', methods=['POST']) 

26def deploy(): 

27 """Trigger remote HART OS provisioning via SSH.""" 

28 data = request.get_json() or {} 

29 

30 target_host = data.get('target_host') 

31 if not target_host: 

32 return jsonify({'error': 'target_host is required'}), 400 

33 

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) 

40 

41 try: 

42 from integrations.agent_engine.network_provisioner import NetworkProvisioner 

43 

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 ) 

54 

55 status_code = 200 if result.get('success') else 500 

56 return jsonify(result), status_code 

57 

58 except Exception as e: 

59 logger.error("Provisioning error: %s", e) 

60 return jsonify({'error': str(e)}), 500 

61 

62 

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 

80 

81 

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 

97 

98 

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') 

104 

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 

115 

116 

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 

123 

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 

129 

130 result = NetworkProvisioner.update_remote( 

131 target_host=node.target_host, 

132 ssh_user=node.ssh_user, 

133 ) 

134 

135 if result.get('success'): 

136 node.last_health_check = datetime.utcnow() 

137 node.status = 'active' 

138 db.commit() 

139 

140 return jsonify(result) 

141 finally: 

142 db.close() 

143 except Exception as e: 

144 return jsonify({'error': str(e)}), 500 

145 

146 

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 

157 

158 node.status = 'decommissioned' 

159 db.commit() 

160 

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 

169 

170 

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 

178 

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 

189 

190 

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 }