Initial project upload
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
"""
|
||||
BIOS Baseline 모델
|
||||
backend/models/bios_baseline.py
|
||||
|
||||
Redfish로 가져온 표준 BIOS 설정을 저장하고 관리하는 모델
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from backend.models.user import db
|
||||
|
||||
class BiosBaseline(db.Model):
|
||||
"""BIOS 설정 기준값 모델 - Redfish로 가져온 표준 설정"""
|
||||
__tablename__ = 'bios_baselines'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
# 기본 정보
|
||||
name = db.Column(db.String(100), nullable=False, unique=True) # 예: "R6615_GPU_Standard_2024"
|
||||
server_model = db.Column(db.String(100), nullable=False) # 예: "PowerEdge R6615"
|
||||
server_type = db.Column(db.String(50)) # 예: "GPU Server", "Storage Server"
|
||||
description = db.Column(db.String(500)) # 설명
|
||||
|
||||
# Redfish로 가져온 원본 데이터
|
||||
bios_data = db.Column(db.JSON, nullable=False) # BIOS 설정값 전체 (JSON)
|
||||
bios_metadata = db.Column(db.JSON, nullable=True) # BIOS 메타데이터 (AttributeName -> DisplayName 매핑)
|
||||
idrac_data = db.Column(db.JSON, nullable=True) # iDRAC 설정값 (JSON)
|
||||
idrac_metadata = db.Column(db.JSON, nullable=True) # iDRAC 메타데이터 (DisplayName 매핑)
|
||||
raid_data = db.Column(db.JSON, nullable=True) # RAID/Storage 구성 (JSON)
|
||||
nic_data = db.Column(db.JSON, nullable=True) # NIC 설정 (Adapters)
|
||||
boot_data = db.Column(db.JSON, nullable=True) # 부팅 설정 (Order)
|
||||
firmware_data = db.Column(db.JSON, nullable=True) # 펌웨어 목록
|
||||
|
||||
# 메타데이터
|
||||
source_ip = db.Column(db.String(50)) # 이 baseline을 생성한 서버 IP
|
||||
source_service_tag = db.Column(db.String(50)) # 원본 서버 Service Tag
|
||||
bios_version = db.Column(db.String(50)) # BIOS 버전
|
||||
total_settings = db.Column(db.Integer) # 총 설정 항목 수
|
||||
|
||||
# 복사 관계 (어떤 baseline에서 복사되었는지)
|
||||
copied_from_id = db.Column(db.Integer, db.ForeignKey('bios_baselines.id'), nullable=True)
|
||||
|
||||
# 상태 및 관리
|
||||
is_active = db.Column(db.Boolean, default=True)
|
||||
created_by = db.Column(db.String(100)) # 생성자
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
# 관계 설정
|
||||
copied_from = db.relationship('BiosBaseline', remote_side=[id], backref='copies')
|
||||
|
||||
def to_dict(self, include_data=False):
|
||||
"""
|
||||
딕셔너리로 변환
|
||||
include_data: True면 bios_data, idrac_data, raid_data 등 상세 데이터 포함 (상세 조회 시)
|
||||
"""
|
||||
data = {
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
'server_model': self.server_model,
|
||||
'server_type': self.server_type,
|
||||
'description': self.description,
|
||||
'source_ip': self.source_ip,
|
||||
'source_service_tag': self.source_service_tag,
|
||||
'bios_version': self.bios_version,
|
||||
'total_settings': self.total_settings,
|
||||
'copied_from_id': self.copied_from_id,
|
||||
'is_active': self.is_active,
|
||||
'created_by': self.created_by,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||||
'updated_at': self.updated_at.isoformat() if self.updated_at else None
|
||||
}
|
||||
|
||||
if include_data:
|
||||
data['bios_data'] = self.bios_data
|
||||
data['bios_metadata'] = self.bios_metadata
|
||||
data['idrac_data'] = self.idrac_data
|
||||
data['idrac_metadata'] = self.idrac_metadata
|
||||
data['raid_data'] = self.raid_data
|
||||
data['nic_data'] = self.nic_data
|
||||
data['boot_data'] = self.boot_data
|
||||
data['firmware_data'] = self.firmware_data
|
||||
|
||||
return data
|
||||
|
||||
def __repr__(self):
|
||||
return f'<BiosBaseline {self.name} ({self.server_model})>'
|
||||
@@ -0,0 +1,134 @@
|
||||
"""
|
||||
펌웨어 버전 관리 모델
|
||||
backend/models/firmware_version.py
|
||||
"""
|
||||
|
||||
from backend.models.user import db
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Column, Integer, String, DateTime, Boolean, Text
|
||||
|
||||
|
||||
class FirmwareVersion(db.Model):
|
||||
"""펌웨어 최신 버전 정보 모델"""
|
||||
__tablename__ = 'firmware_versions'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
|
||||
# 컴포넌트 정보
|
||||
component_name = Column(String(100), nullable=False) # BIOS, iDRAC, PERC 등
|
||||
component_type = Column(String(50)) # Firmware, Driver 등
|
||||
vendor = Column(String(50)) # Dell, Intel, Broadcom 등
|
||||
|
||||
# 서버 모델 (선택적)
|
||||
server_model = Column(String(100)) # PowerEdge R750, R640 등 (비어있으면 모든 모델)
|
||||
|
||||
# 버전 정보
|
||||
latest_version = Column(String(50), nullable=False) # 최신 버전
|
||||
release_date = Column(String(20)) # 릴리즈 날짜
|
||||
|
||||
# 다운로드 정보
|
||||
download_url = Column(String(500)) # DUP 파일 다운로드 URL
|
||||
file_name = Column(String(200)) # DUP 파일명
|
||||
file_size_mb = Column(Integer) # 파일 크기 (MB)
|
||||
|
||||
# 메타 정보
|
||||
notes = Column(Text) # 비고 (버전 변경 사항 등)
|
||||
is_critical = Column(Boolean, default=False) # 중요 업데이트 여부
|
||||
is_active = Column(Boolean, default=True) # 활성화 여부
|
||||
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
def to_dict(self):
|
||||
"""딕셔너리로 변환"""
|
||||
return {
|
||||
'id': self.id,
|
||||
'component_name': self.component_name,
|
||||
'component_type': self.component_type,
|
||||
'vendor': self.vendor,
|
||||
'server_model': self.server_model,
|
||||
'latest_version': self.latest_version,
|
||||
'release_date': self.release_date,
|
||||
'download_url': self.download_url,
|
||||
'file_name': self.file_name,
|
||||
'file_size_mb': self.file_size_mb,
|
||||
'notes': self.notes,
|
||||
'is_critical': self.is_critical,
|
||||
'is_active': self.is_active,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||||
'updated_at': self.updated_at.isoformat() if self.updated_at else None
|
||||
}
|
||||
|
||||
def __repr__(self):
|
||||
return f'<FirmwareVersion {self.component_name} {self.latest_version}>'
|
||||
|
||||
|
||||
class FirmwareComparisonResult:
|
||||
"""펌웨어 버전 비교 결과"""
|
||||
|
||||
def __init__(self, component_name, current_version, latest_version=None):
|
||||
self.component_name = component_name
|
||||
self.current_version = current_version
|
||||
self.latest_version = latest_version
|
||||
self.status = self._compare_versions()
|
||||
self.recommendation = self._get_recommendation()
|
||||
|
||||
def _compare_versions(self):
|
||||
"""버전 비교"""
|
||||
if not self.latest_version:
|
||||
return 'unknown' # 최신 버전 정보 없음
|
||||
|
||||
if self.current_version == self.latest_version:
|
||||
return 'latest' # 최신
|
||||
|
||||
# 버전 비교 (단순 문자열 비교보다 정교하게)
|
||||
if self._is_older(self.current_version, self.latest_version):
|
||||
return 'outdated' # 업데이트 필요
|
||||
else:
|
||||
return 'unknown' # 판단 불가
|
||||
|
||||
def _is_older(self, current, latest):
|
||||
"""
|
||||
버전 비교 (간단한 버전 비교)
|
||||
예: 2.10.0 < 2.15.0
|
||||
"""
|
||||
try:
|
||||
# 버전 문자열을 숫자 리스트로 변환
|
||||
current_parts = [int(x) for x in current.replace('-', '.').split('.') if x.isdigit()]
|
||||
latest_parts = [int(x) for x in latest.replace('-', '.').split('.') if x.isdigit()]
|
||||
|
||||
# 길이 맞추기
|
||||
max_len = max(len(current_parts), len(latest_parts))
|
||||
current_parts += [0] * (max_len - len(current_parts))
|
||||
latest_parts += [0] * (max_len - len(latest_parts))
|
||||
|
||||
# 비교
|
||||
for c, l in zip(current_parts, latest_parts):
|
||||
if c < l:
|
||||
return True
|
||||
elif c > l:
|
||||
return False
|
||||
|
||||
return False # 같음
|
||||
|
||||
except:
|
||||
# 비교 실패 시 문자열 비교
|
||||
return current < latest
|
||||
|
||||
def _get_recommendation(self):
|
||||
"""권장 사항"""
|
||||
if self.status == 'outdated':
|
||||
return f'업데이트 권장: {self.current_version} → {self.latest_version}'
|
||||
elif self.status == 'latest':
|
||||
return '최신 버전'
|
||||
else:
|
||||
return '버전 정보 확인 필요'
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'component_name': self.component_name,
|
||||
'current_version': self.current_version,
|
||||
'latest_version': self.latest_version,
|
||||
'status': self.status,
|
||||
'recommendation': self.recommendation
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
"""
|
||||
Dell iDRAC 서버 관리 모델
|
||||
backend/models/idrac_server.py
|
||||
"""
|
||||
|
||||
from backend.models.user import db
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Column, Integer, String, DateTime, Boolean
|
||||
|
||||
|
||||
class IdracServer(db.Model):
|
||||
"""iDRAC 서버 정보 모델"""
|
||||
__tablename__ = 'idrac_servers'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String(100), nullable=False) # 서버명
|
||||
ip_address = Column(String(50), nullable=False, unique=True) # iDRAC IP
|
||||
username = Column(String(50), default='root') # iDRAC 사용자명
|
||||
password = Column(String(200), nullable=False) # 비밀번호 (암호화 권장)
|
||||
|
||||
# 분류 정보
|
||||
group_name = Column(String(100)) # 그룹명 (고객사, 프로젝트명)
|
||||
location = Column(String(100)) # 위치
|
||||
model = Column(String(100)) # 서버 모델
|
||||
|
||||
# 상태 정보
|
||||
status = Column(String(20), default='registered') # registered, online, offline, updating
|
||||
last_connected = Column(DateTime) # 마지막 연결 시간
|
||||
last_updated = Column(DateTime) # 마지막 업데이트 시간
|
||||
|
||||
# 펌웨어 정보
|
||||
current_bios = Column(String(50)) # 현재 BIOS 버전
|
||||
target_bios = Column(String(50)) # 목표 BIOS 버전
|
||||
|
||||
# 메타 정보
|
||||
notes = Column(String(500)) # 비고
|
||||
is_active = Column(Boolean, default=True) # 활성화 여부
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
def to_dict(self):
|
||||
"""딕셔너리로 변환"""
|
||||
return {
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
'ip_address': self.ip_address,
|
||||
'username': self.username,
|
||||
'group_name': self.group_name,
|
||||
'location': self.location,
|
||||
'model': self.model,
|
||||
'status': self.status,
|
||||
'last_connected': self.last_connected.isoformat() if self.last_connected else None,
|
||||
'last_updated': self.last_updated.isoformat() if self.last_updated else None,
|
||||
'current_bios': self.current_bios,
|
||||
'target_bios': self.target_bios,
|
||||
'notes': self.notes,
|
||||
'is_active': self.is_active,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None
|
||||
}
|
||||
|
||||
def __repr__(self):
|
||||
return f'<IdracServer {self.name} ({self.ip_address})>'
|
||||
@@ -0,0 +1,36 @@
|
||||
from datetime import datetime
|
||||
from backend.models.user import db
|
||||
|
||||
class ServerConfigSnapshot(db.Model):
|
||||
"""
|
||||
서버 설정 스냅샷 (BIOS, iDRAC, System 등)
|
||||
"""
|
||||
__tablename__ = 'server_config_snapshots'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
server_id = db.Column(db.Integer, db.ForeignKey('idrac_servers.id'), nullable=False)
|
||||
|
||||
# 설정 유형: bios, idrac, system
|
||||
config_type = db.Column(db.String(20), nullable=False)
|
||||
|
||||
# JSON 데이터 저장
|
||||
data = db.Column(db.JSON, nullable=True)
|
||||
|
||||
# 데이터 해시 (변경 감지용 - Optional)
|
||||
hash_value = db.Column(db.String(64), nullable=True)
|
||||
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
# 관계 설정 (IdracServer와의 관계는 IdracServer 모델에서 backref로 설정될 수도 있음)
|
||||
server = db.relationship('IdracServer', backref=db.backref('config_snapshots', lazy=True))
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'server_id': self.server_id,
|
||||
'server_name': self.server.name if self.server else 'Unknown',
|
||||
'config_type': self.config_type,
|
||||
'data': self.data,
|
||||
'updated_at': self.updated_at.strftime('%Y-%m-%d %H:%M:%S')
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from backend.models.user import db
|
||||
|
||||
|
||||
class SystemSetting(db.Model):
|
||||
__tablename__ = "system_setting"
|
||||
|
||||
key: Mapped[str] = mapped_column(String(80), primary_key=True)
|
||||
value: Mapped[str] = mapped_column(Text, nullable=False, default="")
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
from backend.models.user import db
|
||||
|
||||
class TelegramBot(db.Model):
|
||||
__tablename__ = 'telegram_bots'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(100), nullable=False) # 봇 식별 이름 (예: 알림용, 경고용)
|
||||
token = db.Column(db.String(200), nullable=False)
|
||||
chat_id = db.Column(db.String(100), nullable=False)
|
||||
is_active = db.Column(db.Boolean, default=True)
|
||||
description = db.Column(db.String(255), nullable=True)
|
||||
# 알림 유형: 콤마로 구분 (예: "auth,activity,system")
|
||||
notification_types = db.Column(db.String(255), default="auth,activity,system")
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
'token': self.token,
|
||||
'chat_id': self.chat_id,
|
||||
'is_active': self.is_active,
|
||||
'description': self.description,
|
||||
'notification_types': self.notification_types
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
from __future__ import annotations
|
||||
from typing import Optional
|
||||
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_login import UserMixin
|
||||
from sqlalchemy import String, Boolean
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from werkzeug.security import check_password_hash as wz_check_password_hash
|
||||
import logging
|
||||
|
||||
# passlib: Argon2id 기본, scrypt/pbkdf2는 검증만 (점진 마이그레이션)
|
||||
from passlib.context import CryptContext
|
||||
|
||||
db = SQLAlchemy()
|
||||
|
||||
# Argon2id를 기본으로 사용하고, scrypt/pbkdf2_sha256은 검증만 허용(=deprecated)
|
||||
# 로그인에 성공하면 자동으로 Argon2id로 재해시 저장합니다.
|
||||
pwd_ctx = CryptContext(
|
||||
schemes=["argon2", "scrypt", "pbkdf2_sha256"],
|
||||
default="argon2",
|
||||
deprecated=["scrypt", "pbkdf2_sha256"],
|
||||
# Argon2id 파라미터 (기본도 충분하지만 서비스 보안수준에 맞춰 조정 가능)
|
||||
# time_cost: 2~4, memory_cost: 64~256 MiB 권장 범위
|
||||
argon2__type="ID",
|
||||
argon2__time_cost=3,
|
||||
argon2__memory_cost=102400, # 100 MiB
|
||||
argon2__parallelism=8,
|
||||
# PBKDF2 라운드 상향 (레거시 검증용)
|
||||
pbkdf2_sha256__rounds=300_000,
|
||||
)
|
||||
|
||||
class User(db.Model, UserMixin):
|
||||
__tablename__ = "user"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
username: Mapped[str] = mapped_column(String(80), unique=True, nullable=False)
|
||||
email: Mapped[str] = mapped_column(String(120), unique=True, nullable=False, index=True)
|
||||
password: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
is_admin: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||
|
||||
# 가입 승인 관련 필드
|
||||
is_approved: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||
approval_token: Mapped[Optional[str]] = mapped_column(String(100), unique=True, nullable=True)
|
||||
|
||||
# ── 유틸 메서드
|
||||
def __repr__(self) -> str: # pragma: no cover
|
||||
return f"<User id={self.id} email={self.email} active={self.is_active}>"
|
||||
|
||||
# 신규 저장/변경 시 항상 Argon2id로 해시
|
||||
def set_password(self, password: str) -> None:
|
||||
self.password = pwd_ctx.hash(password)
|
||||
|
||||
# 혼합 검증: scrypt/pbkdf2/argon2 모두 검증 가능
|
||||
# 검증 성공 + 레거시 스킴이면 Argon2id로 즉시 재해시 & 커밋
|
||||
def check_password(self, password: str) -> bool:
|
||||
"""
|
||||
- 우선 저장된 해시의 '형식'을 보고 검증기를 선택한다.
|
||||
* 'scrypt:' 또는 'pbkdf2:'로 시작하면 → Werkzeug 검증
|
||||
* 그 외 → passlib(CryptContext) 검증
|
||||
- 검증에 성공했고 현재 해시가 Argon2가 아니거나(passlib가 needs_update True),
|
||||
Werkzeug 형식(scrypt/pbkdf2)이라면 즉시 Argon2id로 재해시 저장한다.
|
||||
"""
|
||||
raw = self.password or ""
|
||||
|
||||
ok = False
|
||||
used_werkzeug = False
|
||||
|
||||
try:
|
||||
# 1) Werkzeug 포맷 감지 (예: 'scrypt:32768:8:1$...', 'pbkdf2:sha256:260000$...')
|
||||
if raw.startswith("scrypt:") or raw.startswith("pbkdf2:"):
|
||||
used_werkzeug = True
|
||||
ok = wz_check_password_hash(raw, password) # hashlib 기반 → 형식 그대로 검증
|
||||
else:
|
||||
# 2) passlib 포맷(argon2/$scrypt$/pbkdf2_sha256) 시도
|
||||
ok = pwd_ctx.verify(password, raw)
|
||||
except Exception as e:
|
||||
logging.warning("password verify failed: %s", e)
|
||||
ok = False
|
||||
|
||||
if not ok:
|
||||
return False
|
||||
|
||||
# ── 여기까지 왔으면 검증 성공. 필요 시 Argon2id로 즉시 업그레이드.
|
||||
try:
|
||||
need_upgrade = False
|
||||
|
||||
if used_werkzeug:
|
||||
# Werkzeug 형식(scrypt/pbkdf2)은 우리 정책상 모두 Argon2로 마이그레이션
|
||||
need_upgrade = True
|
||||
else:
|
||||
# passlib가 판단하는 업그레이드 필요 여부(파라미터/알고리즘 기준)
|
||||
need_upgrade = pwd_ctx.needs_update(raw)
|
||||
|
||||
if need_upgrade:
|
||||
self.password = pwd_ctx.hash(password) # Argon2id 기본
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
# 업그레이드 실패해도 로그인 자체는 성공시킨다(다음 로그인 때 재시도)
|
||||
logging.warning("password rehash (argon2) failed: %s", e)
|
||||
db.session.rollback()
|
||||
|
||||
return True
|
||||
|
||||
# Flask-Login 호환 (UserMixin 기본 get_id 사용 가능하지만 명시)
|
||||
def get_id(self) -> str: # pragma: no cover
|
||||
return str(self.id)
|
||||
|
||||
# ── 조회 헬퍼
|
||||
@staticmethod
|
||||
def find_by_email(email: Optional[str]) -> Optional["User"]:
|
||||
q = (email or "").strip().lower()
|
||||
if not q:
|
||||
return None
|
||||
try:
|
||||
return User.query.filter_by(email=q).first()
|
||||
except Exception as e:
|
||||
logging.error(f"User find_by_email error: {e}")
|
||||
db.session.rollback()
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def find_by_username(username: Optional[str]) -> Optional["User"]:
|
||||
q = (username or "").strip()
|
||||
if not q:
|
||||
return None
|
||||
try:
|
||||
return User.query.filter_by(username=q).first()
|
||||
except Exception as e:
|
||||
logging.error(f"User find_by_username error: {e}")
|
||||
db.session.rollback()
|
||||
return None
|
||||
|
||||
|
||||
# Flask-Login user_loader (SQLAlchemy 2.0 방식)
|
||||
def load_user(user_id: str) -> Optional[User]: # pragma: no cover
|
||||
try:
|
||||
return db.session.get(User, int(user_id))
|
||||
except Exception:
|
||||
return None
|
||||
Reference in New Issue
Block a user