From 56557052e1fa43e82b2e9ae3f421e36f82a6cc58 Mon Sep 17 00:00:00 2001 From: Saiful Amin Date: Wed, 25 Mar 2026 11:12:48 +0000 Subject: [PATCH 1/6] feat(container): add container default settings support Add ContainerDefaultSettings model and integrate container-specific settings into instance creation. Updates settings page with new container configuration tab and adds container image filtering. --- core/models.py | 14 ++++ routes/instances.py | 44 +++++++---- routes/settings.py | 108 +++++++++++++++++++++++---- services/instance_tasks.py | 2 +- static/js/settings.js | 135 +++++++++++++++++++++++++++++++--- templates/admin/settings.html | 130 +++++++++++++++++++++++++++++++- 6 files changed, 391 insertions(+), 42 deletions(-) diff --git a/core/models.py b/core/models.py index 169ffd7..a080aee 100644 --- a/core/models.py +++ b/core/models.py @@ -39,6 +39,20 @@ class VMDefaultSettings(Base): cloud_init = Column(Text, nullable=True) +class ContainerDefaultSettings(Base): + __tablename__ = "container_default_settings" + + id = Column(Integer, primary_key=True, index=True) + username = Column(String, default="root") + cpu = Column(Integer, default=2) + memory = Column(Integer, default=2) + disk = Column(Integer, default=10) + image_fingerprint = Column(String, nullable=True) # LXD image fingerprint + image_alias = Column(String, nullable=True) # Image alias (e.g., "ubuntu/24.04") + image_description = Column(String, nullable=True) # Human-readable description + cloud_init = Column(Text, nullable=True) + + class ConnectionTemplate(Base): __tablename__ = "connection_templates" diff --git a/routes/instances.py b/routes/instances.py index 99852af..7dab1b1 100644 --- a/routes/instances.py +++ b/routes/instances.py @@ -92,13 +92,22 @@ async def create_instance( "message": "LXD not configured. Please configure LXD in Settings first." }) - # Get VM default settings for cloud-init - vm_settings = db.query(VMDefaultSettings).first() - cloud_init_template = vm_settings.cloud_init if vm_settings and vm_settings.cloud_init else None - vm_swap = vm_settings.swap if vm_settings and vm_settings.swap else 2 - vm_username = vm_settings.username if vm_settings and vm_settings.username else "ubuntu" - image_fingerprint = vm_settings.image_fingerprint if vm_settings and vm_settings.image_fingerprint else None - + # Get VM or container default settings based on instance type + if instance_type == "container": + from core.models import ContainerDefaultSettings + instance_settings = db.query(ContainerDefaultSettings).first() + cloud_init_template = instance_settings.cloud_init if instance_settings and instance_settings.cloud_init else None + vm_swap = 2 # Not used for containers + vm_username = instance_settings.username if instance_settings else "root" + image_fingerprint = instance_settings.image_fingerprint if instance_settings else None + else: + from core.models import VMDefaultSettings + instance_settings = db.query(VMDefaultSettings).first() + cloud_init_template = instance_settings.cloud_init if instance_settings and instance_settings.cloud_init else None + vm_swap = instance_settings.swap if instance_settings and instance_settings.swap else 2 + vm_username = instance_settings.username if instance_settings and instance_settings.username else "ubuntu" + image_fingerprint = instance_settings.image_fingerprint if instance_settings and instance_settings.image_fingerprint else None + # Pass the raw template (with placeholders) to the background task # The background task will generate SSH keys and process the template cloud_init = cloud_init_template @@ -301,12 +310,21 @@ async def bulk_create_instances( "message": "LXD not configured. Please configure LXD in Settings first." }) - # Get VM default settings - vm_settings = db.query(VMDefaultSettings).first() - cloud_init = vm_settings.cloud_init if vm_settings else None - vm_swap = vm_settings.swap if vm_settings else 2 - vm_username = vm_settings.username if vm_settings else "ubuntu" - image_fingerprint = vm_settings.image_fingerprint if vm_settings else None + # Get VM or container default settings based on instance type + if instance_type == "container": + from core.models import ContainerDefaultSettings + instance_settings = db.query(ContainerDefaultSettings).first() + cloud_init = instance_settings.cloud_init if instance_settings and instance_settings.cloud_init else None + vm_swap = 2 # Not used for containers + vm_username = instance_settings.username if instance_settings else "root" + image_fingerprint = instance_settings.image_fingerprint if instance_settings else None + else: + from core.models import VMDefaultSettings + instance_settings = db.query(VMDefaultSettings).first() + cloud_init = instance_settings.cloud_init if instance_settings and instance_settings.cloud_init else None + vm_swap = instance_settings.swap if instance_settings and instance_settings.swap else 2 + vm_username = instance_settings.username if instance_settings else "ubuntu" + image_fingerprint = instance_settings.image_fingerprint if instance_settings else None lxd_settings = { "use_socket": lxd_settings_db.use_socket, diff --git a/routes/settings.py b/routes/settings.py index 4ab4cb3..5c21006 100644 --- a/routes/settings.py +++ b/routes/settings.py @@ -5,7 +5,7 @@ from sqlalchemy.orm import Session from core.database import get_db -from core.models import AdminUser, LXDSettings, VMDefaultSettings +from core.models import AdminUser, LXDSettings, VMDefaultSettings, ContainerDefaultSettings from core.config import settings from core.security import get_password_hash, verify_password from services.lxd_service import LXDService @@ -58,13 +58,16 @@ async def settings_page( password_error: str = None, vm_success: str = None, vm_error: str = None, + container_success: str = None, + container_error: str = None, templates_success: str = None ): - """Settings page - change password, LXD configuration, and VM defaults""" + """Settings page - change password, LXD configuration, and VM/container defaults""" from core.models import ConnectionTemplate - + lxd_settings = db.query(LXDSettings).first() vm_settings = db.query(VMDefaultSettings).first() + container_settings = db.query(ContainerDefaultSettings).first() connection_templates = db.query(ConnectionTemplate).first() return templates.TemplateResponse("admin/settings.html", { @@ -72,6 +75,7 @@ async def settings_page( "username": user.username, "lxd_settings": lxd_settings, "vm_settings": vm_settings, + "container_settings": container_settings, "connection_templates": connection_templates, "lxd_success": lxd_success, "lxd_error": lxd_error, @@ -79,6 +83,8 @@ async def settings_page( "password_error": password_error, "vm_success": vm_success, "vm_error": vm_error, + "container_success": container_success, + "container_error": container_error, "templates_success": templates_success }) @@ -212,14 +218,14 @@ async def save_vm_settings( """Save default VM settings""" # Validate username from core.validators import validate_username, validate_positive_integer - + is_valid, error = validate_username(username) if not is_valid: return RedirectResponse( url=f"/settings?vm_error={error}", status_code=303 ) - + # Validate cloud-init template if provided if cloud_init.strip(): from services.cloud_init_service import validate_cloud_init_template @@ -261,6 +267,69 @@ async def save_vm_settings( return RedirectResponse(url="/settings?vm_success=VM defaults saved successfully", status_code=303) +@router.post("/settings/container") +async def save_container_settings( + request: Request, + username: str = Form(...), + cpu: int = Form(...), + memory: int = Form(...), + disk: int = Form(...), + image_fingerprint: str = Form(""), + image_alias: str = Form(""), + image_description: str = Form(""), + cloud_init: str = Form(""), + db: Session = Depends(get_db) +): + """Save default container settings""" + # Validate username + from core.validators import validate_username + + is_valid, error = validate_username(username) + if not is_valid: + return RedirectResponse( + url=f"/settings?container_error={error}", + status_code=303 + ) + + # Validate cloud-init template if provided + if cloud_init.strip(): + from services.cloud_init_service import validate_cloud_init_template + is_valid, error_msg = validate_cloud_init_template(cloud_init) + if not is_valid: + return RedirectResponse( + url=f"/settings?container_error={error_msg}", + status_code=303 + ) + + settings = db.query(ContainerDefaultSettings).first() + + if settings: + settings.username = username + settings.cpu = cpu + settings.memory = memory + settings.disk = disk + settings.image_fingerprint = image_fingerprint if image_fingerprint else None + settings.image_alias = image_alias if image_alias else None + settings.image_description = image_description if image_description else None + settings.cloud_init = cloud_init if cloud_init else None + else: + settings = ContainerDefaultSettings( + username=username, + cpu=cpu, + memory=memory, + disk=disk, + image_fingerprint=image_fingerprint if image_fingerprint else None, + image_alias=image_alias if image_alias else None, + image_description=image_description if image_description else None, + cloud_init=cloud_init if cloud_init else None + ) + db.add(settings) + + db.commit() + + return RedirectResponse(url="/settings?container_success=Container defaults saved successfully", status_code=303) + + @router.get("/settings/vm/template") async def get_cloud_init_template(): """Get the default cloud-init template""" @@ -272,25 +341,32 @@ async def get_cloud_init_template(): @router.get("/settings/vm/images") -async def get_available_images(db: Session = Depends(get_db)): - """Get available LXD images for VM creation""" +async def get_available_images( + db: Session = Depends(get_db), + instance_type: str = "virtual-machine" +): + """Get available LXD images for VM or container creation""" from services.lxd_service import LXDService - + lxd_service = LXDService(db) lxd_service.get_client() - + if not lxd_service.is_connected(): return JSONResponse({ "success": False, "message": "LXD not connected" }) - + try: images = [] for img in lxd_service.client.images.all(): + # Filter by instance type + if img.type != instance_type: + continue + # Get image info description = img.properties.get('description', 'Unknown') - + # Handle aliases which can be dicts or objects aliases = [] for a in img.aliases: @@ -300,7 +376,7 @@ async def get_available_images(db: Session = Depends(get_db)): name = getattr(a, 'name', None) if name: aliases.append(name) - + # Handle created_at which might be datetime or string created_at = None if img.created_at: @@ -308,21 +384,21 @@ async def get_available_images(db: Session = Depends(get_db)): created_at = img.created_at.isoformat() else: created_at = str(img.created_at) - + images.append({ "fingerprint": img.fingerprint[:12], # Short fingerprint "full_fingerprint": img.fingerprint, - "description": f"{img.type}: {description}", + "description": f"{description}", "aliases": aliases, "architecture": img.architecture, "type": img.type, "size": img.size, "created_at": created_at }) - + # Sort by description images.sort(key=lambda x: x['description']) - + return JSONResponse({ "success": True, "images": images diff --git a/services/instance_tasks.py b/services/instance_tasks.py index 7d20480..9037c6c 100644 --- a/services/instance_tasks.py +++ b/services/instance_tasks.py @@ -87,7 +87,7 @@ def create_instance_background( creation_tasks[task_id]["progress"] = 25 creation_tasks[task_id]["message"] = "Preparing instance configuration..." - # Generate SSH key pair for this VM (only for VMs, not containers) + # Generate SSH key pair for this instance ssh_keys = None try: creation_tasks[task_id]["message"] = "Generating SSH key pair..." diff --git a/static/js/settings.js b/static/js/settings.js index 69c5182..ba970d7 100644 --- a/static/js/settings.js +++ b/static/js/settings.js @@ -120,22 +120,72 @@ async function loadCloudInitTemplate() { } // Load available LXD images -async function loadImages() { +async function loadImages(instanceType = 'virtual-machine') { const select = document.getElementById('image_select'); const descField = document.getElementById('image_description'); const fpField = document.getElementById('image_fingerprint'); const aliasField = document.getElementById('image_alias'); - + select.disabled = true; select.innerHTML = ''; - + try { - const response = await fetch('/settings/vm/images'); + const response = await fetch(`/settings/vm/images?instance_type=${instanceType}`); const data = await response.json(); - + + if (data.success && data.images.length > 0) { + select.innerHTML = ''; + + data.images.forEach(img => { + const aliasText = img.aliases.length > 0 ? ` (${img.aliases.join(', ')})` : ''; + const option = document.createElement('option'); + option.value = img.fingerprint; + option.textContent = `${img.description}${aliasText}`; + option.dataset.fullFingerprint = img.full_fingerprint; + option.dataset.alias = img.aliases.length > 0 ? img.aliases[0] : ''; + option.dataset.description = img.description; + select.appendChild(option); + }); + + // Restore previously selected image + if (fpField.value) { + for (let opt of select.options) { + if (opt.dataset.fullFingerprint === fpField.value || opt.value === fpField.value) { + opt.selected = true; + descField.value = opt.dataset.description; + aliasField.value = opt.dataset.alias; + break; + } + } + } + } else { + select.innerHTML = ''; + } + } catch (error) { + select.innerHTML = ``; + console.error('Failed to load images:', error); + } finally { + select.disabled = false; + } +} + +// Load available LXD container images +async function loadContainerImages() { + const select = document.getElementById('container_image_select'); + const descField = document.getElementById('container_image_description'); + const fpField = document.getElementById('container_image_fingerprint'); + const aliasField = document.getElementById('container_image_alias'); + + select.disabled = true; + select.innerHTML = ''; + + try { + const response = await fetch('/settings/vm/images?instance_type=container'); + const data = await response.json(); + if (data.success && data.images.length > 0) { select.innerHTML = ''; - + data.images.forEach(img => { const aliasText = img.aliases.length > 0 ? ` (${img.aliases.join(', ')})` : ''; const option = document.createElement('option'); @@ -146,7 +196,7 @@ async function loadImages() { option.dataset.description = img.description; select.appendChild(option); }); - + // Restore previously selected image if (fpField.value) { for (let opt of select.options) { @@ -169,6 +219,28 @@ async function loadImages() { } } +// Load cloud-init template for containers +async function loadContainerCloudInitTemplate() { + const cloudInitField = document.getElementById('container_cloud_init'); + cloudInitField.disabled = true; + cloudInitField.placeholder = 'Loading template...'; + + try { + const response = await fetch('/settings/vm/template'); + const data = await response.json(); + + if (data.success) { + cloudInitField.value = data.template; + } else { + cloudInitField.value = '# Failed to load template'; + } + } catch (error) { + cloudInitField.value = `# Error loading template: ${error.message}`; + } finally { + cloudInitField.disabled = false; + } +} + // Load connection templates async function loadConnectionTemplates() { const sshConfigField = document.getElementById('ssh_config_template'); @@ -191,9 +263,29 @@ function onImageSelect() { const descField = document.getElementById('image_description'); const fpField = document.getElementById('image_fingerprint'); const aliasField = document.getElementById('image_alias'); - + const selectedOption = select.options[select.selectedIndex]; - + + if (selectedOption.value) { + descField.value = selectedOption.dataset.description || selectedOption.textContent; + fpField.value = selectedOption.dataset.fullFingerprint || selectedOption.value; + aliasField.value = selectedOption.dataset.alias || ''; + } else { + descField.value = ''; + fpField.value = ''; + aliasField.value = ''; + } +} + +// Handle container image selection +function onContainerImageSelect() { + const select = document.getElementById('container_image_select'); + const descField = document.getElementById('container_image_description'); + const fpField = document.getElementById('container_image_fingerprint'); + const aliasField = document.getElementById('container_image_alias'); + + const selectedOption = select.options[select.selectedIndex]; + if (selectedOption.value) { descField.value = selectedOption.dataset.description || selectedOption.textContent; fpField.value = selectedOption.dataset.fullFingerprint || selectedOption.value; @@ -219,6 +311,8 @@ document.addEventListener('DOMContentLoaded', function() { const passwordError = document.getElementById('password-error')?.value || ''; const vmSuccess = document.getElementById('vm-success')?.value || ''; const vmError = document.getElementById('vm-error')?.value || ''; + const containerSuccess = document.getElementById('container-success')?.value || ''; + const containerError = document.getElementById('container-error')?.value || ''; const templatesSuccess = document.getElementById('templates-success')?.value || ''; const templatesError = document.getElementById('templates-error')?.value || ''; @@ -227,6 +321,11 @@ document.addEventListener('DOMContentLoaded', function() { templatesTab.show(); // Load templates when tab is shown loadConnectionTemplates(); + } else if (containerSuccess || containerError) { + const containerTab = new bootstrap.Tab(document.getElementById('container-settings-tab')); + containerTab.show(); + // Load container images when container tab is shown + loadContainerImages(); } else if (vmSuccess || vmError) { const vmTab = new bootstrap.Tab(document.getElementById('vm-settings-tab')); vmTab.show(); @@ -236,7 +335,7 @@ document.addEventListener('DOMContentLoaded', function() { const passwordTab = new bootstrap.Tab(document.getElementById('password-tab')); passwordTab.show(); } - + // Load connection templates when tab is clicked const connectionTemplatesTab = document.getElementById('connection-templates-tab'); if (connectionTemplatesTab) { @@ -244,4 +343,20 @@ document.addEventListener('DOMContentLoaded', function() { loadConnectionTemplates(); }); } + + // Load VM images when VM tab is shown + const vmSettingsTab = document.getElementById('vm-settings-tab'); + if (vmSettingsTab) { + vmSettingsTab.addEventListener('shown.bs.tab', function() { + loadImages(); + }); + } + + // Load container images when Container tab is shown + const containerSettingsTab = document.getElementById('container-settings-tab'); + if (containerSettingsTab) { + containerSettingsTab.addEventListener('shown.bs.tab', function() { + loadContainerImages(); + }); + } }); diff --git a/templates/admin/settings.html b/templates/admin/settings.html index 1183f98..6922ce2 100644 --- a/templates/admin/settings.html +++ b/templates/admin/settings.html @@ -17,6 +17,11 @@ VM Settings +