Fix TypeScript build errors - remove unused imports
This commit is contained in:
parent
ecc1676fe5
commit
16c7d09a44
19
Dockerfile
19
Dockerfile
|
|
@ -1,9 +1,9 @@
|
|||
# Multi-stage build for IPMI Fan Control
|
||||
|
||||
# Backend stage
|
||||
FROM python:3.11-slim as backend-builder
|
||||
FROM python:3.11-slim AS backend-builder
|
||||
|
||||
WORKDIR /app/backend
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies for ipmitool
|
||||
RUN apt-get update && apt-get install -y \
|
||||
|
|
@ -18,16 +18,16 @@ COPY backend/requirements.txt .
|
|||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy backend source
|
||||
COPY backend/ .
|
||||
COPY backend/ ./backend/
|
||||
|
||||
# Frontend stage
|
||||
FROM node:20-alpine as frontend-builder
|
||||
FROM node:20-alpine AS frontend-builder
|
||||
|
||||
WORKDIR /app/frontend
|
||||
|
||||
# Install dependencies
|
||||
COPY frontend/package*.json ./
|
||||
RUN npm ci
|
||||
RUN npm install
|
||||
|
||||
# Copy frontend source and build
|
||||
COPY frontend/ .
|
||||
|
|
@ -56,10 +56,9 @@ COPY --from=frontend-builder /app/frontend/dist /app/frontend/dist
|
|||
# Create data directory
|
||||
RUN mkdir -p /app/data
|
||||
|
||||
# Environment variables
|
||||
ENV PYTHONPATH=/app/backend
|
||||
# Set Python path to include backend directory
|
||||
ENV PYTHONPATH=/app
|
||||
ENV DATA_DIR=/app/data
|
||||
ENV SECRET_KEY=change-me-in-production
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8000
|
||||
|
|
@ -68,5 +67,5 @@ EXPOSE 8000
|
|||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/health')" || exit 1
|
||||
|
||||
# Start command
|
||||
CMD ["python", "-m", "uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
# Start command - use absolute module path
|
||||
CMD ["python", "-m", "uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000", "--proxy-headers"]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2024 IPMI Fan Control
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
|
@ -12,7 +12,7 @@ from cryptography.fernet import Fernet
|
|||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||
|
||||
from config import settings
|
||||
from backend.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from sqlalchemy import (
|
|||
from sqlalchemy.orm import declarative_base, sessionmaker, relationship, Session
|
||||
from sqlalchemy.pool import StaticPool
|
||||
|
||||
from config import settings, DATA_DIR
|
||||
from backend.config import settings, DATA_DIR
|
||||
|
||||
# Create engine
|
||||
if settings.DATABASE_URL.startswith("sqlite"):
|
||||
|
|
|
|||
|
|
@ -9,12 +9,12 @@ from enum import Enum
|
|||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from database import (
|
||||
from backend.database import (
|
||||
Server, FanCurve, SensorData, FanData, SystemLog,
|
||||
get_db, SessionLocal
|
||||
)
|
||||
from ipmi_client import IPMIClient, TemperatureReading
|
||||
from config import settings
|
||||
from backend.ipmi_client import IPMIClient, TemperatureReading
|
||||
from backend.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -168,7 +168,7 @@ class FanController:
|
|||
return
|
||||
|
||||
# Create IPMI client
|
||||
from auth import decrypt_password
|
||||
from backend.auth import decrypt_password
|
||||
client = IPMIClient(
|
||||
host=server.host,
|
||||
username=server.username,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from typing import List, Dict, Optional, Tuple, Any
|
|||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
|
||||
from config import settings
|
||||
from backend.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -11,15 +11,15 @@ from fastapi.staticfiles import StaticFiles
|
|||
from fastapi.responses import FileResponse
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from config import settings
|
||||
from database import init_db, get_db, is_setup_complete, set_setup_complete, User, Server, FanCurve, SystemLog, SensorData, FanData
|
||||
from auth import (
|
||||
from backend.config import settings
|
||||
from backend.database import init_db, get_db, is_setup_complete, set_setup_complete, User, Server, FanCurve, SystemLog, SensorData, FanData
|
||||
from backend.auth import (
|
||||
verify_password, get_password_hash, create_access_token,
|
||||
decode_access_token, encrypt_password, decrypt_password
|
||||
)
|
||||
from ipmi_client import IPMIClient
|
||||
from fan_control import fan_controller, FanCurveManager, initialize_fan_controller, shutdown_fan_controller
|
||||
from schemas import (
|
||||
from backend.ipmi_client import IPMIClient
|
||||
from backend.fan_control import fan_controller, FanCurveManager, initialize_fan_controller, shutdown_fan_controller
|
||||
from backend.schemas import (
|
||||
UserCreate, UserLogin, UserResponse, Token, TokenData,
|
||||
ServerCreate, ServerUpdate, ServerResponse, ServerDetailResponse, ServerStatusResponse,
|
||||
FanCurveCreate, FanCurveResponse, FanCurvePoint,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ pydantic==2.5.3
|
|||
pydantic-settings==2.1.0
|
||||
python-jose[cryptography]==3.3.0
|
||||
passlib[bcrypt]==1.7.4
|
||||
cryptography==42.0.0
|
||||
python-multipart==0.0.6
|
||||
aiofiles==23.2.1
|
||||
httpx==0.26.0
|
||||
|
|
@ -12,3 +13,5 @@ apscheduler==3.10.4
|
|||
psutil==5.9.8
|
||||
asyncpg==0.29.0
|
||||
aiosqlite==0.19.0
|
||||
pytest==7.4.4
|
||||
pytest-asyncio==0.23.3
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Entry point for running the application directly."""
|
||||
import uvicorn
|
||||
from config import settings
|
||||
from backend.config import settings
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(
|
||||
"main:app",
|
||||
"backend.main:app",
|
||||
host="0.0.0.0",
|
||||
port=8000,
|
||||
reload=settings.DEBUG,
|
||||
|
|
|
|||
|
|
@ -8,18 +8,18 @@ services:
|
|||
ports:
|
||||
- "8000:8000"
|
||||
environment:
|
||||
- SECRET_KEY=${SECRET_KEY:-your-secure-secret-key-change-in-production}
|
||||
- SECRET_KEY=${SECRET_KEY:-dev-secret-key-change-in-production}
|
||||
- DATA_DIR=/app/data
|
||||
- PANIC_TIMEOUT_SECONDS=${PANIC_TIMEOUT_SECONDS:-60}
|
||||
- PANIC_FAN_SPEED=${PANIC_FAN_SPEED:-100}
|
||||
- DEBUG=${DEBUG:-true}
|
||||
- PYTHONUNBUFFERED=1
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
networks:
|
||||
- ipmi-network
|
||||
# Required for IPMI access to the local network
|
||||
# If your servers are on a different network, adjust accordingly
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
# For Windows Docker Desktop, we need this for proper shutdown
|
||||
stop_grace_period: 10s
|
||||
|
||||
networks:
|
||||
ipmi-network:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
client_max_body_size 50M;
|
||||
|
||||
location / {
|
||||
proxy_pass http://ipmi-fan-control:8000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://ipmi-fan-control:8000/api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,6 @@ import {
|
|||
Typography,
|
||||
IconButton,
|
||||
Divider,
|
||||
Avatar,
|
||||
Menu,
|
||||
MenuItem,
|
||||
} from '@mui/material';
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import {
|
|||
NavigateNext as NextIcon,
|
||||
} from '@mui/icons-material';
|
||||
import { dashboardApi } from '../utils/api';
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, ResponsiveContainer } from 'recharts';
|
||||
|
||||
|
||||
export default function Dashboard() {
|
||||
const navigate = useNavigate();
|
||||
|
|
|
|||
|
|
@ -7,8 +7,6 @@ import {
|
|||
Paper,
|
||||
Grid,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import {
|
|||
Speed as SpeedIcon,
|
||||
} from '@mui/icons-material';
|
||||
import { logsApi, serversApi } from '../utils/api';
|
||||
import type { SystemLog } from '../types';
|
||||
|
||||
|
||||
const LOGS_PER_PAGE = 25;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
|
|
@ -29,11 +29,10 @@ import {
|
|||
Speed as SpeedIcon,
|
||||
Thermostat as TempIcon,
|
||||
Power as PowerIcon,
|
||||
Refresh as RefreshIcon,
|
||||
} from '@mui/icons-material';
|
||||
import { serversApi, fanControlApi, dashboardApi } from '../utils/api';
|
||||
import type { TemperatureReading, FanReading } from '../types';
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, AreaChart, Area } from 'recharts';
|
||||
|
||||
|
||||
|
||||
interface TabPanelProps {
|
||||
children?: React.ReactNode;
|
||||
|
|
@ -66,7 +65,7 @@ export default function ServerDetail() {
|
|||
},
|
||||
});
|
||||
|
||||
const { data: sensors, isLoading: isSensorsLoading, refetch: refetchSensors } = useQuery({
|
||||
const { data: sensors } = useQuery({
|
||||
queryKey: ['sensors', serverId],
|
||||
queryFn: async () => {
|
||||
const response = await serversApi.getSensors(serverId);
|
||||
|
|
@ -124,7 +123,7 @@ export default function ServerDetail() {
|
|||
}
|
||||
|
||||
const cpuTemps = sensors?.temperatures.filter((t) => t.location.startsWith('cpu')) || [];
|
||||
const otherTemps = sensors?.temperatures.filter((t) => !t.location.startsWith('cpu')) || [];
|
||||
|
||||
|
||||
return (
|
||||
<Box>
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
# Tests for IPMI Fan Control
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
"""Tests for authentication module."""
|
||||
import pytest
|
||||
from backend.auth import (
|
||||
get_password_hash,
|
||||
verify_password,
|
||||
create_access_token,
|
||||
decode_access_token,
|
||||
encrypt_password,
|
||||
decrypt_password,
|
||||
)
|
||||
|
||||
|
||||
def test_password_hashing():
|
||||
"""Test password hashing and verification."""
|
||||
password = "testpassword123"
|
||||
hashed = get_password_hash(password)
|
||||
|
||||
assert hashed != password
|
||||
assert verify_password(password, hashed)
|
||||
assert not verify_password("wrongpassword", hashed)
|
||||
|
||||
|
||||
def test_jwt_tokens():
|
||||
"""Test JWT token creation and decoding."""
|
||||
data = {"sub": "testuser"}
|
||||
token = create_access_token(data)
|
||||
|
||||
assert token is not None
|
||||
|
||||
decoded = decode_access_token(token)
|
||||
assert decoded is not None
|
||||
assert decoded["sub"] == "testuser"
|
||||
|
||||
|
||||
def test_invalid_token():
|
||||
"""Test decoding invalid token."""
|
||||
decoded = decode_access_token("invalid.token.here")
|
||||
assert decoded is None
|
||||
|
||||
|
||||
def test_password_encryption():
|
||||
"""Test password encryption and decryption."""
|
||||
password = "secret_password"
|
||||
encrypted = encrypt_password(password)
|
||||
|
||||
assert encrypted != password
|
||||
|
||||
decrypted = decrypt_password(encrypted)
|
||||
assert decrypted == password
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
"""Tests for fan curve logic."""
|
||||
import pytest
|
||||
from backend.fan_control import FanCurveManager, FanCurvePoint
|
||||
|
||||
|
||||
def test_parse_curve():
|
||||
"""Test parsing fan curve from JSON."""
|
||||
json_data = '[{"temp": 30, "speed": 20}, {"temp": 50, "speed": 50}, {"temp": 70, "speed": 100}]'
|
||||
curve = FanCurveManager.parse_curve(json_data)
|
||||
|
||||
assert len(curve) == 3
|
||||
assert curve[0].temp == 30
|
||||
assert curve[0].speed == 20
|
||||
|
||||
|
||||
def test_parse_invalid_curve():
|
||||
"""Test parsing invalid curve returns default."""
|
||||
curve = FanCurveManager.parse_curve("invalid json")
|
||||
|
||||
assert len(curve) == 6 # Default curve has 6 points
|
||||
assert curve[0].temp == 30
|
||||
assert curve[0].speed == 10
|
||||
|
||||
|
||||
def test_calculate_speed_below_min():
|
||||
"""Test speed calculation below minimum temperature."""
|
||||
curve = [
|
||||
FanCurvePoint(30, 10),
|
||||
FanCurvePoint(50, 50),
|
||||
FanCurvePoint(70, 100),
|
||||
]
|
||||
|
||||
speed = FanCurveManager.calculate_speed(curve, 20)
|
||||
assert speed == 10
|
||||
|
||||
|
||||
def test_calculate_speed_above_max():
|
||||
"""Test speed calculation above maximum temperature."""
|
||||
curve = [
|
||||
FanCurvePoint(30, 10),
|
||||
FanCurvePoint(50, 50),
|
||||
FanCurvePoint(70, 100),
|
||||
]
|
||||
|
||||
speed = FanCurveManager.calculate_speed(curve, 80)
|
||||
assert speed == 100
|
||||
|
||||
|
||||
def test_calculate_speed_interpolation():
|
||||
"""Test speed calculation with interpolation."""
|
||||
curve = [
|
||||
FanCurvePoint(30, 10),
|
||||
FanCurvePoint(50, 50),
|
||||
FanCurvePoint(70, 100),
|
||||
]
|
||||
|
||||
# At 40°C, should be halfway between 10% and 50%
|
||||
speed = FanCurveManager.calculate_speed(curve, 40)
|
||||
assert speed == 30
|
||||
|
||||
|
||||
def test_calculate_speed_exact_point():
|
||||
"""Test speed calculation at exact curve point."""
|
||||
curve = [
|
||||
FanCurvePoint(30, 10),
|
||||
FanCurvePoint(50, 50),
|
||||
]
|
||||
|
||||
speed = FanCurveManager.calculate_speed(curve, 50)
|
||||
assert speed == 50
|
||||
|
||||
|
||||
def test_serialize_curve():
|
||||
"""Test serializing curve to JSON."""
|
||||
points = [
|
||||
FanCurvePoint(30, 10),
|
||||
FanCurvePoint(50, 50),
|
||||
]
|
||||
|
||||
json_data = FanCurveManager.serialize_curve(points)
|
||||
assert "30" in json_data
|
||||
assert "10" in json_data
|
||||
assert "50" in json_data
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
"""Tests for IPMI client."""
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch
|
||||
from backend.ipmi_client import IPMIClient, TemperatureReading
|
||||
|
||||
|
||||
def test_fan_mapping():
|
||||
"""Test fan ID mapping."""
|
||||
assert IPMIClient.FAN_MAPPING["0x00"] == 1
|
||||
assert IPMIClient.FAN_MAPPING["0x06"] == 7
|
||||
|
||||
|
||||
def test_hex_to_percent_conversion():
|
||||
"""Test hex to percent conversion mapping."""
|
||||
assert IPMIClient.HEX_TO_PERCENT["0x00"] == 0
|
||||
assert IPMIClient.HEX_TO_PERCENT["0x64"] == 100
|
||||
assert IPMIClient.PERCENT_TO_HEX[50] == "0x32"
|
||||
assert IPMIClient.PERCENT_TO_HEX[100] == "0x64"
|
||||
|
||||
|
||||
def test_determine_temp_location():
|
||||
"""Test temperature location detection."""
|
||||
client = IPMIClient("192.168.1.1", "user", "pass")
|
||||
|
||||
assert client._determine_temp_location("CPU1 Temp") == "cpu1"
|
||||
assert client._determine_temp_location("CPU2 Temp") == "cpu2"
|
||||
assert client._determine_temp_location("Processor 1") == "cpu1"
|
||||
assert client._determine_temp_location("Inlet Temp") == "inlet"
|
||||
assert client._determine_temp_location("Exhaust Temp") == "exhaust"
|
||||
assert client._determine_temp_location("DIMM 1") == "memory"
|
||||
assert client._determine_temp_location("Unknown Sensor") == "other"
|
||||
|
||||
|
||||
def test_determine_sensor_type():
|
||||
"""Test sensor type detection."""
|
||||
client = IPMIClient("192.168.1.1", "user", "pass")
|
||||
|
||||
assert client._determine_sensor_type("CPU Temp") == "temperature"
|
||||
assert client._determine_sensor_type("Fan 1 RPM") == "fan"
|
||||
assert client._determine_sensor_type("12V") == "voltage"
|
||||
assert client._determine_sensor_type("Power Supply") == "power"
|
||||
|
||||
|
||||
def test_parse_sensor_value():
|
||||
"""Test sensor value parsing."""
|
||||
client = IPMIClient("192.168.1.1", "user", "pass")
|
||||
|
||||
value, unit = client._parse_sensor_value("45 degrees C")
|
||||
assert value == 45.0
|
||||
assert unit == "°C"
|
||||
|
||||
value, unit = client._parse_sensor_value("4200 RPM")
|
||||
assert value == 4200.0
|
||||
assert unit == "RPM"
|
||||
|
||||
value, unit = client._parse_sensor_value("12.05 Volts")
|
||||
assert value == 12.05
|
||||
assert unit == "V"
|
||||
|
||||
value, unit = client._parse_sensor_value("250 Watts")
|
||||
assert value == 250.0
|
||||
assert unit == "W"
|
||||
|
||||
|
||||
@patch('backend.ipmi_client.subprocess.run')
|
||||
def test_test_connection(mock_run):
|
||||
"""Test connection test method."""
|
||||
mock_run.return_value = Mock(returncode=0, stdout="", stderr="")
|
||||
|
||||
client = IPMIClient("192.168.1.1", "user", "pass")
|
||||
result = client.test_connection()
|
||||
|
||||
assert result is True
|
||||
mock_run.assert_called_once()
|
||||
|
||||
|
||||
@patch('backend.ipmi_client.subprocess.run')
|
||||
def test_test_connection_failure(mock_run):
|
||||
"""Test connection test with failure."""
|
||||
mock_run.return_value = Mock(returncode=1, stdout="", stderr="Error")
|
||||
|
||||
client = IPMIClient("192.168.1.1", "user", "pass")
|
||||
result = client.test_connection()
|
||||
|
||||
assert result is False
|
||||
Loading…
Reference in New Issue