Updated images model

This commit is contained in:
2025-03-20 21:24:40 +10:30
parent bc78ec6149
commit 505370bb06
4 changed files with 146 additions and 110 deletions
+74 -21
View File
@@ -2,7 +2,7 @@ import random
from flask import json, request, jsonify, abort
import requests
from app import app, db, logger
from app.models.models import Volume, Workload, WorkloadHost, VirtualDataCenter
from app.models.models import Volume, Workload, WorkloadHost, VirtualDataCenter, Image
from datetime import datetime
from app.controller import api_bp
import uuid
@@ -105,13 +105,13 @@ def validate_payload(payload):
if not isinstance(payload, dict):
raise ValueError("Input must be a valid JSON object (dict).")
# Check for the top-level 'vdc' key and validate it as a UUID
if 'vdc' not in payload:
raise ValueError("Top-level 'vdc' key is missing.")
# Check for the top-level 'virtual_data_center' key and validate it as a UUID
if 'virtual_data_center' not in payload:
raise ValueError("Top-level 'virtual_data_center' key is missing.")
try:
uuid.UUID(payload['vdc'], version=4)
uuid.UUID(payload['virtual_data_center'], version=4)
except ValueError:
raise ValueError("'vdc' must be a valid UUID.")
raise ValueError("'virtual_data_center' must be a valid UUID.")
# Check for the 'virtual-machines' key and ensure it is a list with at least one VM
if 'virtual-machines' not in payload:
@@ -121,7 +121,7 @@ def validate_payload(payload):
# Initialize the sanitized payload
sanitized_payload = {
'vdc': payload['vdc'],
'virtual_data_center': payload['virtual_data_center'],
'virtual-machines': []
}
@@ -163,15 +163,23 @@ def validate_payload(payload):
if 'name' not in volume or 'size_gb' not in volume:
raise ValueError("Volume is missing 'name' or 'size_gb'.")
if not isinstance(volume['size_gb'], (int, float)) or volume['size_gb'] <= 0:
raise ValueError("'size_gb' must be a positive number.")
sanitized_volume = {
'name': volume['name'],
'size_gb': volume['size_gb']
'size_gb': volume['size_gb'],
}
if not isinstance(volume['size_gb'], (int, float)) or volume['size_gb'] <= 0:
raise ValueError("'size_gb' must be a positive number.")
# Check if 'image' is present and validate it as a UUID
if 'image' in volume:
try:
uuid.UUID(volume['image'])
except ValueError:
raise ValueError(f"'{volume['image']}' is not a valid UUID.")
sanitized_volume['image'] = volume['image']
# Add optional volume fields if present
if 'boot' in volume:
if not isinstance(volume['boot'], bool):
@@ -183,7 +191,8 @@ def validate_payload(payload):
if 'source' in volume:
sanitized_volume['source'] = volume['source']
sanitized_volumes.append(sanitized_volume)
sanitized_vm['volumes'] = sanitized_volumes
@@ -258,7 +267,7 @@ def add_VirtualMachine_workload():
validated_data = validate_payload(request_data)
# TODO - Check if this user has permissions to VIEW and LAUNCH_VirtualMachine in this VDC
request_vdc = VirtualDataCenter.query.filter_by(id=uuid.UUID(validated_data['vdc'])).first()
request_vdc = VirtualDataCenter.query.filter_by(id=uuid.UUID(validated_data['virtual_data_center'])).first()
if request_vdc is None:
# Raise an error or return a response indicating that no VDC was found
@@ -289,16 +298,50 @@ def add_VirtualMachine_workload():
db.session.add(new_VirtualMachine)
db.session.commit()
logger.debug(f"VirtualMachine {new_VirtualMachine.id} workload added to DB")
# Create volumes in the database
volumes = []
for volume in VirtualMachine.get('volumes', []):
_volume_source=""
_volume_image_id=None
if "image" in volume:
logger.debug(f"Volume had image id specified {volume['image']}, confirming if it's a legitimate image ID or not")
# Validate the image ID
try:
# Convert the image ID to a UUID object
image_id = uuid.UUID(volume['image'])
# Query the image from the database
_image = Image.query.filter_by(id=image_id).first()
if _image is None:
# If the image does not exist, return an error
return {"error": f"Image with ID {volume['image']} does not exist"}, 404
# If the image exists, load the image ID into a variable
_volume_image_id = uuid.UUID(_image.id)
_volume_source = _image.location
logger.debug(f"Image ID {image_id} is valid and loaded.")
except ValueError:
# Handle the case where the image ID is not a valid UUID
return {"error": f"Invalid image ID format: {volume['image']}"}, 400
_volume_path=volume['type'],
new_volume = Volume(
name=volume['name'],
image_id=_volume_image_id,
type=_volume_path,
path=_volume_path,
size_gb=volume['size_gb'],
vdc_id=request_vdc.id,
volume_type="Local" # Default to Local, can be overridden if specified
source=_volume_source
)
logger.debug(new_volume.to_json())
db.session.add(new_volume)
db.session.commit()
volumes.append(new_volume)
@@ -322,8 +365,18 @@ def add_VirtualMachine_workload():
"virtual_machine_config": {
"memory": VirtualMachine['memory'],
"vcpu": VirtualMachine['vcpu'],
"volumes": [{"id": vol.id, "size_gb": vol.size_gb, "format": "qcow2"} for vol in volumes],
"networks": [{"id": net.id, "mac_address": net.mac_address} for net in networks]
"volumes": [{
"id": vol.id,
"name": vol.name,
"size_gb": vol.size_gb,
"type": vol.type,
"path": vol.path,
"source": vol.source,
} for vol in volumes],
"networks": [{
"id": net.id,
"mac_address": net.mac_address
} for net in networks]
}
},
}
@@ -359,8 +412,8 @@ def edit_VirtualMachine_workload(virtual_machine_id):
data = request.json
# Validate new_status
if data.get("new_status") not in ["started", "deleted"]:
error_message = "Invalid status. Must be 'started' or 'deleted'."
if data.get("new_status") not in ["running", "deleted"]:
error_message = "Invalid status. Must be 'running' or 'deleted'."
logger.error(error_message)
return jsonify({"success": False, "message": error_message}), 400
+3
View File
@@ -149,7 +149,10 @@ class Volume(BaseModel):
__tablename__ = "volumes"
volume_type = Column(Enum("Ceph", "Local", name="volume_type"), nullable=False)
path = Column(String, nullable=True)
type = Column(String, nullable=True)
source = Column(String, nullable=True)
size_gb = Column(Float, nullable=False)
boot = Column(Boolean, nullable=False, default=False)
vdc_id = Column(UUID(as_uuid=True), ForeignKey("vdcs.id"), nullable=False)
# Optional foreign key to link a volume to an image
image_id = Column(UUID(as_uuid=True), ForeignKey("images.id"), nullable=True)
+34 -84
View File
@@ -1,34 +1,34 @@
import streamlit as st
import pandas as pd
from streamlit_server.utils.helpers import format_timestamp
def render_list():
"""
Renders a list of all images.
"""
st.header("Image Management")
# Create Image Form
with st.expander("Create New Image"):
with st.form("create_image"):
location = st.text_input("Location (URL or Path)")
size = st.number_input("Size (in MB)", min_value=0.0)
location_type = st.selectbox("Location Type", ["url", "local"])
os_family = st.text_input("OS Family")
os_version = st.text_input("OS Version")
checksum = st.text_input("Checksum")
name = st.text_input("Image Name")
location = st.text_input("Image Location (e.g., /images/image.img)")
location_type = st.selectbox("Location Type", ["Local", "Remote"])
size = st.number_input("Size (GB)", min_value=0.1, step=0.1)
os_family = st.text_input("OS Family (e.g., Linux)")
os_version = st.text_input("OS Version (e.g., 20.04)")
checksum = st.text_input("Checksum (optional)")
submit = st.form_submit_button("Create Image")
if submit and location and size:
result = st.session_state.client.create_image({
"location": location,
"size": size,
"location_type": location_type,
"os_family": os_family,
"os_version": os_version,
"checksum": checksum
})
if submit and name and location:
result = st.session_state.client.create_image(
name=name,
location=location,
location_type=location_type,
size=size,
os_family=os_family,
os_version=os_version,
checksum=checksum
)
if result:
st.success(f"Image created successfully!")
st.success(f"Image '{name}' created successfully!")
# List Images
st.subheader("Existing Images")
@@ -39,95 +39,45 @@ def render_list():
with st.container():
col1, col2, col3 = st.columns([3, 1, 1])
with col1:
st.markdown(f"### {image['location']}")
st.text(f"Size: {image['size']} MB")
st.text(f"OS: {image.get('os_family', 'N/A')} {image.get('os_version', 'N/A')}")
st.markdown(f"### {image['name']}")
st.text(f"Location: {image['location']}")
st.text(f"Size: {image['size']} GB")
with col2:
st.text(f"ID: {image['id'][:8]}...")
with col3:
if st.button("View Details", key=f"view_{image['id']}"):
st.session_state.selected_resource = image
st.session_state.view_type = 'detail'
st.session_state.selected_resource_type='image'
st.session_state.selected_resource_type = 'image'
st.rerun() # Force Streamlit to rerun the script
else:
st.info("No images found.")
def render_detail():
"""
Renders the details of a specific image.
"""
image = st.session_state.client.get_image(st.session_state.selected_resource['id'])
if not image:
st.error("Image not found.")
st.session_state.selected_resource = None
st.session_state.view_type = 'list'
st.session_state.selected_resource_type = None
return
st.title(f"Image: {image['location']}")
st.title(f"Image: {image['name']}")
if st.button("Back to list view"):
st.session_state.view_type = 'list'
st.rerun()
st.header("Image Details")
col1, col2 = st.columns(2)
with col1:
st.markdown(f"**ID:** `{image['id']}`")
st.markdown(f"**Created:** {format_timestamp(image.get('created_at', 'Unknown'))}")
st.markdown(f"**Size:** {image['size']} MB")
st.markdown(f"**Location Type:** {image.get('location_type', 'N/A')}")
st.markdown(f"**Created:** {format_timestamp(image['created_at'])}")
st.markdown(f"**Location:** `{image['location']}`")
st.markdown(f"**Location Type:** `{image['location_type']}`")
with col2:
st.markdown(f"**Size:** `{image['size']} GB`")
st.markdown(f"**OS Family:** `{image['os_family']}`")
st.markdown(f"**OS Version:** `{image['os_version']}`")
if image.get('checksum'):
st.markdown(f"**Checksum:** `{image['checksum']}`")
if image.get('updated_at'):
st.markdown(f"**Last Updated:** {format_timestamp(image['updated_at'])}")
st.markdown(f"**OS Family:** {image.get('os_family', 'N/A')}")
st.markdown(f"**OS Version:** {image.get('os_version', 'N/A')}")
st.markdown(f"**Checksum:** {image.get('checksum', 'N/A')}")
# Edit Image Form
with st.expander("Edit Image"):
with st.form("edit_image"):
new_location = st.text_input("Location (URL or Path)", value=image['location'])
new_size = st.number_input("Size (in MB)", min_value=0.0, value=image['size'])
new_location_type = st.selectbox("Location Type", ["url", "local"], index=0 if image.get('location_type') == "url" else 1)
new_os_family = st.text_input("OS Family", value=image.get('os_family', ''))
new_os_version = st.text_input("OS Version", value=image.get('os_version', ''))
new_checksum = st.text_input("Checksum", value=image.get('checksum', ''))
submit = st.form_submit_button("Update Image")
if submit:
result = st.session_state.client.update_image(image['id'], {
"location": new_location,
"size": new_size,
"location_type": new_location_type,
"os_family": new_os_family,
"os_version": new_os_version,
"checksum": new_checksum
})
if result:
st.success("Image updated successfully!")
st.session_state.selected_resource = image
st.session_state.view_type = 'detail'
st.session_state.selected_resource_type='image'
# Delete Image Button
if st.button("Delete Image"):
result = st.session_state.client.delete_image(image['id'])
if result:
st.success("Image deleted successfully!")
st.session_state.view_type = 'list'
st.session_state.selected_resource_type='image'
# def render():
"""
Main render function for the Images page.
"""
# Read query parameters for routing
if st.session_state.view_type == 'detail' and st.session_state.selected_resource_type=='image':
if st.session_state.view_type == 'detail' and st.session_state.selected_resource_type == 'image':
render_detail()
else:
render_list()
+35 -5
View File
@@ -46,14 +46,44 @@ def render_list():
st.session_state.volumes = []
for i, volume in enumerate(st.session_state.volumes):
col1, col2 = st.columns([3, 2])
col1, col2, col3, col4 = st.columns([3, 2, 2, 2])
with col1:
volume_name = st.text_input(f"Volume Name {i+1}", value=volume['name'], key=f"volume_name_{i}")
with col2:
volume_size = st.number_input(f"Size (GB) {i+1}", min_value=1, value=volume['size_gb'], key=f"volume_size_{i}")
with col3:
volume_type = st.selectbox(f"Type {i+1}", options=["local", "ceph"], index=0 if volume.get('type', 'local') == 'local' else 1, key=f"volume_type_{i}")
with col4:
# Get image options
image_list = st.session_state.client.get_images()
# Create a mapping of image names to IDs
image_name_to_id = {img['name']: img['id'] for img in image_list}
image_name_to_id["blank"] = "" # Add a blank option
# Default selected image name (fallback to 'blank' if not found)
default_image_name = volume.get('image_name', 'blank')
# Dropdown to display image names
selected_image_name = st.selectbox(
f"Image {i+1}",
options=list(image_name_to_id.keys()),
index=list(image_name_to_id.keys()).index(default_image_name) if default_image_name in image_name_to_id else 0,
key=f"volume_image_{i}"
)
# Get the corresponding image ID for the selected image name
selected_image_id = image_name_to_id[selected_image_name]
# Store updated volume info
st.session_state.volumes[i] = {"name": volume_name, "size_gb": volume_size}
if selected_image_name == "blank":
st.session_state.volumes[i] = {"name": volume_name, "size_gb": volume_size, "type": volume_type}
else:
st.session_state.volumes[i] = {"name": volume_name, "size_gb": volume_size, "type": volume_type, "image_id": selected_image_id}
submit = st.form_submit_button("Create VM Workload")
@@ -61,7 +91,7 @@ def render_list():
col1, col2 = st.columns([1, 1])
with col1:
if st.button("Add Volume"):
st.session_state.volumes.append({"name": "", "size_gb": 10})
st.session_state.volumes.append({"name": "", "size_gb": 10, "type": "", "image":""})
st.rerun()
with col2:
@@ -75,7 +105,7 @@ def render_list():
vdc_id = vdc_options[selected_vdc_name_create]
result = st.session_state.client.create_virtual_machine({
"vdc": vdc_id,
"virtual_data_center": vdc_id,
"virtual-machines": [{
"name": vm_name,
"memory": memory,
@@ -157,7 +187,7 @@ def render_list():
status = VirtualMachine['status']
if status == "error":
status_color = "red"
elif status == "started":
elif status == "running":
status_color = "green"
elif status == "pending-allocated":
status_color = "orange"