Initial Django project setup for Postgres cluster management - Created Django project structure with postgres_handholder main app - Added clusters, backups, and monitoring apps - Implemented comprehensive models for PostgresCluster, PostgresInstance, ClusterUser, ClusterDatabase - Added forms and views for cluster management - Created Bootstrap 5 templates with modern UI - Added Docker and Docker Compose configuration - Included Celery for background tasks - Added comprehensive requirements.txt with all dependencies - Updated README with installation and usage instructions - Added environment configuration example - Set up proper URL routing and app structure
This commit is contained in:
1
clusters/__init__.py
Normal file
1
clusters/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# Clusters app for managing Postgres clusters
|
7
clusters/apps.py
Normal file
7
clusters/apps.py
Normal file
@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ClustersConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'clusters'
|
||||
verbose_name = 'Postgres Clusters'
|
152
clusters/forms.py
Normal file
152
clusters/forms.py
Normal file
@ -0,0 +1,152 @@
|
||||
from django import forms
|
||||
from django.contrib.auth.models import User
|
||||
from .models import PostgresCluster, ClusterUser, ClusterDatabase
|
||||
|
||||
|
||||
class PostgresClusterForm(forms.ModelForm):
|
||||
"""Form for creating and editing Postgres clusters."""
|
||||
|
||||
class Meta:
|
||||
model = PostgresCluster
|
||||
fields = [
|
||||
'name', 'description', 'cluster_type', 'deployment_type',
|
||||
'postgres_version', 'port', 'data_directory',
|
||||
'cpu_limit', 'memory_limit', 'storage_size',
|
||||
'host', 'external_port', 'admin_user', 'admin_password',
|
||||
'extensions', 'libraries', 'tls_enabled', 'tls_cert_path',
|
||||
'tls_key_path', 'tls_ca_path', 'pgpool_enabled', 'pgpool_instances'
|
||||
]
|
||||
widgets = {
|
||||
'name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
||||
'cluster_type': forms.Select(attrs={'class': 'form-select'}),
|
||||
'deployment_type': forms.Select(attrs={'class': 'form-select'}),
|
||||
'postgres_version': forms.Select(attrs={'class': 'form-select'}, choices=[
|
||||
('13', 'PostgreSQL 13'),
|
||||
('14', 'PostgreSQL 14'),
|
||||
('15', 'PostgreSQL 15'),
|
||||
('16', 'PostgreSQL 16'),
|
||||
('17', 'PostgreSQL 17'),
|
||||
]),
|
||||
'port': forms.NumberInput(attrs={'class': 'form-control'}),
|
||||
'data_directory': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'cpu_limit': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'memory_limit': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'storage_size': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'host': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'external_port': forms.NumberInput(attrs={'class': 'form-control'}),
|
||||
'admin_user': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'admin_password': forms.PasswordInput(attrs={'class': 'form-control'}),
|
||||
'tls_cert_path': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'tls_key_path': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'tls_ca_path': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'pgpool_instances': forms.NumberInput(attrs={'class': 'form-control'}),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Set default values for extensions and libraries
|
||||
if not self.instance.pk:
|
||||
self.fields['extensions'].initial = [
|
||||
'pg_stat_statements',
|
||||
'pg_auth_mon',
|
||||
]
|
||||
self.fields['libraries'].initial = [
|
||||
'bg_mon',
|
||||
'pg_stat_statements',
|
||||
'pgextwlist',
|
||||
'pg_auth_mon',
|
||||
]
|
||||
|
||||
def clean_name(self):
|
||||
name = self.cleaned_data['name']
|
||||
if not name.replace('-', '').replace('_', '').isalnum():
|
||||
raise forms.ValidationError(
|
||||
'Cluster name can only contain letters, numbers, hyphens, and underscores.'
|
||||
)
|
||||
return name
|
||||
|
||||
def clean_admin_password(self):
|
||||
password = self.cleaned_data['admin_password']
|
||||
if len(password) < 8:
|
||||
raise forms.ValidationError(
|
||||
'Admin password must be at least 8 characters long.'
|
||||
)
|
||||
return password
|
||||
|
||||
|
||||
class ClusterUserForm(forms.ModelForm):
|
||||
"""Form for creating and editing cluster users."""
|
||||
|
||||
confirm_password = forms.CharField(
|
||||
widget=forms.PasswordInput(attrs={'class': 'form-control'}),
|
||||
help_text='Confirm the password'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ClusterUser
|
||||
fields = [
|
||||
'username', 'password', 'is_superuser', 'can_create_db',
|
||||
'can_login', 'permissions'
|
||||
]
|
||||
widgets = {
|
||||
'username': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'password': forms.PasswordInput(attrs={'class': 'form-control'}),
|
||||
'is_superuser': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||
'can_create_db': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||
'can_login': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||
'permissions': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
||||
}
|
||||
|
||||
def clean_username(self):
|
||||
username = self.cleaned_data['username']
|
||||
if not username.replace('_', '').isalnum():
|
||||
raise forms.ValidationError(
|
||||
'Username can only contain letters, numbers, and underscores.'
|
||||
)
|
||||
return username
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
password = cleaned_data.get('password')
|
||||
confirm_password = cleaned_data.get('confirm_password')
|
||||
|
||||
if password and confirm_password and password != confirm_password:
|
||||
raise forms.ValidationError('Passwords do not match.')
|
||||
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class ClusterDatabaseForm(forms.ModelForm):
|
||||
"""Form for creating and editing cluster databases."""
|
||||
|
||||
class Meta:
|
||||
model = ClusterDatabase
|
||||
fields = ['name', 'owner', 'encoding', 'collation', 'ctype']
|
||||
widgets = {
|
||||
'name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'owner': forms.Select(attrs={'class': 'form-select'}),
|
||||
'encoding': forms.Select(attrs={'class': 'form-select'}, choices=[
|
||||
('UTF8', 'UTF8'),
|
||||
('LATIN1', 'LATIN1'),
|
||||
('SQL_ASCII', 'SQL_ASCII'),
|
||||
]),
|
||||
'collation': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'ctype': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
cluster = kwargs.pop('cluster', None)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if cluster:
|
||||
self.fields['owner'].queryset = cluster.users.all()
|
||||
|
||||
def clean_name(self):
|
||||
name = self.cleaned_data['name']
|
||||
if not name.replace('_', '').isalnum():
|
||||
raise forms.ValidationError(
|
||||
'Database name can only contain letters, numbers, and underscores.'
|
||||
)
|
||||
return name
|
200
clusters/models.py
Normal file
200
clusters/models.py
Normal file
@ -0,0 +1,200 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
import json
|
||||
|
||||
|
||||
class PostgresCluster(models.Model):
|
||||
"""Model for managing Postgres cluster configurations."""
|
||||
|
||||
CLUSTER_TYPES = [
|
||||
('single', 'Single Instance'),
|
||||
('replica', 'Primary with Replicas'),
|
||||
('standby', 'Standby Cluster'),
|
||||
]
|
||||
|
||||
DEPLOYMENT_TYPES = [
|
||||
('docker', 'Docker'),
|
||||
('kubernetes', 'Kubernetes'),
|
||||
('local', 'Local Linux'),
|
||||
]
|
||||
|
||||
name = models.CharField(max_length=100, unique=True)
|
||||
description = models.TextField(blank=True)
|
||||
cluster_type = models.CharField(max_length=20, choices=CLUSTER_TYPES, default='single')
|
||||
deployment_type = models.CharField(max_length=20, choices=DEPLOYMENT_TYPES, default='docker')
|
||||
|
||||
# PostgreSQL Configuration
|
||||
postgres_version = models.CharField(max_length=10, default='15')
|
||||
port = models.IntegerField(default=5432, validators=[MinValueValidator(1024), MaxValueValidator(65535)])
|
||||
data_directory = models.CharField(max_length=255, default='/var/lib/postgresql/data')
|
||||
|
||||
# Resource Configuration
|
||||
cpu_limit = models.CharField(max_length=20, default='2')
|
||||
memory_limit = models.CharField(max_length=20, default='4Gi')
|
||||
storage_size = models.CharField(max_length=20, default='10Gi')
|
||||
|
||||
# Network Configuration
|
||||
host = models.CharField(max_length=255, default='localhost')
|
||||
external_port = models.IntegerField(default=5432, validators=[MinValueValidator(1024), MaxValueValidator(65535)])
|
||||
|
||||
# Authentication
|
||||
admin_user = models.CharField(max_length=50, default='postgres')
|
||||
admin_password = models.CharField(max_length=255)
|
||||
|
||||
# Extensions and Libraries
|
||||
extensions = models.JSONField(default=list, blank=True)
|
||||
libraries = models.JSONField(default=list, blank=True)
|
||||
|
||||
# TLS Configuration
|
||||
tls_enabled = models.BooleanField(default=False)
|
||||
tls_cert_path = models.CharField(max_length=255, blank=True)
|
||||
tls_key_path = models.CharField(max_length=255, blank=True)
|
||||
tls_ca_path = models.CharField(max_length=255, blank=True)
|
||||
|
||||
# Connection Pooling
|
||||
pgpool_enabled = models.BooleanField(default=False)
|
||||
pgpool_instances = models.IntegerField(default=1, validators=[MinValueValidator(1), MaxValueValidator(10)])
|
||||
|
||||
# Status
|
||||
status = models.CharField(max_length=20, default='stopped', choices=[
|
||||
('running', 'Running'),
|
||||
('stopped', 'Stopped'),
|
||||
('starting', 'Starting'),
|
||||
('stopping', 'Stopping'),
|
||||
('error', 'Error'),
|
||||
('updating', 'Updating'),
|
||||
])
|
||||
|
||||
# Metadata
|
||||
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-created_at']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_connection_string(self):
|
||||
"""Get the connection string for this cluster."""
|
||||
return f"postgresql://{self.admin_user}:{self.admin_password}@{self.host}:{self.external_port}/postgres"
|
||||
|
||||
def get_extensions_list(self):
|
||||
"""Get the list of enabled extensions."""
|
||||
return self.extensions if isinstance(self.extensions, list) else []
|
||||
|
||||
def get_libraries_list(self):
|
||||
"""Get the list of enabled libraries."""
|
||||
return self.libraries if isinstance(self.libraries, list) else []
|
||||
|
||||
|
||||
class PostgresInstance(models.Model):
|
||||
"""Model for managing individual Postgres instances within a cluster."""
|
||||
|
||||
INSTANCE_TYPES = [
|
||||
('primary', 'Primary'),
|
||||
('replica', 'Replica'),
|
||||
('standby', 'Standby'),
|
||||
]
|
||||
|
||||
cluster = models.ForeignKey(PostgresCluster, on_delete=models.CASCADE, related_name='instances')
|
||||
name = models.CharField(max_length=100)
|
||||
instance_type = models.CharField(max_length=20, choices=INSTANCE_TYPES, default='primary')
|
||||
|
||||
# Instance Configuration
|
||||
host = models.CharField(max_length=255)
|
||||
port = models.IntegerField(validators=[MinValueValidator(1024), MaxValueValidator(65535)])
|
||||
data_directory = models.CharField(max_length=255)
|
||||
|
||||
# Status
|
||||
status = models.CharField(max_length=20, default='stopped', choices=[
|
||||
('running', 'Running'),
|
||||
('stopped', 'Stopped'),
|
||||
('starting', 'Starting'),
|
||||
('stopping', 'Stopping'),
|
||||
('error', 'Error'),
|
||||
('syncing', 'Syncing'),
|
||||
])
|
||||
|
||||
# Replication Configuration (for replicas)
|
||||
replication_slot = models.CharField(max_length=100, blank=True)
|
||||
lag_seconds = models.IntegerField(default=0)
|
||||
|
||||
# Resource Usage
|
||||
cpu_usage = models.FloatField(default=0.0)
|
||||
memory_usage = models.FloatField(default=0.0)
|
||||
disk_usage = models.FloatField(default=0.0)
|
||||
|
||||
# Metadata
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ['cluster', 'name']
|
||||
ordering = ['instance_type', 'name']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.cluster.name} - {self.name}"
|
||||
|
||||
|
||||
class ClusterUser(models.Model):
|
||||
"""Model for managing database users within clusters."""
|
||||
|
||||
cluster = models.ForeignKey(PostgresCluster, on_delete=models.CASCADE, related_name='users')
|
||||
username = models.CharField(max_length=50)
|
||||
password = models.CharField(max_length=255)
|
||||
is_superuser = models.BooleanField(default=False)
|
||||
can_create_db = models.BooleanField(default=False)
|
||||
can_login = models.BooleanField(default=True)
|
||||
|
||||
# Permissions
|
||||
permissions = models.JSONField(default=dict, blank=True)
|
||||
|
||||
# Metadata
|
||||
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ['cluster', 'username']
|
||||
ordering = ['username']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.cluster.name} - {self.username}"
|
||||
|
||||
|
||||
class ClusterDatabase(models.Model):
|
||||
"""Model for managing databases within clusters."""
|
||||
|
||||
cluster = models.ForeignKey(PostgresCluster, on_delete=models.CASCADE, related_name='databases')
|
||||
name = models.CharField(max_length=50)
|
||||
owner = models.ForeignKey(ClusterUser, on_delete=models.CASCADE)
|
||||
|
||||
# Configuration
|
||||
encoding = models.CharField(max_length=20, default='UTF8')
|
||||
collation = models.CharField(max_length=50, default='en_US.utf8')
|
||||
ctype = models.CharField(max_length=50, default='en_US.utf8')
|
||||
|
||||
# Size tracking
|
||||
size_bytes = models.BigIntegerField(default=0)
|
||||
|
||||
# Metadata
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ['cluster', 'name']
|
||||
ordering = ['name']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.cluster.name} - {self.name}"
|
||||
|
||||
def get_size_mb(self):
|
||||
"""Get database size in MB."""
|
||||
return round(self.size_bytes / (1024 * 1024), 2)
|
||||
|
||||
def get_size_gb(self):
|
||||
"""Get database size in GB."""
|
||||
return round(self.size_bytes / (1024 * 1024 * 1024), 2)
|
19
clusters/urls.py
Normal file
19
clusters/urls.py
Normal file
@ -0,0 +1,19 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = 'clusters'
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.cluster_list, name='cluster_list'),
|
||||
path('create/', views.cluster_create, name='cluster_create'),
|
||||
path('<int:cluster_id>/', views.cluster_detail, name='cluster_detail'),
|
||||
path('<int:cluster_id>/edit/', views.cluster_edit, name='cluster_edit'),
|
||||
path('<int:cluster_id>/delete/', views.cluster_delete, name='cluster_delete'),
|
||||
path('<int:cluster_id>/start/', views.cluster_start, name='cluster_start'),
|
||||
path('<int:cluster_id>/stop/', views.cluster_stop, name='cluster_stop'),
|
||||
path('<int:cluster_id>/restart/', views.cluster_restart, name='cluster_restart'),
|
||||
path('<int:cluster_id>/update/', views.cluster_update, name='cluster_update'),
|
||||
path('<int:cluster_id>/users/', views.cluster_users, name='cluster_users'),
|
||||
path('<int:cluster_id>/databases/', views.cluster_databases, name='cluster_databases'),
|
||||
path('<int:cluster_id>/instances/', views.cluster_instances, name='cluster_instances'),
|
||||
]
|
261
clusters/views.py
Normal file
261
clusters/views.py
Normal file
@ -0,0 +1,261 @@
|
||||
from django.shortcuts import render, get_object_or_404, redirect
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib import messages
|
||||
from django.http import JsonResponse
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.core.paginator import Paginator
|
||||
from .models import PostgresCluster, PostgresInstance, ClusterUser, ClusterDatabase
|
||||
from .forms import PostgresClusterForm, ClusterUserForm, ClusterDatabaseForm
|
||||
|
||||
|
||||
@login_required
|
||||
def cluster_list(request):
|
||||
"""Display list of all clusters."""
|
||||
clusters = PostgresCluster.objects.filter(created_by=request.user)
|
||||
|
||||
# Filtering
|
||||
status_filter = request.GET.get('status')
|
||||
if status_filter:
|
||||
clusters = clusters.filter(status=status_filter)
|
||||
|
||||
# Pagination
|
||||
paginator = Paginator(clusters, 10)
|
||||
page_number = request.GET.get('page')
|
||||
page_obj = paginator.get_page(page_number)
|
||||
|
||||
context = {
|
||||
'page_obj': page_obj,
|
||||
'status_choices': PostgresCluster._meta.get_field('status').choices,
|
||||
}
|
||||
return render(request, 'clusters/cluster_list.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def cluster_create(request):
|
||||
"""Create a new Postgres cluster."""
|
||||
if request.method == 'POST':
|
||||
form = PostgresClusterForm(request.POST)
|
||||
if form.is_valid():
|
||||
cluster = form.save(commit=False)
|
||||
cluster.created_by = request.user
|
||||
cluster.save()
|
||||
messages.success(request, f'Cluster "{cluster.name}" created successfully.')
|
||||
return redirect('clusters:cluster_detail', cluster_id=cluster.id)
|
||||
else:
|
||||
form = PostgresClusterForm()
|
||||
|
||||
context = {
|
||||
'form': form,
|
||||
'title': 'Create New Cluster',
|
||||
}
|
||||
return render(request, 'clusters/cluster_form.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def cluster_detail(request, cluster_id):
|
||||
"""Display detailed information about a cluster."""
|
||||
cluster = get_object_or_404(PostgresCluster, id=cluster_id, created_by=request.user)
|
||||
|
||||
context = {
|
||||
'cluster': cluster,
|
||||
'instances': cluster.instances.all(),
|
||||
'users': cluster.users.all(),
|
||||
'databases': cluster.databases.all(),
|
||||
}
|
||||
return render(request, 'clusters/cluster_detail.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def cluster_edit(request, cluster_id):
|
||||
"""Edit an existing cluster."""
|
||||
cluster = get_object_or_404(PostgresCluster, id=cluster_id, created_by=request.user)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = PostgresClusterForm(request.POST, instance=cluster)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, f'Cluster "{cluster.name}" updated successfully.')
|
||||
return redirect('clusters:cluster_detail', cluster_id=cluster.id)
|
||||
else:
|
||||
form = PostgresClusterForm(instance=cluster)
|
||||
|
||||
context = {
|
||||
'form': form,
|
||||
'cluster': cluster,
|
||||
'title': f'Edit Cluster: {cluster.name}',
|
||||
}
|
||||
return render(request, 'clusters/cluster_form.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def cluster_delete(request, cluster_id):
|
||||
"""Delete a cluster."""
|
||||
cluster = get_object_or_404(PostgresCluster, id=cluster_id, created_by=request.user)
|
||||
|
||||
if request.method == 'POST':
|
||||
cluster_name = cluster.name
|
||||
cluster.delete()
|
||||
messages.success(request, f'Cluster "{cluster_name}" deleted successfully.')
|
||||
return redirect('clusters:cluster_list')
|
||||
|
||||
context = {
|
||||
'cluster': cluster,
|
||||
}
|
||||
return render(request, 'clusters/cluster_confirm_delete.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["POST"])
|
||||
def cluster_start(request, cluster_id):
|
||||
"""Start a cluster."""
|
||||
cluster = get_object_or_404(PostgresCluster, id=cluster_id, created_by=request.user)
|
||||
|
||||
try:
|
||||
# TODO: Implement actual cluster start logic
|
||||
cluster.status = 'starting'
|
||||
cluster.save()
|
||||
messages.success(request, f'Starting cluster "{cluster.name}"...')
|
||||
except Exception as e:
|
||||
messages.error(request, f'Failed to start cluster: {str(e)}')
|
||||
|
||||
return redirect('clusters:cluster_detail', cluster_id=cluster.id)
|
||||
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["POST"])
|
||||
def cluster_stop(request, cluster_id):
|
||||
"""Stop a cluster."""
|
||||
cluster = get_object_or_404(PostgresCluster, id=cluster_id, created_by=request.user)
|
||||
|
||||
try:
|
||||
# TODO: Implement actual cluster stop logic
|
||||
cluster.status = 'stopping'
|
||||
cluster.save()
|
||||
messages.success(request, f'Stopping cluster "{cluster.name}"...')
|
||||
except Exception as e:
|
||||
messages.error(request, f'Failed to stop cluster: {str(e)}')
|
||||
|
||||
return redirect('clusters:cluster_detail', cluster_id=cluster.id)
|
||||
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["POST"])
|
||||
def cluster_restart(request, cluster_id):
|
||||
"""Restart a cluster."""
|
||||
cluster = get_object_or_404(PostgresCluster, id=cluster_id, created_by=request.user)
|
||||
|
||||
try:
|
||||
# TODO: Implement actual cluster restart logic
|
||||
cluster.status = 'starting'
|
||||
cluster.save()
|
||||
messages.success(request, f'Restarting cluster "{cluster.name}"...')
|
||||
except Exception as e:
|
||||
messages.error(request, f'Failed to restart cluster: {str(e)}')
|
||||
|
||||
return redirect('clusters:cluster_detail', cluster_id=cluster.id)
|
||||
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["POST"])
|
||||
def cluster_update(request, cluster_id):
|
||||
"""Update a cluster (rolling update)."""
|
||||
cluster = get_object_or_404(PostgresCluster, id=cluster_id, created_by=request.user)
|
||||
|
||||
try:
|
||||
# TODO: Implement actual rolling update logic
|
||||
cluster.status = 'updating'
|
||||
cluster.save()
|
||||
messages.success(request, f'Updating cluster "{cluster.name}"...')
|
||||
except Exception as e:
|
||||
messages.error(request, f'Failed to update cluster: {str(e)}')
|
||||
|
||||
return redirect('clusters:cluster_detail', cluster_id=cluster.id)
|
||||
|
||||
|
||||
@login_required
|
||||
def cluster_users(request, cluster_id):
|
||||
"""Manage users for a cluster."""
|
||||
cluster = get_object_or_404(PostgresCluster, id=cluster_id, created_by=request.user)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = ClusterUserForm(request.POST)
|
||||
if form.is_valid():
|
||||
user = form.save(commit=False)
|
||||
user.cluster = cluster
|
||||
user.created_by = request.user
|
||||
user.save()
|
||||
messages.success(request, f'User "{user.username}" created successfully.')
|
||||
return redirect('clusters:cluster_users', cluster_id=cluster.id)
|
||||
else:
|
||||
form = ClusterUserForm()
|
||||
|
||||
context = {
|
||||
'cluster': cluster,
|
||||
'form': form,
|
||||
'users': cluster.users.all(),
|
||||
}
|
||||
return render(request, 'clusters/cluster_users.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def cluster_databases(request, cluster_id):
|
||||
"""Manage databases for a cluster."""
|
||||
cluster = get_object_or_404(PostgresCluster, id=cluster_id, created_by=request.user)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = ClusterDatabaseForm(request.POST)
|
||||
if form.is_valid():
|
||||
database = form.save(commit=False)
|
||||
database.cluster = cluster
|
||||
database.save()
|
||||
messages.success(request, f'Database "{database.name}" created successfully.')
|
||||
return redirect('clusters:cluster_databases', cluster_id=cluster.id)
|
||||
else:
|
||||
form = ClusterDatabaseForm()
|
||||
|
||||
context = {
|
||||
'cluster': cluster,
|
||||
'form': form,
|
||||
'databases': cluster.databases.all(),
|
||||
}
|
||||
return render(request, 'clusters/cluster_databases.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def cluster_instances(request, cluster_id):
|
||||
"""View instances for a cluster."""
|
||||
cluster = get_object_or_404(PostgresCluster, id=cluster_id, created_by=request.user)
|
||||
|
||||
context = {
|
||||
'cluster': cluster,
|
||||
'instances': cluster.instances.all(),
|
||||
}
|
||||
return render(request, 'clusters/cluster_instances.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def cluster_status_api(request, cluster_id):
|
||||
"""API endpoint to get cluster status."""
|
||||
cluster = get_object_or_404(PostgresCluster, id=cluster_id, created_by=request.user)
|
||||
|
||||
data = {
|
||||
'id': cluster.id,
|
||||
'name': cluster.name,
|
||||
'status': cluster.status,
|
||||
'instances': [
|
||||
{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'type': instance.instance_type,
|
||||
'status': instance.status,
|
||||
'host': instance.host,
|
||||
'port': instance.port,
|
||||
'cpu_usage': instance.cpu_usage,
|
||||
'memory_usage': instance.memory_usage,
|
||||
'disk_usage': instance.disk_usage,
|
||||
}
|
||||
for instance in cluster.instances.all()
|
||||
]
|
||||
}
|
||||
|
||||
return JsonResponse(data)
|
Reference in New Issue
Block a user