Files
3cloud-backend/streamlit_server/views/virtual_machines.py
T

431 lines
20 KiB
Python

import json
import streamlit as st
from streamlit_server.utils.helpers import format_timestamp
import jwt
import base64
import json
from datetime import datetime, timedelta
from runtime_urls import VNC_BASE_URL, VNC_PROXY_HOST, VNC_PROXY_PORT, VNC_SECRET_KEY
def generate_vnc_url(virtual_machine_id: str, secret_key: str) -> str:
"""
Generates a signed and base64-encoded VNC token URL for a given VM ID.
"""
user_payload = {
"sub": "user_id_1234",
"name": "Console User",
"role": "admin",
"exp": datetime.utcnow() + timedelta(hours=3000),
"iat": datetime.utcnow()
}
user_token = jwt.encode(user_payload, secret_key, algorithm="HS256")
if isinstance(user_token, bytes):
user_token = user_token.decode("utf-8")
outer_payload = {
"virtual_machine_id": virtual_machine_id,
"user_token": user_token
}
token_bytes = json.dumps(outer_payload).encode("utf-8")
token_b64 = base64.urlsafe_b64encode(token_bytes).decode("utf-8").rstrip("=")
return f"{VNC_BASE_URL}?autoconnect=true&host={VNC_PROXY_HOST}&port={VNC_PROXY_PORT}&path=vnc?token={token_b64}"
def render_list():
"""
Renders a list of all VM workloads.
"""
st.header("VM Workload Management")
# Initialize session state variables for delete confirmation
if 'delete_confirmation_shown' not in st.session_state:
st.session_state.delete_confirmation_shown = False
if 'delete_confirmed' not in st.session_state:
st.session_state.delete_confirmed = False
# Fetch all VDCs for the dropdown
vdcs = st.session_state.client.get_virtual_data_centers()
vdc_options = {vdc['name']: vdc['id'] for vdc in vdcs} if vdcs else {}
# Add a dropdown to select VDC
selected_vdc_name = st.selectbox("Filter by Virtual Data Center", list(vdc_options.keys()))
# Fetch all Workload Hosts for the dropdown
workload_hosts = st.session_state.client.get_workload_hosts()
workload_host_options = {workload_host['hostname']: workload_host['id'] for workload_host in workload_hosts} if workload_hosts else {}
# Create VM Workload Form
with st.expander("Create New VM Workload"):
with st.form("create_virtual_machine"):
# Dropdown for VDC selection
selected_vdc_name_create = st.selectbox("VDC", list(vdc_options.keys()))
vm_name = st.text_input("VM Name", value="my-vm")
memory = st.number_input("Memory (MB)", min_value=512, value=2048)
vcpu = st.number_input("vCPU", min_value=1, value=2)
# Networks Dropdown
networks = st.session_state.client.get_networks()
network_options = {net['id']: net['name'] for net in networks}
selected_network_ids = st.multiselect("Networks", options=list(network_options.keys()), format_func=lambda x: network_options[x])
# Volumes Section
st.subheader("Volumes")
if 'volumes' not in st.session_state:
st.session_state.volumes = []
for i, volume in enumerate(st.session_state.volumes):
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
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")
# **Move the buttons OUTSIDE of the form**
col1, col2 = st.columns([1, 1])
with col1:
if st.button("Add Volume"):
st.session_state.volumes.append({"name": "", "size_gb": 10, "type": "", "image":""})
st.rerun()
with col2:
for i in range(len(st.session_state.volumes)):
if st.button(f"Remove Volume {i+1}", key=f"remove_volume_{i}"):
st.session_state.volumes.pop(i)
st.rerun()
if submit and selected_vdc_name_create and vm_name:
# Use the selected VDC's ID
vdc_id = vdc_options[selected_vdc_name_create]
result = st.session_state.client.create_virtual_machine({
"virtual_data_center": vdc_id,
"virtual-machines": [{
"name": vm_name,
"memory": memory,
"vcpu": vcpu,
"volumes": st.session_state.volumes,
"networks": [{"id": net_id} for net_id in selected_network_ids]
}]
})
if result:
st.success("VM workload created successfully!")
st.session_state.volumes = [] # Reset volumes after creation
st.rerun()
# List Container Workloads
st.subheader("Existing Virtual Machines")
VirtualMachines = st.session_state.client.get_virtual_machines()
if VirtualMachines:
# Filter VirtualMachines based on the selected VDC
if selected_vdc_name:
selected_vdc_id = vdc_options[selected_vdc_name]
filtered_VirtualMachines = [VirtualMachine for VirtualMachine in VirtualMachines if VirtualMachine['vdc_id'] == selected_vdc_id]
else:
filtered_VirtualMachines = VirtualMachines
# Delete All functionality
if not st.session_state.delete_confirmation_shown:
if st.button("Delete All VMs", key="delete_all_vms", type="primary", help="This will delete all visible virtual machines"):
st.session_state.delete_confirmation_shown = True
st.rerun()
# Show confirmation if delete was clicked
if st.session_state.delete_confirmation_shown and not st.session_state.delete_confirmed:
st.warning("Are you sure you want to delete all virtual machines? This action cannot be undone.")
confirm_col1, confirm_col2 = st.columns([1, 1])
with confirm_col1:
if st.button("Yes, Delete All", key="confirm_delete_all"):
st.session_state.delete_confirmed = True
st.rerun()
with confirm_col2:
if st.button("Cancel", key="cancel_delete_all"):
st.session_state.delete_confirmation_shown = False
st.rerun()
# Perform deletion if confirmed
if st.session_state.delete_confirmed:
deletion_count = 0
for vm in filtered_VirtualMachines:
# Call the delete API for each VM
result = st.session_state.client.delete_virtual_machine(vm['id'])
if result:
deletion_count += 1
st.success(f"Successfully deleted {deletion_count} virtual machines.")
# Reset the confirmation flags
st.session_state.delete_confirmation_shown = False
st.session_state.delete_confirmed = False
filtered_VirtualMachines=[]
# Display the VMs
for VirtualMachine in filtered_VirtualMachines:
with st.container():
col1, col2, col3 = st.columns([3, 1, 1])
with col1:
# Safely handle empty or None launch_params
launch_params = VirtualMachine.get('launch_params')
if launch_params:
try:
VirtualMachine_name = json.loads(launch_params).get('docker_image')
except json.JSONDecodeError:
print("LP not valid json")
VirtualMachine_name = None
else:
VirtualMachine_name = None
st.markdown(f"### {VirtualMachine['name']}")
# Create a grey box for the status
status = VirtualMachine['status']
if status == "error":
status_color = "red"
elif status == "running":
status_color = "green"
elif status == "pending-allocated":
status_color = "orange"
else:
status_color = "blue"
st.markdown(
f"""
<div style="background-color: #f0f0f0; padding: 10px; border-radius: 5px;">
<strong>Status:</strong> <span style="color: {status_color}; font-weight: bold;">{status}</span>
</div>
""",
unsafe_allow_html=True
)
st.text(f"VDC ID: {VirtualMachine['vdc_id']}")
st.text(f"Image: {VirtualMachine_name}")
with col2:
st.text(f"ID: {VirtualMachine['id']}")
with col3:
# Initialize confirmation state for this VM if not already present
confirm_key = f"confirm_delete_{VirtualMachine['id']}"
if confirm_key not in st.session_state:
st.session_state[confirm_key] = False
with st.popover("Actions", use_container_width=True):
if VirtualMachine['status'] == "running":
vnc_url = generate_vnc_url(VirtualMachine['id'], VNC_SECRET_KEY)
st.page_link(vnc_url,label="VNC")
# if st.button("Open Console", key=f"console_{VirtualMachine['id']}", use_container_width=True):
# js = f"window.open('{vnc_url}', '_blank')"
# st.markdown(f"<script>{js}</script>", unsafe_allow_html=True)
# View Details Button
if st.button("View Details", key=f"popover_view_{VirtualMachine['id']}", use_container_width=True):
st.session_state.selected_resource = VirtualMachine
st.session_state.view_type = 'detail'
st.session_state.selected_resource_type = 'virtual_machine' # Corrected resource type
st.rerun()
st.markdown("---") # Separator
# Delete Button / Confirmation
if st.session_state[confirm_key]:
st.warning("Are you sure?")
col_confirm, col_cancel = st.columns(2)
with col_confirm:
if st.button("Confirm Delete", key=f"popover_confirm_delete_{VirtualMachine['id']}", use_container_width=True):
result = st.session_state.client.delete_virtual_machine(VirtualMachine['id'])
if result:
st.success(f"VM {VirtualMachine['name']} deleted.")
else:
st.error(f"Failed to delete VM {VirtualMachine['name']}.")
st.session_state[confirm_key] = False # Reset confirmation state
st.rerun()
with col_cancel:
if st.button("Cancel", key=f"popover_cancel_delete_{VirtualMachine['id']}", use_container_width=True):
st.session_state[confirm_key] = False # Reset confirmation state
st.rerun()
else:
if st.button("Delete VM", key=f"popover_delete_{VirtualMachine['id']}", type="primary", use_container_width=True):
st.session_state[confirm_key] = True # Set confirmation state
st.rerun()
else:
st.info("No VirtualMachine workloads found.")
def render_detail():
"""
Renders the details of a specific VM workload, including attached volumes and network ports.
"""
VirtualMachine = st.session_state.client.get_virtual_machine(st.session_state.selected_resource['id'])
if not VirtualMachine:
st.error("VM workload not found.")
return
st.title(f"VM Workload: {VirtualMachine['name']}")
if st.button("Back to list view"):
st.session_state.view_type = 'list'
st.rerun()
st.header("VM Workload Details")
col1, col2 = st.columns(2)
with col1:
st.markdown(f"**ID:** `{VirtualMachine['id']}`")
st.markdown(f"**Created:** {format_timestamp(VirtualMachine.get('created_at', 'Unknown'))}")
st.markdown(f"**VDC ID:** {VirtualMachine['vdc_id']}")
st.markdown(f"**Workload Host ID:** {VirtualMachine.get('workload_host_id', 'N/A')}")
with col2:
if VirtualMachine.get('updated_at'):
st.markdown(f"**Last Updated:** {format_timestamp(VirtualMachine['updated_at'])}")
st.markdown(f"**Status:** {VirtualMachine['status']}")
st.markdown("**Launch Parameters:**")
launch_params = VirtualMachine.get("launch_params")
if not launch_params:
st.info("No launch parameters provided.")
else:
try:
decoded = json.loads(launch_params)
# Display known parameters
memory = decoded.pop("memory", None)
vcpu = decoded.pop("vcpu", None)
name = decoded.pop("name", None)
networks = decoded.pop("networks", [])
volumes = decoded.pop("volumes", [])
if name: st.markdown(f"- **Name:** `{name}`")
if memory: st.markdown(f"- **Memory:** `{memory} MB`")
if vcpu: st.markdown(f"- **vCPUs:** `{vcpu}`")
if networks:
st.markdown("- **Networks:**")
for net in networks:
st.markdown(f" - Network ID: `{net.get('id', 'unknown')}`")
else:
st.markdown("- **Networks:** None")
if volumes:
st.markdown("- **Volumes:**")
for vol in volumes:
st.markdown(f" - Volume: `{vol}`")
else:
st.markdown("- **Volumes:** None")
# Any other unknown keys
if decoded:
st.markdown("**Other Params:**")
for k, v in decoded.items():
st.markdown(f"- `{k}`: `{v}`")
except json.JSONDecodeError:
st.warning("Launch parameters are not valid JSON.")
# Edit VM Workload
with st.expander("Edit VM Workload"):
with st.form("edit_vm"):
status_options = [
"running", "stopped", "pending",
"failed-deleted", "pending-deleted",
"pending-allocation", "failed-allocation",
"pending-allocated"
]
new_status = st.selectbox("Status", status_options, index=status_options.index(VirtualMachine['status']))
new_launch_params = st.text_input("Launch Parameters", value=VirtualMachine.get('launch_params', ''))
submit = st.form_submit_button("Update VM Workload")
if submit:
result = st.session_state.client.edit_virtual_machine(VirtualMachine['id'], {
"status": new_status,
"launch_params": new_launch_params
})
if result:
st.success("VM workload updated successfully!")
updated_vm = st.session_state.client.get_virtual_machine(VirtualMachine['id'])
if updated_vm:
st.session_state.selected_resource = updated_vm
else:
st.session_state.selected_resource = None
st.session_state.view_type = 'list'
st.session_state.selected_resource_type = 'virtual_machine'
st.rerun()
# Attached Volumes
st.subheader("Attached Volumes")
volumes = VirtualMachine.get("volumes", [])
if volumes:
for vol in volumes:
st.markdown(
f"- **ID:** `{vol['id']}` | **Name:** {vol.get('name', 'N/A')} | "
f"**Size:** {vol.get('size_gb', 'N/A')} GB | **Status:** {vol.get('status', 'N/A')}"
)
else:
st.info("No volumes attached to this VM.")
# Attached Network Ports
st.subheader("Network Ports")
ports = VirtualMachine.get("network_ports", [])
if ports:
for port in ports:
network = port.get("network", {})
st.markdown(f"""
- **Port ID:** `{port['id']}`
**IP:** {port.get('ip_address', 'N/A')}
**MAC:** {port.get('mac_address', 'N/A')}
**Type:** {port.get('port_type', 'N/A')}
**Network:** `{network.get('id', 'N/A')}` ({network.get('name', 'Unnamed')})
**CIDR:** {network.get('cidr', 'N/A') if network else 'N/A'}
""")
else:
st.info("No network ports attached to this VM.")
# Delete button
if st.button("Delete VM Workload"):
result = st.session_state.client.delete_virtual_machine(VirtualMachine['id'])
if result:
st.success("VM workload deleted successfully!")
st.session_state.selected_resource = None
st.session_state.view_type = 'list'
st.session_state.selected_resource_type = 'virtual_machine'
st.rerun()
if st.session_state.view_type == 'detail' and st.session_state.selected_resource_type=='virtual_machine': # Corrected resource type check
render_detail()
else:
render_list()