From a6167fccfa94fb13ee782701f281c13841db8fa7 Mon Sep 17 00:00:00 2001 From: Artemiy Date: Sat, 10 Oct 2026 06:27:51 +0500 Subject: [PATCH] init: server build project template --- .gitea/workflows/build.yml | 16 +++++++++++ .gitignore | 5 ++++ Makefile | 17 ++++++++++++ README.md | 26 +++++++++++++++++ data/components.yaml | 38 +++++++++++++++++++++++++ data/nodes.yaml | 18 ++++++++++++ data/prices.yaml | 9 ++++++ docs/estimate.md | 5 ++++ docs/idea.md | 19 +++++++++++++ docs/requirements.md | 19 +++++++++++++ docs/structure.md | 16 +++++++++++ scripts/build.py | 57 ++++++++++++++++++++++++++++++++++++++ scripts/fetch_prices.py | 10 +++++++ web/app.js | 24 ++++++++++++++++ web/index.html | 25 +++++++++++++++++ 15 files changed, 304 insertions(+) create mode 100644 .gitea/workflows/build.yml create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100644 data/components.yaml create mode 100644 data/nodes.yaml create mode 100644 data/prices.yaml create mode 100644 docs/estimate.md create mode 100644 docs/idea.md create mode 100644 docs/requirements.md create mode 100644 docs/structure.md create mode 100755 scripts/build.py create mode 100755 scripts/fetch_prices.py create mode 100644 web/app.js create mode 100644 web/index.html diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..be727a0 --- /dev/null +++ b/.gitea/workflows/build.yml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..26c2e83 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +generated/ +__pycache__/ +*.pyc +.venv/ +.env diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d93ff88 --- /dev/null +++ b/Makefile @@ -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/* diff --git a/README.md b/README.md new file mode 100644 index 0000000..b41a6d9 --- /dev/null +++ b/README.md @@ -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 + +``` diff --git a/data/components.yaml b/data/components.yaml new file mode 100644 index 0000000..8912044 --- /dev/null +++ b/data/components.yaml @@ -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/" } diff --git a/data/nodes.yaml b/data/nodes.yaml new file mode 100644 index 0000000..41f725b --- /dev/null +++ b/data/nodes.yaml @@ -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 } diff --git a/data/prices.yaml b/data/prices.yaml new file mode 100644 index 0000000..7c9ab3f --- /dev/null +++ b/data/prices.yaml @@ -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 diff --git a/docs/estimate.md b/docs/estimate.md new file mode 100644 index 0000000..fdc1a51 --- /dev/null +++ b/docs/estimate.md @@ -0,0 +1,5 @@ + +Смета +Файл генерируется автоматически: make build. Правки вручную будут перезаписаны. + +(запусти make build) diff --git a/docs/idea.md b/docs/idea.md new file mode 100644 index 0000000..c92cee8 --- /dev/null +++ b/docs/idea.md @@ -0,0 +1,19 @@ + +Общая идея +Цель +Собрать отказоустойчивую серверную инфраструктуру для <задача>. + +Задачи +□ Определить узлы и роли +□ Согласовать бюджет +□ Выбрать комплектующие +□ Закупить и собрать +Ограничения +Бюджет: ~ X ₽ + +Сроки: Q1 2025 + +Энергопотребление / шум / стойка + +Критерии успеха +... diff --git a/docs/requirements.md b/docs/requirements.md new file mode 100644 index 0000000..dea0c99 --- /dev/null +++ b/docs/requirements.md @@ -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 diff --git a/docs/structure.md b/docs/structure.md new file mode 100644 index 0000000..37f5279 --- /dev/null +++ b/docs/structure.md @@ -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 + diff --git a/scripts/build.py b/scripts/build.py new file mode 100755 index 0000000..a266024 --- /dev/null +++ b/scripts/build.py @@ -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() diff --git a/scripts/fetch_prices.py b/scripts/fetch_prices.py new file mode 100755 index 0000000..0370554 --- /dev/null +++ b/scripts/fetch_prices.py @@ -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 diff --git a/web/app.js b/web/app.js new file mode 100644 index 0000000..411c557 --- /dev/null +++ b/web/app.js @@ -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 => `${l.title}`).join(' · '); + tb.insertAdjacentHTML('beforeend', + `${r.node}${r.cat}${r.name}${r.qty} + ${r.price.toLocaleString('ru-RU')} + ${r.sub.toLocaleString('ru-RU')}${links}`); + }); +}); diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..43dc49f --- /dev/null +++ b/web/index.html @@ -0,0 +1,25 @@ + + +Server Build — Dashboard + + +

Server Build — Dashboard

+
— ₽
+
+ + +
+ + + +
УзелКатегорияНаименованиеКол-воЦенаСуммаСсылки
+ +