feat: Add SSH public key storage for users
- Add ssh_public_key column to User model for storing SSH keys - Add database migration for ssh_public_key column - Add SSH key section in user profile settings with validation - Auto-inject user's SSH key when deploying VMs from templates - Auto-inject user's SSH key when creating LXC containers - Support for both regular and smart LXC creation endpoints - Add i18n translations for SSH key UI (ru/en) - Validate SSH key format (ssh-rsa, ssh-ed25519, ecdsa-sha2, ssh-dss) Users can now save their SSH public key in Settings -> Account, and it will be automatically added to any VM or LXC container they create, enabling passwordless SSH access.
This commit is contained in:
@@ -4475,13 +4475,21 @@ async def create_lxc_container(
|
||||
if not vmid:
|
||||
raise HTTPException(status_code=500, detail="Failed to get next VMID")
|
||||
|
||||
# Combine user's SSH key with request SSH keys
|
||||
combined_ssh_keys = ssh_public_keys or ""
|
||||
if current_user.ssh_public_key:
|
||||
if combined_ssh_keys:
|
||||
combined_ssh_keys = f"{combined_ssh_keys}\n{current_user.ssh_public_key}"
|
||||
else:
|
||||
combined_ssh_keys = current_user.ssh_public_key
|
||||
|
||||
upid = client.create_lxc_container(
|
||||
node=node,
|
||||
vmid=vmid,
|
||||
ostemplate=ostemplate,
|
||||
hostname=hostname,
|
||||
password=password,
|
||||
ssh_public_keys=ssh_public_keys,
|
||||
ssh_public_keys=combined_ssh_keys if combined_ssh_keys else None,
|
||||
storage=storage,
|
||||
rootfs_size=rootfs_size,
|
||||
memory=memory,
|
||||
@@ -4550,6 +4558,7 @@ async def create_lxc_container_smart(
|
||||
ostemplate = data.get('ostemplate')
|
||||
hostname = data.get('hostname')
|
||||
password = data.get('password')
|
||||
ssh_public_keys = data.get('ssh_public_keys') # SSH keys from request
|
||||
storage = data.get('storage', 'local-lvm')
|
||||
rootfs_size = data.get('rootfs_size', 8)
|
||||
memory = data.get('memory', 512)
|
||||
@@ -4565,8 +4574,8 @@ async def create_lxc_container_smart(
|
||||
if not all([template_node, target_node, ostemplate, hostname]):
|
||||
raise HTTPException(status_code=400, detail="Missing required parameters")
|
||||
|
||||
if not password:
|
||||
raise HTTPException(status_code=400, detail="Password is required")
|
||||
if not password and not ssh_public_keys and not current_user.ssh_public_key:
|
||||
raise HTTPException(status_code=400, detail="Password or SSH keys are required")
|
||||
|
||||
try:
|
||||
if server.use_password:
|
||||
@@ -4598,6 +4607,14 @@ async def create_lxc_container_smart(
|
||||
|
||||
logger.info(f"[LXC Smart Create] Template on {template_node}, target {target_node}, shared={is_shared}, needs_migration={needs_migration}, net0={net0}")
|
||||
|
||||
# Combine user's SSH key with request SSH keys
|
||||
combined_ssh_keys = ssh_public_keys or ""
|
||||
if current_user.ssh_public_key:
|
||||
if combined_ssh_keys:
|
||||
combined_ssh_keys = f"{combined_ssh_keys}\n{current_user.ssh_public_key}"
|
||||
else:
|
||||
combined_ssh_keys = current_user.ssh_public_key
|
||||
|
||||
# Создаём контейнер
|
||||
upid = client.create_lxc_container(
|
||||
node=create_node,
|
||||
@@ -4605,6 +4622,7 @@ async def create_lxc_container_smart(
|
||||
ostemplate=ostemplate,
|
||||
hostname=hostname,
|
||||
password=password,
|
||||
ssh_public_keys=combined_ssh_keys if combined_ssh_keys else None,
|
||||
storage=storage,
|
||||
rootfs_size=rootfs_size,
|
||||
memory=memory,
|
||||
|
||||
@@ -64,6 +64,7 @@ def set_setting(db: Session, key: str, value: str, description: str = None):
|
||||
class UpdateProfileRequest(BaseModel):
|
||||
full_name: Optional[str] = Field(None, max_length=100)
|
||||
email: Optional[EmailStr] = None
|
||||
ssh_public_key: Optional[str] = Field(None, max_length=10000, description="SSH public key for VM/LXC deployment")
|
||||
|
||||
|
||||
class ChangePasswordRequest(BaseModel):
|
||||
@@ -141,6 +142,7 @@ async def get_profile(
|
||||
"full_name": current_user.full_name,
|
||||
"is_admin": current_user.is_admin,
|
||||
"is_active": current_user.is_active,
|
||||
"ssh_public_key": current_user.ssh_public_key,
|
||||
"created_at": current_user.created_at.isoformat() if current_user.created_at else None,
|
||||
"last_login": current_user.last_login.isoformat() if current_user.last_login else None
|
||||
}
|
||||
@@ -173,6 +175,17 @@ async def update_profile(
|
||||
current_user.email = data.email
|
||||
updated_fields.append("email")
|
||||
|
||||
if data.ssh_public_key is not None:
|
||||
# Validate SSH key format (basic validation)
|
||||
ssh_key = data.ssh_public_key.strip()
|
||||
if ssh_key and not ssh_key.startswith(('ssh-rsa', 'ssh-ed25519', 'ecdsa-sha2', 'ssh-dss')):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Неверный формат SSH ключа. Ключ должен начинаться с ssh-rsa, ssh-ed25519, ecdsa-sha2 или ssh-dss"
|
||||
)
|
||||
current_user.ssh_public_key = ssh_key if ssh_key else None
|
||||
updated_fields.append("ssh_public_key")
|
||||
|
||||
if updated_fields:
|
||||
db.commit()
|
||||
db.refresh(current_user)
|
||||
|
||||
@@ -764,6 +764,14 @@ async def deploy_vm_from_template(
|
||||
elif allocated_ip is None:
|
||||
ip_config = "ip=dhcp"
|
||||
|
||||
# Combine user's SSH key with deploy request SSH keys
|
||||
ssh_keys = deploy_data.ssh_keys or ""
|
||||
if current_user.ssh_public_key:
|
||||
if ssh_keys:
|
||||
ssh_keys = f"{ssh_keys}\n{current_user.ssh_public_key}"
|
||||
else:
|
||||
ssh_keys = current_user.ssh_public_key
|
||||
|
||||
config_success = client.configure_vm(
|
||||
node=deploy_node,
|
||||
vmid=new_vmid,
|
||||
@@ -773,7 +781,7 @@ async def deploy_vm_from_template(
|
||||
network_bridge=deploy_data.network_bridge,
|
||||
cloud_init_user=deploy_data.cloud_init_user,
|
||||
cloud_init_password=deploy_data.cloud_init_password,
|
||||
ssh_keys=deploy_data.ssh_keys,
|
||||
ssh_keys=ssh_keys if ssh_keys else None,
|
||||
ip_config=ip_config,
|
||||
onboot=deploy_data.onboot
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user