一、osd权重调整python脚本文件

1.示例

32 0.43589 1.0000 432G 376G 58091M 86.87 1.83 osd.32 11.33.44.55
48 0.44579 1.0000 432G 346G 47091M 87.87 1.80 osd.48 22.11.99.66
21 0.34579 1.0000 432G 346G 47091M 87.87 1.80 osd.21 10.22.33.44
32 0.43589 1.0000 432G 376G 58091M 86.87 1.83 osd.32 11.33.44.55
48 0.44579 1.0000 432G 346G 47091M 87.87 1.80 osd.48 22.11.99.66
21 0.34579 1.0000 432G 346G 47091M 87.87 1.80 osd.21 10.22.33.44

2.代码

vim app.py
import datetime
import os
import math
from flask import Flask, request, render_template_string, send_file, flash, redirect, url_for, session

app = Flask(__name__)
# 请务必修改为复杂的密钥,这里仅为演示
app.secret_key = 'a_very_secure_secret_key_for_production_environment'
app.config['SESSION_TYPE'] = 'filesystem' # 确保 Session 可用

# 配置目录
#HISTORY_DIR = "history"
#if not os.path.exists(HISTORY_DIR):
#    os.makedirs(HISTORY_DIR)
HISTORY_DIR = os.getenv('HISTORY_DIR', '/app/data/history')
os.makedirs(HISTORY_DIR, exist_ok=True)

# 定义 HTML 模板
# 注意:这里修改了 textarea,增加了 value="{{ saved_input }}"
HTML_TEMPLATE = """
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>OSD 运维命令生成器</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; padding: 20px; max-width: 850px; margin: 0 auto; background-color: #f0f2f5; }
.card { background: #fff; padding: 30px; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.05); }
h1 { text-align: center; color: #333; margin-bottom: 30px; }
.location-time { text-align: center; color: #666; font-size: 14px; margin-bottom: 20px; }
label { font-weight: bold; display: block; margin-bottom: 10px; color: #555; }
textarea { width: 100%; height: 150px; padding: 10px; border: 1px solid #ddd; border-radius: 6px; box-sizing: border-box; font-family: monospace; font-size: 14px; resize: vertical; }
/* 按钮组布局 */
.btn-group { display: flex; gap: 15px; margin-top: 20px; flex-wrap: wrap; align-items: center; }
button { padding: 10px 20px; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 600; transition: opacity 0.2s; }
button:hover { opacity: 0.9; }
button:disabled { background-color: #ccc; cursor: not-allowed; opacity: 0.6; }
.btn-blue { background-color: #007bff; color: white; }
.btn-green { background-color: #28a745; color: white; }
.btn-red { background-color: #dc3545; color: white; }
.example-box { background-color: #f8f9fa; border: 1px dashed #ced4da; padding: 15px; border-radius: 6px; margin-top: 10px; font-family: monospace; font-size: 13px; color: #555; white-space: pre-wrap; }
.history-section { margin-top: 40px; border-top: 1px solid #eee; padding-top: 20px; }
.history-item { padding: 8px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; }
.history-item a { text-decoration: none; color: #007bff; font-family: monospace; }
.alert { padding: 15px; margin-bottom: 20px; border-radius: 6px; color: #155724; background-color: #d4edda; border: 1px solid #c3e6cb; }
</style>
</head>
<body>
<div class="card">
    <!-- 时间与地点显示 -->
    <div class="location-time">
        当前时间:{{ current_time }} | 当前地点:{{ location }}
    </div>
    
    <h1>OSD 运维命令生成器</h1>
    
    {% with messages = get_flashed_messages(with_categories=true) %}
        {% if messages %}
            {% for category, message in messages %}
                <div class="alert">{{ message }}</div>
            {% endfor %}
        {% endif %}
    {% endwith %}

    <form method="POST">
        <label for="osd_data">请输入 OSD 信息 (每行一条):</label>
        <!-- 关键修改:增加 value="{{ saved_input }}" -->
        <textarea id="osd_data" name="osd_data" placeholder="在此粘贴您的 ceph osd df 输出...">{{ saved_input }}</textarea>
        <div class="example-box">
    <b>输入示例:</b>
32 0.43589 1.0000 432G 346G 47091M 87.87 1.80 osd.32 11.33.44.55
48 0.44579 1.0000 432G 346G 47091M 87.87 1.80 osd.48 22.11.99.66
21 0.34579 1.0000 432G 346G 47091M 87.87 1.80 osd.21 10.22.33.44
        </div>

        <!-- 缩容权重按钮组 -->
        <div class="btn-group">
            <button type="submit" name="action" value="generate_reweight" class="btn-blue">生成缩容 OSD 权重命令</button>
            <button type="submit" name="action" value="download_reweight" class="btn-green">下载缩容 OSD 权重命令文件</button>
        </div>
        <!-- 删除 OSD 按钮组 -->
        <div class="btn-group">
            <button type="submit" name="action" value="generate_delete" class="btn-red">生成删除 OSD 命令</button>
            <button type="submit" name="action" value="download_delete" class="btn-green">下载删除 OSD 命令文件</button>
        </div>
    </form>

    <div class="history-section">
        <h3>📂 最近生成的文件 (点击文件名下载):</h3>
        {% if history_files %}
            {% for file in history_files %}
                <div class="history-item">
                    <span>📄 {{ file }}</span>
                    <a href="/download?filename={{ file }}">⬇️ 下载</a>
                </div>
            {% endfor %}
        {% else %}
            <p style="color: #999; font-size: 14px;">暂无历史记录</p>
        {% endif %}
    </div>
</div>
</body>
</html>
"""

def get_history_files():
    """获取有效历史文件列表(排除空文件)"""
    if not os.path.exists(HISTORY_DIR):
        return []
    files = []
    for f in os.listdir(HISTORY_DIR):
        filepath = os.path.join(HISTORY_DIR, f)
        if os.path.isfile(filepath) and os.path.getsize(filepath) > 0:
            files.append(f)
    return sorted(files, key=lambda x: os.path.getmtime(os.path.join(HISTORY_DIR, x)), reverse=True)[:6]

@app.route('/', methods=['GET', 'POST'])
def index():
    # 初始化变量
    history_files = get_history_files()
    saved_input = "" # 定义变量用于回填输入框

    # 1. GET 请求处理(用户访问页面或刷新页面)
    if request.method == 'GET':
        # GET 请求:检查 Session 中是否有上次输入的数据
        # 读取后立即从 Session 中删除,防止下次刷新还显示(一次性)
        if 'osd_data' in session:
            saved_input = session.pop('osd_data', '')
        
        # 这里传入时间地点变量
        current_time = datetime.datetime.now().strftime("%Y-%m-%d %A")
        location = "山东省 烟台市"
        return render_template_string(HTML_TEMPLATE, 
                                    history_files=history_files, 
                                    current_time=current_time, 
                                    location=location,
                                    saved_input=saved_input) # 传给模板

    # 2. POST 请求处理(用户提交了表单)
    # 获取 action (点击了哪个按钮)
    action = request.form.get('action')
    
    # 2.1 处理下载请求 (Download Actions)
    # 注意:这里不生成新文件,只是发送已有文件
    if action == 'download_reweight':
        target_files = [f for f in history_files if f.startswith("osd_reweight")]
        if target_files:
            return send_file(os.path.join(HISTORY_DIR, target_files[0]), as_attachment=True)
        flash("⚠️ 未找到缩容命令文件,请先生成。")
        return redirect(url_for('index')) # 重定向回首页

    elif action == 'download_delete':
        target_files = [f for f in history_files if f.startswith("osd_delete")]
        if target_files:
            return send_file(os.path.join(HISTORY_DIR, target_files[0]), as_attachment=True)
        flash("⚠️ 未找到删除命令文件,请先生成。")
        return redirect(url_for('index')) # 重定向回首页

    # 2.2 处理生成请求 (Generate Actions)
    user_input = request.form.get('osd_data', '').strip()

    # 检查输入是否为空
    if not user_input:
        flash("⚠️ 输入内容不能为空!")
        return redirect(url_for('index'))

    content = []
    success = False
    filename = ""

    if action == 'generate_reweight':
        osds = []
        for line in user_input.split('\n'):
            parts = line.split()
            if len(parts) < 2: continue
            try:
                osd_id = parts[0].replace("osd.", "").strip()
                if not osd_id: continue
                weight = float(parts[1])
                if weight > 0:
                    osds.append({'id': osd_id, 'weight': weight})
            except (ValueError, IndexError):
                continue

        if not osds:
            flash("⚠️ 输入数据中没有有效的OSD信息(ID和正权重),请检查格式。")
        else:
            round_num = 1
            while osds:
                content.append(f"### 第 {round_num} 轮降权 (所有活跃 OSD 权重 -0.1) ###")
                for osd in osds[:]:
                    new_weight = max(0, osd['weight'] - 0.1)
                    osd['weight'] = new_weight
                    weight_str = f"{new_weight:.5f}".rstrip('0').rstrip('.')
                    content.append(f"ceph osd reweight osd.{osd['id']} {weight_str}")
                content.append("")
                osds = [osd for osd in osds if osd['weight'] > 0]
                round_num += 1
            
            timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
            filename = f"osd_reweight_{timestamp}.txt"
            success = True

    elif action == 'generate_delete':
        ip_osd_map = {}
        valid_osds = []
        for line in user_input.split('\n'):
            parts = line.split()
            if len(parts) < 2: continue
            osd_id = parts[0].replace("osd.", "").strip()
            if not osd_id or not osd_id.isdigit(): continue
            ip = parts[-1].strip()
            if not ip: continue
            if ip not in ip_osd_map:
                ip_osd_map[ip] = []
            ip_osd_map[ip].append(osd_id)
            valid_osds.append(osd_id)

        if not valid_osds:
            flash("⚠️ 输入数据中没有有效的OSD信息(需要包含ID和IP),请检查格式。")
        else:
            for ip, osd_ids in ip_osd_map.items():
                content.append(f"# 宿主机IP:{ip}")
                content.append("puppet agent --disable")
                content.append("service ceph status")
                content.append("service ceph stop")
                for osd_id in osd_ids:
                    content.append(f"service ceph stop osd.{osd_id}")
                content.append("")
            
            content.append("# ======================")
            content.append("# 以下为Ceph集群删除命令")
            content.append("# ======================")
            content.append("")
            
            for osd_id in valid_osds:
                content.append(f"ceph osd crush rm osd.{osd_id}; ceph osd rm osd.{osd_id}; ceph auth del osd.{osd_id};")

            timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
            filename = f"osd_delete_{timestamp}.txt"
            success = True

    # 3. 保存文件并重定向 (关键点)
    if success:
        with open(os.path.join(HISTORY_DIR, filename), 'w') as f:
            f.write('\n'.join(content))
        flash(f"成功生成 {action.replace('generate_', '')} 命令: {filename}")
        # ✅ 关键修改:将用户输入存入 Session
        # 这样在重定向后的 GET 请求中可以读取并回填
        session['osd_data'] = user_input 
    # 如果不成功,上面的 flash 已经设置

    # ✅ 核心修复:无论成功与否,都使用 Redirect
    # 这样浏览器地址栏变为 '/' (GET请求),刷新时不会提示重发表单
    return redirect(url_for('index'))

@app.route('/download')
def download():
    filename = request.args.get('filename')
    if filename and os.path.isfile(os.path.join(HISTORY_DIR, filename)):
        return send_file(os.path.join(HISTORY_DIR, filename), as_attachment=True)
    flash("⚠️ 文件未找到或已过期")
    return redirect(url_for('index'))

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=False) # 生产环境建议关闭 debug

二、封装dockerfile镜像

1.dockerfile文件

# 1. 使用官方的轻量级 Python 基础镜像
FROM python:3.9-slim

# 2. 设置工作目录
WORKDIR /app

# 3.安装 curl(健康检查需要)
RUN apt-get update && apt-get install -y --no-install-recommends curl \
    && rm -rf /var/lib/apt/lists/*

# 4. 安装应用依赖
RUN pip install --no-cache-dir Flask

# 5. 将当前目录下的所有文件复制到镜像的工作目录中
COPY . .

# 6. 声明数据卷,用于持久化存储生成的文件
VOLUME ["/app/data"]

# 7. 添加健康检查
# 每隔 30 秒检查一次,超时时间为 3 秒
# 如果连续 3 次失败则标记为不健康,连续 2 次成功则标记为健康
# 使用 curl 检查本地 5000 端口是否返回 HTTP 200 状态码
HEALTHCHECK --interval=30s --timeout=3s --retries=3 --start-period=5s \
  CMD curl -f http://localhost:5000/ || exit 1

# 8. 声明容器运行时监听的端口
EXPOSE 5000

# 9. 定义容器启动时运行的命令
CMD ["python", "app.py"]

2.构建镜像

docker build -t osd-tool:v1 .

3.运行容器

docker run -d \
  -p 5000:5000 \
  --name my-osd-app \
  -v osd_data:/app/data \
  osd-tool:v1

更多推荐