| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351 |
- #!/usr/bin/env python3
- from gevent import pywsgi
- from flask import Flask, request, jsonify, render_template_string
- import yaml
- import subprocess
- import threading
- import os
- import re
- import socket
- NETPLAN_FILE = "/etc/netplan/01-netcfg.yaml"
- PORT = 6000 # 定义端口常量
- app = Flask(__name__)
- # 前端 HTML 模板
- HTML_TEMPLATE = """
- <!DOCTYPE html>
- <html>
- <head>
- <meta charset="utf-8">
- <title>网络配置</title>
- <style>
- body { font-family: Arial, sans-serif; margin: 20px; }
- h2 { color: #333; }
- input {
- margin: 5px 0 15px 0;
- padding: 8px;
- width: 300px;
- border: 1px solid #ccc;
- border-radius: 4px;
- }
- button {
- padding: 10px 20px;
- background: #007bff;
- color: white;
- border: none;
- border-radius: 4px;
- cursor: pointer;
- font-size: 16px;
- }
- button:hover { background: #0056b3; }
- .info { color: #666; margin-top: 10px; font-size: 14px; }
- .countdown { color: #dc3545; font-weight: bold; }
- </style>
- </head>
- <body>
- <h2>网络配置</h2>
- <form id="netForm">
- IP地址: <br><input id="ip" value="{{ ip }}"><br>
- 子网掩码位数: <br><input id="mask" value="{{ mask }}"><br>
- 网关: <br><input id="gateway" value="{{ gateway }}"><br>
- DNS服务器: <br><input id="dns" value="{{ dns }}"><br>
- <button type="button" onclick="save()">保存配置</button>
- <div class="info">注意:保存后会有20秒时间确认,超时自动回滚</div>
- </form>
- <script>
- function save() {
- const saveBtn = event.target;
- const originalText = saveBtn.textContent;
- const newIp = document.getElementById('ip').value;
-
- // 禁用按钮防止重复点击
- saveBtn.disabled = true;
- saveBtn.textContent = '保存中...';
- saveBtn.style.background = '#6c757d';
-
- fetch('/api/network', {
- method: 'POST',
- headers: {'Content-Type': 'application/json'},
- body: JSON.stringify({
- ip: newIp,
- mask: document.getElementById('mask').value,
- gateway: document.getElementById('gateway').value,
- dns: document.getElementById('dns').value
- })
- })
- .then(res => res.json())
- .then(data => {
- if (data.success) {
- // 保存成功,显示倒计时并跳转
- let countdown = 15;
- const message = data.status + '\\n\\n将在 ' + countdown + ' 秒后跳转到新地址...';
- alert(message);
-
- // 创建倒计时显示
- const countdownDiv = document.createElement('div');
- countdownDiv.className = 'countdown';
- countdownDiv.innerHTML = '正在跳转到新IP地址,请稍候... <span id="countdown">' + countdown + '</span> 秒';
- document.body.appendChild(countdownDiv);
-
- // 倒计时并跳转
- const countdownInterval = setInterval(() => {
- countdown--;
- document.getElementById('countdown').textContent = countdown;
-
- if (countdown <= 0) {
- clearInterval(countdownInterval);
- // 跳转到新的IP地址,包含端口号
- window.location.href = 'http://' + newIp + ':' + {{ port }};
- }
- }, 1000);
-
- // 同时尝试自动跳转(如果网络已生效)
- setTimeout(() => {
- window.location.href = 'http://' + newIp + ':' + {{ port }};
- }, 3000);
-
- } else {
- alert('保存失败: ' + data.status);
- }
- })
- .catch(error => {
- alert('保存失败: ' + error);
- })
- .finally(() => {
- // 恢复按钮状态
- saveBtn.disabled = false;
- saveBtn.textContent = originalText;
- saveBtn.style.background = '#007bff';
- });
- }
- </script>
- </body>
- </html>
- """
- def is_valid_ip(ip):
- """验证IP地址格式"""
- if not ip:
- return False
- pattern = r'^(\d{1,3}\.){3}\d{1,3}$'
- if re.match(pattern, ip):
- parts = ip.split('.')
- return all(0 <= int(part) <= 255 for part in parts)
- return False
- def get_current_ip():
- """获取当前服务器的IP地址"""
- try:
- # 获取本机IP
- s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- s.connect(("8.8.8.8", 80))
- current_ip = s.getsockname()[0]
- s.close()
- return current_ip
- except:
- return "127.0.0.1"
- def read_netplan():
- """读取当前网络配置"""
- try:
- with open(NETPLAN_FILE) as f:
- data = yaml.safe_load(f)
-
- eth = data["network"]["ethernets"]["eth0"]
- addresses = eth.get("addresses", [""])
-
- if addresses and addresses[0]:
- ip = addresses[0].split("/")[0]
- mask = addresses[0].split("/")[1]
- else:
- ip = ""
- mask = "24"
-
- gateway = eth.get("routes", [{}])[0].get("via", "")
- dns = eth.get("nameservers", {}).get("addresses", [""])[0]
-
- return {"ip": ip, "mask": mask, "gateway": gateway, "dns": dns}
- except Exception as e:
- print(f"读取配置文件出错: {e}")
- return {"ip": "", "mask": "24", "gateway": "", "dns": "8.8.8.8"}
- def apply_netplan_async(new_ip):
- """异步应用网络配置"""
- try:
- print(f"开始应用网络配置,新IP: {new_ip}")
-
- # 使用 netplan try 应用配置(20秒超时)
- process = subprocess.Popen(
- ["netplan", "try", "--timeout", "20"],
- stdin=subprocess.PIPE,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE,
- text=True
- )
-
- # 自动确认(按回车键)
- try:
- stdout, stderr = process.communicate(input='\n', timeout=5)
- print(f"网络配置已应用,新IP: {new_ip}")
- print(f"输出: {stdout}")
- if stderr:
- print(f"错误: {stderr}")
-
- # 重启网络服务确保配置生效
- subprocess.run(["systemctl", "restart", "systemd-networkd"],
- capture_output=True, text=True)
- print("网络服务已重启")
-
- except subprocess.TimeoutExpired:
- # 如果用户没有手动确认,会自动回滚
- print("等待用户确认中...配置将在20秒后自动回滚")
- process.kill()
- except Exception as e:
- print(f"应用网络配置时出错: {e}")
-
- except Exception as e:
- print(f"启动netplan进程出错: {e}")
- @app.route('/')
- def index():
- net = read_netplan()
- current_ip = get_current_ip()
- print(f"当前访问IP: {request.remote_addr}, 服务器IP: {current_ip}")
- return render_template_string(HTML_TEMPLATE, **net, port=PORT)
- @app.route('/api/network', methods=['POST'])
- def set_network():
- try:
- data = request.json
- ip = data.get("ip", "").strip()
- mask = data.get("mask", "").strip()
- gateway = data.get("gateway", "").strip()
- dns = data.get("dns", "").strip()
-
- # 验证IP地址格式
- if not is_valid_ip(ip):
- return jsonify({
- "success": False,
- "status": "IP地址格式不正确"
- }), 400
-
- if not is_valid_ip(gateway):
- return jsonify({
- "success": False,
- "status": "网关地址格式不正确"
- }), 400
-
- if dns and not is_valid_ip(dns):
- return jsonify({
- "success": False,
- "status": "DNS地址格式不正确"
- }), 400
-
- # 基础验证
- if not all([ip, mask, gateway, dns]):
- return jsonify({
- "success": False,
- "status": "所有字段都必须填写"
- }), 400
-
- # 读取并更新配置
- with open(NETPLAN_FILE) as f:
- config = yaml.safe_load(f)
-
- # 更新网络配置
- eth = config["network"]["ethernets"]["eth0"]
- eth["dhcp4"] = False
- eth["addresses"] = [f"{ip}/{mask}"]
- eth["routes"] = [{"to": "default", "via": gateway}]
- eth["nameservers"] = {"addresses": [dns]}
-
- # 保存配置文件
- with open(NETPLAN_FILE, "w") as f:
- yaml.safe_dump(config, f, default_flow_style=False)
-
- print(f"配置文件已保存,新IP: {ip}")
-
- # 在后台异步应用网络配置
- threading.Thread(target=apply_netplan_async, args=(ip,), daemon=True).start()
-
- return jsonify({
- "success": True,
- "status": f"配置已保存并正在应用!\n\n请在20秒内按Enter键确认,否则配置会自动回滚。\n跳转地址: http://{ip}:{PORT}",
- "new_ip": ip,
- "port": PORT,
- "redirect_url": f"http://{ip}:{PORT}"
- })
-
- except PermissionError:
- return jsonify({
- "success": False,
- "status": "权限不足,请使用sudo运行此程序"
- }), 403
- except FileNotFoundError:
- return jsonify({
- "success": False,
- "status": f"配置文件 {NETPLAN_FILE} 不存在"
- }), 404
- except Exception as e:
- return jsonify({
- "success": False,
- "status": f"保存失败: {str(e)}"
- }), 500
- @app.route('/check_connection')
- def check_connection():
- """检查连接状态"""
- return jsonify({
- "status": "connected",
- "message": "连接成功",
- "server_ip": get_current_ip(),
- "port": PORT
- })
- def main():
- """主函数"""
- # 检查权限
- if os.geteuid() != 0:
- print("警告:请使用sudo运行此程序以获取必要的权限")
- return
-
- current_ip = get_current_ip()
-
- print("=" * 60)
- print("网络配置服务启动")
- print("=" * 60)
- print(f"当前服务器IP: {current_ip}")
- print(f"服务端口: {PORT}")
- print(f"访问地址: http://{current_ip}:{PORT}")
- print("=" * 60)
- print("保存配置后,会自动跳转到新的IP地址")
- print("注意:请确保防火墙允许端口访问")
- print("=" * 60)
-
- # 检查防火墙设置
- try:
- result = subprocess.run(["ufw", "status"], capture_output=True, text=True)
- if "Status: active" in result.stdout:
- print("检测到UFW防火墙,请确保端口已开放:")
- print(f" sudo ufw allow {PORT}")
- print(f" sudo ufw allow 22 # SSH端口(可选)")
- except:
- pass
-
- # 使用gevent WSGI服务器
- server = pywsgi.WSGIServer(('0.0.0.0', PORT), app)
- print(f"服务器已启动,监听端口: {PORT}")
- print("按 Ctrl+C 停止服务")
-
- try:
- server.serve_forever()
- except KeyboardInterrupt:
- print("\n服务已停止")
- except Exception as e:
- print(f"服务器错误: {e}")
- if __name__ == "__main__":
- main()
|