init: server build project template
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
name: build
|
||||
on: [push]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with: { python-version: '3.11' }
|
||||
- run: pip install pyyaml
|
||||
- run: python scripts/build.py
|
||||
- run: |
|
||||
git config user.email ci@gitea && git config user.name ci
|
||||
git add docs/estimate.md web/data.json
|
||||
git diff --staged --quiet || git commit -m "ci: rebuild estimate [skip ci]"
|
||||
git push
|
||||
@@ -0,0 +1,5 @@
|
||||
generated/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.venv/
|
||||
.env
|
||||
@@ -0,0 +1,17 @@
|
||||
.PHONY: build prices serve clean install
|
||||
|
||||
install:
|
||||
pip install --user pyyaml jinja2 requests beautifulsoup4
|
||||
|
||||
build:
|
||||
python3 scripts/build.py
|
||||
|
||||
prices:
|
||||
python3 scripts/fetch_prices.py
|
||||
|
||||
serve:
|
||||
@echo "http://localhost:8000/web/index.html"
|
||||
python3 -m http.server 8000
|
||||
|
||||
clean:
|
||||
rm -rf generated/*
|
||||
@@ -0,0 +1,26 @@
|
||||
# Server Build Project
|
||||
|
||||
Проект сборки серверной инфраструктуры: требования, комплектующие, ссылки, смета.
|
||||
|
||||
## 📚 Документация
|
||||
- [Общая идея](docs/idea.md)
|
||||
- [Структура](docs/structure.md)
|
||||
- [Требования к узлам](docs/requirements.md)
|
||||
- [Смета](docs/estimate.md)
|
||||
|
||||
## 📊 Данные
|
||||
- Комплектующие: [`data/components.yaml`](data/components.yaml)
|
||||
- Узлы: [`data/nodes.yaml`](data/nodes.yaml)
|
||||
- Источники цен: [`data/prices.yaml`](data/prices.yaml)
|
||||
|
||||
## 🛠 Сборка
|
||||
```bash
|
||||
make build # пересобрать docs/estimate.md и web/index.html
|
||||
make prices # обновить цены из источников (заглушка)
|
||||
make serve # локальный предпросмотр HTML-дашборда
|
||||
|
||||
\
|
||||
/
|
||||
end
|
||||
|
||||
```
|
||||
@@ -0,0 +1,38 @@
|
||||
# Каталог комплектующих. Одна позиция = один компонент.
|
||||
# links: можно указать несколько ссылок на разные магазины.
|
||||
items:
|
||||
- id: cpu-epyc-7443p
|
||||
category: CPU
|
||||
name: "AMD EPYC 7443P"
|
||||
price: 130000
|
||||
links:
|
||||
- { title: "DNS", url: "https://www.dns-shop.ru/" }
|
||||
- { title: "Citilink", url: "https://www.citilink.ru/" }
|
||||
|
||||
- id: ram-ddr4-ecc-32
|
||||
category: RAM
|
||||
name: "Samsung DDR4 ECC 32GB"
|
||||
price: 18000
|
||||
links:
|
||||
- { title: "DNS", url: "https://www.dns-shop.ru/" }
|
||||
|
||||
- id: mb-romed8-2t
|
||||
category: Motherboard
|
||||
name: "ASRock Rack ROMED8-2T"
|
||||
price: 65000
|
||||
links:
|
||||
- { title: "ServerFlow", url: "https://serverflow.ru/" }
|
||||
|
||||
- id: case-4u
|
||||
category: Case
|
||||
name: "4U Rackmount 24-bay"
|
||||
price: 35000
|
||||
links:
|
||||
- { title: "AliExpress", url: "https://aliexpress.ru/" }
|
||||
|
||||
- id: psu-1200w
|
||||
category: PSU
|
||||
name: "Seasonic 1200W Platinum"
|
||||
price: 25000
|
||||
links:
|
||||
- { title: "DNS", url: "https://www.dns-shop.ru/" }
|
||||
@@ -0,0 +1,18 @@
|
||||
# Узлы и их состав. qty — количество, price — опционально переопределить цену из каталога.
|
||||
nodes:
|
||||
- id: node-1
|
||||
name: "Hypervisor"
|
||||
role: "Виртуализация"
|
||||
items:
|
||||
- { component_id: cpu-epyc-7443p, qty: 1 }
|
||||
- { component_id: ram-ddr4-ecc-32, qty: 8 }
|
||||
- { component_id: mb-romed8-2t, qty: 1 }
|
||||
- { component_id: case-4u, qty: 1 }
|
||||
- { component_id: psu-1200w, qty: 2 }
|
||||
|
||||
- id: node-2
|
||||
name: "NAS"
|
||||
role: "Хранилище"
|
||||
items:
|
||||
- { component_id: case-4u, qty: 1 }
|
||||
- { component_id: psu-1200w, qty: 1 }
|
||||
@@ -0,0 +1,9 @@
|
||||
# Источники цен. fetch_prices.py может ходить на эти URL.
|
||||
currency: RUB
|
||||
sources:
|
||||
- { id: dns, name: "DNS", base: "https://www.dns-shop.ru/" }
|
||||
- { id: citilink, name: "Citilink", base: "https://www.citilink.ru/" }
|
||||
- { id: serverflow,name: "ServerFlow",base: "https://serverflow.ru/" }
|
||||
# Таблица переопределений (если хочешь фиксировать цену вручную):
|
||||
overrides:
|
||||
# cpu-epyc-7443p: 125000
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
Смета
|
||||
Файл генерируется автоматически: make build. Правки вручную будут перезаписаны.
|
||||
|
||||
(запусти make build)
|
||||
@@ -0,0 +1,19 @@
|
||||
|
||||
Общая идея
|
||||
Цель
|
||||
Собрать отказоустойчивую серверную инфраструктуру для <задача>.
|
||||
|
||||
Задачи
|
||||
□ Определить узлы и роли
|
||||
□ Согласовать бюджет
|
||||
□ Выбрать комплектующие
|
||||
□ Закупить и собрать
|
||||
Ограничения
|
||||
Бюджет: ~ X ₽
|
||||
|
||||
Сроки: Q1 2025
|
||||
|
||||
Энергопотребление / шум / стойка
|
||||
|
||||
Критерии успеха
|
||||
...
|
||||
@@ -0,0 +1,19 @@
|
||||
|
||||
Технические требования к узлам
|
||||
Узел 1: Hypervisor
|
||||
CPU: ≥16 ядер
|
||||
|
||||
RAM: ≥128 ГБ
|
||||
|
||||
Диск: NVMe ≥2 ТБ (mirror)
|
||||
|
||||
Сеть: 2×10GbE
|
||||
|
||||
Узел 2: NAS
|
||||
CPU: ≥4 ядра
|
||||
|
||||
RAM: ≥16 ГБ
|
||||
|
||||
Диск: 4×8 ТБ HDD (RAID10)
|
||||
|
||||
Сеть: 1×10GbE
|
||||
@@ -0,0 +1,16 @@
|
||||
|
||||
Структура
|
||||
Схема узлов
|
||||
graph TD
|
||||
Net[Сеть / Роутер] --> FW[Firewall]
|
||||
FW --> HV[Hypervisor]
|
||||
HV --> VM1[VM: Web]
|
||||
HV --> VM2[VM: DB]
|
||||
HV --> VM3[VM: Backup]
|
||||
HV --> NAS[(NAS / Хранилище)]
|
||||
Роли узлов
|
||||
Узел Роль Критичность
|
||||
Hypervisor Виртуализация Высокая
|
||||
NAS Хранилище бэкапов Средняя
|
||||
EOF
|
||||
|
||||
Executable
+57
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Генерирует docs/estimate.md (с mermaid-графиками) и web/index.html."""
|
||||
from pathlib import Path
|
||||
import json, yaml
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
DATA, GEN, WEB = ROOT/"data", ROOT/"generated", ROOT/"web"
|
||||
GEN.mkdir(exist_ok=True); WEB.mkdir(exist_ok=True)
|
||||
|
||||
def load(p): return yaml.safe_load(p.read_text())
|
||||
|
||||
def main():
|
||||
comps = {c["id"]: c for c in load(DATA/"components.yaml")["items"]}
|
||||
nodes = load(DATA/"nodes.yaml")["nodes"]
|
||||
overrides = load(DATA/"prices.yaml").get("overrides") or {}
|
||||
|
||||
rows, per_node, per_cat, total = [], {}, {}, 0
|
||||
for n in nodes:
|
||||
nsum = 0
|
||||
for it in n["items"]:
|
||||
c = comps[it["component_id"]]
|
||||
price = overrides.get(c["id"], it.get("price", c.get("price", 0)))
|
||||
qty = it.get("qty", 1)
|
||||
sub = price * qty
|
||||
nsum += sub; total += sub
|
||||
per_cat[c["category"]] = per_cat.get(c["category"], 0) + sub
|
||||
rows.append((n["name"], c["category"], c["name"], qty, price, sub, c.get("links", [])))
|
||||
per_node[n["name"]] = nsum
|
||||
|
||||
# ---------- markdown ----------
|
||||
md = ["# Смета\n", f"**Итого: {total:,.0f} ₽**\n".replace(",", " "), "## Таблица\n"]
|
||||
md.append("| Узел | Категория | Наименование | Кол-во | Цена, ₽ | Сумма, ₽ | Ссылки |")
|
||||
md.append("|------|-----------|--------------|:------:|--------:|---------:|--------|")
|
||||
for r in rows:
|
||||
links = " · ".join(f"[{l['title']}]({l['url']})" for l in r[6])
|
||||
md.append(f"| {r[0]} | {r[1]} | {r[2]} | {r[3]} | {r[4]:,} | {r[5]:,} | {links} |".replace(",", " "))
|
||||
md += ["\n## Распределение по узлам\n", "```mermaid", "pie showData", ' title "Бюджет по узлам"']
|
||||
for k, v in per_node.items(): md.append(f' "{k}" : {v}')
|
||||
md.append("```\n")
|
||||
md += ["## Распределение по категориям\n", "```mermaid", "pie showData", ' title "Бюджет по категориям"']
|
||||
for k, v in per_cat.items(): md.append(f' "{k}" : {v}')
|
||||
md.append("```")
|
||||
(ROOT/"docs"/"estimate.md").write_text("\n".join(md))
|
||||
|
||||
# ---------- HTML-дашборд ----------
|
||||
payload = {
|
||||
"total": total,
|
||||
"per_node": per_node,
|
||||
"per_cat": per_cat,
|
||||
"rows": [{"node":r[0],"cat":r[1],"name":r[2],"qty":r[3],"price":r[4],"sub":r[5],
|
||||
"links":r[6]} for r in rows],
|
||||
}
|
||||
(WEB/"data.json").write_text(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||
print(f"[OK] Итого: {total:,.0f} ₽".replace(",", " "))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Заглушка: обходит sources из data/prices.yaml и обновляет цены.
|
||||
Здесь оставлены TODO — реализация парсеров зависит от сайта."""
|
||||
from pathlib import Path
|
||||
import yaml
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
prices = yaml.safe_load((ROOT/"data"/"prices.yaml").read_text())
|
||||
print("Sources:", [s["name"] for s in prices["sources"]])
|
||||
# TODO: requests + BeautifulSoup для каждой позиции из components.yaml
|
||||
# TODO: сохранить результат в data/prices.yaml -> overrides
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
const fmt = n => n.toLocaleString('ru-RU') + ' ₽';
|
||||
fetch('data.json').then(r => r.json()).then(d => {
|
||||
document.getElementById('total').textContent = fmt(d.total);
|
||||
|
||||
new Chart(document.getElementById('chNodes'), {
|
||||
type: 'pie',
|
||||
data: { labels: Object.keys(d.per_node), datasets: [{ data: Object.values(d.per_node) }] },
|
||||
options: { plugins: { title: { display: true, text: 'Бюджет по узлам' } } }
|
||||
});
|
||||
new Chart(document.getElementById('chCats'), {
|
||||
type: 'doughnut',
|
||||
data: { labels: Object.keys(d.per_cat), datasets: [{ data: Object.values(d.per_cat) }] },
|
||||
options: { plugins: { title: { display: true, text: 'Бюджет по категориям' } } }
|
||||
});
|
||||
|
||||
const tb = document.querySelector('#tbl tbody');
|
||||
d.rows.forEach(r => {
|
||||
const links = r.links.map(l => `<a href="${l.url}" target="_blank">${l.title}</a>`).join(' · ');
|
||||
tb.insertAdjacentHTML('beforeend',
|
||||
`<tr><td>${r.node}</td><td>${r.cat}</td><td>${r.name}</td><td>${r.qty}</td>
|
||||
<td class="num">${r.price.toLocaleString('ru-RU')}</td>
|
||||
<td class="num">${r.sub.toLocaleString('ru-RU')}</td><td>${links}</td></tr>`);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
<!doctype html>
|
||||
<html lang="ru"><head>
|
||||
<meta charset="utf-8"><title>Server Build — Dashboard</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<style>
|
||||
body{font:14px/1.5 system-ui,sans-serif;max-width:1100px;margin:2rem auto;padding:0 1rem;color:#222}
|
||||
h1{margin-bottom:.2rem} .total{font-size:2rem;font-weight:600;color:#1f7a1f}
|
||||
.grid{display:grid;grid-template-columns:1fr 1fr;gap:1.5rem;margin:1.5rem 0}
|
||||
table{width:100%;border-collapse:collapse;font-size:13px}
|
||||
th,td{padding:.45rem .6rem;border-bottom:1px solid #eee;text-align:left}
|
||||
th{background:#f6f8fa} td.num{text-align:right;font-variant-numeric:tabular-nums}
|
||||
a{color:#0969da;text-decoration:none} a:hover{text-decoration:underline}
|
||||
</style></head><body>
|
||||
<h1>Server Build — Dashboard</h1>
|
||||
<div class="total" id="total">— ₽</div>
|
||||
<div class="grid">
|
||||
<canvas id="chNodes"></canvas>
|
||||
<canvas id="chCats"></canvas>
|
||||
</div>
|
||||
<table id="tbl"><thead><tr>
|
||||
<th>Узел</th><th>Категория</th><th>Наименование</th><th>Кол-во</th>
|
||||
<th class="num">Цена</th><th class="num">Сумма</th><th>Ссылки</th>
|
||||
</tr></thead><tbody></tbody></table>
|
||||
<script src="app.js"></script>
|
||||
</body></html>
|
||||
Reference in New Issue
Block a user