app.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  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 = 6000 # 定义端口常量
  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 { color: #dc3545; font-weight: bold; }
  42. </style>
  43. </head>
  44. <body>
  45. <h2>网络配置</h2>
  46. <form id="netForm">
  47. IP地址: <br><input id="ip" value="{{ ip }}"><br>
  48. 子网掩码位数: <br><input id="mask" value="{{ mask }}"><br>
  49. 网关: <br><input id="gateway" value="{{ gateway }}"><br>
  50. DNS服务器: <br><input id="dns" value="{{ dns }}"><br>
  51. <button type="button" onclick="save()">保存配置</button>
  52. <div class="info">注意:保存后会有20秒时间确认,超时自动回滚</div>
  53. </form>
  54. <script>
  55. function save() {
  56. const saveBtn = event.target;
  57. const originalText = saveBtn.textContent;
  58. const newIp = document.getElementById('ip').value;
  59. // 禁用按钮防止重复点击
  60. saveBtn.disabled = true;
  61. saveBtn.textContent = '保存中...';
  62. saveBtn.style.background = '#6c757d';
  63. fetch('/api/network', {
  64. method: 'POST',
  65. headers: {'Content-Type': 'application/json'},
  66. body: JSON.stringify({
  67. ip: newIp,
  68. mask: document.getElementById('mask').value,
  69. gateway: document.getElementById('gateway').value,
  70. dns: document.getElementById('dns').value
  71. })
  72. })
  73. .then(res => res.json())
  74. .then(data => {
  75. if (data.success) {
  76. // 保存成功,显示倒计时并跳转
  77. let countdown = 15;
  78. const message = data.status + '\\n\\n将在 ' + countdown + ' 秒后跳转到新地址...';
  79. alert(message);
  80. // 创建倒计时显示
  81. const countdownDiv = document.createElement('div');
  82. countdownDiv.className = 'countdown';
  83. countdownDiv.innerHTML = '正在跳转到新IP地址,请稍候... <span id="countdown">' + countdown + '</span> 秒';
  84. document.body.appendChild(countdownDiv);
  85. // 倒计时并跳转
  86. const countdownInterval = setInterval(() => {
  87. countdown--;
  88. document.getElementById('countdown').textContent = countdown;
  89. if (countdown <= 0) {
  90. clearInterval(countdownInterval);
  91. // 跳转到新的IP地址,包含端口号
  92. window.location.href = 'http://' + newIp + ':' + {{ port }};
  93. }
  94. }, 1000);
  95. // 同时尝试自动跳转(如果网络已生效)
  96. setTimeout(() => {
  97. window.location.href = 'http://' + newIp + ':' + {{ port }};
  98. }, 3000);
  99. } else {
  100. alert('保存失败: ' + data.status);
  101. }
  102. })
  103. .catch(error => {
  104. alert('保存失败: ' + error);
  105. })
  106. .finally(() => {
  107. // 恢复按钮状态
  108. saveBtn.disabled = false;
  109. saveBtn.textContent = originalText;
  110. saveBtn.style.background = '#007bff';
  111. });
  112. }
  113. </script>
  114. </body>
  115. </html>
  116. """
  117. def is_valid_ip(ip):
  118. """验证IP地址格式"""
  119. if not ip:
  120. return False
  121. pattern = r'^(\d{1,3}\.){3}\d{1,3}$'
  122. if re.match(pattern, ip):
  123. parts = ip.split('.')
  124. return all(0 <= int(part) <= 255 for part in parts)
  125. return False
  126. def get_current_ip():
  127. """获取当前服务器的IP地址"""
  128. try:
  129. # 获取本机IP
  130. s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  131. s.connect(("8.8.8.8", 80))
  132. current_ip = s.getsockname()[0]
  133. s.close()
  134. return current_ip
  135. except:
  136. return "127.0.0.1"
  137. def read_netplan():
  138. """读取当前网络配置"""
  139. try:
  140. with open(NETPLAN_FILE) as f:
  141. data = yaml.safe_load(f)
  142. eth = data["network"]["ethernets"]["eth0"]
  143. addresses = eth.get("addresses", [""])
  144. if addresses and addresses[0]:
  145. ip = addresses[0].split("/")[0]
  146. mask = addresses[0].split("/")[1]
  147. else:
  148. ip = ""
  149. mask = "24"
  150. gateway = eth.get("routes", [{}])[0].get("via", "")
  151. dns = eth.get("nameservers", {}).get("addresses", [""])[0]
  152. return {"ip": ip, "mask": mask, "gateway": gateway, "dns": dns}
  153. except Exception as e:
  154. print(f"读取配置文件出错: {e}")
  155. return {"ip": "", "mask": "24", "gateway": "", "dns": "8.8.8.8"}
  156. def apply_netplan_async(new_ip):
  157. """异步应用网络配置"""
  158. try:
  159. print(f"开始应用网络配置,新IP: {new_ip}")
  160. # 使用 netplan try 应用配置(20秒超时)
  161. process = subprocess.Popen(
  162. ["netplan", "try", "--timeout", "20"],
  163. stdin=subprocess.PIPE,
  164. stdout=subprocess.PIPE,
  165. stderr=subprocess.PIPE,
  166. text=True
  167. )
  168. # 自动确认(按回车键)
  169. try:
  170. stdout, stderr = process.communicate(input='\n', timeout=5)
  171. print(f"网络配置已应用,新IP: {new_ip}")
  172. print(f"输出: {stdout}")
  173. if stderr:
  174. print(f"错误: {stderr}")
  175. # 重启网络服务确保配置生效
  176. subprocess.run(["systemctl", "restart", "systemd-networkd"],
  177. capture_output=True, text=True)
  178. print("网络服务已重启")
  179. except subprocess.TimeoutExpired:
  180. # 如果用户没有手动确认,会自动回滚
  181. print("等待用户确认中...配置将在20秒后自动回滚")
  182. process.kill()
  183. except Exception as e:
  184. print(f"应用网络配置时出错: {e}")
  185. except Exception as e:
  186. print(f"启动netplan进程出错: {e}")
  187. @app.route('/')
  188. def index():
  189. net = read_netplan()
  190. current_ip = get_current_ip()
  191. print(f"当前访问IP: {request.remote_addr}, 服务器IP: {current_ip}")
  192. return render_template_string(HTML_TEMPLATE, **net, port=PORT)
  193. @app.route('/api/network', methods=['POST'])
  194. def set_network():
  195. try:
  196. data = request.json
  197. ip = data.get("ip", "").strip()
  198. mask = data.get("mask", "").strip()
  199. gateway = data.get("gateway", "").strip()
  200. dns = data.get("dns", "").strip()
  201. # 验证IP地址格式
  202. if not is_valid_ip(ip):
  203. return jsonify({
  204. "success": False,
  205. "status": "IP地址格式不正确"
  206. }), 400
  207. if not is_valid_ip(gateway):
  208. return jsonify({
  209. "success": False,
  210. "status": "网关地址格式不正确"
  211. }), 400
  212. if dns and not is_valid_ip(dns):
  213. return jsonify({
  214. "success": False,
  215. "status": "DNS地址格式不正确"
  216. }), 400
  217. # 基础验证
  218. if not all([ip, mask, gateway, dns]):
  219. return jsonify({
  220. "success": False,
  221. "status": "所有字段都必须填写"
  222. }), 400
  223. # 读取并更新配置
  224. with open(NETPLAN_FILE) as f:
  225. config = yaml.safe_load(f)
  226. # 更新网络配置
  227. eth = config["network"]["ethernets"]["eth0"]
  228. eth["dhcp4"] = False
  229. eth["addresses"] = [f"{ip}/{mask}"]
  230. eth["routes"] = [{"to": "default", "via": gateway}]
  231. eth["nameservers"] = {"addresses": [dns]}
  232. # 保存配置文件
  233. with open(NETPLAN_FILE, "w") as f:
  234. yaml.safe_dump(config, f, default_flow_style=False)
  235. print(f"配置文件已保存,新IP: {ip}")
  236. # 在后台异步应用网络配置
  237. threading.Thread(target=apply_netplan_async, args=(ip,), daemon=True).start()
  238. return jsonify({
  239. "success": True,
  240. "status": f"配置已保存并正在应用!\n\n请在20秒内按Enter键确认,否则配置会自动回滚。\n跳转地址: http://{ip}:{PORT}",
  241. "new_ip": ip,
  242. "port": PORT,
  243. "redirect_url": f"http://{ip}:{PORT}"
  244. })
  245. except PermissionError:
  246. return jsonify({
  247. "success": False,
  248. "status": "权限不足,请使用sudo运行此程序"
  249. }), 403
  250. except FileNotFoundError:
  251. return jsonify({
  252. "success": False,
  253. "status": f"配置文件 {NETPLAN_FILE} 不存在"
  254. }), 404
  255. except Exception as e:
  256. return jsonify({
  257. "success": False,
  258. "status": f"保存失败: {str(e)}"
  259. }), 500
  260. @app.route('/check_connection')
  261. def check_connection():
  262. """检查连接状态"""
  263. return jsonify({
  264. "status": "connected",
  265. "message": "连接成功",
  266. "server_ip": get_current_ip(),
  267. "port": PORT
  268. })
  269. def main():
  270. """主函数"""
  271. # 检查权限
  272. if os.geteuid() != 0:
  273. print("警告:请使用sudo运行此程序以获取必要的权限")
  274. return
  275. current_ip = get_current_ip()
  276. print("=" * 60)
  277. print("网络配置服务启动")
  278. print("=" * 60)
  279. print(f"当前服务器IP: {current_ip}")
  280. print(f"服务端口: {PORT}")
  281. print(f"访问地址: http://{current_ip}:{PORT}")
  282. print("=" * 60)
  283. print("保存配置后,会自动跳转到新的IP地址")
  284. print("注意:请确保防火墙允许端口访问")
  285. print("=" * 60)
  286. # 检查防火墙设置
  287. try:
  288. result = subprocess.run(["ufw", "status"], capture_output=True, text=True)
  289. if "Status: active" in result.stdout:
  290. print("检测到UFW防火墙,请确保端口已开放:")
  291. print(f" sudo ufw allow {PORT}")
  292. print(f" sudo ufw allow 22 # SSH端口(可选)")
  293. except:
  294. pass
  295. # 使用gevent WSGI服务器
  296. server = pywsgi.WSGIServer(('0.0.0.0', PORT), app)
  297. print(f"服务器已启动,监听端口: {PORT}")
  298. print("按 Ctrl+C 停止服务")
  299. try:
  300. server.serve_forever()
  301. except KeyboardInterrupt:
  302. print("\n服务已停止")
  303. except Exception as e:
  304. print(f"服务器错误: {e}")
  305. if __name__ == "__main__":
  306. main()