Refactor: Java-Klassen aus Services entfernt + kritische Bugs gefixt
- AuthService, SystemInfo, IdentitiesManager Klassen → Modul-Funktionen
- grp.getall() → grp.getgrall() (Bug: Methode existierte nie)
- open('/proc/loadavg') ohne context manager gefixt (File-Handle-Leak)
- rx_packets/tx_packets null-check im Frontend (toLocaleString auf undefined)
- PoolCard onClick: /pools/{name} → /zfs (Route existierte nicht, löste Seitenreload aus)
- Alle Router-Imports auf Modul-Aliase umgestellt
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@ from fastapi import APIRouter, Depends, HTTPException, status
|
|||||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from services.auth import auth_service
|
from services import auth as auth_service
|
||||||
from models import Token
|
from models import Token
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/auth", tags=["auth"])
|
router = APIRouter(prefix="/api/auth", tags=["auth"])
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from typing import List, Optional
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from services.zfs_runner import zfs_runner
|
from services.zfs_runner import zfs_runner
|
||||||
from services.auth import auth_service
|
from services import auth as auth_service
|
||||||
from models import Dataset, DatasetType
|
from models import Dataset, DatasetType
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/datasets", tags=["datasets"])
|
router = APIRouter(prefix="/api/datasets", tags=["datasets"])
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from services.identities import identities_manager
|
from services import identities as identities_manager
|
||||||
from services.auth import auth_service
|
from services import auth as auth_service
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/identities", tags=["identities"])
|
router = APIRouter(prefix="/api/identities", tags=["identities"])
|
||||||
security = HTTPBearer()
|
security = HTTPBearer()
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import jwt
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from services.file_manager import file_manager
|
from services.file_manager import file_manager
|
||||||
from services.auth import auth_service
|
from services import auth as auth_service
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/navigator", tags=["navigator"])
|
router = APIRouter(prefix="/api/navigator", tags=["navigator"])
|
||||||
security = HTTPBearer()
|
security = HTTPBearer()
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from typing import List
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from services.zfs_runner import zfs_runner
|
from services.zfs_runner import zfs_runner
|
||||||
from services.auth import auth_service
|
from services import auth as auth_service
|
||||||
from models import Pool, PoolStatus, PoolHealth
|
from models import Pool, PoolStatus, PoolHealth
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/pools", tags=["pools"])
|
router = APIRouter(prefix="/api/pools", tags=["pools"])
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from pydantic import BaseModel
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from services.shares import share_manager
|
from services.shares import share_manager
|
||||||
from services.auth import auth_service
|
from services import auth as auth_service
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/shares", tags=["shares"])
|
router = APIRouter(prefix="/api/shares", tags=["shares"])
|
||||||
security = HTTPBearer()
|
security = HTTPBearer()
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from pydantic import BaseModel
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from services.zfs_runner import zfs_runner
|
from services.zfs_runner import zfs_runner
|
||||||
from services.auth import auth_service
|
from services import auth as auth_service
|
||||||
from models import Snapshot
|
from models import Snapshot
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/snapshots", tags=["snapshots"])
|
router = APIRouter(prefix="/api/snapshots", tags=["snapshots"])
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ from fastapi import APIRouter, Depends, HTTPException, status
|
|||||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from services.system_info import system_info
|
from services import system_info
|
||||||
from services.auth import auth_service
|
from services import auth as auth_service
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/system", tags=["system"])
|
router = APIRouter(prefix="/api/system", tags=["system"])
|
||||||
security = HTTPBearer()
|
security = HTTPBearer()
|
||||||
|
|||||||
@@ -12,78 +12,51 @@ from jose import JWTError, jwt
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# JWT Configuration
|
|
||||||
SECRET_KEY = os.environ.get("ZFS_SECRET_KEY", "your-secret-key-change-in-production")
|
SECRET_KEY = os.environ.get("ZFS_SECRET_KEY", "your-secret-key-change-in-production")
|
||||||
ALGORITHM = "HS256"
|
ALGORITHM = "HS256"
|
||||||
ACCESS_TOKEN_EXPIRE_HOURS = 8
|
ACCESS_TOKEN_EXPIRE_HOURS = 8
|
||||||
|
|
||||||
# Try to import PAM for system authentication
|
|
||||||
try:
|
try:
|
||||||
import pam
|
import pam
|
||||||
PAM_AVAILABLE = True
|
PAM_AVAILABLE = True
|
||||||
|
logger.info("Using PAM authentication (Linux system users)")
|
||||||
except ImportError:
|
except ImportError:
|
||||||
PAM_AVAILABLE = False
|
PAM_AVAILABLE = False
|
||||||
logger.warning("python-pam not installed, PAM authentication unavailable")
|
logger.warning("python-pam not installed, PAM authentication unavailable")
|
||||||
|
|
||||||
|
|
||||||
class AuthService:
|
def authenticate_user(username: str, password: str) -> Optional[dict]:
|
||||||
def __init__(self):
|
|
||||||
"""Initialize auth service with PAM (Linux system users)"""
|
|
||||||
if PAM_AVAILABLE:
|
|
||||||
logger.info("Using PAM authentication (Linux system users)")
|
|
||||||
else:
|
|
||||||
logger.error("PAM not available - install python-pam for authentication")
|
|
||||||
|
|
||||||
def authenticate_user(self, username: str, password: str) -> Optional[dict]:
|
|
||||||
"""
|
|
||||||
Authenticate user via PAM (Linux system users like 'pi', 'root')
|
|
||||||
Returns user data if valid, None otherwise
|
|
||||||
"""
|
|
||||||
if not PAM_AVAILABLE:
|
if not PAM_AVAILABLE:
|
||||||
logger.error("PAM not available")
|
logger.error("PAM not available")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
p = pam.pam()
|
p = pam.pam()
|
||||||
if p.authenticate(username, password):
|
if p.authenticate(username, password):
|
||||||
logger.info(f"User {username} authenticated via PAM")
|
logger.info(f"User {username} authenticated via PAM")
|
||||||
return {
|
return {"username": username, "source": "pam"}
|
||||||
"username": username,
|
|
||||||
"source": "pam"
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
logger.warning(f"PAM authentication failed for user {username}: {p.reason}")
|
logger.warning(f"PAM authentication failed for user {username}: {p.reason}")
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"PAM authentication error: {e}")
|
logger.error(f"PAM authentication error: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def create_access_token(self, username: str, expires_delta: Optional[timedelta] = None) -> str:
|
|
||||||
"""Create JWT access token"""
|
def create_access_token(username: str, expires_delta: Optional[timedelta] = None) -> str:
|
||||||
if expires_delta is None:
|
if expires_delta is None:
|
||||||
expires_delta = timedelta(hours=ACCESS_TOKEN_EXPIRE_HOURS)
|
expires_delta = timedelta(hours=ACCESS_TOKEN_EXPIRE_HOURS)
|
||||||
|
|
||||||
expire = datetime.utcnow() + expires_delta
|
expire = datetime.utcnow() + expires_delta
|
||||||
to_encode = {"sub": username, "exp": expire}
|
to_encode = {"sub": username, "exp": expire}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||||
return encoded_jwt
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to create token: {e}")
|
logger.error(f"Failed to create token: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def verify_token(self, token: str) -> Optional[str]:
|
|
||||||
"""Verify JWT token and return username"""
|
def verify_token(token: str) -> Optional[str]:
|
||||||
try:
|
try:
|
||||||
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||||
username: str = payload.get("sub")
|
username: str = payload.get("sub")
|
||||||
if username is None:
|
return username if username else None
|
||||||
return None
|
|
||||||
return username
|
|
||||||
except JWTError:
|
except JWTError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
# Global instance
|
|
||||||
auth_service = AuthService()
|
|
||||||
|
|||||||
+274
-419
@@ -8,7 +8,6 @@ import logging
|
|||||||
import pwd
|
import pwd
|
||||||
import grp
|
import grp
|
||||||
from typing import List, Dict, Any, Optional
|
from typing import List, Dict, Any, Optional
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import spwd
|
import spwd
|
||||||
@@ -18,301 +17,7 @@ except ImportError:
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class IdentitiesManager:
|
def _is_user_locked(username: str) -> bool:
|
||||||
"""Manage system users and groups"""
|
|
||||||
|
|
||||||
def list_users(self) -> List[Dict[str, Any]]:
|
|
||||||
"""List all system users"""
|
|
||||||
users = []
|
|
||||||
try:
|
|
||||||
# Use getpwall() which returns an iterator
|
|
||||||
import getpass
|
|
||||||
# Fallback: read /etc/passwd directly
|
|
||||||
result = subprocess.run(
|
|
||||||
["/usr/bin/getent", "passwd"],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=5
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.returncode == 0:
|
|
||||||
for line in result.stdout.strip().split('\n'):
|
|
||||||
if not line:
|
|
||||||
continue
|
|
||||||
parts = line.split(':')
|
|
||||||
if len(parts) < 7:
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
username = parts[0]
|
|
||||||
uid = int(parts[2])
|
|
||||||
gid = int(parts[3])
|
|
||||||
gecos = parts[4]
|
|
||||||
home = parts[5]
|
|
||||||
shell = parts[6]
|
|
||||||
|
|
||||||
users.append({
|
|
||||||
'username': username,
|
|
||||||
'uid': uid,
|
|
||||||
'gid': gid,
|
|
||||||
'gecos': gecos,
|
|
||||||
'home': home,
|
|
||||||
'shell': shell,
|
|
||||||
'locked': self._is_user_locked(username)
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Error parsing user line: {e}")
|
|
||||||
|
|
||||||
return sorted(users, key=lambda x: x['uid'])
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error listing users: {e}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
def list_groups(self) -> List[Dict[str, Any]]:
|
|
||||||
"""List all system groups"""
|
|
||||||
groups = []
|
|
||||||
try:
|
|
||||||
# Use getent to read groups
|
|
||||||
result = subprocess.run(
|
|
||||||
["/usr/bin/getent", "group"],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=5
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.returncode == 0:
|
|
||||||
for line in result.stdout.strip().split('\n'):
|
|
||||||
if not line:
|
|
||||||
continue
|
|
||||||
parts = line.split(':')
|
|
||||||
if len(parts) < 4:
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
groupname = parts[0]
|
|
||||||
gid = int(parts[2])
|
|
||||||
members = [m.strip() for m in parts[3].split(',') if m.strip()]
|
|
||||||
|
|
||||||
groups.append({
|
|
||||||
'groupname': groupname,
|
|
||||||
'gid': gid,
|
|
||||||
'members': members
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Error parsing group line: {e}")
|
|
||||||
|
|
||||||
return sorted(groups, key=lambda x: x['gid'])
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error listing groups: {e}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
def get_user_groups(self, username: str) -> List[str]:
|
|
||||||
"""Get all groups a user belongs to"""
|
|
||||||
try:
|
|
||||||
groups = []
|
|
||||||
for entry in grp.getall():
|
|
||||||
if username in entry.gr_mem:
|
|
||||||
groups.append(entry.gr_name)
|
|
||||||
|
|
||||||
# Also add primary group
|
|
||||||
try:
|
|
||||||
user_entry = pwd.getpwnam(username)
|
|
||||||
primary_group = grp.getgrgid(user_entry.pw_gid)
|
|
||||||
if primary_group.gr_name not in groups:
|
|
||||||
groups.append(primary_group.gr_name)
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return sorted(groups)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error getting groups for {username}: {e}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
def create_user(self, username: str, home_dir: Optional[str] = None,
|
|
||||||
shell: str = "/bin/bash", gecos: str = "") -> bool:
|
|
||||||
"""Create new system user"""
|
|
||||||
try:
|
|
||||||
# Build useradd command
|
|
||||||
cmd = ["/usr/sbin/useradd"]
|
|
||||||
|
|
||||||
if home_dir:
|
|
||||||
cmd.extend(["-d", home_dir])
|
|
||||||
else:
|
|
||||||
cmd.extend(["-d", f"/home/{username}"])
|
|
||||||
|
|
||||||
cmd.extend(["-s", shell])
|
|
||||||
|
|
||||||
if gecos:
|
|
||||||
cmd.extend(["-c", gecos])
|
|
||||||
|
|
||||||
cmd.extend(["-m", username]) # -m to create home directory
|
|
||||||
|
|
||||||
result = subprocess.run(cmd, capture_output=True, timeout=10)
|
|
||||||
|
|
||||||
if result.returncode == 0:
|
|
||||||
logger.info(f"User created: {username}")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
logger.error(f"Failed to create user {username}: {result.stderr.decode()}")
|
|
||||||
return False
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error creating user: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def delete_user(self, username: str, remove_home: bool = True) -> bool:
|
|
||||||
"""Delete system user and Samba user"""
|
|
||||||
try:
|
|
||||||
# First delete Samba user if exists (using pdbedit for better handling)
|
|
||||||
try:
|
|
||||||
subprocess.run(
|
|
||||||
["/usr/bin/pdbedit", "-x", "-u", username],
|
|
||||||
capture_output=True,
|
|
||||||
timeout=10
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
pass # Samba not installed or user doesn't exist
|
|
||||||
|
|
||||||
# Delete system user
|
|
||||||
cmd = ["/usr/sbin/userdel"]
|
|
||||||
if remove_home:
|
|
||||||
cmd.append("-r")
|
|
||||||
cmd.append(username)
|
|
||||||
|
|
||||||
result = subprocess.run(cmd, capture_output=True, timeout=10)
|
|
||||||
|
|
||||||
if result.returncode == 0:
|
|
||||||
logger.info(f"User deleted: {username}")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
logger.error(f"Failed to delete user {username}: {result.stderr.decode()}")
|
|
||||||
return False
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error deleting user: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def create_group(self, groupname: str) -> bool:
|
|
||||||
"""Create new system group"""
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
["/usr/sbin/groupadd", groupname],
|
|
||||||
capture_output=True,
|
|
||||||
timeout=10
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.returncode == 0:
|
|
||||||
logger.info(f"Group created: {groupname}")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
logger.error(f"Failed to create group {groupname}: {result.stderr.decode()}")
|
|
||||||
return False
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error creating group: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def delete_group(self, groupname: str) -> bool:
|
|
||||||
"""Delete system group"""
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
["/usr/sbin/groupdel", groupname],
|
|
||||||
capture_output=True,
|
|
||||||
timeout=10
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.returncode == 0:
|
|
||||||
logger.info(f"Group deleted: {groupname}")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
logger.error(f"Failed to delete group {groupname}: {result.stderr.decode()}")
|
|
||||||
return False
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error deleting group: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def add_user_to_group(self, username: str, groupname: str) -> bool:
|
|
||||||
"""Add user to group"""
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
["/usr/sbin/usermod", "-aG", groupname, username],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=10
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.returncode == 0:
|
|
||||||
logger.info(f"User {username} added to group {groupname}")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
error_msg = result.stderr.strip() or result.stdout.strip() or "Unknown error"
|
|
||||||
logger.error(f"Failed to add user to group: {error_msg}")
|
|
||||||
raise Exception(f"Failed to add user to group: {error_msg}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error adding user to group: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
def remove_user_from_group(self, username: str, groupname: str) -> bool:
|
|
||||||
"""Remove user from group"""
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
["/usr/sbin/gpasswd", "-d", username, groupname],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=10
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.returncode == 0:
|
|
||||||
logger.info(f"User {username} removed from group {groupname}")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
error_msg = result.stderr.strip() or result.stdout.strip() or "Unknown error"
|
|
||||||
logger.error(f"Failed to remove user from group: {error_msg}")
|
|
||||||
raise Exception(f"Failed to remove user from group: {error_msg}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error removing user from group: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
def change_password(self, username: str, password: str) -> bool:
|
|
||||||
"""Change user password via chpasswd"""
|
|
||||||
try:
|
|
||||||
# Use chpasswd for password changes
|
|
||||||
result = subprocess.run(
|
|
||||||
["/usr/sbin/chpasswd"],
|
|
||||||
input=f"{username}:{password}\n",
|
|
||||||
text=True,
|
|
||||||
capture_output=True,
|
|
||||||
timeout=10
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.returncode == 0:
|
|
||||||
logger.info(f"Password changed for {username}")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
logger.error(f"Failed to change password: {result.stderr}")
|
|
||||||
return False
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error changing password: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def change_shell(self, username: str, shell: str) -> bool:
|
|
||||||
"""Change user shell"""
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
["/usr/sbin/usermod", "-s", shell, username],
|
|
||||||
capture_output=True,
|
|
||||||
timeout=10
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.returncode == 0:
|
|
||||||
logger.info(f"Shell changed for {username} to {shell}")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
logger.error(f"Failed to change shell: {result.stderr.decode()}")
|
|
||||||
return False
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error changing shell: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _is_user_locked(self, username: str) -> bool:
|
|
||||||
"""Check if user account is locked"""
|
|
||||||
if not spwd:
|
if not spwd:
|
||||||
return False
|
return False
|
||||||
try:
|
try:
|
||||||
@@ -324,62 +29,291 @@ class IdentitiesManager:
|
|||||||
logger.warning(f"Error checking lock status: {e}")
|
logger.warning(f"Error checking lock status: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def lock_user(self, username: str) -> bool:
|
|
||||||
"""Lock user account"""
|
def _get_full_username(truncated: str) -> Optional[str]:
|
||||||
|
"""Find full username from /etc/passwd when wtmp truncated it to 8 chars."""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["/usr/bin/getent", "passwd"],
|
||||||
|
capture_output=True, text=True, timeout=5
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
for line in result.stdout.strip().split('\n'):
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
parts = line.split(':')
|
||||||
|
if parts and parts[0].startswith(truncated) and len(parts[0]) > len(truncated):
|
||||||
|
return parts[0]
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Error finding full username for '{truncated}': {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def list_users() -> List[Dict[str, Any]]:
|
||||||
|
users = []
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["/usr/bin/getent", "passwd"],
|
||||||
|
capture_output=True, text=True, timeout=5
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
for line in result.stdout.strip().split('\n'):
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
parts = line.split(':')
|
||||||
|
if len(parts) < 7:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
users.append({
|
||||||
|
'username': parts[0],
|
||||||
|
'uid': int(parts[2]),
|
||||||
|
'gid': int(parts[3]),
|
||||||
|
'gecos': parts[4],
|
||||||
|
'home': parts[5],
|
||||||
|
'shell': parts[6],
|
||||||
|
'locked': _is_user_locked(parts[0])
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Error parsing user line: {e}")
|
||||||
|
return sorted(users, key=lambda x: x['uid'])
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error listing users: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def list_groups() -> List[Dict[str, Any]]:
|
||||||
|
groups = []
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["/usr/bin/getent", "group"],
|
||||||
|
capture_output=True, text=True, timeout=5
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
for line in result.stdout.strip().split('\n'):
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
parts = line.split(':')
|
||||||
|
if len(parts) < 4:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
groups.append({
|
||||||
|
'groupname': parts[0],
|
||||||
|
'gid': int(parts[2]),
|
||||||
|
'members': [m.strip() for m in parts[3].split(',') if m.strip()]
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Error parsing group line: {e}")
|
||||||
|
return sorted(groups, key=lambda x: x['gid'])
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error listing groups: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_groups(username: str) -> List[str]:
|
||||||
|
try:
|
||||||
|
groups = [
|
||||||
|
entry.gr_name
|
||||||
|
for entry in grp.getgrall()
|
||||||
|
if username in entry.gr_mem
|
||||||
|
]
|
||||||
|
try:
|
||||||
|
user_entry = pwd.getpwnam(username)
|
||||||
|
primary = grp.getgrgid(user_entry.pw_gid)
|
||||||
|
if primary.gr_name not in groups:
|
||||||
|
groups.append(primary.gr_name)
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
return sorted(groups)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting groups for {username}: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def create_user(username: str, home_dir: Optional[str] = None,
|
||||||
|
shell: str = "/bin/bash", gecos: str = "") -> bool:
|
||||||
|
try:
|
||||||
|
cmd = ["/usr/sbin/useradd", "-d", home_dir or f"/home/{username}", "-s", shell]
|
||||||
|
if gecos:
|
||||||
|
cmd.extend(["-c", gecos])
|
||||||
|
cmd.extend(["-m", username])
|
||||||
|
result = subprocess.run(cmd, capture_output=True, timeout=10)
|
||||||
|
if result.returncode == 0:
|
||||||
|
logger.info(f"User created: {username}")
|
||||||
|
return True
|
||||||
|
logger.error(f"Failed to create user {username}: {result.stderr.decode()}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error creating user: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def delete_user(username: str, remove_home: bool = True) -> bool:
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
["/usr/bin/pdbedit", "-x", "-u", username],
|
||||||
|
capture_output=True, timeout=10
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
cmd = ["/usr/sbin/userdel"]
|
||||||
|
if remove_home:
|
||||||
|
cmd.append("-r")
|
||||||
|
cmd.append(username)
|
||||||
|
result = subprocess.run(cmd, capture_output=True, timeout=10)
|
||||||
|
if result.returncode == 0:
|
||||||
|
logger.info(f"User deleted: {username}")
|
||||||
|
return True
|
||||||
|
logger.error(f"Failed to delete user {username}: {result.stderr.decode()}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error deleting user: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def create_group(groupname: str) -> bool:
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["/usr/sbin/groupadd", groupname],
|
||||||
|
capture_output=True, timeout=10
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
logger.info(f"Group created: {groupname}")
|
||||||
|
return True
|
||||||
|
logger.error(f"Failed to create group {groupname}: {result.stderr.decode()}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error creating group: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def delete_group(groupname: str) -> bool:
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["/usr/sbin/groupdel", groupname],
|
||||||
|
capture_output=True, timeout=10
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
logger.info(f"Group deleted: {groupname}")
|
||||||
|
return True
|
||||||
|
logger.error(f"Failed to delete group {groupname}: {result.stderr.decode()}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error deleting group: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def add_user_to_group(username: str, groupname: str) -> bool:
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["/usr/sbin/usermod", "-aG", groupname, username],
|
||||||
|
capture_output=True, text=True, timeout=10
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
logger.info(f"User {username} added to group {groupname}")
|
||||||
|
return True
|
||||||
|
error_msg = result.stderr.strip() or result.stdout.strip() or "Unknown error"
|
||||||
|
logger.error(f"Failed to add user to group: {error_msg}")
|
||||||
|
raise Exception(f"Failed to add user to group: {error_msg}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error adding user to group: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def remove_user_from_group(username: str, groupname: str) -> bool:
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["/usr/sbin/gpasswd", "-d", username, groupname],
|
||||||
|
capture_output=True, text=True, timeout=10
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
logger.info(f"User {username} removed from group {groupname}")
|
||||||
|
return True
|
||||||
|
error_msg = result.stderr.strip() or result.stdout.strip() or "Unknown error"
|
||||||
|
logger.error(f"Failed to remove user from group: {error_msg}")
|
||||||
|
raise Exception(f"Failed to remove user from group: {error_msg}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error removing user from group: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def change_password(username: str, password: str) -> bool:
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["/usr/sbin/chpasswd"],
|
||||||
|
input=f"{username}:{password}\n",
|
||||||
|
text=True, capture_output=True, timeout=10
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
logger.info(f"Password changed for {username}")
|
||||||
|
return True
|
||||||
|
logger.error(f"Failed to change password: {result.stderr}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error changing password: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def change_shell(username: str, shell: str) -> bool:
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["/usr/sbin/usermod", "-s", shell, username],
|
||||||
|
capture_output=True, timeout=10
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
logger.info(f"Shell changed for {username} to {shell}")
|
||||||
|
return True
|
||||||
|
logger.error(f"Failed to change shell: {result.stderr.decode()}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error changing shell: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def lock_user(username: str) -> bool:
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["/usr/sbin/usermod", "-L", username],
|
["/usr/sbin/usermod", "-L", username],
|
||||||
capture_output=True,
|
capture_output=True, timeout=10
|
||||||
timeout=10
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
logger.info(f"User locked: {username}")
|
logger.info(f"User locked: {username}")
|
||||||
return True
|
return True
|
||||||
else:
|
|
||||||
logger.error(f"Failed to lock user: {result.stderr.decode()}")
|
logger.error(f"Failed to lock user: {result.stderr.decode()}")
|
||||||
return False
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error locking user: {e}")
|
logger.error(f"Error locking user: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def unlock_user(self, username: str) -> bool:
|
|
||||||
"""Unlock user account"""
|
def unlock_user(username: str) -> bool:
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["/usr/sbin/usermod", "-U", username],
|
["/usr/sbin/usermod", "-U", username],
|
||||||
capture_output=True,
|
capture_output=True, timeout=10
|
||||||
timeout=10
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
logger.info(f"User unlocked: {username}")
|
logger.info(f"User unlocked: {username}")
|
||||||
return True
|
return True
|
||||||
else:
|
|
||||||
logger.error(f"Failed to unlock user: {result.stderr.decode()}")
|
logger.error(f"Failed to unlock user: {result.stderr.decode()}")
|
||||||
return False
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error unlocking user: {e}")
|
logger.error(f"Error unlocking user: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def set_samba_password(self, username: str, password: str) -> bool:
|
|
||||||
"""Set Samba password for user"""
|
def set_samba_password(username: str, password: str) -> bool:
|
||||||
try:
|
try:
|
||||||
# Use smbpasswd to set Samba password
|
|
||||||
# -a flag: add/update user
|
|
||||||
# -s flag: read password from stdin
|
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["/usr/bin/smbpasswd", "-a", "-s", username],
|
["/usr/bin/smbpasswd", "-a", "-s", username],
|
||||||
input=f"{password}\n{password}\n",
|
input=f"{password}\n{password}\n",
|
||||||
text=True,
|
text=True, capture_output=True, timeout=10
|
||||||
capture_output=True,
|
|
||||||
timeout=10
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
logger.info(f"Samba password set for {username}")
|
logger.info(f"Samba password set for {username}")
|
||||||
return True
|
return True
|
||||||
else:
|
|
||||||
logger.error(f"Failed to set Samba password: {result.stderr}")
|
logger.error(f"Failed to set Samba password: {result.stderr}")
|
||||||
return False
|
return False
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
@@ -389,97 +323,61 @@ class IdentitiesManager:
|
|||||||
logger.error(f"Error setting Samba password: {e}")
|
logger.error(f"Error setting Samba password: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_login_history(self, limit: int = 50) -> List[Dict[str, Any]]:
|
|
||||||
"""Get recent login history using last command"""
|
def get_login_history(limit: int = 50) -> List[Dict[str, Any]]:
|
||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime as dt
|
||||||
|
|
||||||
logins = []
|
logins = []
|
||||||
days_of_week = {'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'}
|
days_of_week = {'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'}
|
||||||
months = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
|
months = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
|
||||||
'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
|
'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
|
||||||
current_year = datetime.now().year
|
current_year = dt.now().year
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["last", "-n", str(limit)],
|
["last", "-n", str(limit)],
|
||||||
capture_output=True,
|
capture_output=True, text=True, timeout=10
|
||||||
text=True,
|
|
||||||
timeout=10
|
|
||||||
)
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
return []
|
||||||
|
|
||||||
if result.returncode == 0:
|
|
||||||
for line in result.stdout.strip().split('\n'):
|
for line in result.stdout.strip().split('\n'):
|
||||||
if not line.strip():
|
if not line.strip() or 'wtmp' in line or 'begins' in line:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Skip header, footer, and system entries
|
|
||||||
if 'wtmp' in line or 'begins' in line:
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Find the day-of-week anchor (reliable marker for date section)
|
|
||||||
tokens = line.split()
|
tokens = line.split()
|
||||||
day_idx = -1
|
day_idx = next((i for i, t in enumerate(tokens) if t in days_of_week), -1)
|
||||||
|
if day_idx < 1:
|
||||||
for i, token in enumerate(tokens):
|
|
||||||
if token in days_of_week:
|
|
||||||
day_idx = i
|
|
||||||
break
|
|
||||||
|
|
||||||
if day_idx < 1: # Need at least username before day
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Extract components from token positions
|
|
||||||
username = tokens[0]
|
username = tokens[0]
|
||||||
|
|
||||||
# Skip system entries
|
|
||||||
if username in ['wtmp', 'reboot', 'kernel']:
|
if username in ['wtmp', 'reboot', 'kernel']:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Try to find full username from /etc/passwd (wtmp truncates to 8 chars)
|
full_username = _get_full_username(username)
|
||||||
full_username = self._get_full_username(username)
|
|
||||||
if full_username:
|
if full_username:
|
||||||
username = full_username
|
username = full_username
|
||||||
|
|
||||||
# Everything between username and day-of-week is TTY/host
|
|
||||||
device_host_tokens = tokens[1:day_idx]
|
device_host_tokens = tokens[1:day_idx]
|
||||||
|
tty, host = '-', '-'
|
||||||
# TTY is typically first token if it contains '/' or starts with 'pts'/'tty'
|
|
||||||
tty = '-'
|
|
||||||
host = '-'
|
|
||||||
|
|
||||||
if device_host_tokens:
|
if device_host_tokens:
|
||||||
first = device_host_tokens[0]
|
first = device_host_tokens[0]
|
||||||
if '/' in first or first.startswith('pts') or first.startswith('tty'):
|
if '/' in first or first.startswith('pts') or first.startswith('tty'):
|
||||||
tty = first
|
tty = first
|
||||||
# Remaining tokens are host
|
host = ' '.join(device_host_tokens[1:]) if len(device_host_tokens) > 1 else '-'
|
||||||
if len(device_host_tokens) > 1:
|
|
||||||
host = ' '.join(device_host_tokens[1:])
|
|
||||||
else:
|
else:
|
||||||
# No TTY, all tokens are host
|
|
||||||
host = ' '.join(device_host_tokens)
|
host = ' '.join(device_host_tokens)
|
||||||
|
|
||||||
# Extract date components (should be: day month date time)
|
if day_idx + 3 >= len(tokens):
|
||||||
# Note: year is NOT in standard last output, we need to infer it
|
continue
|
||||||
if day_idx + 3 < len(tokens):
|
|
||||||
day = tokens[day_idx]
|
day = tokens[day_idx]
|
||||||
month = tokens[day_idx + 1]
|
month = tokens[day_idx + 1]
|
||||||
date = tokens[day_idx + 2]
|
date = tokens[day_idx + 2]
|
||||||
time_str = tokens[day_idx + 3]
|
time_str = tokens[day_idx + 3]
|
||||||
|
|
||||||
# Validate month
|
|
||||||
if month not in months:
|
if month not in months:
|
||||||
logger.debug(f"Invalid month '{month}' in line: {line}")
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Use current year (last entries are usually recent)
|
|
||||||
year = current_year
|
|
||||||
else:
|
|
||||||
logger.debug(f"Not enough date tokens in line: {line}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Extract duration from end of line (in parentheses)
|
|
||||||
duration_match = re.search(r'\(([^)]+)\)\s*$', line)
|
duration_match = re.search(r'\(([^)]+)\)\s*$', line)
|
||||||
duration = duration_match.group(1) if duration_match else 'still logged in'
|
duration = duration_match.group(1) if duration_match else 'still logged in'
|
||||||
|
|
||||||
@@ -487,10 +385,10 @@ class IdentitiesManager:
|
|||||||
'username': username,
|
'username': username,
|
||||||
'tty': tty,
|
'tty': tty,
|
||||||
'host': host,
|
'host': host,
|
||||||
'date': f"{year}-{months[month]:02d}-{date.zfill(2)}",
|
'date': f"{current_year}-{months[month]:02d}-{date.zfill(2)}",
|
||||||
'time': time_str,
|
'time': time_str,
|
||||||
'duration': duration,
|
'duration': duration,
|
||||||
'login_str': f"{day} {month} {date} {time_str} {year}"
|
'login_str': f"{day} {month} {date} {time_str} {current_year}"
|
||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"Error parsing login line '{line}': {e}")
|
logger.debug(f"Error parsing login line '{line}': {e}")
|
||||||
@@ -501,69 +399,29 @@ class IdentitiesManager:
|
|||||||
logger.error(f"Error getting login history: {e}")
|
logger.error(f"Error getting login history: {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def _get_full_username(self, truncated: str) -> Optional[str]:
|
|
||||||
"""Find full username from /etc/passwd when wtmp has truncated it (8 char limit)
|
|
||||||
|
|
||||||
wtmp truncates usernames to 8 characters, so we need to look up the full name
|
def list_samba_users() -> List[Dict[str, Any]]:
|
||||||
in /etc/passwd by matching usernames that start with the truncated name.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
["/usr/bin/getent", "passwd"],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=5
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.returncode == 0:
|
|
||||||
for line in result.stdout.strip().split('\n'):
|
|
||||||
if not line:
|
|
||||||
continue
|
|
||||||
parts = line.split(':')
|
|
||||||
if parts and parts[0].startswith(truncated):
|
|
||||||
# Found a username that starts with the truncated name
|
|
||||||
if len(parts[0]) > len(truncated):
|
|
||||||
# It's longer than the truncated version
|
|
||||||
return parts[0]
|
|
||||||
|
|
||||||
return None
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug(f"Error finding full username for '{truncated}': {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def list_samba_users(self) -> List[Dict[str, Any]]:
|
|
||||||
"""List all Samba users using pdbedit"""
|
|
||||||
users = []
|
users = []
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["/usr/bin/pdbedit", "-L"],
|
["/usr/bin/pdbedit", "-L"],
|
||||||
capture_output=True,
|
capture_output=True, text=True, timeout=5
|
||||||
text=True,
|
|
||||||
timeout=5
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
for line in result.stdout.strip().split('\n'):
|
for line in result.stdout.strip().split('\n'):
|
||||||
if not line.strip():
|
if not line.strip():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# pdbedit output format: username:uid:comment
|
|
||||||
parts = line.split(':')
|
parts = line.split(':')
|
||||||
if len(parts) >= 2:
|
if len(parts) >= 2:
|
||||||
try:
|
try:
|
||||||
username = parts[0]
|
|
||||||
uid = int(parts[1])
|
|
||||||
comment = parts[2] if len(parts) > 2 else ""
|
|
||||||
|
|
||||||
users.append({
|
users.append({
|
||||||
'username': username,
|
'username': parts[0],
|
||||||
'uid': uid,
|
'uid': int(parts[1]),
|
||||||
'comment': comment,
|
'comment': parts[2] if len(parts) > 2 else "",
|
||||||
'type': 'samba'
|
'type': 'samba'
|
||||||
})
|
})
|
||||||
except (ValueError, IndexError) as e:
|
except (ValueError, IndexError) as e:
|
||||||
logger.warning(f"Error parsing Samba user line: {e}")
|
logger.warning(f"Error parsing Samba user line: {e}")
|
||||||
|
|
||||||
return sorted(users, key=lambda x: x['username'])
|
return sorted(users, key=lambda x: x['username'])
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
logger.warning("pdbedit not found - Samba may not be installed")
|
logger.warning("pdbedit not found - Samba may not be installed")
|
||||||
@@ -571,6 +429,3 @@ class IdentitiesManager:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error listing Samba users: {e}")
|
logger.error(f"Error listing Samba users: {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
identities_manager = IdentitiesManager()
|
|
||||||
|
|||||||
+58
-202
@@ -13,12 +13,7 @@ from datetime import datetime
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class SystemInfo:
|
|
||||||
"""Get system information"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_hostname() -> Dict[str, str]:
|
def get_hostname() -> Dict[str, str]:
|
||||||
"""Get system hostname"""
|
|
||||||
try:
|
try:
|
||||||
with open("/etc/hostname", "r") as f:
|
with open("/etc/hostname", "r") as f:
|
||||||
hostname = f.read().strip()
|
hostname = f.read().strip()
|
||||||
@@ -27,29 +22,24 @@ class SystemInfo:
|
|||||||
logger.error(f"Error getting hostname: {e}")
|
logger.error(f"Error getting hostname: {e}")
|
||||||
return {"error": str(e)}
|
return {"error": str(e)}
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def set_hostname(hostname: str) -> Dict[str, str]:
|
def set_hostname(hostname: str) -> Dict[str, str]:
|
||||||
"""Set system hostname"""
|
|
||||||
try:
|
try:
|
||||||
with open("/etc/hostname", "w") as f:
|
with open("/etc/hostname", "w") as f:
|
||||||
f.write(hostname)
|
f.write(hostname)
|
||||||
|
|
||||||
# Also update hostnamectl if available
|
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
["hostnamectl", "set-hostname", hostname],
|
["hostnamectl", "set-hostname", hostname],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
check=False
|
check=False
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"Set hostname to {hostname}")
|
logger.info(f"Set hostname to {hostname}")
|
||||||
return {"status": "success", "hostname": hostname}
|
return {"status": "success", "hostname": hostname}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error setting hostname: {e}")
|
logger.error(f"Error setting hostname: {e}")
|
||||||
return {"error": str(e)}
|
return {"error": str(e)}
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_system_info() -> Dict[str, Any]:
|
def get_system_info() -> Dict[str, Any]:
|
||||||
"""Get general system information"""
|
|
||||||
try:
|
try:
|
||||||
uname = platform.uname()
|
uname = platform.uname()
|
||||||
info = {
|
info = {
|
||||||
@@ -61,39 +51,31 @@ class SystemInfo:
|
|||||||
"python": platform.python_version()
|
"python": platform.python_version()
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get machine ID
|
|
||||||
try:
|
try:
|
||||||
with open("/etc/machine-id", "r") as f:
|
with open("/etc/machine-id", "r") as f:
|
||||||
info["machine_id"] = f.read().strip()
|
info["machine_id"] = f.read().strip()
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Get hardware model
|
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["dmidecode", "-s", "system-product-name"],
|
["dmidecode", "-s", "system-product-name"],
|
||||||
capture_output=True,
|
capture_output=True, text=True, timeout=5
|
||||||
text=True,
|
|
||||||
timeout=5
|
|
||||||
)
|
)
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
info["model"] = result.stdout.strip()
|
info["model"] = result.stdout.strip()
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Get domain name
|
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["domainname"],
|
["domainname"], capture_output=True, text=True, timeout=5
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=5
|
|
||||||
)
|
)
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
domain = result.stdout.strip()
|
domain = result.stdout.strip()
|
||||||
if domain and domain != "(none)":
|
if domain and domain != "(none)":
|
||||||
info["domain"] = domain
|
info["domain"] = domain
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return info
|
return info
|
||||||
@@ -101,9 +83,8 @@ class SystemInfo:
|
|||||||
logger.error(f"Error getting system info: {e}")
|
logger.error(f"Error getting system info: {e}")
|
||||||
return {"error": str(e)}
|
return {"error": str(e)}
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_uptime() -> Dict[str, Any]:
|
def get_uptime() -> Dict[str, Any]:
|
||||||
"""Get system uptime and boot time"""
|
|
||||||
try:
|
try:
|
||||||
import time
|
import time
|
||||||
with open("/proc/uptime", "r") as f:
|
with open("/proc/uptime", "r") as f:
|
||||||
@@ -112,28 +93,20 @@ class SystemInfo:
|
|||||||
days = uptime_seconds // 86400
|
days = uptime_seconds // 86400
|
||||||
hours = (uptime_seconds % 86400) // 3600
|
hours = (uptime_seconds % 86400) // 3600
|
||||||
minutes = (uptime_seconds % 3600) // 60
|
minutes = (uptime_seconds % 3600) // 60
|
||||||
|
boot_timestamp = time.time() - uptime_seconds
|
||||||
# Calculate boot timestamp
|
|
||||||
current_time = time.time()
|
|
||||||
boot_timestamp = current_time - uptime_seconds
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"uptime_seconds": uptime_seconds,
|
"uptime_seconds": uptime_seconds,
|
||||||
"uptime_string": f"{days}d {hours}h {minutes}m",
|
"uptime_string": f"{days}d {hours}h {minutes}m",
|
||||||
"uptime_formatted": {
|
"uptime_formatted": {"days": days, "hours": hours, "minutes": minutes},
|
||||||
"days": days,
|
|
||||||
"hours": hours,
|
|
||||||
"minutes": minutes
|
|
||||||
},
|
|
||||||
"boot_time": int(boot_timestamp)
|
"boot_time": int(boot_timestamp)
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error getting uptime: {e}")
|
logger.error(f"Error getting uptime: {e}")
|
||||||
return {"error": str(e)}
|
return {"error": str(e)}
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_memory() -> Dict[str, Any]:
|
def get_memory() -> Dict[str, Any]:
|
||||||
"""Get memory usage"""
|
|
||||||
try:
|
try:
|
||||||
with open("/proc/meminfo", "r") as f:
|
with open("/proc/meminfo", "r") as f:
|
||||||
lines = f.readlines()
|
lines = f.readlines()
|
||||||
@@ -141,7 +114,7 @@ class SystemInfo:
|
|||||||
meminfo = {}
|
meminfo = {}
|
||||||
for line in lines:
|
for line in lines:
|
||||||
key, value = line.split(":")
|
key, value = line.split(":")
|
||||||
meminfo[key.strip()] = int(value.split()[0]) * 1024 # Convert to bytes
|
meminfo[key.strip()] = int(value.split()[0]) * 1024
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"total": meminfo.get("MemTotal", 0),
|
"total": meminfo.get("MemTotal", 0),
|
||||||
@@ -156,9 +129,8 @@ class SystemInfo:
|
|||||||
logger.error(f"Error getting memory info: {e}")
|
logger.error(f"Error getting memory info: {e}")
|
||||||
return {"error": str(e)}
|
return {"error": str(e)}
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_cpu_info() -> Dict[str, Any]:
|
def get_cpu_info() -> Dict[str, Any]:
|
||||||
"""Get CPU information"""
|
|
||||||
try:
|
try:
|
||||||
import psutil
|
import psutil
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@@ -167,11 +139,9 @@ class SystemInfo:
|
|||||||
with open("/proc/cpuinfo", "r") as f:
|
with open("/proc/cpuinfo", "r") as f:
|
||||||
cpuinfo_text = f.read()
|
cpuinfo_text = f.read()
|
||||||
cpu_count = cpuinfo_text.count("processor")
|
cpu_count = cpuinfo_text.count("processor")
|
||||||
|
with open("/proc/loadavg") as f:
|
||||||
return {
|
load = f.read()
|
||||||
"count": cpu_count,
|
return {"count": cpu_count, "load_average": load.split()[:3]}
|
||||||
"load_average": open("/proc/loadavg").read().split()[:3]
|
|
||||||
}
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error getting CPU info: {e}")
|
logger.error(f"Error getting CPU info: {e}")
|
||||||
return {"error": str(e)}
|
return {"error": str(e)}
|
||||||
@@ -186,9 +156,8 @@ class SystemInfo:
|
|||||||
logger.error(f"Error getting CPU info with psutil: {e}")
|
logger.error(f"Error getting CPU info with psutil: {e}")
|
||||||
return {"error": str(e)}
|
return {"error": str(e)}
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_time() -> Dict[str, str]:
|
def get_time() -> Dict[str, str]:
|
||||||
"""Get system time"""
|
|
||||||
try:
|
try:
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
return {
|
return {
|
||||||
@@ -200,44 +169,30 @@ class SystemInfo:
|
|||||||
logger.error(f"Error getting time: {e}")
|
logger.error(f"Error getting time: {e}")
|
||||||
return {"error": str(e)}
|
return {"error": str(e)}
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def set_time(iso_string: str) -> Dict[str, str]:
|
def set_time(iso_string: str) -> Dict[str, str]:
|
||||||
"""Set system time (requires root)"""
|
|
||||||
try:
|
try:
|
||||||
dt = datetime.fromisoformat(iso_string)
|
dt = datetime.fromisoformat(iso_string)
|
||||||
|
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["date", "-s", dt.strftime("%Y-%m-%d %H:%M:%S")],
|
["date", "-s", dt.strftime("%Y-%m-%d %H:%M:%S")],
|
||||||
capture_output=True,
|
capture_output=True, text=True, check=False
|
||||||
text=True,
|
|
||||||
check=False
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
return {"error": result.stderr}
|
return {"error": result.stderr}
|
||||||
|
|
||||||
# Sync hardware clock
|
|
||||||
subprocess.run(["hwclock", "--systohc"], check=False)
|
subprocess.run(["hwclock", "--systohc"], check=False)
|
||||||
|
|
||||||
logger.info(f"Set time to {iso_string}")
|
logger.info(f"Set time to {iso_string}")
|
||||||
return {"status": "success", "time": iso_string}
|
return {"status": "success", "time": iso_string}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error setting time: {e}")
|
logger.error(f"Error setting time: {e}")
|
||||||
return {"error": str(e)}
|
return {"error": str(e)}
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_updates() -> Dict[str, Any]:
|
def get_updates() -> Dict[str, Any]:
|
||||||
"""Check available updates"""
|
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["apt", "list", "--upgradable"],
|
["apt", "list", "--upgradable"],
|
||||||
capture_output=True,
|
capture_output=True, text=True, timeout=10, check=False
|
||||||
text=True,
|
|
||||||
timeout=10,
|
|
||||||
check=False
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
return {"error": result.stderr}
|
return {"error": result.stderr}
|
||||||
|
|
||||||
@@ -250,18 +205,13 @@ class SystemInfo:
|
|||||||
"package": parts[0].strip(),
|
"package": parts[0].strip(),
|
||||||
"current": parts[1].split("[")[0].strip() if "[" in line else ""
|
"current": parts[1].split("[")[0].strip() if "[" in line else ""
|
||||||
})
|
})
|
||||||
|
return {"available": len(packages), "packages": packages}
|
||||||
return {
|
|
||||||
"available": len(packages),
|
|
||||||
"packages": packages
|
|
||||||
}
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error checking updates: {e}")
|
logger.error(f"Error checking updates: {e}")
|
||||||
return {"error": str(e)}
|
return {"error": str(e)}
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def reboot() -> Dict[str, str]:
|
def reboot() -> Dict[str, str]:
|
||||||
"""Reboot system (requires root)"""
|
|
||||||
try:
|
try:
|
||||||
subprocess.Popen(["shutdown", "-r", "now"])
|
subprocess.Popen(["shutdown", "-r", "now"])
|
||||||
return {"status": "success", "message": "System rebooting..."}
|
return {"status": "success", "message": "System rebooting..."}
|
||||||
@@ -269,9 +219,8 @@ class SystemInfo:
|
|||||||
logger.error(f"Error rebooting: {e}")
|
logger.error(f"Error rebooting: {e}")
|
||||||
return {"error": str(e)}
|
return {"error": str(e)}
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def shutdown() -> Dict[str, str]:
|
def shutdown() -> Dict[str, str]:
|
||||||
"""Shutdown system (requires root)"""
|
|
||||||
try:
|
try:
|
||||||
subprocess.Popen(["shutdown", "-h", "now"])
|
subprocess.Popen(["shutdown", "-h", "now"])
|
||||||
return {"status": "success", "message": "System shutting down..."}
|
return {"status": "success", "message": "System shutting down..."}
|
||||||
@@ -279,30 +228,23 @@ class SystemInfo:
|
|||||||
logger.error(f"Error shutting down: {e}")
|
logger.error(f"Error shutting down: {e}")
|
||||||
return {"error": str(e)}
|
return {"error": str(e)}
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_network_info() -> Dict[str, Any]:
|
def get_network_info() -> Dict[str, Any]:
|
||||||
"""Get network interface information"""
|
|
||||||
try:
|
try:
|
||||||
# Try ip -j addr (JSON output) first
|
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["/usr/sbin/ip", "-j", "addr"],
|
["/usr/sbin/ip", "-j", "addr"],
|
||||||
capture_output=True,
|
capture_output=True, text=True, timeout=5
|
||||||
text=True,
|
|
||||||
timeout=5
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
import json
|
import json
|
||||||
interfaces = []
|
|
||||||
try:
|
try:
|
||||||
data = json.loads(result.stdout)
|
data = json.loads(result.stdout)
|
||||||
|
interfaces = []
|
||||||
for iface in data:
|
for iface in data:
|
||||||
addr_info = []
|
addr_info = [
|
||||||
for addr in iface.get("addr_info", []):
|
{"family": addr.get("family"), "local": addr.get("local")}
|
||||||
addr_info.append({
|
for addr in iface.get("addr_info", [])
|
||||||
"family": addr.get("family"),
|
]
|
||||||
"local": addr.get("local")
|
|
||||||
})
|
|
||||||
interfaces.append({
|
interfaces.append({
|
||||||
"name": iface.get("ifname"),
|
"name": iface.get("ifname"),
|
||||||
"state": iface.get("operstate", "UNKNOWN"),
|
"state": iface.get("operstate", "UNKNOWN"),
|
||||||
@@ -312,42 +254,31 @@ class SystemInfo:
|
|||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Fallback: read from /proc/net/dev
|
|
||||||
with open("/proc/net/dev", "r") as f:
|
with open("/proc/net/dev", "r") as f:
|
||||||
lines = f.readlines()
|
lines = f.readlines()
|
||||||
|
|
||||||
interfaces = []
|
interfaces = []
|
||||||
for line in lines[2:]: # Skip header lines
|
for line in lines[2:]:
|
||||||
if ":" in line:
|
if ":" in line:
|
||||||
name = line.split(":")[0].strip()
|
name = line.split(":")[0].strip()
|
||||||
interfaces.append({
|
interfaces.append({"name": name, "state": "UP", "addresses": []})
|
||||||
"name": name,
|
|
||||||
"state": "UP",
|
|
||||||
"addresses": []
|
|
||||||
})
|
|
||||||
|
|
||||||
return {"interfaces": interfaces}
|
return {"interfaces": interfaces}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error getting network info: {e}")
|
logger.error(f"Error getting network info: {e}")
|
||||||
return {"error": str(e)}
|
return {"error": str(e)}
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_network_traffic() -> Dict[str, Any]:
|
def get_network_traffic() -> Dict[str, Any]:
|
||||||
"""Get network interface traffic (RX/TX bytes)"""
|
|
||||||
try:
|
try:
|
||||||
with open("/proc/net/dev", "r") as f:
|
with open("/proc/net/dev", "r") as f:
|
||||||
lines = f.readlines()
|
lines = f.readlines()
|
||||||
|
|
||||||
interfaces = []
|
interfaces = []
|
||||||
# /proc/net/dev format (after colon):
|
for line in lines[2:]:
|
||||||
# RX bytes, RX packets, RX errors, RX drops, RX fifo, RX frame, RX compressed, RX multicast,
|
|
||||||
# TX bytes, TX packets, TX errors, TX drops, TX fifo, TX collisions, TX carrier, TX compressed
|
|
||||||
for line in lines[2:]: # Skip header lines
|
|
||||||
if ":" in line:
|
if ":" in line:
|
||||||
name, stats_str = line.split(":")
|
name, stats_str = line.split(":")
|
||||||
name = name.strip()
|
name = name.strip()
|
||||||
stats = stats_str.split()
|
stats = stats_str.split()
|
||||||
|
|
||||||
if len(stats) >= 16:
|
if len(stats) >= 16:
|
||||||
interfaces.append({
|
interfaces.append({
|
||||||
"name": name,
|
"name": name,
|
||||||
@@ -360,95 +291,63 @@ class SystemInfo:
|
|||||||
"tx_errors": int(stats[10]),
|
"tx_errors": int(stats[10]),
|
||||||
"tx_drops": int(stats[11])
|
"tx_drops": int(stats[11])
|
||||||
})
|
})
|
||||||
|
|
||||||
return {"interfaces": interfaces}
|
return {"interfaces": interfaces}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error getting network traffic: {e}")
|
logger.error(f"Error getting network traffic: {e}")
|
||||||
return {"error": str(e)}
|
return {"error": str(e)}
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_disk_io() -> Dict[str, Any]:
|
def get_disk_io() -> Dict[str, Any]:
|
||||||
"""Get disk I/O statistics (read/write operations and bytes)"""
|
|
||||||
try:
|
try:
|
||||||
with open("/proc/diskstats", "r") as f:
|
with open("/proc/diskstats", "r") as f:
|
||||||
lines = f.readlines()
|
lines = f.readlines()
|
||||||
|
|
||||||
disks = []
|
disks = []
|
||||||
# /proc/diskstats format:
|
|
||||||
# major minor name reads_completed reads_merged reads_sectors reads_time_ms
|
|
||||||
# writes_completed writes_merged writes_sectors writes_time_ms in_progress io_time_ms weighted_io_time_ms
|
|
||||||
for line in lines:
|
for line in lines:
|
||||||
fields = line.split()
|
fields = line.split()
|
||||||
if len(fields) >= 14:
|
if len(fields) >= 14:
|
||||||
major = int(fields[0])
|
|
||||||
minor = int(fields[1])
|
|
||||||
name = fields[2]
|
name = fields[2]
|
||||||
|
|
||||||
# Skip loop devices, ram disks, and other virtual disks
|
|
||||||
if name.startswith(('dm-', 'loop', 'ram', 'sr', 'zram')):
|
if name.startswith(('dm-', 'loop', 'ram', 'sr', 'zram')):
|
||||||
continue
|
continue
|
||||||
# Only include actual storage devices (sda, sdb, nvme0n1, etc.)
|
if not any(name.startswith(p) for p in ['sd', 'nvme', 'hd', 'vd']):
|
||||||
if not any(name.startswith(prefix) for prefix in ['sd', 'nvme', 'hd', 'vd']):
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
reads_completed = int(fields[3])
|
|
||||||
reads_sectors = int(fields[5])
|
|
||||||
writes_completed = int(fields[7])
|
|
||||||
writes_sectors = int(fields[9])
|
|
||||||
|
|
||||||
# Sectors are typically 512 bytes
|
|
||||||
reads_bytes = reads_sectors * 512
|
|
||||||
writes_bytes = writes_sectors * 512
|
|
||||||
|
|
||||||
disks.append({
|
disks.append({
|
||||||
"name": name,
|
"name": name,
|
||||||
"reads_completed": reads_completed,
|
"reads_completed": int(fields[3]),
|
||||||
"reads_bytes": reads_bytes,
|
"reads_bytes": int(fields[5]) * 512,
|
||||||
"writes_completed": writes_completed,
|
"writes_completed": int(fields[7]),
|
||||||
"writes_bytes": writes_bytes
|
"writes_bytes": int(fields[9]) * 512
|
||||||
})
|
})
|
||||||
|
|
||||||
return {"disks": disks}
|
return {"disks": disks}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error getting disk I/O: {e}")
|
logger.error(f"Error getting disk I/O: {e}")
|
||||||
return {"error": str(e)}
|
return {"error": str(e)}
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_services() -> Dict[str, Any]:
|
def get_services() -> Dict[str, Any]:
|
||||||
"""Get running systemd services"""
|
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["/usr/bin/systemctl", "list-units", "--type=service", "--state=running", "--no-pager", "--output=json"],
|
["/usr/bin/systemctl", "list-units", "--type=service", "--state=running",
|
||||||
capture_output=True,
|
"--no-pager", "--output=json"],
|
||||||
text=True,
|
capture_output=True, text=True, timeout=10
|
||||||
timeout=10
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
import json
|
import json
|
||||||
try:
|
try:
|
||||||
services = json.loads(result.stdout)
|
services = json.loads(result.stdout)
|
||||||
return {
|
return {
|
||||||
"services": [
|
"services": [
|
||||||
{
|
{"name": svc.get("unit"), "state": svc.get("active"), "description": svc.get("description")}
|
||||||
"name": svc.get("unit"),
|
|
||||||
"state": svc.get("active"),
|
|
||||||
"description": svc.get("description")
|
|
||||||
}
|
|
||||||
for svc in services if svc.get("unit", "").endswith(".service")
|
for svc in services if svc.get("unit", "").endswith(".service")
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Fallback: parse text output
|
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["/usr/bin/systemctl", "list-units", "--type=service", "--state=running", "--no-pager"],
|
["/usr/bin/systemctl", "list-units", "--type=service", "--state=running", "--no-pager"],
|
||||||
capture_output=True,
|
capture_output=True, text=True, timeout=10
|
||||||
text=True,
|
|
||||||
timeout=10
|
|
||||||
)
|
)
|
||||||
|
|
||||||
services = []
|
services = []
|
||||||
for line in result.stdout.split("\n")[1:]:
|
for line in result.stdout.split("\n")[1:]:
|
||||||
if line.strip() and ".service" in line:
|
if line.strip() and ".service" in line:
|
||||||
@@ -459,45 +358,30 @@ class SystemInfo:
|
|||||||
"state": "running",
|
"state": "running",
|
||||||
"description": " ".join(parts[2:]) if len(parts) > 2 else ""
|
"description": " ".join(parts[2:]) if len(parts) > 2 else ""
|
||||||
})
|
})
|
||||||
|
|
||||||
return {"services": services}
|
return {"services": services}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error getting services: {e}")
|
logger.error(f"Error getting services: {e}")
|
||||||
return {"error": str(e)}
|
return {"error": str(e)}
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_all_units() -> Dict[str, Any]:
|
def get_all_units() -> Dict[str, Any]:
|
||||||
"""Get all systemd units (services, targets, sockets, timers, paths)"""
|
units = {"services": [], "targets": [], "sockets": [], "timers": [], "paths": []}
|
||||||
try:
|
try:
|
||||||
# Get all units without filtering by state
|
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["/usr/bin/systemctl", "list-units", "--all", "--no-pager", "--output=json"],
|
["/usr/bin/systemctl", "list-units", "--all", "--no-pager", "--output=json"],
|
||||||
capture_output=True,
|
capture_output=True, text=True, timeout=10
|
||||||
text=True,
|
|
||||||
timeout=10
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
import json
|
import json
|
||||||
try:
|
try:
|
||||||
units_data = json.loads(result.stdout)
|
for unit in json.loads(result.stdout):
|
||||||
units = {
|
|
||||||
"services": [],
|
|
||||||
"targets": [],
|
|
||||||
"sockets": [],
|
|
||||||
"timers": [],
|
|
||||||
"paths": []
|
|
||||||
}
|
|
||||||
|
|
||||||
for unit in units_data:
|
|
||||||
name = unit.get("unit", "")
|
name = unit.get("unit", "")
|
||||||
item = {
|
item = {
|
||||||
"name": name,
|
"name": name,
|
||||||
"active": unit.get("active"), # active/inactive
|
"active": unit.get("active"),
|
||||||
"sub": unit.get("sub"), # sub-state like "running", "exited", "enabled", etc.
|
"sub": unit.get("sub"),
|
||||||
"description": unit.get("description", "")
|
"description": unit.get("description", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
if name.endswith(".service"):
|
if name.endswith(".service"):
|
||||||
units["services"].append(item)
|
units["services"].append(item)
|
||||||
elif name.endswith(".target"):
|
elif name.endswith(".target"):
|
||||||
@@ -508,45 +392,27 @@ class SystemInfo:
|
|||||||
units["timers"].append(item)
|
units["timers"].append(item)
|
||||||
elif name.endswith(".path"):
|
elif name.endswith(".path"):
|
||||||
units["paths"].append(item)
|
units["paths"].append(item)
|
||||||
|
|
||||||
return units
|
return units
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Fallback: parse text output
|
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["/usr/bin/systemctl", "list-units", "--all", "--no-pager"],
|
["/usr/bin/systemctl", "list-units", "--all", "--no-pager"],
|
||||||
capture_output=True,
|
capture_output=True, text=True, timeout=10
|
||||||
text=True,
|
|
||||||
timeout=10
|
|
||||||
)
|
)
|
||||||
|
|
||||||
units = {
|
|
||||||
"services": [],
|
|
||||||
"targets": [],
|
|
||||||
"sockets": [],
|
|
||||||
"timers": [],
|
|
||||||
"paths": []
|
|
||||||
}
|
|
||||||
|
|
||||||
for line in result.stdout.split("\n")[1:]:
|
for line in result.stdout.split("\n")[1:]:
|
||||||
if not line.strip():
|
if not line.strip():
|
||||||
continue
|
continue
|
||||||
parts = line.split()
|
parts = line.split()
|
||||||
if len(parts) < 2:
|
if len(parts) < 2:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
name = parts[0]
|
name = parts[0]
|
||||||
active = parts[1] if len(parts) > 1 else "unknown"
|
|
||||||
description = " ".join(parts[3:]) if len(parts) > 3 else ""
|
|
||||||
|
|
||||||
item = {
|
item = {
|
||||||
"name": name,
|
"name": name,
|
||||||
"active": active,
|
"active": parts[1] if len(parts) > 1 else "unknown",
|
||||||
"sub": parts[2] if len(parts) > 2 else "",
|
"sub": parts[2] if len(parts) > 2 else "",
|
||||||
"description": description
|
"description": " ".join(parts[3:]) if len(parts) > 3 else ""
|
||||||
}
|
}
|
||||||
|
|
||||||
if name.endswith(".service"):
|
if name.endswith(".service"):
|
||||||
units["services"].append(item)
|
units["services"].append(item)
|
||||||
elif name.endswith(".target"):
|
elif name.endswith(".target"):
|
||||||
@@ -557,32 +423,22 @@ class SystemInfo:
|
|||||||
units["timers"].append(item)
|
units["timers"].append(item)
|
||||||
elif name.endswith(".path"):
|
elif name.endswith(".path"):
|
||||||
units["paths"].append(item)
|
units["paths"].append(item)
|
||||||
|
|
||||||
return units
|
return units
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error getting units: {e}")
|
logger.error(f"Error getting units: {e}")
|
||||||
return {"error": str(e)}
|
return {"error": str(e)}
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_journal_logs(limit: int = 20) -> Dict[str, Any]:
|
def get_journal_logs(limit: int = 20) -> Dict[str, Any]:
|
||||||
"""Get recent journal logs"""
|
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["/usr/bin/journalctl", "-n", str(limit), "--no-pager", "--output=short"],
|
["/usr/bin/journalctl", "-n", str(limit), "--no-pager", "--output=short"],
|
||||||
capture_output=True,
|
capture_output=True, text=True, timeout=5
|
||||||
text=True,
|
|
||||||
timeout=5
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
logs = [line.strip() for line in result.stdout.split("\n") if line.strip()]
|
logs = [line.strip() for line in result.stdout.split("\n") if line.strip()]
|
||||||
return {"logs": logs}
|
return {"logs": logs}
|
||||||
|
|
||||||
return {"logs": []}
|
return {"logs": []}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error getting journal logs: {e}")
|
logger.error(f"Error getting journal logs: {e}")
|
||||||
return {"error": str(e)}
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
# Global instance
|
|
||||||
system_info = SystemInfo()
|
|
||||||
|
|||||||
@@ -502,12 +502,12 @@ export default function Dashboard() {
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-muted-foreground mb-1">RX</p>
|
<p className="text-xs text-muted-foreground mb-1">RX</p>
|
||||||
<p className="text-sm font-semibold">{formatBytes(iface.rx_bytes)}</p>
|
<p className="text-sm font-semibold">{formatBytes(iface.rx_bytes)}</p>
|
||||||
<p className="text-xs text-muted-foreground">{iface.rx_packets.toLocaleString()} packets</p>
|
<p className="text-xs text-muted-foreground">{(iface.rx_packets ?? 0).toLocaleString()} packets</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-muted-foreground mb-1">TX</p>
|
<p className="text-xs text-muted-foreground mb-1">TX</p>
|
||||||
<p className="text-sm font-semibold">{formatBytes(iface.tx_bytes)}</p>
|
<p className="text-sm font-semibold">{formatBytes(iface.tx_bytes)}</p>
|
||||||
<p className="text-xs text-muted-foreground">{iface.tx_packets.toLocaleString()} packets</p>
|
<p className="text-xs text-muted-foreground">{(iface.tx_packets ?? 0).toLocaleString()} packets</p>
|
||||||
</div>
|
</div>
|
||||||
{(iface.rx_drops > 0 || iface.tx_drops > 0) && (
|
{(iface.rx_drops > 0 || iface.tx_drops > 0) && (
|
||||||
<div className="pt-2 border-t border-border/30">
|
<div className="pt-2 border-t border-border/30">
|
||||||
@@ -571,7 +571,7 @@ export default function Dashboard() {
|
|||||||
<PoolCard
|
<PoolCard
|
||||||
key={pool.name}
|
key={pool.name}
|
||||||
pool={pool}
|
pool={pool}
|
||||||
onClick={() => router.push(`/pools/${pool.name}`)}
|
onClick={() => router.push("/zfs")}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user