Updated images model
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user