app.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  1. #!/usr/bin/env python3
  2. from gevent import pywsgi
  3. from flask import Flask, request, jsonify, render_template_string
  4. import yaml
  5. import subprocess
  6. import threading
  7. import os
  8. import re
  9. import socket
  10. NETPLAN_FILE = "/etc/netplan/01-netcfg.yaml"
  11. PORT = 8000 # 定义端口常量
  12. app = Flask(__name__)
  13. # 前端 HTML 模板
  14. HTML_TEMPLATE = """
  15. <!DOCTYPE html>
  16. <html>
  17. <head>
  18. <meta charset="utf-8">
  19. <title>网络配置</title>
  20. <style>
  21. body { font-family: Arial, sans-serif; margin: 20px; }
  22. h2 { color: #333; }
  23. input {
  24. margin: 5px 0 15px 0;
  25. padding: 8px;
  26. width: 300px;
  27. border: 1px solid #ccc;
  28. border-radius: 4px;
  29. }
  30. button {
  31. padding: 10px 20px;
  32. background: #007bff;
  33. color: white;
  34. border: none;
  35. border-radius: 4px;
  36. cursor: pointer;
  37. font-size: 16px;
  38. }
  39. button:hover { background: #0056b3; }
  40. .info { color: #666; margin-top: 10px; font-size: 14px; }
  41. .countdown {
  42. color: #dc3545;
  43. font-weight: bold;
  44. margin-top: 20px;
  45. padding: 15px;
  46. background-color: #f8f9fa;
  47. border-left: 4px solid #dc3545;
  48. border-radius: 4px;
  49. }
  50. .status-message {
  51. margin-top: 20px;
  52. padding: 15px;
  53. background-color: #d4edda;
  54. border-left: 4px solid #28a745;
  55. border-radius: 4px;
  56. color: #155724;
  57. }
  58. .error-message {
  59. margin-top: 20px;
  60. padding: 15px;
  61. background-color: #f8d7da;
  62. border-left: 4px solid #dc3545;
  63. border-radius: 4px;
  64. color: #721c24;
  65. }
  66. .hidden { display: none; }
  67. </style>
  68. </head>
  69. <body>
  70. <h2>网络配置</h2>
  71. <form id="netForm">
  72. IP地址: <br><input id="ip" value="{{ ip }}"><br>
  73. 子网掩码位数: <br><input id="mask" value="{{ mask }}"><br>
  74. 网关: <br><input id="gateway" value="{{ gateway }}"><br>
  75. DNS服务器: <br><input id="dns" value="{{ dns }}"><br>
  76. <button type="button" onclick="save()" id="saveBtn">保存配置</button>
  77. </form>
  78. <div id="statusMessage" class="hidden"></div>
  79. <script>
  80. function save() {
  81. const saveBtn = document.getElementById('saveBtn');
  82. const originalText = saveBtn.textContent;
  83. const newIp = document.getElementById('ip').value;
  84. // 清除之前的消息
  85. const statusDiv = document.getElementById('statusMessage');
  86. statusDiv.className = 'hidden';
  87. statusDiv.innerHTML = '';
  88. // 禁用按钮防止重复点击
  89. saveBtn.disabled = true;
  90. saveBtn.textContent = '保存中...';
  91. saveBtn.style.background = '#6c757d';
  92. fetch('/api/network', {
  93. method: 'POST',
  94. headers: {'Content-Type': 'application/json'},
  95. body: JSON.stringify({
  96. ip: newIp,
  97. mask: document.getElementById('mask').value,
  98. gateway: document.getElementById('gateway').value,
  99. dns: document.getElementById('dns').value
  100. })
  101. })
  102. .then(res => res.json())
  103. .then(data => {
  104. if (data.success) {
  105. // 显示成功消息
  106. statusDiv.className = 'status-message';
  107. statusDiv.innerHTML = data.status;
  108. // 开始倒计时
  109. startCountdown(newIp);
  110. } else {
  111. // 显示错误消息
  112. statusDiv.className = 'error-message';
  113. statusDiv.innerHTML = '保存失败: ' + data.status;
  114. }
  115. })
  116. .catch(error => {
  117. // 显示错误消息
  118. statusDiv.className = 'error-message';
  119. statusDiv.innerHTML = '保存失败: ' + error;
  120. })
  121. .finally(() => {
  122. // 恢复按钮状态
  123. saveBtn.disabled = false;
  124. saveBtn.textContent = originalText;
  125. saveBtn.style.background = '#007bff';
  126. });
  127. }
  128. function startCountdown(newIp) {
  129. let countdown = 15;
  130. // 创建倒计时显示区域
  131. let countdownDiv = document.getElementById('countdownDisplay');
  132. if (!countdownDiv) {
  133. countdownDiv = document.createElement('div');
  134. countdownDiv.id = 'countdownDisplay';
  135. countdownDiv.className = 'countdown';
  136. document.body.appendChild(countdownDiv);
  137. }
  138. countdownDiv.innerHTML = '配置保存成功!正在应用网络配置...<br>';
  139. countdownDiv.innerHTML += '将在 <span id="countdown">' + countdown + '</span> 秒后自动跳转到新地址<br>';
  140. countdownDiv.innerHTML += '跳转地址: http://' + newIp + ':' + {{ port }};
  141. // 更新状态消息,显示更多信息
  142. const statusDiv = document.getElementById('statusMessage');
  143. statusDiv.innerHTML += '<br><br>网络配置已保存,正在后台应用...';
  144. // statusDiv.innerHTML += '<br>请在20秒内到服务器控制台按Enter键确认,否则配置会自动回滚。';
  145. statusDiv.innerHTML += '<br>系统将在15秒后尝试自动跳转到新地址。';
  146. // 倒计时函数
  147. const updateCountdown = () => {
  148. countdown--;
  149. document.getElementById('countdown').textContent = countdown;
  150. if (countdown <= 0) {
  151. clearInterval(countdownInterval);
  152. // 跳转到新的IP地址,包含端口号
  153. window.location.href = 'http://' + newIp + ':' + {{ port }};
  154. }
  155. };
  156. // 开始倒计时
  157. const countdownInterval = setInterval(updateCountdown, 1000);
  158. // 在倒计时开始3秒后尝试第一次跳转(如果网络已生效)
  159. setTimeout(() => {
  160. try {
  161. window.location.href = 'http://' + newIp + ':' + {{ port }};
  162. } catch (e) {
  163. console.log('第一次跳转尝试失败,继续等待...');
  164. }
  165. }, 3000);
  166. // 在倒计时开始10秒后尝试第二次跳转
  167. setTimeout(() => {
  168. try {
  169. window.location.href = 'http://' + newIp + ':' + {{ port }};
  170. } catch (e) {
  171. console.log('第二次跳转尝试失败,继续等待...');
  172. }
  173. }, 10000);
  174. }
  175. </script>
  176. </body>
  177. </html>
  178. """
  179. def is_valid_ip(ip):
  180. """验证IP地址格式"""
  181. if not ip:
  182. return False
  183. pattern = r'^(\d{1,3}\.){3}\d{1,3}$'
  184. if re.match(pattern, ip):
  185. parts = ip.split('.')
  186. return all(0 <= int(part) <= 255 for part in parts)
  187. return False
  188. def get_current_ip():
  189. """获取当前服务器的IP地址"""
  190. try:
  191. # 获取本机IP
  192. s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  193. s.connect(("8.8.8.8", 80))
  194. current_ip = s.getsockname()[0]
  195. s.close()
  196. return current_ip
  197. except:
  198. return "127.0.0.1"
  199. def read_netplan():
  200. """读取当前网络配置"""
  201. try:
  202. with open(NETPLAN_FILE) as f:
  203. data = yaml.safe_load(f)
  204. eth = data["network"]["ethernets"]["eth0"]
  205. addresses = eth.get("addresses", [""])
  206. if addresses and addresses[0]:
  207. ip = addresses[0].split("/")[0]
  208. mask = addresses[0].split("/")[1]
  209. else:
  210. ip = ""
  211. mask = "24"
  212. gateway = eth.get("routes", [{}])[0].get("via", "")
  213. dns = eth.get("nameservers", {}).get("addresses", [""])[0]
  214. return {"ip": ip, "mask": mask, "gateway": gateway, "dns": dns}
  215. except Exception as e:
  216. print(f"读取配置文件出错: {e}")
  217. return {"ip": "", "mask": "24", "gateway": "", "dns": "8.8.8.8"}
  218. def apply_netplan_async(new_ip):
  219. """异步应用网络配置"""
  220. try:
  221. print(f"开始应用网络配置,新IP: {new_ip}")
  222. # 使用 netplan try 应用配置(20秒超时)
  223. process = subprocess.Popen(
  224. ["netplan", "try", "--timeout", "20"],
  225. stdin=subprocess.PIPE,
  226. stdout=subprocess.PIPE,
  227. stderr=subprocess.PIPE,
  228. text=True
  229. )
  230. # 自动确认(按回车键)
  231. try:
  232. stdout, stderr = process.communicate(input='\n', timeout=5)
  233. print(f"网络配置已应用,新IP: {new_ip}")
  234. print(f"输出: {stdout}")
  235. if stderr:
  236. print(f"错误: {stderr}")
  237. # 重启网络服务确保配置生效
  238. subprocess.run(["systemctl", "restart", "systemd-networkd"],
  239. capture_output=True, text=True)
  240. print("网络服务已重启")
  241. except subprocess.TimeoutExpired:
  242. # 如果用户没有手动确认,会自动回滚
  243. print("等待用户确认中...配置将在20秒后自动回滚")
  244. process.kill()
  245. except Exception as e:
  246. print(f"应用网络配置时出错: {e}")
  247. except Exception as e:
  248. print(f"启动netplan进程出错: {e}")
  249. @app.route('/')
  250. def index():
  251. net = read_netplan()
  252. current_ip = get_current_ip()
  253. print(f"当前访问IP: {request.remote_addr}, 服务器IP: {current_ip}")
  254. return render_template_string(HTML_TEMPLATE, **net, port=PORT)
  255. @app.route('/api/network', methods=['POST'])
  256. def set_network():
  257. try:
  258. data = request.json
  259. ip = data.get("ip", "").strip()
  260. mask = data.get("mask", "").strip()
  261. gateway = data.get("gateway", "").strip()
  262. dns = data.get("dns", "").strip()
  263. # 验证IP地址格式
  264. if not is_valid_ip(ip):
  265. return jsonify({
  266. "success": False,
  267. "status": "IP地址格式不正确"
  268. }), 400
  269. if not is_valid_ip(gateway):
  270. return jsonify({
  271. "success": False,
  272. "status": "网关地址格式不正确"
  273. }), 400
  274. if dns and not is_valid_ip(dns):
  275. return jsonify({
  276. "success": False,
  277. "status": "DNS地址格式不正确"
  278. }), 400
  279. # 基础验证
  280. if not all([ip, mask, gateway, dns]):
  281. return jsonify({
  282. "success": False,
  283. "status": "所有字段都必须填写"
  284. }), 400
  285. # 读取并更新配置
  286. with open(NETPLAN_FILE) as f:
  287. config = yaml.safe_load(f)
  288. # 更新网络配置
  289. eth = config["network"]["ethernets"]["eth0"]
  290. eth["dhcp4"] = False
  291. eth["addresses"] = [f"{ip}/{mask}"]
  292. eth["routes"] = [{"to": "default", "via": gateway}]
  293. eth["nameservers"] = {"addresses": [dns]}
  294. # 保存配置文件
  295. with open(NETPLAN_FILE, "w") as f:
  296. yaml.safe_dump(config, f, default_flow_style=False)
  297. print(f"配置文件已保存,新IP: {ip}")
  298. # 在后台异步应用网络配置
  299. threading.Thread(target=apply_netplan_async, args=(ip,), daemon=True).start()
  300. return jsonify({
  301. "success": True,
  302. "status": "配置保存成功!正在应用网络配置...",
  303. "new_ip": ip,
  304. "port": PORT,
  305. "redirect_url": f"http://{ip}:{PORT}"
  306. })
  307. except PermissionError:
  308. return jsonify({
  309. "success": False,
  310. "status": "权限不足,请使用sudo运行此程序"
  311. }), 403
  312. except FileNotFoundError:
  313. return jsonify({
  314. "success": False,
  315. "status": f"配置文件 {NETPLAN_FILE} 不存在"
  316. }), 404
  317. except Exception as e:
  318. return jsonify({
  319. "success": False,
  320. "status": f"保存失败: {str(e)}"
  321. }), 500
  322. @app.route('/check_connection')
  323. def check_connection():
  324. """检查连接状态"""
  325. return jsonify({
  326. "status": "connected",
  327. "message": "连接成功",
  328. "server_ip": get_current_ip(),
  329. "port": PORT
  330. })
  331. def main():
  332. """主函数"""
  333. # 检查权限
  334. if os.geteuid() != 0:
  335. print("警告:请使用sudo运行此程序以获取必要的权限")
  336. return
  337. current_ip = get_current_ip()
  338. print("=" * 60)
  339. print("网络配置服务启动")
  340. print("=" * 60)
  341. print(f"当前服务器IP: {current_ip}")
  342. print(f"服务端口: {PORT}")
  343. print(f"访问地址: http://{current_ip}:{PORT}")
  344. print("=" * 60)
  345. print("保存配置后,会自动跳转到新的IP地址")
  346. print("注意:请确保防火墙允许端口访问")
  347. print("=" * 60)
  348. # 检查防火墙设置
  349. try:
  350. result = subprocess.run(["ufw", "status"], capture_output=True, text=True)
  351. if "Status: active" in result.stdout:
  352. print("检测到UFW防火墙,请确保端口已开放:")
  353. print(f" sudo ufw allow {PORT}")
  354. print(f" sudo ufw allow 22 # SSH端口(可选)")
  355. except:
  356. pass
  357. # 使用gevent WSGI服务器
  358. server = pywsgi.WSGIServer(('0.0.0.0', PORT), app)
  359. print(f"服务器已启动,监听端口: {PORT}")
  360. print("按 Ctrl+C 停止服务")
  361. try:
  362. server.serve_forever()
  363. except KeyboardInterrupt:
  364. print("\n服务已停止")
  365. except Exception as e:
  366. print(f"服务器错误: {e}")
  367. if __name__ == "__main__":
  368. main()