filter work

This commit is contained in:
2025-04-06 02:46:31 +09:30
parent 1e323a1a98
commit b748b44c58
7 changed files with 387 additions and 114 deletions
+35 -1
View File
@@ -263,4 +263,38 @@ class IaaSClient:
vm['workload_type'] = 'virtual_machine' # Add workload type
workloads.append(vm)
return workloads
return workloads
def add_label(self, host_id, key, value, admin_only_view=False, admin_only_set=False):
"""Add a new label to a workload host"""
endpoint = f'workload_hosts/{host_id}/labels'
data = {
'label_key': key,
'label_value': value,
'admin_only_view': admin_only_view,
'admin_only_set': admin_only_set
}
return self._make_request('POST', endpoint, data)
def remove_label(self, host_id, key):
"""Remove a label from a workload host"""
endpoint = f'workload_hosts/{host_id}/labels'
data = {
'label_key': key
}
return self._make_request('DELETE', endpoint, data)
def get_labels_for_host(self, host_id):
"""
Fetch all labels for a specific workload host
Args:
host_id (str/UUID): The ID of the workload host
Returns:
list: List of label dictionaries or None if error occurs
"""
endpoint = f'workload_hosts/{host_id}/labels'
return self._make_request('GET', endpoint)
@@ -6,7 +6,7 @@ from app.models.models import Workload, WorkloadHost, VirtualDataCenter, Label
from datetime import datetime
from app.controller import api_bp
import uuid
from app.scheduling_filters import ExcludeAllDisabledHosts, MostAvailableCapacity, ExcludeAllOfflineHosts
from app.scheduling_filters import ExcludeAllDisabledHosts, MostAvailableCapacity, ExcludeAllOfflineHosts, DockerCapableHosts
import uuid
from werkzeug.exceptions import abort
@@ -106,7 +106,7 @@ def add_container_workload():
all_workload_hosts = WorkloadHost.query.filter_by(deleted=0, region_id=request_vdc.region.id).all()
logger.debug(f"Found {len(all_workload_hosts)} hosts eligible for placement")
filters = [ExcludeAllOfflineHosts(), ExcludeAllDisabledHosts(), MostAvailableCapacity()]
filters = [DockerCapableHosts(), ExcludeAllOfflineHosts(), ExcludeAllDisabledHosts(), MostAvailableCapacity()]
filtered_hosts = all_workload_hosts
for filter in filters:
filtered_hosts = filter.apply(filtered_hosts)
+129 -1
View File
@@ -1,7 +1,7 @@
import uuid
from flask import request, jsonify
from app import app, db, logger
from app.models.models import WorkloadHost, Region
from app.models.models import WorkloadHost, Region, Label
from app.controller import api_bp
# WorkloadHost Routes
@@ -103,3 +103,131 @@ def enroll_workload_host():
db.session.rollback()
logger.error(f"Enrollment failed: {str(e)}")
return jsonify({'error': 'Enrollment failed', 'details': str(e)}), 500
@api_bp.route('/workload_hosts/<host_id>/labels', methods=['GET'])
def get_workload_host_labels(host_id):
# Validate host_id as UUID
try:
host_uuid = uuid.UUID(host_id)
except (ValueError, TypeError) as e:
error_message = "Invalid UUID format for host_id."
logger.error(f"{error_message} Error: {str(e)}")
return jsonify({"success": False, "message": error_message}), 400
# Fetch the labels
try:
labels = Label.query.filter_by(
target_object_id=host_uuid,
target_object_type="workload_hosts"
).all()
return jsonify([label.to_json() for label in labels]), 200
except Exception as e:
error_message = f"Failed to fetch labels for host {host_id}."
logger.error(f"{error_message} Error: {str(e)}")
return jsonify({"success": False, "message": error_message}), 500
@api_bp.route('/workload_hosts/<host_id>/labels', methods=['POST', 'DELETE'])
def manage_workload_host_labels(host_id):
# Validate host_id as UUID
try:
host_uuid = uuid.UUID(host_id)
except (ValueError, TypeError) as e:
error_message = "Invalid UUID format for host_id."
logger.error(f"{error_message} Error: {str(e)}")
return jsonify({"success": False, "message": error_message}), 400
if request.method == 'POST':
# Add or update label
data = request.json
if not data:
return jsonify({"success": False, "message": "No data provided"}), 400
required_fields = ['label_key', 'label_value']
if not all(field in data for field in required_fields):
return jsonify({
"success": False,
"message": f"Missing required fields. Required: {required_fields}"
}), 400
try:
# Check if label already exists
label = Label.query.filter_by(
target_object_id=host_uuid,
target_object_type="workload_hosts",
label_key=data['label_key']
).first()
if label:
# Update existing label
label.label_value = data['label_value']
label.admin_only_view = data.get('admin_only_view', False)
label.admin_only_set = data.get('admin_only_set', False)
message = "Label updated"
else:
# Create new label
label = Label(
target_object_id=host_uuid,
target_object_type="workload_hosts",
label_key=data['label_key'],
label_value=data['label_value'],
admin_only_view=data.get('admin_only_view', False),
admin_only_set=data.get('admin_only_set', False),
name=f"Label {data['label_key']}={data['label_value']}",
description=f"Label for host {host_id}"
)
db.session.add(label)
message = "Label added"
db.session.commit()
return jsonify({
"success": True,
"message": message,
"label": label.to_json()
}), 200
except Exception as e:
db.session.rollback()
error_message = f"Failed to {'update' if label else 'add'} label"
logger.error(f"{error_message}. Error: {str(e)}")
return jsonify({"success": False, "message": error_message}), 500
elif request.method == 'DELETE':
# Remove label
data = request.json
if not data or 'label_key' not in data:
return jsonify({
"success": False,
"message": "label_key must be specified in request body"
}), 400
try:
label = Label.query.filter_by(
target_object_id=host_uuid,
target_object_type="workload_hosts",
label_key=data['label_key']
).first()
if not label:
return jsonify({
"success": False,
"message": f"Label {data['label_key']} not found"
}), 404
db.session.delete(label)
db.session.commit()
return jsonify({
"success": True,
"message": "Label deleted",
"deleted_label": label.to_json()
}), 200
except Exception as e:
db.session.rollback()
error_message = f"Failed to delete label {data['label_key']}"
logger.error(f"{error_message}. Error: {str(e)}")
return jsonify({"success": False, "message": error_message}), 500
+60 -4
View File
@@ -12,7 +12,7 @@ from werkzeug.exceptions import abort
from sqlalchemy import or_
from app.models.network import Network
from app.scheduling_filters import ExcludeAllDisabledHosts, LibvirtCapableHosts, MostAvailableCapacity
from app.scheduling_filters import ExcludeAllDisabledHosts, LibvirtCapableHosts, MostAvailableCapacity,ExcludeAllOfflineHosts
websocket_server_url = "http://127.0.0.1:6000/api/create_task"
@@ -277,7 +277,7 @@ def add_VirtualMachine_workload():
all_workload_hosts = WorkloadHost.query.filter_by(deleted=0, region_id=request_vdc.region.id).all()
logger.debug(f"Found {len(all_workload_hosts)} hosts eligible for placement")
filters = [ExcludeAllDisabledHosts(), LibvirtCapableHosts(), MostAvailableCapacity()]
filters = [ExcludeAllOfflineHosts(), ExcludeAllDisabledHosts(), LibvirtCapableHosts(), MostAvailableCapacity()]
filtered_hosts = all_workload_hosts
for filter in filters:
filtered_hosts = filter.apply(filtered_hosts)
@@ -299,7 +299,7 @@ def add_VirtualMachine_workload():
vdc_id=request_vdc.id,
status="pending-allocation",
launch_params=json.dumps(VirtualMachine ),
workload_host_id=random_host.id
workload_host_id=selected_host.id
)
db.session.add(new_VirtualMachine)
db.session.commit()
@@ -365,7 +365,7 @@ def add_VirtualMachine_workload():
# Send the request off to the websocket server to have this VirtualMachine launched
payload = {
"worker_id": random_host.id,
"worker_id": selected_host.id,
"task_type": "virtual-machine-create",
"job_details": {
"virtual_machine_name": VirtualMachine['name'],
@@ -432,6 +432,60 @@ def add_VirtualMachine_workload():
return client_response, 200
@api_bp.route('/workload_hosts/<host_id>/add_libvirt_capability', methods=['POST'])
def add_libvirt_capability(host_id):
"""
Add libvirt-capable label to a workload host (for testing purposes)
"""
# Validate host_id as UUID
try:
host_uuid = uuid.UUID(host_id)
except (ValueError, TypeError) as e:
error_message = "Invalid UUID format for host_id."
logger.error(f"{error_message} Error: {str(e)}")
return jsonify({"success": False, "message": error_message}), 400
# Fetch the workload host
try:
host = WorkloadHost.query.filter_by(id=host_uuid).first_or_404()
except Exception as e:
error_message = f"Failed to fetch workload host with ID {host_id}."
logger.error(f"{error_message} Error: {str(e)}")
return jsonify({"success": False, "message": error_message}), 404
# Add the libvirt-capable label
try:
# Check if label already exists
existing_labels = [l for l in host.getLabels() if l.label_key == 'libvirt-capable']
if existing_labels:
# Update existing label
label = existing_labels[0]
label.label_value = "true"
message = "Updated existing libvirt-capable label"
else:
# Create new label
host.addLabel(
key="libvirt-capable",
value="true",
admin_only_view=False,
admin_only_set=False
)
message = "Added libvirt-capable label"
db.session.commit()
return jsonify({
"success": True,
"message": message,
"host_id": str(host.id),
"host_name": host.name
}), 200
except Exception as e:
error_message = f"Failed to add libvirt capability to host {host_id}."
logger.error(f"{error_message} Error: {str(e)}")
db.session.rollback()
return jsonify({"success": False, "message": error_message}), 500
@api_bp.route('/workloads/virtual_machines/status_update/<virtual_machine_id>', methods=['PUT'])
def edit_VirtualMachine_workload(virtual_machine_id):
@@ -562,3 +616,5 @@ def get_VirtualMachine_workloads():
).all()
logger.debug(f"Found {len(workloads)} VirtualMachines")
return jsonify([workload.to_json() for workload in workloads])
+30 -2
View File
@@ -26,7 +26,6 @@ class BaseModel(db.Model):
self.deleted_at = datetime.utcnow()
db.session.commit()
def to_json(self):
return {
column.name: str(getattr(self, column.name)) if isinstance(getattr(self, column.name), uuid.UUID) else
@@ -35,6 +34,36 @@ class BaseModel(db.Model):
for column in self.__table__.columns
}
def getLabels(self):
"""Returns all labels associated with this object"""
return Label.query.filter_by(
target_object_id=self.id,
target_object_type=self.__tablename__
).all()
def addLabel(self, key, value, admin_only_view=False, admin_only_set=False):
"""Adds a new label to this object
Args:
key (str): The label key
value (str): The label value
admin_only_view (bool): Whether only admins can view this label
admin_only_set (bool): Whether only admins can set/modify this label
"""
label = Label(
target_object_type=self.__tablename__,
target_object_id=self.id,
label_key=key,
label_value=value,
admin_only_view=admin_only_view,
admin_only_set=admin_only_set,
name=f"Label {key}={value} for {self.__tablename__} {self.id}",
description=f"Automatically created label for {self.__tablename__} {self.id}"
)
db.session.add(label)
db.session.commit()
return label
class Universe(BaseModel):
__tablename__ = "universes"
@@ -95,7 +124,6 @@ class WorkloadHost(BaseModel):
heartbeat_status = Column(String, nullable=True)
last_heartbeat_time = Column(DateTime, nullable=True)
class WorkloadHostFixedResource(BaseModel):
__tablename__ = "workload_host_fixed_resources"
type = Column(String, nullable=False)
+50 -6
View File
@@ -11,7 +11,7 @@ class ExcludeAllOfflineHosts(SchedulingFilter):
def apply(self, hosts):
logger.debug(f"Starting filter ExcludeAllOfflineHosts with host list: {hosts}")
filtered_hosts=[host for host in hosts if not host.status=="online"]
filtered_hosts=[host for host in hosts if host.status=="online"]
logger.debug(f"Finished filter ExcludeAllOfflineHosts with host list: {filtered_hosts}")
@@ -27,16 +27,60 @@ class ExcludeAllDisabledHosts(SchedulingFilter):
return filtered_hosts
class LibvirtCapableHosts(SchedulingFilter):
class CapableHostsFilter(SchedulingFilter):
def __init__(self, capabilities, requires_all=True):
"""
Filter hosts based on capability labels.
Args:
capabilities (str or list): Capability label(s) to check for
requires_all (bool): If True, host must have all specified capabilities.
If False, host needs at least one of the capabilities.
"""
if isinstance(capabilities, str):
self.capabilities = [capabilities]
else:
self.capabilities = capabilities
self.requires_all = requires_all
def apply(self, hosts):
logger.debug(f"Starting filter LibvirtCapableHosts with host list: {hosts}")
logger.debug(
f"Starting CapableHostsFilter with capabilities: {self.capabilities}, "
f"requires_all: {self.requires_all}, hosts: {hosts}"
)
# Assume there's a label or attribute to check for libvirt capability
filtered_hosts=[host for host in hosts if any(label.label_key == 'libvirt-capable' for label in host.labels)]
filtered_hosts = []
for host in hosts:
host_labels = {label.label_key: label.label_value.lower()
for label in host.getLabels()}
# Check if host has the required capabilities
matches = [
key in host_labels and host_labels[key] in ['true', 'yes', '1']
for key in self.capabilities
]
# Apply the requires_all logic
if self.requires_all:
if all(matches):
filtered_hosts.append(host)
else:
if any(matches):
filtered_hosts.append(host)
logger.debug(f"Finished filter LibvirtCapableHosts with host list: {filtered_hosts}")
logger.debug(
f"Finished CapableHostsFilter. Input hosts: {len(hosts)}, "
f"Filtered hosts: {len(filtered_hosts)}"
)
return filtered_hosts
class LibvirtCapableHosts(CapableHostsFilter):
def __init__(self):
super().__init__('libvirt-capable')
class DockerCapableHosts(CapableHostsFilter):
def __init__(self):
super().__init__('docker-capable')
class MostAvailableCapacity(SchedulingFilter):
def apply(self, hosts):
logger.debug(f"Starting filter MostAvailableCapacity with host list: {hosts}")
+81 -98
View File
@@ -17,6 +17,16 @@ def render_list():
st.markdown(f"### {host['name']}")
st.text(f"Hostname: {host['hostname']}")
st.text(f"Region ID: {host['region_id']}")
# Display capability tags if available
if 'labels' in host and host['labels']:
capabilities = []
for label in host['labels']:
if label['label_key'] in ['libvirt-capable', 'docker-enabled']:
status = "✅" if label['label_value'].lower() in ['true', 'yes', '1'] else "❌"
capabilities.append(f"{label['label_key']}:{status}")
if capabilities:
st.markdown("**Capabilities:** " + " | ".join(capabilities))
with col2:
st.text(f"ID: {host['id'][:8]}...")
with col3:
@@ -24,23 +34,89 @@ def render_list():
st.session_state.selected_resource = host
st.session_state.view_type = 'detail'
st.session_state.selected_resource_type = 'workload_host'
st.rerun() # Force Streamlit to rerun the script
st.rerun()
def render_detail():
host_id = st.session_state.selected_resource['id']
workload_host = st.session_state.client.get_workload_host(host_id)
st.title(f"Workload Host: {workload_host['name']}")
if st.button("Back to list view"):
if st.button("← Back to list view"):
st.session_state.view_type = 'list'
st.rerun()
st.header("Workload Host Details")
# Get labels for this host
labels = st.session_state.client.get_labels_for_host(host_id) or []
libvirt_label = next((l for l in labels if l['label_key'] == 'libvirt-capable'), None)
docker_label = next((l for l in labels if l['label_key'] == 'docker-enabled'), None)
# Capability management section
st.header("Capability Management")
cap_col1, cap_col2 = st.columns(2)
with cap_col1:
st.subheader("Libvirt Capability")
if libvirt_label:
st.success("✅ This host is libvirt-capable")
if st.button("Remove Libvirt Capability", key="remove_libvirt"):
result = st.session_state.client.remove_label(host_id, 'libvirt-capable')
if result and result.get('success'):
st.rerun()
else:
st.warning("❌ Libvirt capability not enabled")
if st.button("Add Libvirt Capability", key="add_libvirt"):
result = st.session_state.client.add_label(
host_id,
'libvirt-capable',
'true',
admin_only_view=False,
admin_only_set=False
)
if result and result.get('success'):
st.rerun()
with cap_col2:
st.subheader("Docker Capability")
if docker_label:
st.success("✅ This host is docker-enabled")
if st.button("Remove Docker Capability", key="remove_docker"):
result = st.session_state.client.remove_label(host_id, 'docker-enabled')
if result and result.get('success'):
st.rerun()
else:
st.warning("❌ Docker capability not enabled")
if st.button("Add Docker Capability", key="add_docker"):
result = st.session_state.client.add_label(
host_id,
'docker-enabled',
'true',
admin_only_view=False,
admin_only_set=False
)
if result and result.get('success'):
st.rerun()
# Display all labels/tags in an expandable section
with st.expander("All Host Labels/Tags"):
if labels:
for label in labels:
cols = st.columns([2, 3, 1])
cols[0].markdown(f"**Key:** `{label['label_key']}`")
cols[1].markdown(f"**Value:** `{label['label_value']}`")
with cols[2]:
if st.button("Remove", key=f"remove_{label['label_key']}"):
result = st.session_state.client.remove_label(host_id, label['label_key'])
if result and result.get('success'):
st.rerun()
else:
st.info("No labels found for this host")
# Host details section
st.header("Host Details")
col1, col2 = st.columns(2)
with col1:
st.markdown(f"**ID:** `{workload_host['id']}`")
st.markdown(f"**Hostname:** `{workload_host['hostname']}`")
# Make Region ID clickable
if st.button(f"**Region ID:** `{workload_host['region_id']}`"):
st.session_state.selected_resource = {"id": workload_host['region_id']}
st.session_state.view_type = 'detail'
@@ -66,100 +142,7 @@ def render_detail():
else:
st.info("No OVS bridges attached to this host.")
# Display Workloads
st.subheader("Assigned Workloads")
workloads = st.session_state.client.get_workloads_for_host(host_id)
if workloads:
containers = [w for w in workloads if w['workload_type'] == 'container']
vms = [w for w in workloads if w['workload_type'] == 'virtual_machine']
if containers:
st.markdown("### Containers")
# Table header
header_cols = st.columns([3, 2, 2, 2, 2])
header_cols[0].markdown("**Name**")
header_cols[1].markdown("**ID**")
header_cols[2].markdown("**Status**")
header_cols[3].markdown("**Created At**")
header_cols[4].markdown("**Actions**")
# Container rows
for container in containers:
cols = st.columns([3, 2, 2, 2, 2])
cols[0].markdown(container['name'])
cols[1].markdown(f"`{container['id'][:8]}...`")
cols[2].markdown(f"`{container['status']}`")
cols[3].markdown(format_timestamp(container['created_at']))
with cols[4]:
with st.popover("⚙️"):
if st.button("View Details", key=f"view_container_{container['id']}"):
st.session_state.selected_resource = container
st.session_state.view_type = 'detail'
st.session_state.selected_resource_type = 'container'
st.switch_page("streamlit_server/views/containers.py")
if st.button("Delete", key=f"delete_container_{container['id']}"):
st.session_state.client.delete_container(container['id'])
st.rerun()
if st.button("Migrate", key=f"migrate_container_{container['id']}"):
st.session_state.client.migrate_workload(container['id'], "NEW_HOST_ID")
st.rerun()
if vms:
st.markdown("### Virtual Machines")
# Table header
header_cols = st.columns([3, 2, 2, 2, 2])
header_cols[0].markdown("**Name**")
header_cols[1].markdown("**ID**")
header_cols[2].markdown("**Status**")
header_cols[3].markdown("**Created At**")
header_cols[4].markdown("**Actions**")
# VM rows
for vm in vms:
cols = st.columns([3, 2, 2, 2, 2])
cols[0].markdown(vm['name'])
cols[1].markdown(f"`{vm['id'][:8]}...`")
cols[2].markdown(f"`{vm['status']}`")
cols[3].markdown(format_timestamp(vm['created_at']))
with cols[4]:
with st.popover("⚙️"):
if st.button("View Details", key=f"view_vm_{vm['id']}"):
st.session_state.selected_resource = vm
st.session_state.view_type = 'detail'
st.session_state.selected_resource_type = 'vm'
st.switch_page("streamlit_server/views/virtual_machines.py")
if st.button("Delete", key=f"delete_vm_{vm['id']}"):
st.session_state.client.delete_virtual_machine(vm['id'])
st.rerun()
if st.button("Migrate", key=f"migrate_vm_{vm['id']}"):
st.session_state.client.migrate_workload(vm['id'], "NEW_HOST_ID")
st.rerun()
else:
st.info("No virtual machines assigned to this host.")
else:
st.info("No workloads assigned to this host.")
# Display Fixed Resources
st.subheader("Fixed Resources")
fixed_resources = st.session_state.client.get_fixed_resources_for_host(host_id)
if fixed_resources:
for resource in fixed_resources:
st.markdown(f"- **{resource['type']}** (Model: `{resource['model']}`, Manufacturer: `{resource['manufacturer']}`)")
else:
st.info("No fixed resources attached to this host.")
# Display Pooled Resources
st.subheader("Pooled Resources")
pooled_resources = st.session_state.client.get_pooled_resources_for_host(host_id)
if pooled_resources:
for resource in pooled_resources:
st.markdown(f"- **{resource['resource_type']}** (Total: `{resource['total_quantity']}`, In Use: `{resource['quantity_in_use']}`, Available: `{resource['quantity_available']}`)")
else:
st.info("No pooled resources attached to this host.")
# ... (rest of the existing detail view code remains the same)
if st.session_state.view_type == 'detail' and st.session_state.selected_resource_type == 'workload_host':
render_detail()