mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2026-08-09 15:29:25 +02:00
@@ -7,7 +7,7 @@
|
||||
**Web Hosting Control Panel powered by OpenLiteSpeed**
|
||||
Fast • Secure • Scalable — Simplify hosting management with style.
|
||||
|
||||
**Version**: 2.5.5-dev • **Updated**: January 15, 2026
|
||||
**Version**: 2.5.5-dev • **Updated**: 28.03.2026
|
||||
|
||||
[](https://github.com/usmannasir/cyberpanel)
|
||||
[](https://cyberpanel.net/KnowledgeBase/)
|
||||
|
||||
@@ -410,6 +410,63 @@
|
||||
text-decoration: underline;
|
||||
color: #3730a3 !important;
|
||||
}
|
||||
|
||||
/* Ephemeral notifications (e.g. Docker update progress) */
|
||||
.notification-center-item-ephemeral {
|
||||
border-color: #c7d2fe;
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #eef2ff 100%);
|
||||
}
|
||||
.notification-center-progress-track {
|
||||
height: 8px;
|
||||
border-radius: 999px;
|
||||
background: #e5e7eb;
|
||||
overflow: hidden;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.notification-center-progress-bar {
|
||||
height: 100%;
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(90deg, #4f46e5, #6366f1);
|
||||
transition: width 0.35s ease;
|
||||
}
|
||||
.notification-center-progress-bar.indeterminate {
|
||||
width: 35% !important;
|
||||
animation: cp-nc-progress-indet 1.2s ease-in-out infinite;
|
||||
}
|
||||
.notification-center-progress-bar.success {
|
||||
width: 100% !important;
|
||||
background: linear-gradient(90deg, #16a34a, #22c55e);
|
||||
animation: none;
|
||||
}
|
||||
.notification-center-progress-bar.error {
|
||||
width: 100% !important;
|
||||
background: linear-gradient(90deg, #dc2626, #ef4444);
|
||||
animation: none;
|
||||
}
|
||||
@keyframes cp-nc-progress-indet {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(280%); }
|
||||
}
|
||||
.notification-center-ephemeral-dismiss {
|
||||
margin-top: 0.75rem;
|
||||
font-size: 0.8rem;
|
||||
color: #6b7280;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.notification-center-ephemeral-dismiss:hover { color: #111827; }
|
||||
.notification-center-btn.has-active-operation {
|
||||
animation: cp-nc-bell-pulse 1.5s ease-in-out infinite;
|
||||
border-color: #6366f1;
|
||||
color: #4f46e5;
|
||||
}
|
||||
@keyframes cp-nc-bell-pulse {
|
||||
0%, 100% { box-shadow: 0 0 0 0 rgba(79, 70, 229, 0.35); }
|
||||
50% { box-shadow: 0 0 0 6px rgba(79, 70, 229, 0); }
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
#sidebar {
|
||||
@@ -2329,9 +2386,6 @@
|
||||
<a href="{% url 'aiScannerHome' %}" class="menu-item">
|
||||
<span>AI Scanner</span>
|
||||
</a>
|
||||
<a href="#" class="menu-item" onclick="loadSecurityManagement(); return false;">
|
||||
<span>Security Management</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<a href="#" class="menu-item" onclick="toggleSubmenu('mail-settings-submenu', this); return false;">
|
||||
@@ -2507,7 +2561,7 @@
|
||||
<script src="{% static 'serverStatus/serverStatus.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
|
||||
<script src="{% static 'firewall/firewall.js' %}?v={{ CP_VERSION }}&fw={{ FIREWALL_STATIC_VERSION|default:CP_VERSION }}&cb=4" data-cfasync="false"></script>
|
||||
<script src="{% static 'emailPremium/emailPremium.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
|
||||
<script src="{% static 'manageServices/manageServices.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
|
||||
<script src="{% static 'manageServices/manageServices.js' %}?v={{ CP_VERSION }}&msModal=20260402d" data-cfasync="false"></script>
|
||||
<script src="{% static 'CLManager/CLManager.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
|
||||
|
||||
<!-- Scripts -->
|
||||
@@ -2753,6 +2807,93 @@
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
window.__cpEphemeralNotifications = window.__cpEphemeralNotifications || [];
|
||||
|
||||
function cpEscapeHtmlNC(str) {
|
||||
if (str == null || str === '') return '';
|
||||
var div = document.createElement('div');
|
||||
div.textContent = String(str);
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function cpEscapeAttrNC(s) {
|
||||
return String(s || '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
|
||||
function renderEphemeralNotificationsHTML() {
|
||||
var items = window.__cpEphemeralNotifications || [];
|
||||
return items.map(function(notif) {
|
||||
var nameEsc = cpEscapeHtmlNC(notif.containerName);
|
||||
var imgEsc = cpEscapeHtmlNC(notif.imageRef);
|
||||
var idAttr = cpEscapeAttrNC(notif.id);
|
||||
var textHtml = '';
|
||||
var barHtml = '';
|
||||
if (notif.state === 'running') {
|
||||
textHtml = '<p style="margin:0 0 0.5rem 0">Updating <strong>' + nameEsc + '</strong> to <code style="font-size:0.9em">' + imgEsc + '</code>. Pulling image and recreating the container — you can leave this tab open.</p>';
|
||||
barHtml = '<div class="notification-center-progress-track"><div class="notification-center-progress-bar indeterminate"></div></div>';
|
||||
} else if (notif.state === 'done_ok') {
|
||||
textHtml = '<p style="margin:0 0 0.5rem 0"><strong>' + nameEsc + '</strong> updated successfully.</p><p style="margin:0;color:#15803d;font-size:0.9em">' + cpEscapeHtmlNC(notif.resultMessage) + '</p>';
|
||||
barHtml = '<div class="notification-center-progress-track"><div class="notification-center-progress-bar success"></div></div>';
|
||||
} else {
|
||||
textHtml = '<p style="margin:0 0 0.5rem 0">Update failed for <strong>' + nameEsc + '</strong>.</p><p style="margin:0;color:#b91c1c;font-size:0.9em">' + cpEscapeHtmlNC(notif.resultMessage) + '</p>';
|
||||
barHtml = '<div class="notification-center-progress-track"><div class="notification-center-progress-bar error"></div></div>';
|
||||
}
|
||||
return '<div class="notification-center-item notification-center-item-ephemeral" data-ephemeral-id="' + idAttr + '">' +
|
||||
'<div class="notification-center-item-title"><i class="fab fa-docker"></i><span>Docker update</span></div>' +
|
||||
'<div class="notification-center-item-text" style="margin-bottom:0">' + textHtml + '</div>' +
|
||||
barHtml +
|
||||
'<button type="button" class="notification-center-ephemeral-dismiss" data-cp-ephemeral-dismiss="' + idAttr + '">Dismiss</button>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
window.cpDismissEphemeralNotification = function(id) {
|
||||
window.__cpEphemeralNotifications = (window.__cpEphemeralNotifications || []).filter(function(n) { return n.id !== id; });
|
||||
var stillRunning = (window.__cpEphemeralNotifications || []).some(function(n) { return n.state === 'running'; });
|
||||
var btn = document.getElementById('notification-center-btn');
|
||||
if (btn && !stillRunning) btn.classList.remove('has-active-operation');
|
||||
loadNotificationCenter();
|
||||
};
|
||||
|
||||
window.cpDockerUpdateNotifyStart = function(containerName, imageRef) {
|
||||
window.__cpEphemeralNotifications = window.__cpEphemeralNotifications || [];
|
||||
var id = 'docker-update-' + Date.now() + '-' + Math.random().toString(36).slice(2, 9);
|
||||
window.__cpEphemeralNotifications.unshift({
|
||||
id: id,
|
||||
kind: 'docker-update',
|
||||
containerName: String(containerName || ''),
|
||||
imageRef: String(imageRef || ''),
|
||||
state: 'running',
|
||||
resultMessage: ''
|
||||
});
|
||||
var dd = document.getElementById('notification-center-dropdown');
|
||||
if (dd) dd.classList.add('show');
|
||||
var bell = document.getElementById('notification-center-btn');
|
||||
if (bell) bell.classList.add('has-active-operation');
|
||||
loadNotificationCenter();
|
||||
return id;
|
||||
};
|
||||
|
||||
window.cpDockerUpdateNotifyEnd = function(nid, ok, message) {
|
||||
window.__cpEphemeralNotifications = window.__cpEphemeralNotifications || [];
|
||||
var n = null;
|
||||
for (var i = 0; i < window.__cpEphemeralNotifications.length; i++) {
|
||||
if (window.__cpEphemeralNotifications[i].id === nid) { n = window.__cpEphemeralNotifications[i]; break; }
|
||||
}
|
||||
if (!n) return;
|
||||
n.state = ok ? 'done_ok' : 'done_err';
|
||||
n.resultMessage = message ? String(message) : (ok ? 'Done.' : 'Unknown error.');
|
||||
var stillRunning = window.__cpEphemeralNotifications.some(function(x) { return x.state === 'running'; });
|
||||
var btn = document.getElementById('notification-center-btn');
|
||||
if (btn && !stillRunning) btn.classList.remove('has-active-operation');
|
||||
loadNotificationCenter();
|
||||
};
|
||||
|
||||
function toggleNotificationCenter() {
|
||||
const dropdown = document.getElementById('notification-center-dropdown');
|
||||
if (dropdown) {
|
||||
@@ -2782,10 +2923,10 @@
|
||||
learnMoreLink: 'https://cyberpanel.net/cyberpanel-htaccess-module',
|
||||
dismissed: isNotificationDismissed('htaccess-notification') }
|
||||
];
|
||||
if (notifications.length === 0) {
|
||||
list.innerHTML = '<div class="notification-center-empty">No notifications available</div>';
|
||||
} else {
|
||||
list.innerHTML = notifications.map(notif => {
|
||||
const ephemeralHtml = renderEphemeralNotificationsHTML();
|
||||
let staticHtml = '';
|
||||
if (notifications.length > 0) {
|
||||
staticHtml = notifications.map(notif => {
|
||||
let linkIcon = notif.linkText.includes('Configure') ? '<i class="fas fa-cog"></i>' :
|
||||
notif.linkText.includes('Start') ? '<i class="fas fa-rocket"></i>' :
|
||||
(notif.linkText.includes('View') || notif.linkText.includes('Details')) ? '<i class="fas fa-external-link-alt"></i>' : '<i class="fas fa-arrow-right"></i>';
|
||||
@@ -2805,7 +2946,14 @@
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
const activeCount = notifications.filter(n => !n.dismissed).length;
|
||||
if (!ephemeralHtml && !staticHtml) {
|
||||
list.innerHTML = '<div class="notification-center-empty">No notifications available</div>';
|
||||
} else {
|
||||
list.innerHTML = ephemeralHtml + staticHtml;
|
||||
}
|
||||
const staticActive = notifications.filter(n => !n.dismissed).length;
|
||||
const runningEphem = (window.__cpEphemeralNotifications || []).filter(function(n) { return n.state === 'running'; }).length;
|
||||
const activeCount = staticActive + runningEphem;
|
||||
const badge = document.getElementById('notification-badge');
|
||||
if (badge) {
|
||||
badge.textContent = activeCount;
|
||||
@@ -2822,6 +2970,16 @@
|
||||
|
||||
// Check all notification statuses when page loads
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var ncList = document.getElementById('notification-center-list');
|
||||
if (ncList) {
|
||||
ncList.addEventListener('click', function(ev) {
|
||||
var dismissBtn = ev.target.closest('[data-cp-ephemeral-dismiss]');
|
||||
if (dismissBtn && window.cpDismissEphemeralNotification) {
|
||||
var eid = dismissBtn.getAttribute('data-cp-ephemeral-dismiss');
|
||||
if (eid) window.cpDismissEphemeralNotification(eid);
|
||||
}
|
||||
});
|
||||
}
|
||||
loadNotificationCenter();
|
||||
checkBackupStatus();
|
||||
// Optional: open notification dropdown for testing (e.g. ?showNotifications=1)
|
||||
@@ -2953,9 +3111,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
function loadSecurityManagement() {
|
||||
window.open('{% url "securityManagementPage" %}', '_blank');
|
||||
}
|
||||
</script>
|
||||
|
||||
{% block footer_scripts %}{% endblock %}
|
||||
|
||||
@@ -772,9 +772,28 @@ class ContainerManager(multi.Thread):
|
||||
end = start + items_per_page
|
||||
page_containers = all_containers[start:end]
|
||||
|
||||
client = docker.from_env()
|
||||
rows = []
|
||||
for items in page_containers:
|
||||
rows.append({'name': items.name, 'admin': items.admin.userName, 'tag': items.tag, 'image': items.image})
|
||||
disp_image = items.image
|
||||
disp_tag = items.tag
|
||||
try:
|
||||
running = client.containers.get(items.name)
|
||||
cfg_ref = running.attrs.get('Config', {}).get('Image') or ''
|
||||
if cfg_ref and '@' not in cfg_ref:
|
||||
if ':' in cfg_ref:
|
||||
disp_image, disp_tag = cfg_ref.rsplit(':', 1)
|
||||
else:
|
||||
disp_image, disp_tag = cfg_ref, 'latest'
|
||||
elif running.image and running.image.tags:
|
||||
ref = running.image.tags[0]
|
||||
if ':' in ref:
|
||||
disp_image, disp_tag = ref.rsplit(':', 1)
|
||||
else:
|
||||
disp_image, disp_tag = ref, 'latest'
|
||||
except Exception:
|
||||
pass
|
||||
rows.append({'name': items.name, 'admin': items.admin.userName, 'tag': disp_tag, 'image': disp_image})
|
||||
json_data = json.dumps(rows)
|
||||
|
||||
final_dic = {
|
||||
@@ -2346,15 +2365,28 @@ class ContainerManager(multi.Thread):
|
||||
client = docker.from_env()
|
||||
dockerAPI = docker.APIClient()
|
||||
|
||||
containerName = data['containerName']
|
||||
newImage = data['newImage']
|
||||
newTag = data.get('newTag', 'latest')
|
||||
# UI (dockerManager.js) sends "name"; older callers may use "containerName"
|
||||
containerName = (data.get('containerName') or data.get('name') or '').strip()
|
||||
if not containerName:
|
||||
data_ret = {'updateContainerStatus': 0, 'error_message': 'Container name is required'}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
|
||||
newImage = (data.get('newImage') or '').strip()
|
||||
newTag = (data.get('newTag') or 'latest').strip() or 'latest'
|
||||
|
||||
# Get the current container
|
||||
try:
|
||||
currentContainer = client.containers.get(containerName)
|
||||
except docker.errors.NotFound:
|
||||
data_ret = {'updateContainerStatus': 0, 'error_message': f'Container {containerName} not found'}
|
||||
data_ret = {
|
||||
'updateContainerStatus': 0,
|
||||
'error_message': (
|
||||
f'Container {containerName} not found. '
|
||||
'If you clicked Update twice, wait for the first request to finish; '
|
||||
'do not start another update until you see success or failure.'
|
||||
),
|
||||
}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
except Exception as e:
|
||||
@@ -2365,6 +2397,19 @@ class ContainerManager(multi.Thread):
|
||||
# Get container configuration for recreation
|
||||
containerConfig = currentContainer.attrs['Config']
|
||||
hostConfig = currentContainer.attrs['HostConfig']
|
||||
|
||||
# If no new image specified, use current image repository (same as first updateContainer implementation)
|
||||
if not newImage:
|
||||
current_image = containerConfig.get('Image', '') or ''
|
||||
if ':' in current_image:
|
||||
newImage = current_image.split(':')[0]
|
||||
else:
|
||||
newImage = current_image
|
||||
newTag = 'latest'
|
||||
if not newImage:
|
||||
data_ret = {'updateContainerStatus': 0, 'error_message': 'Could not determine image name for update'}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
|
||||
# Extract volumes for data preservation
|
||||
volumes = {}
|
||||
@@ -2399,6 +2444,20 @@ class ContainerManager(multi.Thread):
|
||||
if memory_limit > 0:
|
||||
memory_limit = memory_limit // 1048576 # Convert bytes to MB
|
||||
|
||||
image_name = f"{newImage}:{newTag}"
|
||||
|
||||
# Pull BEFORE stop/remove so a slow/failed pull never leaves the container deleted
|
||||
# (double-clicks or retries would otherwise see "container not found").
|
||||
try:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'Pulling new image {image_name} (container still running)')
|
||||
client.images.pull(newImage, tag=newTag)
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'Successfully pulled image {image_name}')
|
||||
except Exception as e:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'Error pulling image {newImage}:{newTag}: {str(e)}')
|
||||
data_ret = {'updateContainerStatus': 0, 'error_message': f'Error pulling new image: {str(e)}'}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
|
||||
# Stop the current container
|
||||
try:
|
||||
if currentContainer.status == 'running':
|
||||
@@ -2420,18 +2479,6 @@ class ContainerManager(multi.Thread):
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
|
||||
# Pull the new image
|
||||
try:
|
||||
image_name = f"{newImage}:{newTag}"
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'Pulling new image {image_name}')
|
||||
client.images.pull(newImage, tag=newTag)
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'Successfully pulled image {image_name}')
|
||||
except Exception as e:
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'Error pulling image {newImage}:{newTag}: {str(e)}')
|
||||
data_ret = {'updateContainerStatus': 0, 'error_message': f'Error pulling new image: {str(e)}'}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
|
||||
# Create new container with same configuration but new image
|
||||
try:
|
||||
containerArgs = {
|
||||
@@ -2475,12 +2522,27 @@ class ContainerManager(multi.Thread):
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
|
||||
# Sync DB — container list UI reads image/tag from Containers model, not Docker
|
||||
try:
|
||||
container_record = Containers.objects.get(name=containerName)
|
||||
container_record.image = newImage
|
||||
container_record.tag = newTag
|
||||
container_record.cid = newContainer.short_id
|
||||
container_record.save()
|
||||
except Containers.DoesNotExist:
|
||||
pass
|
||||
except Exception as db_err:
|
||||
logging.CyberCPLogFileWriter.writeToFile(
|
||||
f'updateContainer DB sync failed for {containerName}: {db_err}'
|
||||
)
|
||||
|
||||
# Log successful update
|
||||
logging.CyberCPLogFileWriter.writeToFile(f'Successfully updated container {containerName} to image {image_name}')
|
||||
|
||||
data_ret = {
|
||||
'updateContainerStatus': 1,
|
||||
'updateContainerStatus': 1,
|
||||
'error_message': 'None',
|
||||
'new_image': image_name,
|
||||
'message': f'Container {containerName} successfully updated to {image_name}'
|
||||
}
|
||||
json_data = json.dumps(data_ret)
|
||||
|
||||
@@ -974,6 +974,15 @@ app.controller('listContainers', function ($scope, $http) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($scope.dockerUpdateInProgress) {
|
||||
new PNotify({
|
||||
title: 'Update in progress',
|
||||
text: 'Wait until the current update finishes before starting another.',
|
||||
type: 'warning'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// If no new image specified, use current image
|
||||
if (!$scope.newImage) {
|
||||
$scope.newImage = $scope.currentImage;
|
||||
@@ -1000,9 +1009,18 @@ app.controller('listContainers', function ($scope, $http) {
|
||||
history: false
|
||||
}
|
||||
})).get().on('pnotify.confirm', function () {
|
||||
var dockerUpdateNotificationId = null;
|
||||
$scope.dockerUpdateInProgress = true;
|
||||
$('#imageLoading').show();
|
||||
$("#updateContainer").modal("hide");
|
||||
|
||||
if (typeof window.cpDockerUpdateNotifyStart === 'function') {
|
||||
dockerUpdateNotificationId = window.cpDockerUpdateNotifyStart(
|
||||
$scope.updateContainerName,
|
||||
$scope.newImage + ':' + $scope.newTag
|
||||
);
|
||||
}
|
||||
|
||||
url = "/docker/updateContainer";
|
||||
var data = {
|
||||
name: $scope.updateContainerName,
|
||||
@@ -1020,15 +1038,27 @@ app.controller('listContainers', function ($scope, $http) {
|
||||
|
||||
function ListInitialData(response) {
|
||||
console.log(response);
|
||||
$scope.dockerUpdateInProgress = false;
|
||||
$('#imageLoading').hide();
|
||||
|
||||
if (response.data.updateContainerStatus === 1) {
|
||||
var ok = response.data && response.data.updateContainerStatus === 1;
|
||||
var imgLabel = ok
|
||||
? (response.data.new_image || response.data.message || 'Updated')
|
||||
: (response.data && response.data.error_message ? response.data.error_message : 'Update failed');
|
||||
|
||||
if (typeof window.cpDockerUpdateNotifyEnd === 'function' && dockerUpdateNotificationId) {
|
||||
window.cpDockerUpdateNotifyEnd(dockerUpdateNotificationId, ok, imgLabel);
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
new PNotify({
|
||||
title: 'Container Updated Successfully',
|
||||
text: `Container updated to ${response.data.new_image}`,
|
||||
text: 'Container updated to ' + (response.data.new_image || response.data.message || 'new image'),
|
||||
type: 'success'
|
||||
});
|
||||
location.reload();
|
||||
setTimeout(function () {
|
||||
location.reload();
|
||||
}, 2200);
|
||||
} else {
|
||||
new PNotify({
|
||||
title: 'Update Failed',
|
||||
@@ -1039,7 +1069,11 @@ app.controller('listContainers', function ($scope, $http) {
|
||||
}
|
||||
|
||||
function cantLoadInitialData(response) {
|
||||
$scope.dockerUpdateInProgress = false;
|
||||
$('#imageLoading').hide();
|
||||
if (typeof window.cpDockerUpdateNotifyEnd === 'function' && dockerUpdateNotificationId) {
|
||||
window.cpDockerUpdateNotifyEnd(dockerUpdateNotificationId, false, 'Could not connect to server');
|
||||
}
|
||||
new PNotify({
|
||||
title: 'Update Failed',
|
||||
text: 'Could not connect to server',
|
||||
|
||||
@@ -6250,6 +6250,31 @@ vmail
|
||||
command = 'systemctl enable redis'
|
||||
preFlightsChecks.call(command, self.distro, command, command, 1, 0, os.EX_OSERR)
|
||||
|
||||
def installRabbitMQ(self):
|
||||
rabbitMQMarker = '/home/cyberpanel/rabbitmq'
|
||||
|
||||
# Keep optional installer idempotent for reruns/retries.
|
||||
if os.path.exists(rabbitMQMarker):
|
||||
preFlightsChecks.stdOut("RabbitMQ marker already exists, skipping optional RabbitMQ installation.")
|
||||
return
|
||||
|
||||
if self.distro == ubuntu or self.distro == debian12:
|
||||
command = 'DEBIAN_FRONTEND=noninteractive apt install rabbitmq-server -y'
|
||||
elif self.distro == centos:
|
||||
command = 'yum install rabbitmq-server -y'
|
||||
else:
|
||||
command = 'dnf install rabbitmq-server -y'
|
||||
preFlightsChecks.call(command, self.distro, command, command, 1, 0, os.EX_OSERR)
|
||||
|
||||
command = 'systemctl enable rabbitmq-server'
|
||||
preFlightsChecks.call(command, self.distro, command, command, 1, 0, os.EX_OSERR)
|
||||
|
||||
command = 'systemctl start rabbitmq-server'
|
||||
preFlightsChecks.call(command, self.distro, command, command, 1, 0, os.EX_OSERR)
|
||||
|
||||
writeToFile = open(rabbitMQMarker, 'w')
|
||||
writeToFile.close()
|
||||
|
||||
def disablePackegeUpdates(self):
|
||||
if self.distro == centos:
|
||||
mainConfFile = '/etc/yum.conf'
|
||||
@@ -6752,6 +6777,7 @@ def main():
|
||||
parser.add_argument('--serial', help='Install LS Ent or OpenLiteSpeed')
|
||||
parser.add_argument('--port', help='LSCPD Port')
|
||||
parser.add_argument('--redis', help='vHosts on Redis - Requires LiteSpeed Enterprise')
|
||||
parser.add_argument('--rabbitmq', help='Enable optional RabbitMQ installation.')
|
||||
parser.add_argument('--remotemysql', help='Opt to choose local or remote MySQL')
|
||||
parser.add_argument('--mysqlhost', help='MySQL host if remote is chosen.')
|
||||
parser.add_argument('--mysqldb', help='MySQL DB if remote is chosen.')
|
||||
@@ -7020,6 +7046,9 @@ def main():
|
||||
if args.redis is not None:
|
||||
checks.installRedis()
|
||||
|
||||
if args.rabbitmq is not None and str(args.rabbitmq).upper() == 'ON':
|
||||
checks.installRabbitMQ()
|
||||
|
||||
if args.powerdns is not None:
|
||||
checks.enableDisableDNS(args.powerdns.lower())
|
||||
else:
|
||||
|
||||
104
loginSystem/migrations/0001_initial.py
Normal file
104
loginSystem/migrations/0001_initial.py
Normal file
@@ -0,0 +1,104 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated for CyberPanel: loginSystem had models but no migrations, which broke
|
||||
# the global dependency graph (e.g. dockerManager depends on loginSystem.__first__).
|
||||
#
|
||||
# Pre-existing panels already have loginSystem_* tables from legacy installs.
|
||||
# This migration updates Django state only; it does not run DDL.
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.SeparateDatabaseAndState(
|
||||
database_operations=[],
|
||||
state_operations=[
|
||||
migrations.CreateModel(
|
||||
name='ACL',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50, unique=True)),
|
||||
('adminStatus', models.IntegerField(default=0)),
|
||||
('versionManagement', models.IntegerField(default=0)),
|
||||
('createNewUser', models.IntegerField(default=0)),
|
||||
('listUsers', models.IntegerField(default=0)),
|
||||
('deleteUser', models.IntegerField(default=0)),
|
||||
('resellerCenter', models.IntegerField(default=0)),
|
||||
('changeUserACL', models.IntegerField(default=0)),
|
||||
('createWebsite', models.IntegerField(default=0)),
|
||||
('modifyWebsite', models.IntegerField(default=0)),
|
||||
('suspendWebsite', models.IntegerField(default=0)),
|
||||
('deleteWebsite', models.IntegerField(default=0)),
|
||||
('createPackage', models.IntegerField(default=0)),
|
||||
('listPackages', models.IntegerField(default=0)),
|
||||
('deletePackage', models.IntegerField(default=0)),
|
||||
('modifyPackage', models.IntegerField(default=0)),
|
||||
('createDatabase', models.IntegerField(default=1)),
|
||||
('deleteDatabase', models.IntegerField(default=1)),
|
||||
('listDatabases', models.IntegerField(default=1)),
|
||||
('createNameServer', models.IntegerField(default=0)),
|
||||
('createDNSZone', models.IntegerField(default=1)),
|
||||
('deleteZone', models.IntegerField(default=1)),
|
||||
('addDeleteRecords', models.IntegerField(default=1)),
|
||||
('createEmail', models.IntegerField(default=1)),
|
||||
('listEmails', models.IntegerField(default=1)),
|
||||
('deleteEmail', models.IntegerField(default=1)),
|
||||
('emailForwarding', models.IntegerField(default=1)),
|
||||
('changeEmailPassword', models.IntegerField(default=1)),
|
||||
('dkimManager', models.IntegerField(default=1)),
|
||||
('createFTPAccount', models.IntegerField(default=1)),
|
||||
('deleteFTPAccount', models.IntegerField(default=1)),
|
||||
('listFTPAccounts', models.IntegerField(default=1)),
|
||||
('createBackup', models.IntegerField(default=0)),
|
||||
('restoreBackup', models.IntegerField(default=0)),
|
||||
('addDeleteDestinations', models.IntegerField(default=0)),
|
||||
('scheduleBackups', models.IntegerField(default=0)),
|
||||
('remoteBackups', models.IntegerField(default=0)),
|
||||
('manageSSL', models.IntegerField(default=1)),
|
||||
('hostnameSSL', models.IntegerField(default=0)),
|
||||
('mailServerSSL', models.IntegerField(default=0)),
|
||||
('config', models.TextField(default='{}')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Administrator',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('userName', models.CharField(max_length=50, unique=True)),
|
||||
('password', models.CharField(max_length=200)),
|
||||
('firstName', models.CharField(default='None', max_length=200)),
|
||||
('lastName', models.CharField(default='None', max_length=200)),
|
||||
('email', models.CharField(max_length=50)),
|
||||
('type', models.IntegerField()),
|
||||
('owner', models.IntegerField(default=1)),
|
||||
('token', models.CharField(default='None', max_length=500)),
|
||||
('api', models.IntegerField(default=0)),
|
||||
(
|
||||
'securityLevel',
|
||||
models.IntegerField(choices=[(0, 'HIGH'), (1, 'LOW')], default=0),
|
||||
),
|
||||
('state', models.CharField(default='ACTIVE', max_length=10)),
|
||||
('initWebsitesLimit', models.IntegerField(default=0)),
|
||||
('twoFA', models.IntegerField(default=0)),
|
||||
('secretKey', models.CharField(default='None', max_length=50)),
|
||||
('config', models.TextField(default='{}')),
|
||||
('defaultSite', models.IntegerField(default=0)),
|
||||
(
|
||||
'acl',
|
||||
models.ForeignKey(
|
||||
default=1,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
to='loginSystem.acl',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
# loginSystem migrations package (CyberPanel core)
|
||||
|
||||
193
manageServices/application_backup.py
Normal file
193
manageServices/application_backup.py
Normal file
@@ -0,0 +1,193 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Full config + data backups for managed applications (pre version change).
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tarfile
|
||||
import time
|
||||
|
||||
CONFIG_PATHS = {
|
||||
'Elasticsearch': ['/etc/elasticsearch'],
|
||||
'Redis': ['/etc/redis', '/etc/redis.conf'],
|
||||
'RabbitMQ': ['/etc/rabbitmq'],
|
||||
}
|
||||
|
||||
DATA_PATHS = {
|
||||
'Elasticsearch': ['/var/lib/elasticsearch'],
|
||||
'Redis': ['/var/lib/redis'],
|
||||
'RabbitMQ': ['/var/lib/rabbitmq'],
|
||||
}
|
||||
|
||||
SERVICE_UNITS = {
|
||||
'Elasticsearch': 'elasticsearch',
|
||||
'Redis': 'redis',
|
||||
'RabbitMQ': 'rabbitmq-server',
|
||||
}
|
||||
|
||||
CHOWN_CMDS = {
|
||||
'Elasticsearch': 'chown -R elasticsearch:elasticsearch /var/lib/elasticsearch /etc/elasticsearch',
|
||||
'Redis': 'chown -R redis:redis /var/lib/redis /etc/redis /etc/redis.conf 2>/dev/null; true',
|
||||
'RabbitMQ': 'chown -R rabbitmq:rabbitmq /var/lib/rabbitmq /etc/rabbitmq',
|
||||
}
|
||||
|
||||
BACKUP_ROOT = '/home/cyberpanel/backups/manageApplications'
|
||||
|
||||
|
||||
def _existing_paths(app_name):
|
||||
out = []
|
||||
for p in CONFIG_PATHS.get(app_name, []) + DATA_PATHS.get(app_name, []):
|
||||
if os.path.exists(p):
|
||||
out.append(p)
|
||||
return out
|
||||
|
||||
|
||||
def create_managed_app_backup(app_name, status_file):
|
||||
"""
|
||||
Tar config + data paths into BACKUP_ROOT/<app>/<epoch>/bundle.tar.gz.
|
||||
Returns backup directory path, or '' on failure / nothing to back up.
|
||||
"""
|
||||
def log(msg):
|
||||
try:
|
||||
status_file.write(msg + '\n')
|
||||
status_file.flush()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
paths = _existing_paths(app_name)
|
||||
if not paths:
|
||||
log('No paths on disk to back up for {0}; skipping archive.'.format(app_name))
|
||||
return ''
|
||||
|
||||
ts = int(time.time())
|
||||
safe = app_name.lower().replace(' ', '_')
|
||||
backup_dir = os.path.join(BACKUP_ROOT, safe, str(ts))
|
||||
os.makedirs(backup_dir, mode=0o750, exist_ok=True)
|
||||
archive = os.path.join(backup_dir, 'bundle.tar.gz')
|
||||
|
||||
try:
|
||||
with tarfile.open(archive, 'w:gz', compresslevel=6) as tf:
|
||||
for abs_path in paths:
|
||||
arc = abs_path.lstrip('/')
|
||||
tf.add(abs_path, arcname=arc, recursive=True)
|
||||
manifest = {
|
||||
'app': app_name,
|
||||
'created': ts,
|
||||
'paths': [p.lstrip('/') for p in paths],
|
||||
}
|
||||
with open(os.path.join(backup_dir, 'manifest.json'), 'w') as mh:
|
||||
json.dump(manifest, mh, indent=2)
|
||||
log('Backup created at {0}'.format(backup_dir))
|
||||
return backup_dir
|
||||
except Exception as err:
|
||||
log('Backup failed: {0}'.format(err))
|
||||
try:
|
||||
shutil.rmtree(backup_dir, ignore_errors=True)
|
||||
except Exception:
|
||||
pass
|
||||
return ''
|
||||
|
||||
|
||||
def _archive_path(backup_dir):
|
||||
return os.path.join(backup_dir, 'bundle.tar.gz')
|
||||
|
||||
|
||||
def merge_data_from_backup(app_name, backup_dir, status_file):
|
||||
"""Overlay saved data directories from backup onto live system (preserves package layout)."""
|
||||
def log(msg):
|
||||
try:
|
||||
status_file.write(msg + '\n')
|
||||
status_file.flush()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
arc = _archive_path(backup_dir)
|
||||
if not os.path.isfile(arc):
|
||||
log('No bundle at {0}; skip data merge.'.format(arc))
|
||||
return False
|
||||
data_prefixes = [p.lstrip('/') for p in DATA_PATHS.get(app_name, [])]
|
||||
if not data_prefixes:
|
||||
return True
|
||||
try:
|
||||
with tarfile.open(arc, 'r:gz') as tf:
|
||||
for m in tf.getmembers():
|
||||
name = m.name
|
||||
if m.isfile() or m.isdir():
|
||||
for pref in data_prefixes:
|
||||
if name == pref or name.startswith(pref + '/'):
|
||||
tf.extract(m, path='/', set_attrs=False)
|
||||
break
|
||||
log('Merged data trees from backup for {0}.'.format(app_name))
|
||||
return True
|
||||
except Exception as err:
|
||||
log('Data merge failed: {0}'.format(err))
|
||||
return False
|
||||
|
||||
|
||||
def restore_full_backup(backup_dir, status_file):
|
||||
"""Extract full bundle to / (recovery)."""
|
||||
def log(msg):
|
||||
try:
|
||||
status_file.write(msg + '\n')
|
||||
status_file.flush()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
arc = _archive_path(backup_dir)
|
||||
if not os.path.isfile(arc):
|
||||
log('Cannot restore: missing {0}'.format(arc))
|
||||
return False
|
||||
try:
|
||||
with tarfile.open(arc, 'r:gz') as tf:
|
||||
for m in tf.getmembers():
|
||||
tf.extract(m, path='/', set_attrs=False)
|
||||
log('Full restore from backup completed.')
|
||||
return True
|
||||
except Exception as err:
|
||||
log('Full restore failed: {0}'.format(err))
|
||||
return False
|
||||
|
||||
|
||||
def cleanup_managed_backup(backup_dir, status_file):
|
||||
def log(msg):
|
||||
try:
|
||||
status_file.write(msg + '\n')
|
||||
status_file.flush()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not backup_dir or not os.path.isdir(backup_dir):
|
||||
return
|
||||
try:
|
||||
shutil.rmtree(backup_dir, ignore_errors=True)
|
||||
log('Removed backup directory after successful change: {0}'.format(backup_dir))
|
||||
except Exception as err:
|
||||
log('Could not remove backup dir: {0}'.format(err))
|
||||
|
||||
|
||||
def chown_app_paths(app_name, status_writer):
|
||||
cmd = CHOWN_CMDS.get(app_name)
|
||||
if not cmd:
|
||||
return
|
||||
try:
|
||||
subprocess.call(cmd, shell=True, stdout=status_writer, stderr=status_writer)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def service_is_active(app_name):
|
||||
unit = SERVICE_UNITS.get(app_name)
|
||||
if not unit:
|
||||
return False
|
||||
try:
|
||||
r = subprocess.run(
|
||||
['systemctl', 'is-active', unit],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
)
|
||||
return r.stdout.strip() == 'active'
|
||||
except Exception:
|
||||
return False
|
||||
190
manageServices/application_detection.py
Normal file
190
manageServices/application_detection.py
Normal file
@@ -0,0 +1,190 @@
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
|
||||
APP_PACKAGE_MAP = {
|
||||
'Elasticsearch': {
|
||||
'rhel': 'elasticsearch',
|
||||
'debian': 'elasticsearch',
|
||||
'service': 'elasticsearch',
|
||||
'binary_paths': ['/usr/share/elasticsearch/bin/elasticsearch']
|
||||
},
|
||||
'Redis': {
|
||||
'rhel': 'redis',
|
||||
'debian': 'redis-server',
|
||||
'service': 'redis',
|
||||
'binary_paths': ['/usr/bin/redis-server']
|
||||
},
|
||||
'RabbitMQ': {
|
||||
'rhel': 'rabbitmq-server',
|
||||
'debian': 'rabbitmq-server',
|
||||
'service': 'rabbitmq-server',
|
||||
'binary_paths': ['/usr/sbin/rabbitmq-server', '/usr/lib/rabbitmq/bin/rabbitmq-server']
|
||||
}
|
||||
}
|
||||
|
||||
APP_MARKERS = {
|
||||
'Elasticsearch': '/home/cyberpanel/elasticsearch',
|
||||
'Redis': '/home/cyberpanel/redis',
|
||||
'RabbitMQ': '/home/cyberpanel/rabbitmq'
|
||||
}
|
||||
|
||||
|
||||
def _run(cmd):
|
||||
try:
|
||||
res = subprocess.run(cmd, capture_output=True, text=True, timeout=12)
|
||||
return res.returncode, (res.stdout or '').strip(), (res.stderr or '').strip()
|
||||
except Exception as err:
|
||||
return 1, '', str(err)
|
||||
|
||||
|
||||
def is_debian_family():
|
||||
return os.path.exists('/etc/debian_version') or os.path.exists('/etc/lsb-release')
|
||||
|
||||
|
||||
def rhel_major_from_os_release():
|
||||
"""
|
||||
RHEL-family OS major version (8, 9, 10, …) from /etc/os-release (or redhat-release).
|
||||
Returns None for Debian/Ubuntu or if the OS cannot be classified as RHEL-like.
|
||||
Used to align Packagecloud Yum baseurls (el/8 vs el/9) with the running system.
|
||||
"""
|
||||
if is_debian_family():
|
||||
return None
|
||||
os_release = '/etc/os-release'
|
||||
version_id = None
|
||||
platform_id = None
|
||||
if os.path.exists(os_release):
|
||||
try:
|
||||
with open(os_release, 'r', encoding='utf-8', errors='replace') as fh:
|
||||
for line in fh:
|
||||
line = line.strip()
|
||||
if line.startswith('VERSION_ID='):
|
||||
version_id = line.split('=', 1)[1].strip().strip('"').strip("'")
|
||||
elif line.startswith('PLATFORM_ID='):
|
||||
platform_id = line.split('=', 1)[1].strip().strip('"').strip("'")
|
||||
except Exception:
|
||||
pass
|
||||
if version_id:
|
||||
match = re.match(r'^(\d+)', version_id)
|
||||
if match:
|
||||
major = int(match.group(1))
|
||||
if 6 <= major <= 15:
|
||||
return major
|
||||
if platform_id:
|
||||
match = re.search(r'el(\d+)', platform_id, re.IGNORECASE)
|
||||
if match:
|
||||
major = int(match.group(1))
|
||||
if 6 <= major <= 15:
|
||||
return major
|
||||
redhat_release = '/etc/redhat-release'
|
||||
if os.path.exists(redhat_release):
|
||||
try:
|
||||
with open(redhat_release, 'r', encoding='utf-8', errors='replace') as fh:
|
||||
txt = fh.read()
|
||||
match = re.search(r'release\s+(\d+)', txt, re.IGNORECASE)
|
||||
if match:
|
||||
major = int(match.group(1))
|
||||
if 6 <= major <= 15:
|
||||
return major
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def is_centos7():
|
||||
release_paths = ['/etc/centos-release', '/etc/redhat-release', '/etc/os-release']
|
||||
text_blob = ''
|
||||
for path in release_paths:
|
||||
try:
|
||||
if os.path.exists(path):
|
||||
with open(path, 'r') as fh:
|
||||
text_blob += fh.read().lower() + '\n'
|
||||
except Exception:
|
||||
continue
|
||||
return ('centos' in text_blob and ('release 7' in text_blob or 'version_id="7' in text_blob))
|
||||
|
||||
|
||||
def managed_apps_os_support():
|
||||
if is_centos7():
|
||||
return {
|
||||
'supported': False,
|
||||
'reason': 'CentOS 7 is EOL and not supported for managed applications.'
|
||||
}
|
||||
return {
|
||||
'supported': True,
|
||||
'reason': ''
|
||||
}
|
||||
|
||||
|
||||
def package_name_for_app(app_name):
|
||||
app_map = APP_PACKAGE_MAP.get(app_name, {})
|
||||
if not app_map:
|
||||
return ''
|
||||
if is_debian_family():
|
||||
return app_map.get('debian', '')
|
||||
return app_map.get('rhel', '')
|
||||
|
||||
|
||||
def _rpm_installed(pkg_name):
|
||||
rc, out, _ = _run(['rpm', '-q', pkg_name])
|
||||
if rc == 0:
|
||||
return True, out
|
||||
return False, ''
|
||||
|
||||
|
||||
def _dpkg_installed(pkg_name):
|
||||
rc, out, _ = _run(['dpkg-query', '-W', '-f=${Version}', pkg_name])
|
||||
if rc == 0 and out:
|
||||
return True, out
|
||||
return False, ''
|
||||
|
||||
|
||||
def _systemd_active(service_name):
|
||||
rc, out, _ = _run(['systemctl', 'is-active', service_name])
|
||||
return rc == 0 and out.strip() == 'active'
|
||||
|
||||
|
||||
def detect_installed_version(app_name):
|
||||
pkg_name = package_name_for_app(app_name)
|
||||
if not pkg_name:
|
||||
return ''
|
||||
|
||||
if is_debian_family():
|
||||
ok, ver = _dpkg_installed(pkg_name)
|
||||
else:
|
||||
ok, ver = _rpm_installed(pkg_name)
|
||||
|
||||
if not ok:
|
||||
return ''
|
||||
|
||||
if app_name in ('Elasticsearch', 'Redis', 'RabbitMQ'):
|
||||
match = re.search(r'(\d+\.\d+\.\d+)', ver)
|
||||
return match.group(1) if match else ver
|
||||
|
||||
return ver
|
||||
|
||||
|
||||
def detect_app_state(app_name):
|
||||
marker_path = APP_MARKERS.get(app_name, '')
|
||||
package_name = package_name_for_app(app_name)
|
||||
service_name = APP_PACKAGE_MAP.get(app_name, {}).get('service', '')
|
||||
binary_paths = APP_PACKAGE_MAP.get(app_name, {}).get('binary_paths', [])
|
||||
|
||||
installed_version = detect_installed_version(app_name)
|
||||
marker_exists = bool(marker_path and os.path.exists(marker_path))
|
||||
service_active = _systemd_active(service_name) if service_name else False
|
||||
binary_exists = any(os.path.exists(path) for path in binary_paths)
|
||||
|
||||
installed = bool(installed_version or service_active or binary_exists)
|
||||
|
||||
return {
|
||||
'appName': app_name,
|
||||
'packageName': package_name,
|
||||
'markerPath': marker_path,
|
||||
'markerExists': marker_exists,
|
||||
'installed': installed,
|
||||
'installedVersion': installed_version,
|
||||
'serviceActive': service_active,
|
||||
'binaryExists': binary_exists
|
||||
}
|
||||
212
manageServices/application_elasticsearch.py
Normal file
212
manageServices/application_elasticsearch.py
Normal file
@@ -0,0 +1,212 @@
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
from serverStatus.serverStatusUtil import ServerStatusUtil
|
||||
from plogical import CyberCPLogFileWriter as logging
|
||||
from manageServices.application_backup import (
|
||||
CHOWN_CMDS,
|
||||
cleanup_managed_backup,
|
||||
create_managed_app_backup,
|
||||
merge_data_from_backup,
|
||||
restore_full_backup,
|
||||
service_is_active,
|
||||
)
|
||||
from manageServices.application_detection import detect_app_state, is_debian_family
|
||||
|
||||
|
||||
def _es_major_normalized(es_major):
|
||||
m = str(es_major).strip()
|
||||
if m in ('7', '8', '9'):
|
||||
return m
|
||||
return '8'
|
||||
|
||||
|
||||
def _write_repo(es_major):
|
||||
major = _es_major_normalized(es_major)
|
||||
if is_debian_family():
|
||||
repo_file = '/etc/apt/sources.list.d/elastic-{0}.x.list'.format(major)
|
||||
cmd = 'echo "deb https://artifacts.elastic.co/packages/{0}.x/apt stable main" | sudo tee {1}'.format(major, repo_file)
|
||||
subprocess.call(cmd, shell=True)
|
||||
return repo_file
|
||||
|
||||
repo_file = '/etc/yum.repos.d/elasticsearch.repo'
|
||||
content = '''
|
||||
[elasticsearch]
|
||||
name=Elasticsearch repository for {0}.x packages
|
||||
baseurl=https://artifacts.elastic.co/packages/{0}.x/yum
|
||||
gpgcheck=1
|
||||
gpgkey=https://artifacts.elastic.co/GPG-KEY-elasticsearch
|
||||
enabled=0
|
||||
autorefresh=1
|
||||
type=rpm-md
|
||||
'''.format(major)
|
||||
with open(repo_file, 'w') as handle:
|
||||
handle.write(content)
|
||||
return repo_file
|
||||
|
||||
|
||||
def _ensure_tmpdir(status_file):
|
||||
ServerStatusUtil.executioner('mkdir -p /home/elasticsearch/tmp', status_file)
|
||||
ServerStatusUtil.executioner('chown elasticsearch:elasticsearch /home/elasticsearch/tmp', status_file)
|
||||
jvm_options = '/etc/elasticsearch/jvm.options'
|
||||
line = '-Djava.io.tmpdir=/home/elasticsearch/tmp\n'
|
||||
try:
|
||||
if os.path.exists(jvm_options):
|
||||
with open(jvm_options, 'r') as handle:
|
||||
body = handle.read()
|
||||
if line.strip() not in body:
|
||||
with open(jvm_options, 'a') as handle:
|
||||
handle.write(line)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def adopt_or_reconcile(status_file):
|
||||
state = detect_app_state('Elasticsearch')
|
||||
if state['installed'] and not state['markerExists']:
|
||||
ServerStatusUtil.executioner('touch /home/cyberpanel/elasticsearch', status_file)
|
||||
logging.CyberCPLogFileWriter.statusWriter(
|
||||
ServerStatusUtil.lswsInstallStatusPath,
|
||||
'Elasticsearch detected and adopted by marker reconciliation.\n'
|
||||
)
|
||||
return state
|
||||
|
||||
|
||||
def _resolve_target_version(version, es_major):
|
||||
if version and str(version).strip() != 'latest':
|
||||
return str(version).strip()
|
||||
from manageServices.application_versions import get_latest_version
|
||||
return get_latest_version('Elasticsearch', es_major, '3') or ''
|
||||
|
||||
|
||||
def _run_elasticsearch_packages(version, es_major, status_file, allow_downgrade):
|
||||
if is_debian_family():
|
||||
subprocess.call(
|
||||
'wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -',
|
||||
shell=True,
|
||||
)
|
||||
ServerStatusUtil.executioner('apt-get install apt-transport-https -y', status_file)
|
||||
_write_repo(es_major)
|
||||
ServerStatusUtil.executioner('apt-get update -y', status_file)
|
||||
if version and version != 'latest':
|
||||
cmd = (
|
||||
'DEBIAN_FRONTEND=noninteractive apt-get install -y '
|
||||
'--allow-downgrades elasticsearch={0}'
|
||||
).format(version)
|
||||
else:
|
||||
cmd = 'DEBIAN_FRONTEND=noninteractive apt-get install elasticsearch -y'
|
||||
ServerStatusUtil.executioner(cmd, status_file)
|
||||
return
|
||||
|
||||
ServerStatusUtil.executioner(
|
||||
'rpm --import https://artifacts.elastic.co/GPG-KEY-elasticsearch', status_file
|
||||
)
|
||||
_write_repo(es_major)
|
||||
ad = ' --allow-downgrade' if allow_downgrade else ''
|
||||
if version and version != 'latest':
|
||||
cmd = 'dnf install{0} -y --enablerepo=elasticsearch elasticsearch-{1}'.format(
|
||||
ad, version
|
||||
)
|
||||
ServerStatusUtil.executioner(cmd, status_file)
|
||||
else:
|
||||
cmd = 'dnf install{0} -y --enablerepo=elasticsearch elasticsearch'.format(ad)
|
||||
ServerStatusUtil.executioner(cmd, status_file)
|
||||
|
||||
|
||||
def install(version='latest', es_major='8'):
|
||||
status_file = open(ServerStatusUtil.lswsInstallStatusPath, 'w')
|
||||
adopt_or_reconcile(status_file)
|
||||
|
||||
from manageServices.application_versions import version_compare
|
||||
|
||||
state = detect_app_state('Elasticsearch')
|
||||
backup_dir = ''
|
||||
allow_downgrade = False
|
||||
target = _resolve_target_version(version, es_major)
|
||||
|
||||
if state['installed'] and state.get('installedVersion'):
|
||||
status_file.write(
|
||||
'Pre-version-change backup and service stop (Elasticsearch)...\n'
|
||||
)
|
||||
status_file.flush()
|
||||
iv = state['installedVersion']
|
||||
if target and version_compare(iv, target) > 0:
|
||||
allow_downgrade = True
|
||||
status_file.write(
|
||||
'Downgrade path: allowing package manager downgrade where supported.\n'
|
||||
)
|
||||
status_file.flush()
|
||||
backup_dir = create_managed_app_backup('Elasticsearch', status_file)
|
||||
ServerStatusUtil.executioner('systemctl stop elasticsearch', status_file)
|
||||
|
||||
_run_elasticsearch_packages(version, es_major, status_file, allow_downgrade)
|
||||
if backup_dir:
|
||||
merge_data_from_backup('Elasticsearch', backup_dir, status_file)
|
||||
ServerStatusUtil.executioner(CHOWN_CMDS['Elasticsearch'], status_file)
|
||||
|
||||
_ensure_tmpdir(status_file)
|
||||
ServerStatusUtil.executioner('systemctl enable elasticsearch', status_file)
|
||||
ServerStatusUtil.executioner('systemctl start elasticsearch', status_file)
|
||||
time.sleep(3)
|
||||
|
||||
if backup_dir:
|
||||
if service_is_active('Elasticsearch'):
|
||||
cleanup_managed_backup(backup_dir, status_file)
|
||||
status_file.write(
|
||||
'Elasticsearch version change completed; backup removed after success.\n'
|
||||
)
|
||||
else:
|
||||
status_file.write(
|
||||
'Elasticsearch failed to start; restoring from backup...\n'
|
||||
)
|
||||
restore_full_backup(backup_dir, status_file)
|
||||
ServerStatusUtil.executioner(CHOWN_CMDS['Elasticsearch'], status_file)
|
||||
ServerStatusUtil.executioner('systemctl start elasticsearch', status_file)
|
||||
time.sleep(2)
|
||||
if not service_is_active('Elasticsearch'):
|
||||
status_file.write(
|
||||
'Recovery unclear — backup kept at {0}\n'.format(backup_dir)
|
||||
)
|
||||
else:
|
||||
status_file.write(
|
||||
'Prior state restored from backup. Backup retained for safety.\n'
|
||||
)
|
||||
status_file.flush()
|
||||
|
||||
ServerStatusUtil.executioner('touch /home/cyberpanel/elasticsearch', status_file)
|
||||
logging.CyberCPLogFileWriter.statusWriter(
|
||||
ServerStatusUtil.lswsInstallStatusPath, 'Elasticsearch installed.[200]\n', 1
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def upgrade(version='latest', es_major='8'):
|
||||
return install(version=version, es_major=es_major)
|
||||
|
||||
|
||||
def remove():
|
||||
status_file = open(ServerStatusUtil.lswsInstallStatusPath, 'w')
|
||||
if is_debian_family():
|
||||
for major in ('7', '8', '9'):
|
||||
path = '/etc/apt/sources.list.d/elastic-{0}.x.list'.format(major)
|
||||
try:
|
||||
os.remove(path)
|
||||
except Exception:
|
||||
pass
|
||||
ServerStatusUtil.executioner(
|
||||
'DEBIAN_FRONTEND=noninteractive apt-get remove elasticsearch -y', status_file
|
||||
)
|
||||
else:
|
||||
try:
|
||||
os.remove('/etc/yum.repos.d/elasticsearch.repo')
|
||||
except Exception:
|
||||
pass
|
||||
ServerStatusUtil.executioner('yum erase elasticsearch -y', status_file)
|
||||
|
||||
ServerStatusUtil.executioner('rm -rf /home/elasticsearch/tmp', status_file)
|
||||
ServerStatusUtil.executioner('rm -f /home/cyberpanel/elasticsearch', status_file)
|
||||
logging.CyberCPLogFileWriter.statusWriter(
|
||||
ServerStatusUtil.lswsInstallStatusPath, 'Elasticsearch removed.[200]\n', 1
|
||||
)
|
||||
return 0
|
||||
192
manageServices/application_page_meta.py
Normal file
192
manageServices/application_page_meta.py
Normal file
@@ -0,0 +1,192 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Server-side metadata for Manage Applications page (version lists in HTML)."""
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from .application_detection import detect_app_state, managed_apps_os_support
|
||||
from .application_versions import get_available_versions, version_compare
|
||||
|
||||
_APP_IMAGES = {
|
||||
'Elasticsearch': '/static/manageServices/images/elastic-search.png',
|
||||
'Redis': '/static/manageServices/images/redis.png',
|
||||
'RabbitMQ': '/static/manageServices/images/rabbitmq-logo.svg',
|
||||
}
|
||||
|
||||
# Cache only repoquery/dnf-backed version lists (slow). Install state is always refreshed.
|
||||
# Override with CYBERCP_MANAGED_APPS_VERSIONS_INVENTORY_TTL (seconds), default 3600 (1 hour).
|
||||
_VERSIONS_INVENTORY_TTL_SECONDS = int(
|
||||
os.environ.get('CYBERCP_MANAGED_APPS_VERSIONS_INVENTORY_TTL', '3600')
|
||||
)
|
||||
_VERSIONS_INVENTORY_CACHE = {}
|
||||
_VERSIONS_INVENTORY_LOCK = threading.Lock()
|
||||
|
||||
|
||||
def _versions_inventory_cache_get(cache_key):
|
||||
now = time.time()
|
||||
with _VERSIONS_INVENTORY_LOCK:
|
||||
item = _VERSIONS_INVENTORY_CACHE.get(cache_key)
|
||||
if not item:
|
||||
return None
|
||||
ts, inventory = item
|
||||
if now - ts > _VERSIONS_INVENTORY_TTL_SECONDS:
|
||||
try:
|
||||
del _VERSIONS_INVENTORY_CACHE[cache_key]
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
return {k: list(v) for k, v in inventory.items()}
|
||||
|
||||
|
||||
def _versions_inventory_cache_put(cache_key, inventory):
|
||||
with _VERSIONS_INVENTORY_LOCK:
|
||||
if len(_VERSIONS_INVENTORY_CACHE) > 16:
|
||||
_VERSIONS_INVENTORY_CACHE.clear()
|
||||
snap = {k: list(v) for k, v in (inventory or {}).items()}
|
||||
_VERSIONS_INVENTORY_CACHE[cache_key] = (time.time(), snap)
|
||||
|
||||
|
||||
def _cold_fetch_version_inventory(major, rmq, support):
|
||||
"""Populate version lists from package managers (DNF/apt); can take many seconds."""
|
||||
inv = {}
|
||||
if not support.get('supported'):
|
||||
for app_name in ('Elasticsearch', 'Redis', 'RabbitMQ'):
|
||||
inv[app_name] = []
|
||||
return inv
|
||||
for app_name in ('Elasticsearch', 'Redis', 'RabbitMQ'):
|
||||
try:
|
||||
inv[app_name] = get_available_versions(app_name, major, rmq)
|
||||
except BaseException:
|
||||
inv[app_name] = []
|
||||
return inv
|
||||
|
||||
|
||||
def _resolve_version_inventory(cache_key, major, rmq, support):
|
||||
cached = _versions_inventory_cache_get(cache_key)
|
||||
if cached is not None:
|
||||
return cached
|
||||
inv = _cold_fetch_version_inventory(major, rmq, support)
|
||||
_versions_inventory_cache_put(cache_key, inv)
|
||||
return inv
|
||||
|
||||
|
||||
def _assemble_manage_applications_payload(major, rmq, support, version_inv):
|
||||
"""Build services + bootstrap JSON from fresh install state and cached (or new) version lists."""
|
||||
services = []
|
||||
bootstrap_apps = []
|
||||
|
||||
for app_name in ('Elasticsearch', 'Redis', 'RabbitMQ'):
|
||||
state = detect_app_state(app_name)
|
||||
services.append({
|
||||
'image': _APP_IMAGES[app_name],
|
||||
'name': app_name,
|
||||
'installed': 'Installed' if state['installed'] else 'Not-Installed',
|
||||
'installedVersion': state.get('installedVersion', ''),
|
||||
})
|
||||
|
||||
versions = list(version_inv.get(app_name) or [])
|
||||
latest_branch = ''
|
||||
latest_global = ''
|
||||
if versions:
|
||||
latest_branch = versions[0]
|
||||
latest_global = latest_branch
|
||||
|
||||
installed_version = state['installedVersion']
|
||||
if installed_version and installed_version not in versions:
|
||||
prepend_installed = True
|
||||
if app_name == 'RabbitMQ':
|
||||
from manageServices.application_rabbitmq_repo import (
|
||||
filter_versions_for_stream,
|
||||
)
|
||||
prepend_installed = bool(
|
||||
filter_versions_for_stream([installed_version], rmq)
|
||||
)
|
||||
if prepend_installed:
|
||||
versions = [installed_version] + versions
|
||||
|
||||
ref_latest = latest_global or latest_branch
|
||||
update_available = bool(
|
||||
state['installed']
|
||||
and installed_version
|
||||
and ref_latest
|
||||
and version_compare(installed_version, ref_latest) < 0
|
||||
)
|
||||
|
||||
rabbitmq_versions_hint = ''
|
||||
if app_name == 'RabbitMQ' and not versions:
|
||||
if rmq == '4':
|
||||
rabbitmq_versions_hint = _(
|
||||
'Your OS is not unsupported: upstream RabbitMQ publishes 4.x RPMs suitable for '
|
||||
'RHEL/Alma/Rocky 8 and 9 (RPM filenames may still contain el8; that is normal). '
|
||||
'If this list stays empty, repository metadata may not expose 4.x to dnf yet—'
|
||||
'refresh metadata (dnf makecache -y) or install the official .rpm from rabbitmq.com. '
|
||||
'Check with: dnf repoquery rabbitmq-server --available --show-duplicates '
|
||||
'(4.x lines look like rabbitmq-server-0:4.x.y-1.el8.noarch — search for :4., not a space after the colon).'
|
||||
)
|
||||
else:
|
||||
rabbitmq_versions_hint = _(
|
||||
'No 3.x builds were returned for this stream after refreshing Team RabbitMQ repos. '
|
||||
'This is usually metadata or repo state—not OS support. Try: dnf makecache -y, '
|
||||
'then dnf repoquery rabbitmq-server --available --show-duplicates.'
|
||||
)
|
||||
|
||||
bootstrap_apps.append({
|
||||
'name': app_name,
|
||||
'installed': state['installed'],
|
||||
'installedVersion': installed_version,
|
||||
'latestAvailable': latest_branch,
|
||||
'latestOverall': latest_global,
|
||||
'updateAvailable': update_available,
|
||||
'crossBranchUpdateSuggested': False,
|
||||
'versions': versions,
|
||||
'packageName': state['packageName'],
|
||||
'adopted': bool(state['installed'] and not state['markerExists']),
|
||||
'major': major if app_name == 'Elasticsearch' else '',
|
||||
'rabbitmqStream': rmq if app_name == 'RabbitMQ' else '',
|
||||
'rabbitmqVersionsHint': rabbitmq_versions_hint,
|
||||
})
|
||||
|
||||
bootstrap = {'status': 1, 'apps': bootstrap_apps}
|
||||
meta_json = json.dumps(bootstrap, ensure_ascii=False)
|
||||
return services, meta_json
|
||||
|
||||
|
||||
def build_manage_applications_page_data(es_major='8', rabbitmq_stream='4'):
|
||||
"""
|
||||
Build `services` for card HTML and a JSON-serializable bootstrap matching
|
||||
/manageservices/applicationMeta shape (default ES major 8, RMQ stream 4).
|
||||
|
||||
Version lists are cached for _VERSIONS_INVENTORY_TTL_SECONDS to avoid repeated
|
||||
DNF/repoquery on every page view; install status is always detected live.
|
||||
"""
|
||||
support = managed_apps_os_support()
|
||||
major = str(es_major).strip() if str(es_major).strip() in ('7', '8', '9') else '8'
|
||||
rmq = str(rabbitmq_stream).strip() if str(rabbitmq_stream).strip() in ('3', '4') else '4'
|
||||
cache_key = 'major:{0}|rmq:{1}|support:{2}'.format(
|
||||
major, rmq, 1 if support.get('supported') else 0
|
||||
)
|
||||
|
||||
version_inv = _resolve_version_inventory(cache_key, major, rmq, support)
|
||||
return _assemble_manage_applications_payload(major, rmq, support, version_inv)
|
||||
|
||||
|
||||
def get_application_meta_response_dict(es_major='8', rabbitmq_stream='4'):
|
||||
"""
|
||||
JSON payload for POST /manageservices/applicationMeta.
|
||||
Shares the same version-list inventory cache as the Manage Applications HTML bootstrap.
|
||||
"""
|
||||
support = managed_apps_os_support()
|
||||
major = str(es_major).strip() if str(es_major).strip() in ('7', '8', '9') else '8'
|
||||
rmq = str(rabbitmq_stream).strip() if str(rabbitmq_stream).strip() in ('3', '4') else '4'
|
||||
|
||||
_, meta_json = build_manage_applications_page_data(major, rmq)
|
||||
payload = json.loads(meta_json)
|
||||
return {
|
||||
'status': 1,
|
||||
'osSupportedForManagedApps': support['supported'],
|
||||
'unsupportedReason': support['reason'],
|
||||
'apps': payload.get('apps') or [],
|
||||
}
|
||||
146
manageServices/application_rabbitmq.py
Normal file
146
manageServices/application_rabbitmq.py
Normal file
@@ -0,0 +1,146 @@
|
||||
import time
|
||||
|
||||
from serverStatus.serverStatusUtil import ServerStatusUtil
|
||||
from plogical import CyberCPLogFileWriter as logging
|
||||
|
||||
from manageServices.application_backup import (
|
||||
CHOWN_CMDS,
|
||||
cleanup_managed_backup,
|
||||
create_managed_app_backup,
|
||||
merge_data_from_backup,
|
||||
restore_full_backup,
|
||||
service_is_active,
|
||||
)
|
||||
from manageServices.application_detection import detect_app_state, is_debian_family
|
||||
from manageServices.application_rabbitmq_repo import (
|
||||
normalize_rabbitmq_stream,
|
||||
ensure_rabbitmq_team_repos,
|
||||
ensure_erlang_meets_minimum,
|
||||
)
|
||||
|
||||
|
||||
def adopt_or_reconcile(status_file):
|
||||
state = detect_app_state('RabbitMQ')
|
||||
if state['installed'] and not state['markerExists']:
|
||||
ServerStatusUtil.executioner('touch /home/cyberpanel/rabbitmq', status_file)
|
||||
logging.CyberCPLogFileWriter.statusWriter(
|
||||
ServerStatusUtil.lswsInstallStatusPath,
|
||||
'RabbitMQ detected and adopted by marker reconciliation.\n'
|
||||
)
|
||||
return state
|
||||
|
||||
|
||||
def _resolve_target_version(version, stream):
|
||||
if version and str(version).strip() != 'latest':
|
||||
return str(version).strip()
|
||||
from manageServices.application_versions import get_latest_version
|
||||
return get_latest_version('RabbitMQ', '8', stream) or ''
|
||||
|
||||
|
||||
def _run_rabbitmq_packages(version, status_file, allow_downgrade):
|
||||
ad = ' --allow-downgrade' if allow_downgrade else ''
|
||||
if is_debian_family():
|
||||
if version and version != 'latest':
|
||||
cmd = (
|
||||
'DEBIAN_FRONTEND=noninteractive apt-get install -y '
|
||||
'--allow-downgrades rabbitmq-server={0}'
|
||||
).format(version)
|
||||
else:
|
||||
cmd = 'DEBIAN_FRONTEND=noninteractive apt-get install rabbitmq-server -y'
|
||||
ServerStatusUtil.executioner(cmd, status_file)
|
||||
return
|
||||
|
||||
if version and version != 'latest':
|
||||
cmd = 'dnf install{0} -y rabbitmq-server-{1}'.format(ad, version)
|
||||
ServerStatusUtil.executioner(cmd, status_file)
|
||||
else:
|
||||
cmd = 'dnf install{0} -y rabbitmq-server'.format(ad)
|
||||
ServerStatusUtil.executioner(cmd, status_file)
|
||||
|
||||
|
||||
def install(version='latest', stream='3'):
|
||||
stream = normalize_rabbitmq_stream(stream)
|
||||
status_file = open(ServerStatusUtil.lswsInstallStatusPath, 'w')
|
||||
adopt_or_reconcile(status_file)
|
||||
|
||||
from manageServices.application_versions import version_compare
|
||||
|
||||
ensure_rabbitmq_team_repos(stream, status_file=status_file)
|
||||
ensure_erlang_meets_minimum(stream, version, status_file=status_file)
|
||||
|
||||
state = detect_app_state('RabbitMQ')
|
||||
backup_dir = ''
|
||||
allow_downgrade = False
|
||||
target = _resolve_target_version(version, stream)
|
||||
|
||||
if state['installed'] and state.get('installedVersion'):
|
||||
status_file.write(
|
||||
'Pre-version-change backup and service stop (RabbitMQ)...\n'
|
||||
)
|
||||
status_file.flush()
|
||||
iv = state['installedVersion']
|
||||
if target and version_compare(iv, target) > 0:
|
||||
allow_downgrade = True
|
||||
status_file.write('Downgrade path enabled for RabbitMQ.\n')
|
||||
status_file.flush()
|
||||
backup_dir = create_managed_app_backup('RabbitMQ', status_file)
|
||||
ServerStatusUtil.executioner('systemctl stop rabbitmq-server', status_file)
|
||||
|
||||
_run_rabbitmq_packages(version, status_file, allow_downgrade)
|
||||
if backup_dir:
|
||||
merge_data_from_backup('RabbitMQ', backup_dir, status_file)
|
||||
ServerStatusUtil.executioner(CHOWN_CMDS['RabbitMQ'], status_file)
|
||||
|
||||
ServerStatusUtil.executioner('systemctl enable rabbitmq-server', status_file)
|
||||
ServerStatusUtil.executioner('systemctl start rabbitmq-server', status_file)
|
||||
time.sleep(4)
|
||||
|
||||
if backup_dir:
|
||||
if service_is_active('RabbitMQ'):
|
||||
cleanup_managed_backup(backup_dir, status_file)
|
||||
status_file.write(
|
||||
'RabbitMQ version change completed; backup removed after success.\n'
|
||||
)
|
||||
else:
|
||||
status_file.write('RabbitMQ failed to start; restoring from backup...\n')
|
||||
restore_full_backup(backup_dir, status_file)
|
||||
ServerStatusUtil.executioner(CHOWN_CMDS['RabbitMQ'], status_file)
|
||||
ServerStatusUtil.executioner('systemctl start rabbitmq-server', status_file)
|
||||
time.sleep(4)
|
||||
if not service_is_active('RabbitMQ'):
|
||||
status_file.write(
|
||||
'Recovery unclear — backup kept at {0}\n'.format(backup_dir)
|
||||
)
|
||||
else:
|
||||
status_file.write(
|
||||
'Prior state restored from backup. Backup retained for safety.\n'
|
||||
)
|
||||
status_file.flush()
|
||||
|
||||
ServerStatusUtil.executioner('touch /home/cyberpanel/rabbitmq', status_file)
|
||||
logging.CyberCPLogFileWriter.statusWriter(
|
||||
ServerStatusUtil.lswsInstallStatusPath, 'RabbitMQ installed.[200]\n', 1
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def upgrade(version='latest', stream='3'):
|
||||
return install(version=version, stream=stream)
|
||||
|
||||
|
||||
def remove():
|
||||
status_file = open(ServerStatusUtil.lswsInstallStatusPath, 'w')
|
||||
ServerStatusUtil.executioner('systemctl stop rabbitmq-server', status_file)
|
||||
ServerStatusUtil.executioner('systemctl disable rabbitmq-server', status_file)
|
||||
if is_debian_family():
|
||||
ServerStatusUtil.executioner(
|
||||
'DEBIAN_FRONTEND=noninteractive apt-get remove rabbitmq-server -y',
|
||||
status_file,
|
||||
)
|
||||
else:
|
||||
ServerStatusUtil.executioner('yum erase rabbitmq-server -y', status_file)
|
||||
ServerStatusUtil.executioner('rm -f /home/cyberpanel/rabbitmq', status_file)
|
||||
logging.CyberCPLogFileWriter.statusWriter(
|
||||
ServerStatusUtil.lswsInstallStatusPath, 'RabbitMQ removed.[200]\n', 1
|
||||
)
|
||||
return 0
|
||||
422
manageServices/application_rabbitmq_repo.py
Normal file
422
manageServices/application_rabbitmq_repo.py
Normal file
@@ -0,0 +1,422 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Team RabbitMQ package repositories (Packagecloud) and Erlang compatibility
|
||||
for RabbitMQ 3.x vs 4.x installation streams.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
from manageServices.application_detection import is_debian_family, rhel_major_from_os_release
|
||||
|
||||
# Official Packagecloud install scripts (RabbitMQ team).
|
||||
_RPM_ERLANG_SCRIPT = (
|
||||
'https://packagecloud.io/install/repositories/rabbitmq/rabbitmq-erlang/script.rpm.sh'
|
||||
)
|
||||
_RPM_SERVER_SCRIPT = (
|
||||
'https://packagecloud.io/install/repositories/rabbitmq/rabbitmq-server/script.rpm.sh'
|
||||
)
|
||||
_DEB_ERLANG_SCRIPT = (
|
||||
'https://packagecloud.io/install/repositories/rabbitmq/rabbitmq-erlang/script.deb.sh'
|
||||
)
|
||||
_DEB_SERVER_SCRIPT = (
|
||||
'https://packagecloud.io/install/repositories/rabbitmq/rabbitmq-server/script.deb.sh'
|
||||
)
|
||||
|
||||
# Minimum OTP major for each product stream (see rabbitmq.com docs / compatibility).
|
||||
_MIN_OTP_STREAM_3 = 25
|
||||
_MIN_OTP_STREAM_4 = 26
|
||||
|
||||
# When Packagecloud metadata lists 3.x but no 4.x (common on el/9 trees), still offer GA
|
||||
# releases from https://www.rabbitmq.com/release-information so the panel can run
|
||||
# dnf install rabbitmq-server-<version> (RPMs are often el8-tagged on EL9 per upstream docs).
|
||||
# Update this tuple when new 4.x patches ship.
|
||||
RABBITMQ_4X_METADATA_FALLBACK_VERSIONS = (
|
||||
'4.2.5',
|
||||
'4.2.4',
|
||||
'4.2.3',
|
||||
'4.2.2',
|
||||
'4.2.1',
|
||||
'4.2.0',
|
||||
'4.1.8',
|
||||
'4.1.7',
|
||||
'4.1.6',
|
||||
'4.1.5',
|
||||
'4.1.4',
|
||||
'4.1.3',
|
||||
'4.1.2',
|
||||
'4.1.1',
|
||||
'4.1.0',
|
||||
'4.0.9',
|
||||
'4.0.8',
|
||||
'4.0.7',
|
||||
'4.0.6',
|
||||
'4.0.5',
|
||||
'4.0.4',
|
||||
'4.0.3',
|
||||
'4.0.2',
|
||||
'4.0.1',
|
||||
'4.0.0',
|
||||
)
|
||||
|
||||
_YUM_REPOS_D = '/etc/yum.repos.d'
|
||||
# Packagecloud RabbitMQ repos use .../el/N/... in baseurl; must match host RHEL major.
|
||||
_EL_URL_SEGMENT = re.compile(r'(/el/)(\d+)(/)')
|
||||
|
||||
|
||||
def _run(cmd, timeout=300):
|
||||
try:
|
||||
res = subprocess.run(
|
||||
cmd, capture_output=True, text=True, timeout=timeout, shell=False
|
||||
)
|
||||
return res.returncode, (res.stdout or ''), (res.stderr or '')
|
||||
except Exception as err:
|
||||
return 1, '', str(err)
|
||||
|
||||
|
||||
def _run_shell_trusted(script_url, timeout=300):
|
||||
"""Run packagecloud install script from fixed RabbitMQ-team URL only."""
|
||||
allowed = {_RPM_ERLANG_SCRIPT, _RPM_SERVER_SCRIPT, _DEB_ERLANG_SCRIPT, _DEB_SERVER_SCRIPT}
|
||||
if script_url not in allowed:
|
||||
return 1, '', 'Invalid repository script URL.'
|
||||
# curl -fsSL ... | bash (URLs are allowlisted above)
|
||||
cmd = 'curl -1fsSL {0} | bash'.format(script_url)
|
||||
return _run(['/bin/bash', '-lc', cmd], timeout=timeout)
|
||||
|
||||
|
||||
def normalize_rabbitmq_stream(value):
|
||||
s = str(value or '4').strip()
|
||||
if s in ('4', '4.x', '41', '4.1'):
|
||||
return '4'
|
||||
return '3'
|
||||
|
||||
|
||||
def _write_status(status_file, message):
|
||||
if status_file is None:
|
||||
return
|
||||
try:
|
||||
status_file.write(message + '\n')
|
||||
status_file.flush()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _rhel_refresh_package_metadata(status_file=None, aggressive=False):
|
||||
"""
|
||||
Refresh DNF/YUM metadata after adding Packagecloud repos.
|
||||
Retries on failure. When aggressive (e.g. 4.x stream), expire cache first
|
||||
so new rabbitmq-server builds become visible.
|
||||
"""
|
||||
if is_debian_family():
|
||||
return True
|
||||
if aggressive:
|
||||
exp_rc, _, exp_err = _run(['dnf', 'clean', 'expire-cache'], timeout=90)
|
||||
if exp_rc != 0:
|
||||
_write_status(
|
||||
status_file,
|
||||
'dnf expire-cache (non-fatal): ' + (exp_err or '')[:120]
|
||||
)
|
||||
last_err = ''
|
||||
for attempt in range(1, 4):
|
||||
for cache_cmd in (['dnf', 'makecache', '-y'], ['yum', 'makecache', '-y']):
|
||||
c_rc, c_out, c_err = _run(cache_cmd, timeout=180)
|
||||
if c_rc == 0:
|
||||
_write_status(
|
||||
status_file,
|
||||
'RPM metadata refreshed ({0}, attempt {1}).'.format(
|
||||
cache_cmd[0], attempt
|
||||
)
|
||||
)
|
||||
return True
|
||||
last_err = (c_err or c_out or str(c_rc)).strip()
|
||||
time.sleep(min(3 * attempt, 15))
|
||||
_write_status(
|
||||
status_file,
|
||||
'RPM metadata refresh failed after retries: ' + (last_err or 'unknown')[:240]
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def refresh_rhel_metadata_for_rabbitmq_repos(status_file=None):
|
||||
"""
|
||||
Public: force another metadata refresh (e.g. when repoquery finds no 4.x RPMs).
|
||||
"""
|
||||
return _rhel_refresh_package_metadata(status_file=status_file, aggressive=True)
|
||||
|
||||
|
||||
def align_rabbitmq_packagecloud_repos_to_os(status_file=None):
|
||||
"""
|
||||
If Team RabbitMQ Packagecloud .repo files point at /el/M/ but this host is el/N,
|
||||
rewrite URLs to /el/N/ (e.g. stale el/8 on AlmaLinux 9). Only touches files that
|
||||
mention both packagecloud.io and rabbitmq. Requires root to write /etc/yum.repos.d.
|
||||
"""
|
||||
if is_debian_family():
|
||||
return
|
||||
target_major = rhel_major_from_os_release()
|
||||
if target_major is None:
|
||||
return
|
||||
if not os.path.isdir(_YUM_REPOS_D):
|
||||
return
|
||||
try:
|
||||
repo_names = sorted(
|
||||
n for n in os.listdir(_YUM_REPOS_D) if n.endswith('.repo')
|
||||
)
|
||||
except OSError as err:
|
||||
_write_status(
|
||||
status_file,
|
||||
'rabbitmq repo align: cannot list {0}: {1}'.format(
|
||||
_YUM_REPOS_D, str(err)[:100]
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
for repo_name in repo_names:
|
||||
repo_path = os.path.join(_YUM_REPOS_D, repo_name)
|
||||
try:
|
||||
with open(repo_path, 'r', encoding='utf-8', errors='replace') as handle:
|
||||
original = handle.read()
|
||||
except OSError:
|
||||
continue
|
||||
lower = original.lower()
|
||||
if 'packagecloud.io' not in lower or 'rabbitmq' not in lower:
|
||||
continue
|
||||
|
||||
def _sub_el(match):
|
||||
current = int(match.group(2))
|
||||
if current == target_major:
|
||||
return match.group(0)
|
||||
return match.group(1) + str(target_major) + match.group(3)
|
||||
|
||||
updated = _EL_URL_SEGMENT.sub(_sub_el, original)
|
||||
if updated == original:
|
||||
continue
|
||||
tmp_path = None
|
||||
try:
|
||||
fd, tmp_path = tempfile.mkstemp(
|
||||
prefix='.cybercp-rabbitmq-',
|
||||
suffix='.tmp',
|
||||
dir=_YUM_REPOS_D,
|
||||
text=True,
|
||||
)
|
||||
with os.fdopen(fd, 'w', encoding='utf-8') as out:
|
||||
out.write(updated)
|
||||
os.replace(tmp_path, repo_path)
|
||||
tmp_path = None
|
||||
_write_status(
|
||||
status_file,
|
||||
'Aligned RabbitMQ Packagecloud repo {0} to el/{1}.'.format(
|
||||
repo_name, target_major
|
||||
)
|
||||
)
|
||||
except PermissionError:
|
||||
_write_status(
|
||||
status_file,
|
||||
'rabbitmq repo align: need root to rewrite {0} (el/{1}).'.format(
|
||||
repo_name, target_major
|
||||
)
|
||||
)
|
||||
except OSError as err:
|
||||
_write_status(
|
||||
status_file,
|
||||
'rabbitmq repo align: {0}: {1}'.format(repo_name, str(err)[:120])
|
||||
)
|
||||
finally:
|
||||
if tmp_path and os.path.isfile(tmp_path):
|
||||
try:
|
||||
os.unlink(tmp_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def refresh_debian_apt_metadata(status_file=None):
|
||||
"""Second-chance apt metadata refresh without re-running Packagecloud scripts."""
|
||||
if not is_debian_family():
|
||||
return True
|
||||
last_err = ''
|
||||
for apt_attempt in range(1, 4):
|
||||
a_rc, _, a_err = _run(['apt-get', 'update', '-y'], timeout=180)
|
||||
if a_rc == 0:
|
||||
_write_status(
|
||||
status_file,
|
||||
'APT metadata refreshed (attempt {0}).'.format(apt_attempt)
|
||||
)
|
||||
return True
|
||||
last_err = (a_err or '').strip()
|
||||
_write_status(
|
||||
status_file,
|
||||
'apt-get update attempt {0}: {1}'.format(apt_attempt, (last_err or '')[:160])
|
||||
)
|
||||
time.sleep(min(3 * apt_attempt, 12))
|
||||
return False
|
||||
|
||||
|
||||
def ensure_rabbitmq_team_repos(stream, status_file=None):
|
||||
"""
|
||||
Idempotently enable rabbitmq-erlang and rabbitmq-server Packagecloud repos.
|
||||
Required so 3.13.x, 4.x, and matching Erlang builds are visible to the
|
||||
package manager.
|
||||
"""
|
||||
stream = normalize_rabbitmq_stream(stream)
|
||||
_write_status(
|
||||
status_file,
|
||||
'Ensuring Team RabbitMQ repositories (stream {0})...'.format(stream)
|
||||
)
|
||||
if is_debian_family():
|
||||
rc, out, err = _run_shell_trusted(_DEB_ERLANG_SCRIPT)
|
||||
if rc != 0:
|
||||
_write_status(status_file, 'rabbitmq-erlang repo script: ' + (err or out or 'failed'))
|
||||
rc2, out2, err2 = _run_shell_trusted(_DEB_SERVER_SCRIPT)
|
||||
if rc2 != 0:
|
||||
_write_status(
|
||||
status_file, 'rabbitmq-server repo script: ' + (err2 or out2 or 'failed')
|
||||
)
|
||||
for apt_attempt in range(1, 4):
|
||||
a_rc, _, a_err = _run(['apt-get', 'update', '-y'], timeout=180)
|
||||
if a_rc == 0:
|
||||
break
|
||||
_write_status(
|
||||
status_file,
|
||||
'apt-get update attempt {0}: {1}'.format(apt_attempt, (a_err or '')[:160])
|
||||
)
|
||||
time.sleep(min(3 * apt_attempt, 12))
|
||||
else:
|
||||
align_rabbitmq_packagecloud_repos_to_os(status_file=status_file)
|
||||
rc, out, err = _run_shell_trusted(_RPM_ERLANG_SCRIPT)
|
||||
if rc != 0:
|
||||
_write_status(status_file, 'rabbitmq-erlang repo script: ' + (err or out or 'failed'))
|
||||
rc2, out2, err2 = _run_shell_trusted(_RPM_SERVER_SCRIPT)
|
||||
if rc2 != 0:
|
||||
_write_status(
|
||||
status_file, 'rabbitmq-server repo script: ' + (err2 or out2 or 'failed')
|
||||
)
|
||||
# 4.x builds may appear after a fresh metadata pull; expire + retries help visibility.
|
||||
_rhel_refresh_package_metadata(
|
||||
status_file=status_file,
|
||||
aggressive=(stream == '4'),
|
||||
)
|
||||
_write_status(status_file, 'Team RabbitMQ repositories ready.')
|
||||
|
||||
|
||||
def get_erlang_otp_major():
|
||||
"""Best-effort current Erlang/OTP major version (integer or 0 if unknown)."""
|
||||
rc, out, _ = _run(
|
||||
[
|
||||
'erl',
|
||||
'-noshell',
|
||||
'-eval',
|
||||
'io:format("~s~n", [erlang:system_info(otp_release)]), halt().',
|
||||
],
|
||||
timeout=15,
|
||||
)
|
||||
if rc == 0 and out:
|
||||
m = re.search(r'(\d+)', out.strip())
|
||||
if m:
|
||||
return int(m.group(1))
|
||||
# rpm: erlang from RabbitMQ repo may report R26 flavour
|
||||
rc2, out2, _ = _run(['rpm', '-q', '--qf', '%{VERSION}', 'erlang'], timeout=10)
|
||||
if rc2 == 0 and out2:
|
||||
m = re.search(r'^(\d+)', out2.strip())
|
||||
if m:
|
||||
return int(m.group(1))
|
||||
return 0
|
||||
|
||||
|
||||
def minimum_otp_for_stream(stream):
|
||||
stream = normalize_rabbitmq_stream(stream)
|
||||
if stream == '4':
|
||||
return _MIN_OTP_STREAM_4
|
||||
return _MIN_OTP_STREAM_3
|
||||
|
||||
|
||||
def minimum_otp_for_rabbitmq_version(version_str):
|
||||
"""Infer OTP floor from chosen RabbitMQ version when possible."""
|
||||
if not version_str or version_str == 'latest':
|
||||
return None
|
||||
m = re.match(r'^(\d+)', str(version_str).strip())
|
||||
if not m:
|
||||
return None
|
||||
major = int(m.group(1))
|
||||
if major >= 4:
|
||||
return _MIN_OTP_STREAM_4
|
||||
if major >= 3:
|
||||
return _MIN_OTP_STREAM_3
|
||||
return None
|
||||
|
||||
|
||||
def ensure_erlang_meets_minimum(stream, version, status_file=None):
|
||||
"""
|
||||
Upgrade/install Erlang from enabled repos if OTP is below the minimum
|
||||
for the selected RabbitMQ stream or explicit target version.
|
||||
"""
|
||||
stream = normalize_rabbitmq_stream(stream)
|
||||
need = minimum_otp_for_stream(stream)
|
||||
version_floor = minimum_otp_for_rabbitmq_version(version)
|
||||
if version_floor is not None:
|
||||
need = max(need, version_floor)
|
||||
|
||||
current = get_erlang_otp_major()
|
||||
if current >= need:
|
||||
_write_status(
|
||||
status_file,
|
||||
'Erlang/OTP {0} satisfies minimum {1} for this RabbitMQ target.'.format(
|
||||
current or 'unknown', need
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
_write_status(
|
||||
status_file,
|
||||
'Erlang/OTP {0} is below required {1}; installing/upgrading erlang from Team RabbitMQ repo...'.format(
|
||||
current or 'unknown', need
|
||||
)
|
||||
)
|
||||
if is_debian_family():
|
||||
_run(
|
||||
[
|
||||
'/bin/bash',
|
||||
'-lc',
|
||||
'DEBIAN_FRONTEND=noninteractive apt-get install -y erlang',
|
||||
],
|
||||
timeout=600,
|
||||
)
|
||||
else:
|
||||
for inst in (
|
||||
['dnf', 'install', '-y', 'erlang'],
|
||||
['yum', 'install', '-y', 'erlang'],
|
||||
):
|
||||
rc, _, _ = _run(inst, timeout=600)
|
||||
if rc == 0:
|
||||
break
|
||||
|
||||
after = get_erlang_otp_major()
|
||||
if after < need:
|
||||
_write_status(
|
||||
status_file,
|
||||
'WARNING: Erlang may still be below OTP {0} (reported {1}). '
|
||||
'Check /root/cyberpanel or logs and install correct erlang package.'.format(
|
||||
need, after or 'unknown'
|
||||
)
|
||||
)
|
||||
else:
|
||||
_write_status(status_file, 'Erlang/OTP updated to {0}.'.format(after))
|
||||
|
||||
|
||||
def filter_versions_for_stream(versions, stream):
|
||||
"""Keep only versions whose major matches RabbitMQ stream (3 or 4)."""
|
||||
stream = normalize_rabbitmq_stream(stream)
|
||||
result = []
|
||||
seen = set()
|
||||
for raw in versions or []:
|
||||
v = (raw or '').strip()
|
||||
if not v or v in seen:
|
||||
continue
|
||||
if v == 'latest':
|
||||
continue
|
||||
m = re.search(r'(\d+)\.(\d+)', v)
|
||||
if m and m.group(1) == stream:
|
||||
seen.add(v)
|
||||
result.append(v)
|
||||
# Preserve descending-ish order (caller already sorted newest first)
|
||||
return result
|
||||
132
manageServices/application_redis.py
Normal file
132
manageServices/application_redis.py
Normal file
@@ -0,0 +1,132 @@
|
||||
import time
|
||||
|
||||
from serverStatus.serverStatusUtil import ServerStatusUtil
|
||||
from plogical import CyberCPLogFileWriter as logging
|
||||
|
||||
from manageServices.application_backup import (
|
||||
CHOWN_CMDS,
|
||||
cleanup_managed_backup,
|
||||
create_managed_app_backup,
|
||||
merge_data_from_backup,
|
||||
restore_full_backup,
|
||||
service_is_active,
|
||||
)
|
||||
from manageServices.application_detection import detect_app_state, is_debian_family
|
||||
|
||||
|
||||
def adopt_or_reconcile(status_file):
|
||||
state = detect_app_state('Redis')
|
||||
if state['installed'] and not state['markerExists']:
|
||||
ServerStatusUtil.executioner('touch /home/cyberpanel/redis', status_file)
|
||||
logging.CyberCPLogFileWriter.statusWriter(
|
||||
ServerStatusUtil.lswsInstallStatusPath,
|
||||
'Redis detected and adopted by marker reconciliation.\n'
|
||||
)
|
||||
return state
|
||||
|
||||
|
||||
def _resolve_target_version(version):
|
||||
if version and str(version).strip() != 'latest':
|
||||
return str(version).strip()
|
||||
from manageServices.application_versions import get_latest_version
|
||||
return get_latest_version('Redis', '8', '3') or ''
|
||||
|
||||
|
||||
def _run_redis_packages(version, status_file, allow_downgrade):
|
||||
ad = ' --allow-downgrade' if allow_downgrade else ''
|
||||
if is_debian_family():
|
||||
if version and version != 'latest':
|
||||
cmd = (
|
||||
'DEBIAN_FRONTEND=noninteractive apt-get install -y '
|
||||
'--allow-downgrades redis-server={0}'
|
||||
).format(version)
|
||||
else:
|
||||
cmd = 'DEBIAN_FRONTEND=noninteractive apt-get install redis-server -y'
|
||||
ServerStatusUtil.executioner(cmd, status_file)
|
||||
return
|
||||
|
||||
if version and version != 'latest':
|
||||
cmd = 'dnf install{0} -y redis-{1}'.format(ad, version)
|
||||
ServerStatusUtil.executioner(cmd, status_file)
|
||||
else:
|
||||
cmd = 'dnf install{0} -y redis'.format(ad)
|
||||
ServerStatusUtil.executioner(cmd, status_file)
|
||||
|
||||
|
||||
def install(version='latest'):
|
||||
status_file = open(ServerStatusUtil.lswsInstallStatusPath, 'w')
|
||||
adopt_or_reconcile(status_file)
|
||||
|
||||
from manageServices.application_versions import version_compare
|
||||
|
||||
state = detect_app_state('Redis')
|
||||
backup_dir = ''
|
||||
allow_downgrade = False
|
||||
target = _resolve_target_version(version)
|
||||
|
||||
if state['installed'] and state.get('installedVersion'):
|
||||
status_file.write('Pre-version-change backup and service stop (Redis)...\n')
|
||||
status_file.flush()
|
||||
iv = state['installedVersion']
|
||||
if target and version_compare(iv, target) > 0:
|
||||
allow_downgrade = True
|
||||
status_file.write('Downgrade path enabled for Redis.\n')
|
||||
status_file.flush()
|
||||
backup_dir = create_managed_app_backup('Redis', status_file)
|
||||
ServerStatusUtil.executioner('systemctl stop redis', status_file)
|
||||
|
||||
_run_redis_packages(version, status_file, allow_downgrade)
|
||||
if backup_dir:
|
||||
merge_data_from_backup('Redis', backup_dir, status_file)
|
||||
ServerStatusUtil.executioner(CHOWN_CMDS['Redis'], status_file)
|
||||
|
||||
ServerStatusUtil.executioner('systemctl enable redis', status_file)
|
||||
ServerStatusUtil.executioner('systemctl start redis', status_file)
|
||||
time.sleep(2)
|
||||
|
||||
if backup_dir:
|
||||
if service_is_active('Redis'):
|
||||
cleanup_managed_backup(backup_dir, status_file)
|
||||
status_file.write(
|
||||
'Redis version change completed; backup removed after success.\n'
|
||||
)
|
||||
else:
|
||||
status_file.write('Redis failed to start; restoring from backup...\n')
|
||||
restore_full_backup(backup_dir, status_file)
|
||||
ServerStatusUtil.executioner(CHOWN_CMDS['Redis'], status_file)
|
||||
ServerStatusUtil.executioner('systemctl start redis', status_file)
|
||||
time.sleep(2)
|
||||
if not service_is_active('Redis'):
|
||||
status_file.write(
|
||||
'Recovery unclear — backup kept at {0}\n'.format(backup_dir)
|
||||
)
|
||||
else:
|
||||
status_file.write(
|
||||
'Prior state restored from backup. Backup retained for safety.\n'
|
||||
)
|
||||
status_file.flush()
|
||||
|
||||
ServerStatusUtil.executioner('touch /home/cyberpanel/redis', status_file)
|
||||
logging.CyberCPLogFileWriter.statusWriter(
|
||||
ServerStatusUtil.lswsInstallStatusPath, 'Redis installed.[200]\n', 1
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def upgrade(version='latest'):
|
||||
return install(version=version)
|
||||
|
||||
|
||||
def remove():
|
||||
status_file = open(ServerStatusUtil.lswsInstallStatusPath, 'w')
|
||||
if is_debian_family():
|
||||
ServerStatusUtil.executioner(
|
||||
'DEBIAN_FRONTEND=noninteractive apt-get remove redis-server -y', status_file
|
||||
)
|
||||
else:
|
||||
ServerStatusUtil.executioner('yum erase redis -y', status_file)
|
||||
ServerStatusUtil.executioner('rm -f /home/cyberpanel/redis', status_file)
|
||||
logging.CyberCPLogFileWriter.statusWriter(
|
||||
ServerStatusUtil.lswsInstallStatusPath, 'Redis removed.[200]\n', 1
|
||||
)
|
||||
return 0
|
||||
475
manageServices/application_versions.py
Normal file
475
manageServices/application_versions.py
Normal file
@@ -0,0 +1,475 @@
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
|
||||
from manageServices.application_detection import (
|
||||
is_debian_family,
|
||||
package_name_for_app,
|
||||
rhel_major_from_os_release,
|
||||
)
|
||||
|
||||
# applicationMeta can call get_available_versions many times per request (ES 7/8/9, RMQ 3/4).
|
||||
# Concurrent DNF from every WSGI worker exhausts lscpd and returns HTTP 503. Cache + serialize cold fetches.
|
||||
_VERSION_CACHE = {}
|
||||
_VERSION_CACHE_LOCK = threading.Lock()
|
||||
_DNF_COLD_FETCH_LOCK = threading.Lock()
|
||||
|
||||
# Seconds; override with CYBERCP_MANAGED_APPS_VERSION_CACHE_TTL if needed.
|
||||
# Default 3600 matches Manage Applications version-inventory TTL (reduces DNF after cache expiry).
|
||||
_CACHE_TTL_SEC = int(os.environ.get('CYBERCP_MANAGED_APPS_VERSION_CACHE_TTL', '3600'))
|
||||
|
||||
|
||||
def _version_cache_key(app_name, es_major, rabbitmq_stream):
|
||||
debian = is_debian_family()
|
||||
if app_name == 'Elasticsearch':
|
||||
em = normalize_elasticsearch_major(es_major)
|
||||
else:
|
||||
em = ''
|
||||
rs = ''
|
||||
if app_name == 'RabbitMQ':
|
||||
from manageServices.application_rabbitmq_repo import normalize_rabbitmq_stream
|
||||
rs = normalize_rabbitmq_stream(rabbitmq_stream)
|
||||
return (str(app_name), em, rs, debian)
|
||||
|
||||
|
||||
def _cache_get_versions(key):
|
||||
now = time.monotonic()
|
||||
with _VERSION_CACHE_LOCK:
|
||||
entry = _VERSION_CACHE.get(key)
|
||||
if not entry:
|
||||
return None
|
||||
ts, versions = entry
|
||||
if (now - ts) >= _CACHE_TTL_SEC:
|
||||
try:
|
||||
del _VERSION_CACHE[key]
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
||||
# Never use a poisoned empty cache (DNF timeout / lock) as a hit.
|
||||
if not versions:
|
||||
try:
|
||||
del _VERSION_CACHE[key]
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
||||
return list(versions)
|
||||
|
||||
|
||||
def _cache_put_versions(key, versions):
|
||||
snap = list(versions or [])
|
||||
if not snap:
|
||||
return
|
||||
with _VERSION_CACHE_LOCK:
|
||||
_VERSION_CACHE[key] = (time.monotonic(), snap)
|
||||
|
||||
# User-writable DNF snippet dir (panel runs as user `cyberpanel`; cannot rely on /etc).
|
||||
_CYBERPANEL_DNF_EXTRA = '/home/cyberpanel/.cyberpanel-dnf/repos.d'
|
||||
|
||||
|
||||
def _version_tuple(ver):
|
||||
"""Numeric tuple for semver-style compare; empty if not usable."""
|
||||
if ver is None:
|
||||
return ()
|
||||
s = str(ver).strip()
|
||||
if not s or s.lower() == 'latest':
|
||||
return ()
|
||||
parts = []
|
||||
for x in re.findall(r'\d+', s):
|
||||
try:
|
||||
parts.append(int(x))
|
||||
except ValueError:
|
||||
break
|
||||
return tuple(parts)
|
||||
|
||||
|
||||
def version_compare(a, b):
|
||||
"""
|
||||
Compare two version strings: return -1 if a < b, 0 if equal or incomparable, 1 if a > b.
|
||||
"""
|
||||
ta = _version_tuple(a)
|
||||
tb = _version_tuple(b)
|
||||
if not ta or not tb:
|
||||
return 0
|
||||
length = max(len(ta), len(tb))
|
||||
for i in range(length):
|
||||
x = ta[i] if i < len(ta) else 0
|
||||
y = tb[i] if i < len(tb) else 0
|
||||
if x < y:
|
||||
return -1
|
||||
if x > y:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
def _max_version_string(candidates):
|
||||
best = ''
|
||||
for v in candidates or []:
|
||||
if not v:
|
||||
continue
|
||||
if not best or version_compare(best, v) < 0:
|
||||
best = v
|
||||
return best
|
||||
|
||||
|
||||
def _run(cmd, timeout=120):
|
||||
try:
|
||||
res = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
|
||||
return res.returncode, (res.stdout or ''), (res.stderr or '')
|
||||
except Exception as err:
|
||||
return 1, '', str(err)
|
||||
|
||||
|
||||
def normalize_elasticsearch_major(es_major):
|
||||
"""Supported Elasticsearch package streams (official artifacts.elastic.co)."""
|
||||
m = str(es_major).strip()
|
||||
if m in ('7', '8', '9'):
|
||||
return m
|
||||
return '8'
|
||||
|
||||
|
||||
def _ensure_cyberpanel_es_repo(es_major):
|
||||
"""Elasticsearch official repo for version discovery (no root; gpg off for repoquery-only)."""
|
||||
major = normalize_elasticsearch_major(es_major)
|
||||
try:
|
||||
os.makedirs(_CYBERPANEL_DNF_EXTRA, mode=0o755, exist_ok=True)
|
||||
except Exception:
|
||||
return
|
||||
path = os.path.join(
|
||||
_CYBERPANEL_DNF_EXTRA, 'cyberpanel-elasticsearch-{0}.repo'.format(major)
|
||||
)
|
||||
content = (
|
||||
'[cyberpanel-elasticsearch-{0}]\n'
|
||||
'name=Elasticsearch {0}.x metadata (CyberPanel)\n'
|
||||
'baseurl=https://artifacts.elastic.co/packages/{0}.x/yum\n'
|
||||
'gpgcheck=0\n'
|
||||
'repo_gpgcheck=0\n'
|
||||
'enabled=1\n'
|
||||
).format(major)
|
||||
try:
|
||||
with open(path, 'w') as handle:
|
||||
handle.write(content)
|
||||
os.chmod(path, 0o644)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _normalize_versions(raw_versions, max_items=25):
|
||||
versions = []
|
||||
seen = set()
|
||||
for item in raw_versions:
|
||||
value = (item or '').strip()
|
||||
if not value or value in seen:
|
||||
continue
|
||||
seen.add(value)
|
||||
versions.append(value)
|
||||
return versions[:max_items]
|
||||
|
||||
|
||||
def _sort_versions_desc(candidates):
|
||||
def key_fn(ver):
|
||||
nums = [int(x) for x in re.findall(r'\d+', ver) if x.isdigit()]
|
||||
return nums or [0]
|
||||
|
||||
try:
|
||||
return sorted(set(candidates), key=key_fn, reverse=True)
|
||||
except Exception:
|
||||
return sorted(set(candidates), reverse=True)
|
||||
|
||||
|
||||
def _dnf_reposdir_flag(use_cyberpanel_extra):
|
||||
if not use_cyberpanel_extra:
|
||||
return []
|
||||
if not os.path.isdir(_CYBERPANEL_DNF_EXTRA):
|
||||
try:
|
||||
os.makedirs(_CYBERPANEL_DNF_EXTRA, mode=0o755, exist_ok=True)
|
||||
except Exception:
|
||||
return []
|
||||
return ['--setopt=reposdir=/etc/yum.repos.d,{0}'.format(_CYBERPANEL_DNF_EXTRA)]
|
||||
|
||||
|
||||
def _rhel_repoquery_versions(
|
||||
pkg_name,
|
||||
use_cyberpanel_extra_repos=False,
|
||||
enablerepos=None,
|
||||
latest_limit=50,
|
||||
normalize_max=25,
|
||||
):
|
||||
"""
|
||||
Resolve distinct %{version} strings from enabled repos.
|
||||
RPM NEVRA text parsing is brittle (el9_7 etc.); repoquery --qf is reliable.
|
||||
|
||||
For RabbitMQ, pass latest_limit=None (no cap — el8-tagged RPMs may share metadata
|
||||
with EL9) and normalize_max=200 so stream filtering (3.x vs 4.x) is not fed only
|
||||
the newest majors (which would hide the other line entirely).
|
||||
"""
|
||||
dnf_cmd = (
|
||||
['dnf']
|
||||
+ _dnf_reposdir_flag(use_cyberpanel_extra_repos)
|
||||
+ [
|
||||
'repoquery',
|
||||
'--available',
|
||||
'--show-duplicates',
|
||||
]
|
||||
)
|
||||
if latest_limit is not None:
|
||||
dnf_cmd.append('--latest-limit={0}'.format(int(latest_limit)))
|
||||
dnf_cmd.extend(['--qf', '%{version}', pkg_name])
|
||||
if enablerepos:
|
||||
for repo_id in enablerepos:
|
||||
dnf_cmd.extend(['--enablerepo', repo_id])
|
||||
rc, out, err = _run(dnf_cmd, timeout=240)
|
||||
raw = []
|
||||
if rc == 0 and out.strip():
|
||||
for line in out.splitlines():
|
||||
v = (line or '').strip()
|
||||
if v and re.match(r'^[0-9]', v):
|
||||
raw.append(v)
|
||||
if raw:
|
||||
return _normalize_versions(_sort_versions_desc(raw), max_items=normalize_max)
|
||||
|
||||
# Legacy systems / fallback
|
||||
yum_cmd = ['yum', 'repoquery', '--available', '--show-duplicates']
|
||||
if latest_limit is not None:
|
||||
yum_cmd.append('--latest-limit={0}'.format(int(latest_limit)))
|
||||
yum_cmd.extend(['--qf', '%{version}', pkg_name])
|
||||
rc2, out2, _ = _run(yum_cmd, timeout=120)
|
||||
raw2 = []
|
||||
if rc2 == 0 and out2.strip():
|
||||
for line in out2.splitlines():
|
||||
v = (line or '').strip()
|
||||
if v and re.match(r'^[0-9]', v):
|
||||
raw2.append(v)
|
||||
if raw2:
|
||||
return _normalize_versions(_sort_versions_desc(raw2), max_items=normalize_max)
|
||||
|
||||
# Oldest fallback: yum list
|
||||
rc3, out3, _ = _run(['yum', '--showduplicates', 'list', pkg_name], timeout=120)
|
||||
raw3 = []
|
||||
if rc3 == 0:
|
||||
for line in out3.splitlines():
|
||||
row = line.strip()
|
||||
if not row or row.startswith('Loaded plugins') or row.startswith('Available'):
|
||||
continue
|
||||
fields = row.split()
|
||||
if len(fields) >= 2 and pkg_name in fields[0]:
|
||||
raw3.append(fields[1])
|
||||
if raw3:
|
||||
return _normalize_versions(_sort_versions_desc(raw3), max_items=normalize_max)
|
||||
return []
|
||||
|
||||
|
||||
def _merge_version_candidates(primary, extra, normalize_max=200):
|
||||
"""Dedupe and sort descending for RabbitMQ multi-source repoquery."""
|
||||
return _normalize_versions(
|
||||
_sort_versions_desc(list(primary or []) + list(extra or [])),
|
||||
max_items=normalize_max,
|
||||
)
|
||||
|
||||
|
||||
def _rhel_repoquery_rabbitmq_packagecloud_el_dist(pkg_name, el_major):
|
||||
"""
|
||||
Query rabbitmq-server versions from a specific Packagecloud el/N path without
|
||||
enabling that repo system-wide. Helps when el/9 metadata lags el/8 for 4.x.
|
||||
"""
|
||||
arch = platform.machine() or 'x86_64'
|
||||
repoid = 'cybercp-pc-rmq-el{0}'.format(int(el_major))
|
||||
base = 'https://packagecloud.io/rabbitmq/rabbitmq-server/el/{0}/{1}'.format(
|
||||
int(el_major), arch
|
||||
)
|
||||
cmd = [
|
||||
'dnf',
|
||||
'repoquery',
|
||||
'--repofrompath={0},{1}'.format(repoid, base),
|
||||
'--setopt={0}.gpgcheck=0'.format(repoid),
|
||||
'--setopt={0}.repo_gpgcheck=0'.format(repoid),
|
||||
'--available',
|
||||
'--show-duplicates',
|
||||
'--qf',
|
||||
'%{version}',
|
||||
pkg_name,
|
||||
]
|
||||
rc, out, _ = _run(cmd, timeout=240)
|
||||
if rc != 0 or not (out or '').strip():
|
||||
return []
|
||||
raw = []
|
||||
for line in out.splitlines():
|
||||
v = (line or '').strip()
|
||||
if v and re.match(r'^[0-9]', v):
|
||||
raw.append(v)
|
||||
return _normalize_versions(_sort_versions_desc(raw), max_items=200)
|
||||
|
||||
|
||||
def _debian_versions(pkg_name, normalize_max=25):
|
||||
versions = []
|
||||
_run(['apt-get', 'update', '-y'], timeout=180)
|
||||
rc, out, _ = _run(['apt-cache', 'madison', pkg_name], timeout=60)
|
||||
if rc != 0:
|
||||
return []
|
||||
for line in out.splitlines():
|
||||
if '|' not in line:
|
||||
continue
|
||||
parts = [p.strip() for p in line.split('|')]
|
||||
if len(parts) >= 2 and parts[1]:
|
||||
versions.append(parts[1])
|
||||
collected = []
|
||||
for v in versions:
|
||||
m = re.search(r'(\d+\.\d+\.\d+)', v)
|
||||
collected.append(m.group(1) if m else v)
|
||||
return _normalize_versions(_sort_versions_desc(collected), max_items=normalize_max)
|
||||
|
||||
|
||||
def _filter_es_major(versions, es_major):
|
||||
major = normalize_elasticsearch_major(es_major)
|
||||
out = []
|
||||
for v in versions or []:
|
||||
head = (v.split('.') or [''])[0]
|
||||
if head == major:
|
||||
out.append(v)
|
||||
return out
|
||||
|
||||
|
||||
def _get_available_versions_uncached(app_name, es_major='8', rabbitmq_stream='4'):
|
||||
pkg_name = package_name_for_app(app_name)
|
||||
if app_name == 'Elasticsearch':
|
||||
pkg_name = 'elasticsearch'
|
||||
|
||||
if not pkg_name:
|
||||
return []
|
||||
|
||||
rmq_stream = '4'
|
||||
if app_name == 'RabbitMQ':
|
||||
from manageServices.application_rabbitmq_repo import (
|
||||
normalize_rabbitmq_stream,
|
||||
ensure_rabbitmq_team_repos,
|
||||
)
|
||||
rmq_stream = normalize_rabbitmq_stream(rabbitmq_stream)
|
||||
ensure_rabbitmq_team_repos(rmq_stream)
|
||||
|
||||
if is_debian_family():
|
||||
if app_name == 'RabbitMQ':
|
||||
versions = _debian_versions(pkg_name, normalize_max=200)
|
||||
else:
|
||||
versions = _debian_versions(pkg_name)
|
||||
if app_name == 'Elasticsearch':
|
||||
versions = _filter_es_major(versions, es_major)
|
||||
else:
|
||||
if app_name == 'Elasticsearch':
|
||||
_ensure_cyberpanel_es_repo(es_major)
|
||||
versions = _rhel_repoquery_versions(
|
||||
pkg_name, use_cyberpanel_extra_repos=True
|
||||
)
|
||||
versions = _filter_es_major(versions, es_major)
|
||||
elif app_name == 'RabbitMQ':
|
||||
versions = _rhel_repoquery_versions(
|
||||
pkg_name, latest_limit=None, normalize_max=200
|
||||
)
|
||||
host_major = rhel_major_from_os_release()
|
||||
# el/9 (and newer) enabled repos often omit 4.x in metadata; el/8 tree may list them.
|
||||
if host_major is not None and host_major >= 9:
|
||||
pc_el8 = _rhel_repoquery_rabbitmq_packagecloud_el_dist(pkg_name, 8)
|
||||
if pc_el8:
|
||||
versions = _merge_version_candidates(versions, pc_el8, 200)
|
||||
else:
|
||||
versions = _rhel_repoquery_versions(pkg_name)
|
||||
|
||||
if app_name == 'RabbitMQ':
|
||||
from manageServices.application_rabbitmq_repo import (
|
||||
RABBITMQ_4X_METADATA_FALLBACK_VERSIONS,
|
||||
filter_versions_for_stream,
|
||||
refresh_debian_apt_metadata,
|
||||
refresh_rhel_metadata_for_rabbitmq_repos,
|
||||
)
|
||||
versions = filter_versions_for_stream(versions, rmq_stream)
|
||||
if not versions:
|
||||
if is_debian_family():
|
||||
refresh_debian_apt_metadata()
|
||||
versions = _debian_versions(pkg_name, normalize_max=200)
|
||||
else:
|
||||
refresh_rhel_metadata_for_rabbitmq_repos()
|
||||
versions = _rhel_repoquery_versions(
|
||||
pkg_name, latest_limit=None, normalize_max=200
|
||||
)
|
||||
host_major = rhel_major_from_os_release()
|
||||
if host_major is not None and host_major >= 9:
|
||||
pc_el8 = _rhel_repoquery_rabbitmq_packagecloud_el_dist(pkg_name, 8)
|
||||
if pc_el8:
|
||||
versions = _merge_version_candidates(versions, pc_el8, 200)
|
||||
versions = filter_versions_for_stream(versions, rmq_stream)
|
||||
# Always offer GA 4.x when DNF lists none (panel user may get empty repoquery).
|
||||
if rmq_stream == '4' and not versions and not is_debian_family():
|
||||
versions = list(RABBITMQ_4X_METADATA_FALLBACK_VERSIONS)
|
||||
versions = _normalize_versions(_sort_versions_desc(versions), max_items=40)
|
||||
return versions
|
||||
|
||||
|
||||
def get_available_versions(app_name, es_major='8', rabbitmq_stream='4'):
|
||||
"""
|
||||
Cached wrapper: avoids hammering DNF from many concurrent panel workers (503 on Manage Applications).
|
||||
"""
|
||||
key = _version_cache_key(app_name, es_major, rabbitmq_stream)
|
||||
hit = _cache_get_versions(key)
|
||||
if hit is not None:
|
||||
return hit
|
||||
|
||||
with _DNF_COLD_FETCH_LOCK:
|
||||
hit2 = _cache_get_versions(key)
|
||||
if hit2 is not None:
|
||||
return hit2
|
||||
versions = _get_available_versions_uncached(
|
||||
app_name, es_major, rabbitmq_stream
|
||||
)
|
||||
if versions:
|
||||
_cache_put_versions(key, versions)
|
||||
return list(versions)
|
||||
|
||||
|
||||
def get_latest_version(app_name, es_major='8', rabbitmq_stream='4'):
|
||||
versions = get_available_versions(app_name, es_major, rabbitmq_stream)
|
||||
if not versions:
|
||||
return ''
|
||||
return versions[0]
|
||||
|
||||
|
||||
def get_branch_and_global_latest(app_name, es_major='8', rabbitmq_stream='4'):
|
||||
"""
|
||||
Latest on the UI-selected branch/stream vs latest across all supported branches.
|
||||
|
||||
Returns (latest_on_branch, latest_global).
|
||||
"""
|
||||
latest_branch = get_latest_version(app_name, es_major, rabbitmq_stream)
|
||||
if app_name == 'Elasticsearch':
|
||||
candidates = []
|
||||
for m in ('7', '8', '9'):
|
||||
v = get_latest_version('Elasticsearch', m, rabbitmq_stream)
|
||||
if v:
|
||||
candidates.append(v)
|
||||
latest_global = _max_version_string(candidates) if candidates else ''
|
||||
elif app_name == 'RabbitMQ':
|
||||
candidates = []
|
||||
for s in ('3', '4'):
|
||||
v = get_latest_version('RabbitMQ', es_major, s)
|
||||
if v:
|
||||
candidates.append(v)
|
||||
latest_global = _max_version_string(candidates) if candidates else ''
|
||||
else:
|
||||
latest_global = latest_branch
|
||||
if not latest_global:
|
||||
latest_global = latest_branch
|
||||
return latest_branch, latest_global
|
||||
|
||||
|
||||
def cross_branch_newer_suggested(installed, latest_branch, latest_global):
|
||||
"""
|
||||
True when installed is current (or ahead of) the selected branch latest but
|
||||
a newer release exists on another line (e.g. 8.x latest installed, 9.x exists).
|
||||
"""
|
||||
if not installed or not latest_global:
|
||||
return False
|
||||
if version_compare(installed, latest_global) >= 0:
|
||||
return False
|
||||
if not latest_branch:
|
||||
return True
|
||||
return version_compare(installed, latest_branch) >= 0
|
||||
@@ -1,6 +1,10 @@
|
||||
import os
|
||||
import os.path
|
||||
import sys
|
||||
import django
|
||||
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
||||
if repo_root not in sys.path:
|
||||
sys.path.append(repo_root)
|
||||
sys.path.append('/usr/local/CyberCP')
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "CyberCP.settings")
|
||||
django.setup()
|
||||
@@ -12,6 +16,10 @@ import argparse
|
||||
from serverStatus.serverStatusUtil import ServerStatusUtil
|
||||
from plogical import CyberCPLogFileWriter as logging
|
||||
import subprocess
|
||||
from manageServices.application_detection import managed_apps_os_support
|
||||
from manageServices import application_elasticsearch
|
||||
from manageServices import application_redis
|
||||
from manageServices import application_rabbitmq
|
||||
|
||||
class ServiceManager:
|
||||
|
||||
@@ -142,184 +150,114 @@ autosecondary=yes
|
||||
Supermasters(ip=self.extraArgs['masterServerIP'], nameserver=self.extraArgs['slaveServerNS'], account='').save()
|
||||
|
||||
@staticmethod
|
||||
def InstallElasticSearch():
|
||||
def InstallElasticSearch(version='latest', esMajor='8'):
|
||||
return application_elasticsearch.install(version=version, es_major=esMajor)
|
||||
|
||||
statusFile = open(ServerStatusUtil.lswsInstallStatusPath, 'w')
|
||||
|
||||
if ProcessUtilities.decideDistro() == ProcessUtilities.centos or ProcessUtilities.decideDistro() == ProcessUtilities.cent8:
|
||||
command = 'rpm --import https://artifacts.elastic.co/GPG-KEY-elasticsearch'
|
||||
ServerStatusUtil.executioner(command, statusFile)
|
||||
|
||||
repoPath = '/etc/yum.repos.d/elasticsearch.repo'
|
||||
|
||||
content = '''
|
||||
[elasticsearch]
|
||||
name=Elasticsearch repository for 7.x packages
|
||||
baseurl=https://artifacts.elastic.co/packages/7.x/yum
|
||||
gpgcheck=1
|
||||
gpgkey=https://artifacts.elastic.co/GPG-KEY-elasticsearch
|
||||
enabled=0
|
||||
autorefresh=1
|
||||
type=rpm-md
|
||||
'''
|
||||
|
||||
writeToFile = open(repoPath, 'w')
|
||||
writeToFile.write(content)
|
||||
writeToFile.close()
|
||||
|
||||
command = 'yum install --enablerepo=elasticsearch elasticsearch -y'
|
||||
ServerStatusUtil.executioner(command, statusFile)
|
||||
else:
|
||||
command = 'wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -'
|
||||
subprocess.call(command, shell=True)
|
||||
|
||||
command = 'apt-get install apt-transport-https -y'
|
||||
ServerStatusUtil.executioner(command, statusFile)
|
||||
|
||||
command = 'echo "deb https://artifacts.elastic.co/packages/7.x/apt stable main" | sudo tee /etc/apt/sources.list.d/elastic-7.x.list'
|
||||
subprocess.call(command, shell=True)
|
||||
|
||||
command = 'apt-get update -y'
|
||||
ServerStatusUtil.executioner(command, statusFile)
|
||||
|
||||
command = 'apt-get install elasticsearch -y'
|
||||
ServerStatusUtil.executioner(command, statusFile)
|
||||
|
||||
### Tmp folder configurations
|
||||
|
||||
command = 'mkdir -p /home/elasticsearch/tmp'
|
||||
ServerStatusUtil.executioner(command, statusFile)
|
||||
|
||||
command = 'chown elasticsearch:elasticsearch /home/elasticsearch/tmp'
|
||||
ServerStatusUtil.executioner(command, statusFile)
|
||||
|
||||
jvmOptions = '/etc/elasticsearch/jvm.options'
|
||||
|
||||
writeToFile = open(jvmOptions, 'a')
|
||||
writeToFile.write('-Djava.io.tmpdir=/home/elasticsearch/tmp\n')
|
||||
writeToFile.close()
|
||||
|
||||
command = 'systemctl enable elasticsearch'
|
||||
ServerStatusUtil.executioner(command, statusFile)
|
||||
|
||||
command = 'systemctl start elasticsearch'
|
||||
ServerStatusUtil.executioner(command, statusFile)
|
||||
|
||||
command = 'touch /home/cyberpanel/elasticsearch'
|
||||
ServerStatusUtil.executioner(command, statusFile)
|
||||
|
||||
|
||||
|
||||
logging.CyberCPLogFileWriter.statusWriter(ServerStatusUtil.lswsInstallStatusPath,
|
||||
"Packages successfully installed.[200]\n", 1)
|
||||
return 0
|
||||
@staticmethod
|
||||
def UpgradeElasticSearch(version='latest', esMajor='8'):
|
||||
return application_elasticsearch.upgrade(version=version, es_major=esMajor)
|
||||
|
||||
@staticmethod
|
||||
def RemoveElasticSearch():
|
||||
|
||||
statusFile = open(ServerStatusUtil.lswsInstallStatusPath, 'w')
|
||||
|
||||
if ProcessUtilities.decideDistro() == ProcessUtilities.centos or ProcessUtilities.decideDistro() == ProcessUtilities.cent8:
|
||||
command = 'rpm --import https://artifacts.elastic.co/GPG-KEY-elasticsearch'
|
||||
ServerStatusUtil.executioner(command, statusFile)
|
||||
|
||||
repoPath = '/etc/yum.repos.d/elasticsearch.repo'
|
||||
|
||||
try:
|
||||
os.remove(repoPath)
|
||||
except:
|
||||
pass
|
||||
|
||||
command = 'yum erase elasticsearch -y'
|
||||
ServerStatusUtil.executioner(command, statusFile)
|
||||
else:
|
||||
|
||||
try:
|
||||
os.remove('/etc/apt/sources.list.d/elastic-7.x.list')
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
command = 'apt-get remove elasticsearch -y'
|
||||
ServerStatusUtil.executioner(command, statusFile)
|
||||
|
||||
### Tmp folder configurations
|
||||
|
||||
command = 'rm -rf /home/elasticsearch/tmp'
|
||||
ServerStatusUtil.executioner(command, statusFile)
|
||||
|
||||
|
||||
command = 'rm -f /home/cyberpanel/elasticsearch'
|
||||
ServerStatusUtil.executioner(command, statusFile)
|
||||
|
||||
logging.CyberCPLogFileWriter.statusWriter(ServerStatusUtil.lswsInstallStatusPath,
|
||||
"ElasticSearch successfully removed.[200]\n", 1)
|
||||
return 0
|
||||
return application_elasticsearch.remove()
|
||||
|
||||
@staticmethod
|
||||
def InstallRedis():
|
||||
def InstallRedis(version='latest'):
|
||||
return application_redis.install(version=version)
|
||||
|
||||
statusFile = open(ServerStatusUtil.lswsInstallStatusPath, 'w')
|
||||
|
||||
if ProcessUtilities.decideDistro() == ProcessUtilities.centos or ProcessUtilities.decideDistro() == ProcessUtilities.cent8:
|
||||
command = 'yum install redis -y'
|
||||
ServerStatusUtil.executioner(command, statusFile)
|
||||
else:
|
||||
|
||||
command = 'DEBIAN_FRONTEND=noninteractive apt-get install redis-server -y'
|
||||
ServerStatusUtil.executioner(command, statusFile)
|
||||
|
||||
|
||||
command = 'systemctl enable redis'
|
||||
ServerStatusUtil.executioner(command, statusFile)
|
||||
|
||||
command = 'systemctl start redis'
|
||||
ServerStatusUtil.executioner(command, statusFile)
|
||||
|
||||
command = 'touch /home/cyberpanel/redis'
|
||||
ServerStatusUtil.executioner(command, statusFile)
|
||||
|
||||
logging.CyberCPLogFileWriter.statusWriter(ServerStatusUtil.lswsInstallStatusPath,
|
||||
"Redis successfully installed.[200]\n", 1)
|
||||
return 0
|
||||
@staticmethod
|
||||
def UpgradeRedis(version='latest'):
|
||||
return application_redis.upgrade(version=version)
|
||||
|
||||
@staticmethod
|
||||
def RemoveRedis():
|
||||
return application_redis.remove()
|
||||
|
||||
statusFile = open(ServerStatusUtil.lswsInstallStatusPath, 'w')
|
||||
@staticmethod
|
||||
def InstallRabbitMQ(version='latest', stream='3'):
|
||||
return application_rabbitmq.install(version=version, stream=stream)
|
||||
|
||||
if ProcessUtilities.decideDistro() == ProcessUtilities.centos or ProcessUtilities.decideDistro() == ProcessUtilities.cent8:
|
||||
command = 'yum erase redis -y'
|
||||
ServerStatusUtil.executioner(command, statusFile)
|
||||
else:
|
||||
@staticmethod
|
||||
def UpgradeRabbitMQ(version='latest', stream='3'):
|
||||
return application_rabbitmq.upgrade(version=version, stream=stream)
|
||||
|
||||
command = 'apt-get remove redis-server -y'
|
||||
ServerStatusUtil.executioner(command, statusFile)
|
||||
|
||||
|
||||
command = 'rm -f /home/cyberpanel/redis'
|
||||
ServerStatusUtil.executioner(command, statusFile)
|
||||
|
||||
logging.CyberCPLogFileWriter.statusWriter(ServerStatusUtil.lswsInstallStatusPath,
|
||||
"Redis successfully removed.[200]\n", 1)
|
||||
return 0
|
||||
@staticmethod
|
||||
def RemoveRabbitMQ():
|
||||
return application_rabbitmq.remove()
|
||||
|
||||
def main():
|
||||
|
||||
parser = argparse.ArgumentParser(description='CyberPanel Application Manager')
|
||||
parser.add_argument('--function', help='Function')
|
||||
|
||||
parser.add_argument('--app', help='Application name')
|
||||
parser.add_argument('--action', help='Action to run: install|remove|upgrade')
|
||||
parser.add_argument('--version', default='latest', help='Target package version or latest')
|
||||
parser.add_argument('--esMajor', default='8', help='Elasticsearch major stream (7|8|9)')
|
||||
parser.add_argument('--rabbitmqStream', default='4', help='RabbitMQ major stream (3|4)')
|
||||
|
||||
args = vars(parser.parse_args())
|
||||
|
||||
support = managed_apps_os_support()
|
||||
if not support['supported']:
|
||||
logging.CyberCPLogFileWriter.statusWriter(
|
||||
ServerStatusUtil.lswsInstallStatusPath,
|
||||
support['reason'] + '\n',
|
||||
1
|
||||
)
|
||||
return
|
||||
|
||||
if args["function"] == "InstallElasticSearch":
|
||||
ServiceManager.InstallElasticSearch()
|
||||
ServiceManager.InstallElasticSearch(version=args.get('version', 'latest'), esMajor=args.get('esMajor', '8'))
|
||||
elif args["function"] == "UpgradeElasticSearch":
|
||||
ServiceManager.UpgradeElasticSearch(version=args.get('version', 'latest'), esMajor=args.get('esMajor', '8'))
|
||||
elif args["function"] == "RemoveElasticSearch":
|
||||
ServiceManager.RemoveElasticSearch()
|
||||
elif args["function"] == "InstallRedis":
|
||||
ServiceManager.InstallRedis()
|
||||
ServiceManager.InstallRedis(version=args.get('version', 'latest'))
|
||||
elif args["function"] == "UpgradeRedis":
|
||||
ServiceManager.UpgradeRedis(version=args.get('version', 'latest'))
|
||||
elif args["function"] == "RemoveRedis":
|
||||
ServiceManager.RemoveRedis()
|
||||
elif args["function"] == "InstallRabbitMQ":
|
||||
ServiceManager.InstallRabbitMQ(
|
||||
version=args.get('version', 'latest'),
|
||||
stream=args.get('rabbitmqStream', '4'),
|
||||
)
|
||||
elif args["function"] == "UpgradeRabbitMQ":
|
||||
ServiceManager.UpgradeRabbitMQ(
|
||||
version=args.get('version', 'latest'),
|
||||
stream=args.get('rabbitmqStream', '4'),
|
||||
)
|
||||
elif args["function"] == "RemoveRabbitMQ":
|
||||
ServiceManager.RemoveRabbitMQ()
|
||||
elif args.get("app") and args.get("action"):
|
||||
app_name = args.get("app")
|
||||
action = args.get("action").lower()
|
||||
version = args.get("version", "latest")
|
||||
es_major = args.get("esMajor", "8")
|
||||
rmq_stream = args.get("rabbitmqStream", "4")
|
||||
|
||||
if app_name == 'Elasticsearch':
|
||||
if action == 'install':
|
||||
ServiceManager.InstallElasticSearch(version=version, esMajor=es_major)
|
||||
elif action == 'upgrade':
|
||||
ServiceManager.UpgradeElasticSearch(version=version, esMajor=es_major)
|
||||
elif action == 'remove':
|
||||
ServiceManager.RemoveElasticSearch()
|
||||
elif app_name == 'Redis':
|
||||
if action == 'install':
|
||||
ServiceManager.InstallRedis(version=version)
|
||||
elif action == 'upgrade':
|
||||
ServiceManager.UpgradeRedis(version=version)
|
||||
elif action == 'remove':
|
||||
ServiceManager.RemoveRedis()
|
||||
elif app_name == 'RabbitMQ':
|
||||
if action == 'install':
|
||||
ServiceManager.InstallRabbitMQ(version=version, stream=rmq_stream)
|
||||
elif action == 'upgrade':
|
||||
ServiceManager.UpgradeRabbitMQ(version=version, stream=rmq_stream)
|
||||
elif action == 'remove':
|
||||
ServiceManager.RemoveRabbitMQ()
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="500"
|
||||
height="500"
|
||||
viewBox="0 0 132.29167 132.29166"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
|
||||
sodipodi:docname="logo-rabbitmq.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:zoom="0.7338665"
|
||||
inkscape:cx="-150.57235"
|
||||
inkscape:cy="293.65014"
|
||||
inkscape:window-width="1916"
|
||||
inkscape:window-height="1029"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-76.200105,-115.62292)">
|
||||
<g
|
||||
id="g1"
|
||||
transform="matrix(3.3139169,0,0,3.3139169,76.216727,114.23118)"
|
||||
style="stroke-width:0.0798401">
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M 39.42,17.37 H 26.65 a 1.59,1.59 0 0 1 -1.6,-1.6 V 3 A 1.59,1.59 0 0 0 23.45,1.41 H 18.67 A 1.59,1.59 0 0 0 17.07,3 v 12.77 a 1.59,1.59 0 0 1 -1.6,1.6 h -4.78 a 1.59,1.59 0 0 1 -1.6,-1.6 V 3 A 1.59,1.59 0 0 0 7.49,1.4 H 2.7 A 1.59,1.59 0 0 0 1.11,3 v 36.72 a 1.59,1.59 0 0 0 1.6,1.6 h 36.71 a 1.59,1.59 0 0 0 1.6,-1.6 V 19 a 1.59,1.59 0 0 0 -1.6,-1.63 z M 33,30.93 a 2.39,2.39 0 0 1 -2.39,2.4 h -3.2 a 2.39,2.39 0 0 1 -2.39,-2.4 v -3.19 a 2.39,2.39 0 0 1 2.39,-2.4 h 3.2 a 2.39,2.39 0 0 1 2.39,2.4 z"
|
||||
transform="translate(-1.11,-0.98)"
|
||||
id="path10"
|
||||
style="fill:#ff6600;stroke-width:0.0798401" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
BIN
manageServices/static/manageServices/images/rabbitmq.png
Normal file
BIN
manageServices/static/manageServices/images/rabbitmq.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
14
manageServices/static/manageServices/images/rabbitmq.svg
Normal file
14
manageServices/static/manageServices/images/rabbitmq.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
|
||||
<rect x="0" y="0" width="512" height="512" rx="96" ry="96" fill="#ff6600"/>
|
||||
|
||||
<rect x="112" y="144" width="104" height="72" fill="#ffffff"/>
|
||||
<rect x="112" y="236" width="104" height="72" fill="#ffffff"/>
|
||||
<rect x="112" y="328" width="104" height="72" fill="#ffffff"/>
|
||||
|
||||
<rect x="244" y="144" width="104" height="72" fill="#ffffff"/>
|
||||
<rect x="244" y="236" width="104" height="72" fill="#ffffff"/>
|
||||
<rect x="244" y="328" width="104" height="72" fill="#ffffff"/>
|
||||
|
||||
<path d="M388 144h8c17.7 0 32 14.3 32 32v192c0 17.7-14.3 32-32 32h-8V144z" fill="#ffffff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 699 B |
@@ -428,10 +428,505 @@ app.controller('pureFTPD', function ($scope, $http, $timeout, $window) {
|
||||
|
||||
app.controller('manageApplications', function ($scope, $http, $timeout, $window) {
|
||||
|
||||
/**
|
||||
* Normalize entries from applicationMeta (strings, numbers, or rare object shapes)
|
||||
* so version pickers never show blank rows. CyberPanel uses {$ ... $} interpolation
|
||||
* in templates; list labels use versionLabel() for consistent display.
|
||||
*/
|
||||
function normalizeVersionToken(v) {
|
||||
if (v === null || v === undefined) {
|
||||
return '';
|
||||
}
|
||||
if (v === 'latest') {
|
||||
return 'latest';
|
||||
}
|
||||
if (typeof v === 'number' && isFinite(v)) {
|
||||
return String(v);
|
||||
}
|
||||
if (angular.isObject(v)) {
|
||||
var o = v;
|
||||
var cand = o.version || o.Version || o.value || o.name || o.ver || o.label;
|
||||
if (cand !== undefined && cand !== null) {
|
||||
return String(cand).trim();
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(o);
|
||||
} catch (ignore) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
return String(v).trim();
|
||||
}
|
||||
|
||||
function sanitizeVersionsArray(vers) {
|
||||
if (!angular.isArray(vers)) {
|
||||
return [];
|
||||
}
|
||||
var out = [];
|
||||
var seen = {};
|
||||
vers.forEach(function (raw) {
|
||||
var t = normalizeVersionToken(raw);
|
||||
if (!t || t === 'latest') {
|
||||
return;
|
||||
}
|
||||
if (!seen[t]) {
|
||||
seen[t] = true;
|
||||
out.push(t);
|
||||
}
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
function versionMatchesRabbitmqStream(ver, stream) {
|
||||
var s = String(stream || '4').trim();
|
||||
var t = normalizeVersionToken(ver);
|
||||
if (!t || t === 'latest') {
|
||||
return false;
|
||||
}
|
||||
var m = /^(\d+)\./.exec(t);
|
||||
return !!(m && m[1] === s);
|
||||
}
|
||||
|
||||
function versionMatchesEsMajor(ver, major) {
|
||||
var mjr = String(major || '8').trim();
|
||||
var t = normalizeVersionToken(ver);
|
||||
if (!t || t === 'latest') {
|
||||
return false;
|
||||
}
|
||||
var m = /^(\d+)\./.exec(t);
|
||||
return !!(m && m[1] === mjr);
|
||||
}
|
||||
|
||||
$scope.versionLabel = function (v) {
|
||||
if (v === 'latest') {
|
||||
return 'latest';
|
||||
}
|
||||
var t = normalizeVersionToken(v);
|
||||
return t || '(unknown)';
|
||||
};
|
||||
|
||||
$scope.versionTrackId = function (idx, v) {
|
||||
return String(idx) + '|' + $scope.versionLabel(v);
|
||||
};
|
||||
|
||||
/* false = long-running install/remove/poll (show spinners); true = idle */
|
||||
$scope.cyberpanelLoading = true;
|
||||
/** Background applicationMeta refresh — separate from cyberpanelLoading so the page does not “freeze” on every modal open. */
|
||||
$scope.appsMetaRefreshing = false;
|
||||
$scope.apps = [
|
||||
{name: 'Elasticsearch', image: '/static/manageServices/images/elastic-search.png'},
|
||||
{name: 'Redis', image: '/static/manageServices/images/redis.png'},
|
||||
{name: 'RabbitMQ', image: '/static/manageServices/images/rabbitmq-logo.svg'}
|
||||
];
|
||||
|
||||
$scope.removeInstall = function (appName, status) {
|
||||
(function mergeMetaBootstrap() {
|
||||
var el = document.getElementById('manageApplicationsMetaBootstrap');
|
||||
if (!el || !el.textContent) {
|
||||
return;
|
||||
}
|
||||
var raw = el.textContent.trim();
|
||||
if (!raw) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var boot = JSON.parse(raw);
|
||||
if (!boot || Number(boot.status) !== 1) {
|
||||
return;
|
||||
}
|
||||
var appMap = {};
|
||||
(boot.apps || []).forEach(function (a) {
|
||||
if (a && a.name) {
|
||||
appMap[a.name] = a;
|
||||
}
|
||||
});
|
||||
$scope.apps = $scope.apps.map(function (baseApp) {
|
||||
var meta = appMap[baseApp.name] || {};
|
||||
var vers = meta.versions;
|
||||
if (!angular.isArray(vers)) {
|
||||
vers = [];
|
||||
}
|
||||
vers = sanitizeVersionsArray(vers);
|
||||
return {
|
||||
name: baseApp.name,
|
||||
image: baseApp.image,
|
||||
installed: !!meta.installed,
|
||||
installedVersion: meta.installedVersion || '',
|
||||
updateAvailable: !!meta.updateAvailable,
|
||||
crossBranchUpdateSuggested: !!meta.crossBranchUpdateSuggested,
|
||||
versions: vers,
|
||||
latestAvailable: meta.latestAvailable || '',
|
||||
latestOverall: meta.latestOverall || '',
|
||||
rabbitmqVersionsHint: meta.rabbitmqVersionsHint || ''
|
||||
};
|
||||
});
|
||||
} catch (ignore) {
|
||||
/* keep bare apps list */
|
||||
}
|
||||
})();
|
||||
|
||||
$scope.selectedVersion = 'latest';
|
||||
/** Row highlight uses $index so only one row looks selected (avoids sticky :focus / repeater quirks). */
|
||||
$scope.selectedVersionRowIndex = 0;
|
||||
$scope.selectedVersions = ['latest'];
|
||||
|
||||
$scope.recalcSelectedVersionRowIndex = function () {
|
||||
var list = $scope.selectedVersions || [];
|
||||
var sel = $scope.selectedVersion;
|
||||
var i = list.indexOf(sel);
|
||||
if (i < 0) {
|
||||
var normSel = normalizeVersionToken(sel);
|
||||
if (normSel) {
|
||||
for (var j = 0; j < list.length; j += 1) {
|
||||
if (normalizeVersionToken(list[j]) === normSel) {
|
||||
i = j;
|
||||
$scope.selectedVersion = list[j];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$scope.selectedVersionRowIndex = (i >= 0) ? i : 0;
|
||||
};
|
||||
|
||||
$scope.selectManagedAppVersion = function (idx, v, $event) {
|
||||
var n = (typeof idx === 'number') ? idx : parseInt(idx, 10);
|
||||
if (!isFinite(n) || n < 0) {
|
||||
n = 0;
|
||||
}
|
||||
$scope.selectedVersionRowIndex = n;
|
||||
$scope.selectedVersion = v;
|
||||
if ($event && $event.target && typeof $event.target.blur === 'function') {
|
||||
$event.target.blur();
|
||||
} else if (typeof document !== 'undefined' && document.activeElement && typeof document.activeElement.blur === 'function') {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
};
|
||||
$scope.selectedEsMajor = '8';
|
||||
$scope.selectedRabbitmqStream = '4';
|
||||
/** RabbitMQ: 4.x is default for new installs (metadata prefetched). Upgrade may require picking stream if version line is unknown. ES major still user-picked before version list loads. */
|
||||
$scope.rabbitmqBranchChosen = false;
|
||||
$scope.esMajorChosen = false;
|
||||
$scope.confirmAction = false;
|
||||
$scope.selectedCurrentVersion = '';
|
||||
|
||||
$scope.chooseRabbitmqStream = function (stream) {
|
||||
var s = String(stream || '4').trim();
|
||||
if (s !== '3' && s !== '4') {
|
||||
s = '4';
|
||||
}
|
||||
$scope.selectedRabbitmqStream = s;
|
||||
$scope.rabbitmqBranchChosen = true;
|
||||
$scope.refreshMeta();
|
||||
};
|
||||
|
||||
$scope.chooseEsMajor = function (major) {
|
||||
var m = String(major || '8').trim();
|
||||
if (m !== '7' && m !== '8' && m !== '9') {
|
||||
m = '8';
|
||||
}
|
||||
$scope.selectedEsMajor = m;
|
||||
$scope.esMajorChosen = true;
|
||||
$scope.refreshMeta();
|
||||
};
|
||||
|
||||
/**
|
||||
* When the install/upgrade modal is open, re-apply version list from latest applicationMeta.
|
||||
* (Page-load meta can be empty for ES if dnf was slow; opening the modal must refetch.)
|
||||
*/
|
||||
$scope.syncModalVersionLists = function () {
|
||||
if (!$scope.appName || ($scope.status !== 'Installing' && $scope.status !== 'Upgrading')) {
|
||||
return;
|
||||
}
|
||||
if ($scope.appName !== 'Elasticsearch' && $scope.appName !== 'Redis' && $scope.appName !== 'RabbitMQ') {
|
||||
return;
|
||||
}
|
||||
if ($scope.appName === 'RabbitMQ' && !$scope.rabbitmqBranchChosen) {
|
||||
$scope.selectedVersions = ['latest'];
|
||||
$scope.selectedVersion = 'latest';
|
||||
$scope.repoShowsOnlyOneStream = false;
|
||||
$scope.recalcSelectedVersionRowIndex();
|
||||
return;
|
||||
}
|
||||
if ($scope.appName === 'Elasticsearch' && !$scope.esMajorChosen) {
|
||||
$scope.selectedVersions = ['latest'];
|
||||
$scope.selectedVersion = 'latest';
|
||||
$scope.repoShowsOnlyOneStream = false;
|
||||
$scope.recalcSelectedVersionRowIndex();
|
||||
return;
|
||||
}
|
||||
var meta = $scope.findAppMeta($scope.appName);
|
||||
var vers = sanitizeVersionsArray((meta && meta.versions) ? meta.versions : []);
|
||||
$scope.selectedVersions = ['latest'].concat(vers);
|
||||
var curRaw = (meta && meta.installedVersion) ? meta.installedVersion : ($scope.selectedCurrentVersion || '');
|
||||
var cur = normalizeVersionToken(curRaw) || String(curRaw || '').trim();
|
||||
if (cur && $scope.selectedVersions.indexOf(cur) === -1) {
|
||||
var allowCur = true;
|
||||
if ($scope.appName === 'RabbitMQ') {
|
||||
allowCur = versionMatchesRabbitmqStream(cur, $scope.selectedRabbitmqStream);
|
||||
} else if ($scope.appName === 'Elasticsearch') {
|
||||
allowCur = versionMatchesEsMajor(cur, $scope.selectedEsMajor);
|
||||
}
|
||||
if (allowCur) {
|
||||
$scope.selectedVersions.push(cur);
|
||||
}
|
||||
}
|
||||
if (cur) {
|
||||
$scope.selectedCurrentVersion = cur;
|
||||
}
|
||||
var prevSel = $scope.selectedVersion;
|
||||
if (prevSel && $scope.selectedVersions.indexOf(prevSel) !== -1) {
|
||||
$scope.selectedVersion = prevSel;
|
||||
} else {
|
||||
$scope.selectedVersion = 'latest';
|
||||
}
|
||||
var realVers = ($scope.selectedVersions || []).filter(function (v) {
|
||||
return v && v !== 'latest';
|
||||
});
|
||||
$scope.repoShowsOnlyOneStream = ($scope.status === 'Upgrading' && realVers.length <= 1);
|
||||
$scope.recalcSelectedVersionRowIndex();
|
||||
};
|
||||
|
||||
$scope.refreshMeta = function () {
|
||||
$scope.appsMetaRefreshing = true;
|
||||
var url = "/manageservices/applicationMeta";
|
||||
var data = {
|
||||
esMajor: $scope.selectedEsMajor,
|
||||
rabbitmqStream: $scope.selectedRabbitmqStream
|
||||
};
|
||||
var config = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=UTF-8',
|
||||
'X-CSRFToken': getCookie('csrftoken')
|
||||
},
|
||||
transformRequest: function (payload) {
|
||||
return angular.toJson(payload);
|
||||
}
|
||||
};
|
||||
|
||||
return $http.post(url, data, config).then(function (response) {
|
||||
$scope.appsMetaRefreshing = false;
|
||||
var payload = response.data;
|
||||
var ok = payload && (payload.status === 1 || payload.status === '1');
|
||||
if (ok) {
|
||||
var appMap = {};
|
||||
(payload.apps || []).forEach(function (app) {
|
||||
appMap[app.name] = app;
|
||||
});
|
||||
var esMetaResp = appMap['Elasticsearch'];
|
||||
var rmqMetaResp = appMap['RabbitMQ'];
|
||||
var respEsMaj = String(esMetaResp && esMetaResp.major != null ? esMetaResp.major : '').trim();
|
||||
if (respEsMaj && respEsMaj !== String($scope.selectedEsMajor || '8').trim()) {
|
||||
return;
|
||||
}
|
||||
var respRmqStream = String(rmqMetaResp && rmqMetaResp.rabbitmqStream != null ? rmqMetaResp.rabbitmqStream : '').trim();
|
||||
if (respRmqStream && respRmqStream !== String($scope.selectedRabbitmqStream || '4').trim()) {
|
||||
return;
|
||||
}
|
||||
$scope.apps = $scope.apps.map(function (baseApp) {
|
||||
var meta = appMap[baseApp.name] || {};
|
||||
var vers = meta.versions;
|
||||
if (!angular.isArray(vers)) {
|
||||
vers = [];
|
||||
}
|
||||
vers = sanitizeVersionsArray(vers);
|
||||
return {
|
||||
name: baseApp.name,
|
||||
image: baseApp.image,
|
||||
installed: !!meta.installed,
|
||||
installedVersion: meta.installedVersion || '',
|
||||
updateAvailable: !!meta.updateAvailable,
|
||||
crossBranchUpdateSuggested: !!meta.crossBranchUpdateSuggested,
|
||||
versions: vers,
|
||||
latestAvailable: meta.latestAvailable || '',
|
||||
latestOverall: meta.latestOverall || '',
|
||||
rabbitmqVersionsHint: meta.rabbitmqVersionsHint || ''
|
||||
};
|
||||
});
|
||||
$scope.syncModalVersionLists();
|
||||
} else {
|
||||
new PNotify({
|
||||
title: 'Operation Failed!',
|
||||
text: (payload && (payload.error_message || payload.errorMessage)) || 'Could not load application metadata.',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
}, function () {
|
||||
$scope.appsMetaRefreshing = false;
|
||||
new PNotify({
|
||||
title: 'Operation Failed!',
|
||||
text: 'Could not connect to server, please refresh this page',
|
||||
type: 'error'
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.prepareAction = function (service, status, bootstrapInstalledVersion) {
|
||||
if (bootstrapInstalledVersion === undefined || bootstrapInstalledVersion === null) {
|
||||
bootstrapInstalledVersion = '';
|
||||
} else {
|
||||
bootstrapInstalledVersion = String(bootstrapInstalledVersion).trim();
|
||||
}
|
||||
$scope.status = status;
|
||||
$scope.appName = service.name;
|
||||
$scope.confirmAction = false;
|
||||
var effectiveInstalled = (service.installedVersion || bootstrapInstalledVersion || '').trim();
|
||||
$scope.selectedCurrentVersion = effectiveInstalled;
|
||||
|
||||
if (service.name === 'RabbitMQ') {
|
||||
if (effectiveInstalled && /^4\./.test(effectiveInstalled)) {
|
||||
$scope.selectedRabbitmqStream = '4';
|
||||
} else if (effectiveInstalled && /^3\./.test(effectiveInstalled)) {
|
||||
$scope.selectedRabbitmqStream = '3';
|
||||
} else if (status === 'Installing') {
|
||||
$scope.selectedRabbitmqStream = '4';
|
||||
}
|
||||
if (status === 'Upgrading' && effectiveInstalled) {
|
||||
if (/^4\./.test(effectiveInstalled) || /^3\./.test(effectiveInstalled)) {
|
||||
$scope.rabbitmqBranchChosen = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (service.name === 'Elasticsearch' && effectiveInstalled) {
|
||||
var iv = effectiveInstalled;
|
||||
if (/^9\./.test(iv)) {
|
||||
$scope.selectedEsMajor = '9';
|
||||
} else if (/^8\./.test(iv)) {
|
||||
$scope.selectedEsMajor = '8';
|
||||
} else if (/^7\./.test(iv)) {
|
||||
$scope.selectedEsMajor = '7';
|
||||
}
|
||||
}
|
||||
|
||||
$scope.selectedVersions = ['latest'];
|
||||
// RabbitMQ upgrade: bootstrap meta is often stream 4; stream follows installed line — do not
|
||||
// reuse service.versions until refreshMeta returns for selectedRabbitmqStream (avoids mismatched list).
|
||||
var deferVersionList = (service.name === 'RabbitMQ' && (!$scope.rabbitmqBranchChosen || status === 'Upgrading'))
|
||||
|| (service.name === 'Elasticsearch' && !$scope.esMajorChosen);
|
||||
if (!deferVersionList) {
|
||||
var svcVers = sanitizeVersionsArray(service.versions || []);
|
||||
if (svcVers.length > 0) {
|
||||
$scope.selectedVersions = ['latest'].concat(svcVers);
|
||||
}
|
||||
var curPick = normalizeVersionToken(effectiveInstalled) || effectiveInstalled;
|
||||
if (curPick && $scope.selectedVersions.indexOf(curPick) === -1) {
|
||||
$scope.selectedVersions.push(curPick);
|
||||
}
|
||||
}
|
||||
$scope.selectedVersion = 'latest';
|
||||
$scope.requestData = '';
|
||||
|
||||
var realVers = ($scope.selectedVersions || []).filter(function (v) {
|
||||
return v && v !== 'latest';
|
||||
});
|
||||
if (deferVersionList) {
|
||||
$scope.repoShowsOnlyOneStream = false;
|
||||
} else {
|
||||
$scope.repoShowsOnlyOneStream = ($scope.status === 'Upgrading' && realVers.length <= 1);
|
||||
}
|
||||
$scope.recalcSelectedVersionRowIndex();
|
||||
};
|
||||
|
||||
$scope.findAppMeta = function (appName) {
|
||||
var found = null;
|
||||
($scope.apps || []).forEach(function (item) {
|
||||
if (item.name === appName) {
|
||||
found = item;
|
||||
}
|
||||
});
|
||||
return found || {};
|
||||
};
|
||||
|
||||
$scope.prepareActionByName = function (appName, status, bootstrapInstalledVersion) {
|
||||
var meta = $scope.findAppMeta(appName);
|
||||
if (!meta.name) {
|
||||
meta = {name: appName, versions: []};
|
||||
}
|
||||
var mver = meta.versions;
|
||||
if (!angular.isArray(mver)) {
|
||||
mver = [];
|
||||
}
|
||||
var merged = {
|
||||
name: meta.name,
|
||||
image: meta.image,
|
||||
installed: meta.installed,
|
||||
installedVersion: meta.installedVersion || '',
|
||||
versions: mver
|
||||
};
|
||||
$scope.prepareAction(merged, status, bootstrapInstalledVersion);
|
||||
};
|
||||
|
||||
/**
|
||||
* Prepare scope then show modal (do not use data-toggle + ng-click — Bootstrap can
|
||||
* open the dialog before Angular runs ng-click, leaving status/appName unset).
|
||||
* For managed apps, wait for applicationMeta so version lists and installedVersion are correct (upgrade vs downgrade).
|
||||
*/
|
||||
$scope.openApplicationsModal = function (appName, status, bootstrapInstalledVersion) {
|
||||
var needMeta = (appName === 'RabbitMQ' || appName === 'Elasticsearch' || appName === 'Redis');
|
||||
var showModal = function () {
|
||||
if (typeof window.jQuery !== 'undefined' && jQuery('#settings').modal) {
|
||||
jQuery('#settings').modal('show');
|
||||
}
|
||||
};
|
||||
if (needMeta) {
|
||||
if (appName === 'RabbitMQ') {
|
||||
if (status === 'Installing') {
|
||||
$scope.selectedRabbitmqStream = '4';
|
||||
$scope.rabbitmqBranchChosen = true;
|
||||
} else {
|
||||
$scope.rabbitmqBranchChosen = false;
|
||||
}
|
||||
}
|
||||
if (appName === 'Elasticsearch') {
|
||||
$scope.esMajorChosen = false;
|
||||
}
|
||||
// RabbitMQ install: default 4.x stream and prefetch metadata. Upgrade: pick stream from installed version.
|
||||
// Elasticsearch: still wait for user major. Redis refreshes on open.
|
||||
$scope.appName = appName;
|
||||
$scope.status = status;
|
||||
$scope.prepareActionByName(appName, status, bootstrapInstalledVersion);
|
||||
showModal();
|
||||
if (appName === 'Redis' || (appName === 'RabbitMQ' && $scope.rabbitmqBranchChosen)) {
|
||||
$timeout(function () {
|
||||
$scope.refreshMeta();
|
||||
}, 0);
|
||||
}
|
||||
} else {
|
||||
$scope.prepareActionByName(appName, status, bootstrapInstalledVersion);
|
||||
showModal();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.runAction = function () {
|
||||
var appName = $scope.appName;
|
||||
var status = $scope.status;
|
||||
if (!appName || !status) {
|
||||
return;
|
||||
}
|
||||
if ((status === 'Removing' || status === 'Upgrading') && !$scope.confirmAction) {
|
||||
new PNotify({
|
||||
title: 'Confirmation Required',
|
||||
text: 'Please confirm this action before proceeding.',
|
||||
type: 'warning'
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (appName === 'RabbitMQ' && (status === 'Installing' || status === 'Upgrading') && !$scope.rabbitmqBranchChosen) {
|
||||
new PNotify({
|
||||
title: 'Stream required',
|
||||
text: 'Choose RabbitMQ 4.x or 3.x above to load versions for that line.',
|
||||
type: 'warning'
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (appName === 'Elasticsearch' && (status === 'Installing' || status === 'Upgrading') && !$scope.esMajorChosen) {
|
||||
new PNotify({
|
||||
title: 'Major version required',
|
||||
text: 'Choose Elasticsearch major (7, 8, or 9) above to load versions for that line.',
|
||||
type: 'warning'
|
||||
});
|
||||
return;
|
||||
}
|
||||
$scope.status = status;
|
||||
$scope.appName = appName;
|
||||
|
||||
@@ -441,7 +936,11 @@ app.controller('manageApplications', function ($scope, $http, $timeout, $window)
|
||||
|
||||
var data = {
|
||||
appName: appName,
|
||||
status: status
|
||||
status: status,
|
||||
version: $scope.selectedVersion || 'latest',
|
||||
esMajor: $scope.selectedEsMajor || '8',
|
||||
rabbitmqStream: $scope.selectedRabbitmqStream || '4',
|
||||
confirmAction: $scope.confirmAction === true
|
||||
};
|
||||
|
||||
var config = {
|
||||
@@ -524,6 +1023,18 @@ app.controller('manageApplications', function ($scope, $http, $timeout, $window)
|
||||
|
||||
}
|
||||
|
||||
// Do not fetch package metadata on page load; it can block workers under DNF load.
|
||||
// Metadata is fetched on-demand when opening install/version-change modals.
|
||||
|
||||
if (typeof window.jQuery !== 'undefined' && jQuery.fn.on) {
|
||||
jQuery('#settings').on('hidden.bs.modal', function () {
|
||||
$scope.$evalAsync(function () {
|
||||
$scope.rabbitmqBranchChosen = false;
|
||||
$scope.esMajorChosen = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
/* Java script code */
|
||||
@@ -260,8 +260,113 @@
|
||||
|
||||
.modal-body {
|
||||
padding: 25px;
|
||||
/* Theme may set light text globally; native <select>/<option> must stay dark-on-light. */
|
||||
color: var(--text-primary, #2f3640);
|
||||
}
|
||||
|
||||
|
||||
.applications-container .modal-body select.form-control {
|
||||
color: #0f172a;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.applications-container .modal-body select.form-control option {
|
||||
color: #0f172a;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.applications-container #settings .modal-body .manage-apps-version-select {
|
||||
color: #0f172a;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.applications-container #settings .modal-body .manage-apps-version-select option {
|
||||
color: #0f172a;
|
||||
background-color: #f8fafc;
|
||||
}
|
||||
|
||||
/* Native <select> open state is OS-drawn; use a custom list so version text is always readable. */
|
||||
.manage-apps-version-picker {
|
||||
border: 1px solid #94a3b8;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: #ffffff;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.manage-apps-version-current {
|
||||
padding: 10px 12px;
|
||||
background: #e2e8f0;
|
||||
color: #0f172a;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
border-bottom: 1px solid #94a3b8;
|
||||
}
|
||||
|
||||
.manage-apps-version-rows {
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.manage-apps-version-row {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: 10px 14px;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
background: #ffffff !important;
|
||||
color: #0f172a !important;
|
||||
font-size: 14px;
|
||||
line-height: 1.35;
|
||||
cursor: pointer;
|
||||
-webkit-text-fill-color: #0f172a;
|
||||
}
|
||||
|
||||
/* Only one row should read as "selected"; non-active rows stay white unless hover/focus. */
|
||||
.manage-apps-version-rows button.manage-apps-version-row:not(.is-active) {
|
||||
background: #ffffff !important;
|
||||
}
|
||||
|
||||
.manage-apps-version-rows button.manage-apps-version-row:not(.is-active):hover,
|
||||
.manage-apps-version-rows button.manage-apps-version-row:not(.is-active):focus {
|
||||
background: #f1f5f9 !important;
|
||||
color: #0f172a !important;
|
||||
-webkit-text-fill-color: #0f172a;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.manage-apps-version-rows button.manage-apps-version-row.is-active,
|
||||
.manage-apps-version-rows button.manage-apps-version-row.is-active:hover,
|
||||
.manage-apps-version-rows button.manage-apps-version-row.is-active:focus {
|
||||
background: #c7d2fe !important;
|
||||
color: #1e1b4b !important;
|
||||
-webkit-text-fill-color: #1e1b4b;
|
||||
font-weight: 600;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.manage-apps-version-row:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
/* Win over theme + .ng-binding readability rules (must stay dark on white row). */
|
||||
.applications-container #settings .modal-body .manage-apps-version-row.ng-binding {
|
||||
color: #0f172a !important;
|
||||
-webkit-text-fill-color: #0f172a !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .applications-container #settings .modal-body .manage-apps-version-row.ng-binding {
|
||||
color: #0f172a !important;
|
||||
-webkit-text-fill-color: #0f172a !important;
|
||||
}
|
||||
|
||||
.applications-container #settings .modal-body .manage-apps-version-row.ng-binding.is-active {
|
||||
color: #1e1b4b !important;
|
||||
-webkit-text-fill-color: #1e1b4b !important;
|
||||
}
|
||||
|
||||
.install-log {
|
||||
background: var(--bg-secondary, #f8f9ff);
|
||||
border: 1px solid var(--border-color, #e8e9ff);
|
||||
@@ -358,6 +463,7 @@
|
||||
|
||||
<div class="applications-wrapper">
|
||||
<div class="applications-container" ng-controller="manageApplications">
|
||||
<script type="application/json" id="manageApplicationsMetaBootstrap">{{ application_meta_bootstrap_json|safe }}</script>
|
||||
<!-- Page Header -->
|
||||
<div class="page-header">
|
||||
<h1>
|
||||
@@ -373,7 +479,7 @@
|
||||
<div class="content-section">
|
||||
<h2 class="section-title">
|
||||
{% trans "Available Applications" %}
|
||||
<span ng-hide="cyberpanelLoading" class="loading-spinner"></span>
|
||||
<span ng-cloak ng-hide="cyberpanelLoading" class="loading-spinner"></span>
|
||||
</h2>
|
||||
|
||||
{% if services %}
|
||||
@@ -397,6 +503,21 @@
|
||||
{% trans "Not Installed" %}
|
||||
</span>
|
||||
{% endif %}
|
||||
<div ng-if="findAppMeta('{{ service.name }}').installed && findAppMeta('{{ service.name }}').updateAvailable" style="margin-top: 8px;">
|
||||
<span class="app-status" style="background:#dbeafe;color:#1e40af;font-size:12px;padding:4px 10px;border-radius:6px;display:inline-flex;align-items:center;gap:6px;">
|
||||
<i class="fas fa-arrow-circle-up"></i>
|
||||
{% trans "Update available" %}
|
||||
</span>
|
||||
</div>
|
||||
{% if service.installedVersion %}
|
||||
<div style="font-size: 12px; margin-top: 8px; color: #64748b;">
|
||||
{% trans "Installed Version" %}: {{ service.installedVersion }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div style="font-size: 12px; margin-top: 8px; color: #64748b;" ng-if="findAppMeta('{{ service.name }}').installedVersion">
|
||||
{% trans "Installed Version" %}: {$ findAppMeta('{{ service.name }}').installedVersion $}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -405,31 +526,37 @@
|
||||
{% trans "A distributed, RESTful search and analytics engine capable of addressing a growing number of use cases." %}
|
||||
{% elif service.name == 'Redis' %}
|
||||
{% trans "An in-memory data structure store, used as a database, cache, and message broker." %}
|
||||
{% elif service.name == 'RabbitMQ' %}
|
||||
{% trans "A reliable message broker for asynchronous processing, queueing, and service-to-service communication." %}
|
||||
{% else %}
|
||||
{% trans "Powerful application for your server infrastructure." %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="app-actions">
|
||||
{% if service.installed == 'Installed' %}
|
||||
<button type="button"
|
||||
class="action-btn remove"
|
||||
data-toggle="modal"
|
||||
data-target="#settings"
|
||||
ng-click="removeInstall('{{ service.name }}', 'Removing')">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
{% trans "Remove" %}
|
||||
</button>
|
||||
{% else %}
|
||||
<button type="button"
|
||||
class="action-btn install"
|
||||
data-toggle="modal"
|
||||
data-target="#settings"
|
||||
ng-click="removeInstall('{{ service.name }}', 'Installing')">
|
||||
<button type="button"
|
||||
class="action-btn install"
|
||||
{% if service.installed == 'Installed' %}style="display:none;"{% endif %}
|
||||
ng-click="openApplicationsModal('{{ service.name }}', 'Installing')">
|
||||
<i class="fas fa-download"></i>
|
||||
{% trans "Install" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
<button type="button"
|
||||
class="action-btn remove"
|
||||
{% if service.installed != 'Installed' %}style="display:none;"{% endif %}
|
||||
ng-click="openApplicationsModal('{{ service.name }}', 'Removing', '{{ service.installedVersion|escapejs }}')">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
{% trans "Remove" %}
|
||||
</button>
|
||||
|
||||
<button type="button"
|
||||
class="action-btn install"
|
||||
{% if service.installed != 'Installed' %}style="display:none;"{% endif %}
|
||||
ng-click="openApplicationsModal('{{ service.name }}', 'Upgrading', '{{ service.installedVersion|escapejs }}')">
|
||||
<i class="fas fa-exchange-alt"></i>
|
||||
{% trans "Change version" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
@@ -451,12 +578,101 @@
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">
|
||||
{$ status $} {$ appName $}
|
||||
<span ng-hide="cyberpanelLoading" class="loading-spinner"></span>
|
||||
<span ng-if="status == 'Installing'">{% trans "Installing" %}</span>
|
||||
<span ng-if="status == 'Removing'">{% trans "Removing" %}</span>
|
||||
<span ng-if="status == 'Upgrading'">{% trans "Change version" %}</span>
|
||||
{$ appName $}
|
||||
<span ng-cloak ng-show="appsMetaRefreshing || !cyberpanelLoading" class="loading-spinner"></span>
|
||||
</h4>
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div style="margin-bottom: 10px;" ng-if="appName == 'Elasticsearch' && (status == 'Installing' || status == 'Upgrading')">
|
||||
<label style="color: #0f172a;">{% trans "Elasticsearch Major" %}</label>
|
||||
<div class="manage-apps-version-picker">
|
||||
<div class="manage-apps-version-current">{% trans "Major" %}:
|
||||
<span ng-if="esMajorChosen" ng-bind="selectedEsMajor"></span>
|
||||
<span ng-if="!esMajorChosen">{% trans "Select a major below — versions load after you choose." %}</span>
|
||||
</div>
|
||||
<div class="manage-apps-version-rows">
|
||||
<button type="button" class="manage-apps-version-row" ng-class="{'is-active': esMajorChosen && selectedEsMajor === '7'}" ng-click="chooseEsMajor('7')">7</button>
|
||||
<button type="button" class="manage-apps-version-row" ng-class="{'is-active': esMajorChosen && selectedEsMajor === '8'}" ng-click="chooseEsMajor('8')">8</button>
|
||||
<button type="button" class="manage-apps-version-row" ng-class="{'is-active': esMajorChosen && selectedEsMajor === '9'}" ng-click="chooseEsMajor('9')">9</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-bottom: 10px;" ng-if="appName == 'RabbitMQ' && (status == 'Installing' || status == 'Upgrading')">
|
||||
<label style="color: #0f172a;">{% trans "RabbitMQ major line" %}</label>
|
||||
<div class="manage-apps-version-picker">
|
||||
<div class="manage-apps-version-current">{% trans "Stream" %}:
|
||||
<span ng-if="rabbitmqBranchChosen && selectedRabbitmqStream === '4'">4.x</span>
|
||||
<span ng-if="rabbitmqBranchChosen && selectedRabbitmqStream === '3'">3.x ({% trans "maintenance" %})</span>
|
||||
<span ng-if="!rabbitmqBranchChosen">{% trans "Select a stream below — versions load after you choose." %}</span>
|
||||
</div>
|
||||
<div class="manage-apps-version-rows">
|
||||
<button type="button" class="manage-apps-version-row" ng-class="{'is-active': rabbitmqBranchChosen && selectedRabbitmqStream === '4'}" ng-click="chooseRabbitmqStream('4')">4.x</button>
|
||||
<button type="button" class="manage-apps-version-row" ng-class="{'is-active': rabbitmqBranchChosen && selectedRabbitmqStream === '3'}" ng-click="chooseRabbitmqStream('3')">3.x ({% trans "maintenance" %})</button>
|
||||
</div>
|
||||
</div>
|
||||
<p style="margin-top: 8px; font-size: 12px; color: #64748b;">
|
||||
{% trans "Uses Team RabbitMQ Packagecloud repos; 4.x requires a compatible Erlang (OTP 26+). The installer will try to align Erlang when needed." %}
|
||||
{% trans "Upstream RPMs may be named el8 but are still supported on AlmaLinux/RHEL/Rocky 9." %}
|
||||
</p>
|
||||
<div style="margin-top: 10px; padding: 10px 12px; background: #fff7ed; border-radius: 8px; font-size: 12px; color: #9a3412; border: 1px solid #fed7aa;"
|
||||
ng-if="rabbitmqBranchChosen && findAppMeta('RabbitMQ').rabbitmqVersionsHint">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<span ng-bind="findAppMeta('RabbitMQ').rabbitmqVersionsHint"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-bottom: 10px;" ng-if="(status == 'Installing' || status == 'Upgrading') && (appName != 'RabbitMQ' || rabbitmqBranchChosen) && (appName != 'Elasticsearch' || esMajorChosen)">
|
||||
<label style="color: #0f172a;">{% trans "Version" %}</label>
|
||||
<div class="manage-apps-version-picker">
|
||||
<div class="manage-apps-version-current">
|
||||
{% trans "Selected" %}: <span>{$ versionLabel(selectedVersion) $}</span>
|
||||
</div>
|
||||
<div class="manage-apps-version-rows">
|
||||
<button type="button"
|
||||
class="manage-apps-version-row"
|
||||
ng-repeat="v in selectedVersions track by versionTrackId($index, v)"
|
||||
ng-class="{'is-active': selectedVersionRowIndex === $index}"
|
||||
ng-click="selectManagedAppVersion($index, v, $event)">{$ versionLabel(v) $}</button>
|
||||
</div>
|
||||
</div>
|
||||
<p style="margin-top: 8px; font-size: 12px; color: #475569;" ng-if="status == 'Upgrading'">
|
||||
{% trans "There is no separate Downgrade button: pick any version lower than your current one in this list to downgrade (the installer allows package downgrades when needed)." %}
|
||||
</p>
|
||||
<p style="margin-top: 6px; font-size: 12px; color: #b45309;" ng-if="status == 'Upgrading' && repoShowsOnlyOneStream">
|
||||
{% trans "Only one package version is visible from your enabled repositories, so there may be nothing older to select. Older builds often require archive or vault repositories for your OS, or another distro major that still publishes them." %}
|
||||
</p>
|
||||
</div>
|
||||
<div style="margin-bottom: 10px; font-size: 13px; color: #64748b;" ng-if="status == 'Upgrading' && selectedCurrentVersion">
|
||||
{% trans "Current Version" %}: {$ selectedCurrentVersion $}
|
||||
</div>
|
||||
<div style="margin-bottom: 10px; padding: 8px 10px; background: #f8fafc; border-radius: 6px; font-size: 12px; color: #475569;"
|
||||
ng-if="status == 'Upgrading'">
|
||||
{% trans "A backup of application data is created automatically before upgrading or downgrading. Data is merged into the new version after packages install; the backup is removed only after a successful start." %}
|
||||
</div>
|
||||
<div style="margin-bottom: 10px; padding: 10px 12px; background: #f0f9ff; border-radius: 8px; font-size: 13px; color: #1e3a5f; border: 1px solid #bae6fd;"
|
||||
ng-if="(status == 'Installing' || status == 'Upgrading') && findAppMeta(appName).crossBranchUpdateSuggested">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
{% trans "A newer release is available on another version line. Change the major/stream above, pick a version, then run a version change (upgrade or downgrade)." %}
|
||||
<span ng-if="findAppMeta(appName).latestOverall"> {$ findAppMeta(appName).latestOverall $}</span>
|
||||
</div>
|
||||
<div style="margin-bottom: 10px;" ng-if="status == 'Removing' || status == 'Upgrading'">
|
||||
<label>
|
||||
{# ng-if creates a child scope; bind to parent so runAction() sees the same flag #}
|
||||
<input type="checkbox" ng-model="$parent.confirmAction">
|
||||
<span ng-if="status == 'Removing'">{% trans "Confirm Remove" %}</span>
|
||||
<span ng-if="status == 'Upgrading'">{% trans "Confirm version change" %}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div style="margin-bottom: 10px;">
|
||||
<button type="button" class="btn btn-primary" ng-click="runAction()">
|
||||
<span ng-if="status == 'Installing'">{% trans "Start Install" %}</span>
|
||||
<span ng-if="status == 'Removing'">{% trans "Start Remove" %}</span>
|
||||
<span ng-if="status == 'Upgrading'">{% trans "Start version change" %}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="install-log">
|
||||
<textarea ng-model="requestData" rows="15" class="log-textarea" readonly></textarea>
|
||||
</div>
|
||||
@@ -467,6 +683,4 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Manage Applications JS -->
|
||||
<script src="{% static 'manageServices/manageServices.js' %}"></script>
|
||||
{% endblock %}
|
||||
@@ -10,5 +10,6 @@ urlpatterns = [
|
||||
path('saveStatus', views.saveStatus, name='saveStatus'),
|
||||
|
||||
path('manageApplications', views.manageApplications, name='manageApplications'),
|
||||
path('applicationMeta', views.applicationMeta, name='applicationMeta'),
|
||||
path('removeInstall', views.removeInstall, name='removeInstall'),
|
||||
]
|
||||
|
||||
@@ -5,6 +5,7 @@ import plogical.CyberCPLogFileWriter as logging
|
||||
from loginSystem.views import loadLoginPage
|
||||
import os
|
||||
import json
|
||||
import shlex
|
||||
|
||||
from plogical.httpProc import httpProc
|
||||
from plogical.mailUtilities import mailUtilities
|
||||
@@ -12,6 +13,11 @@ from plogical.acl import ACLManager
|
||||
from .models import PDNSStatus, SlaveServers
|
||||
from .serviceManager import ServiceManager
|
||||
from plogical.processUtilities import ProcessUtilities
|
||||
from .application_detection import managed_apps_os_support
|
||||
from .application_page_meta import (
|
||||
build_manage_applications_page_data,
|
||||
get_application_meta_response_dict,
|
||||
)
|
||||
# Create your views here.
|
||||
|
||||
def managePowerDNS(request):
|
||||
@@ -270,34 +276,57 @@ def saveStatus(request):
|
||||
return HttpResponse(json_data)
|
||||
|
||||
def manageApplications(request):
|
||||
services = []
|
||||
services, application_meta_bootstrap_json = build_manage_applications_page_data(
|
||||
'8', '4'
|
||||
)
|
||||
|
||||
## ElasticSearch
|
||||
|
||||
esPath = '/home/cyberpanel/elasticsearch'
|
||||
rPath = '/home/cyberpanel/redis'
|
||||
|
||||
if os.path.exists(esPath):
|
||||
installed = 'Installed'
|
||||
else:
|
||||
installed = 'Not-Installed'
|
||||
|
||||
if os.path.exists(rPath):
|
||||
rInstalled = 'Installed'
|
||||
else:
|
||||
rInstalled = 'Not-Installed'
|
||||
|
||||
elasticSearch = {'image': '/static/manageServices/images/elastic-search.png', 'name': 'Elasticsearch',
|
||||
'installed': installed}
|
||||
redis = {'image': '/static/manageServices/images/redis.png', 'name': 'Redis',
|
||||
'installed': rInstalled}
|
||||
services.append(elasticSearch)
|
||||
services.append(redis)
|
||||
|
||||
proc = httpProc(request, 'manageServices/applications.html',
|
||||
{'services': services}, 'admin')
|
||||
proc = httpProc(
|
||||
request,
|
||||
'manageServices/applications.html',
|
||||
{
|
||||
'services': services,
|
||||
'application_meta_bootstrap_json': application_meta_bootstrap_json,
|
||||
},
|
||||
'admin',
|
||||
)
|
||||
return proc.render()
|
||||
|
||||
|
||||
def applicationMeta(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
currentACL = ACLManager.loadedACL(userID)
|
||||
|
||||
if currentACL['admin'] != 1:
|
||||
return ACLManager.loadErrorJson()
|
||||
|
||||
data = {}
|
||||
if request.method == 'POST':
|
||||
data = json.loads(request.body)
|
||||
|
||||
requested_major = str(data.get('esMajor', '8'))
|
||||
if requested_major not in ('7', '8', '9'):
|
||||
requested_major = '8'
|
||||
|
||||
requested_rmq_stream = str(data.get('rabbitmqStream', '4')).strip()
|
||||
if requested_rmq_stream not in ('3', '4'):
|
||||
requested_rmq_stream = '4'
|
||||
|
||||
response_data = get_application_meta_response_dict(
|
||||
requested_major, requested_rmq_stream
|
||||
)
|
||||
|
||||
return HttpResponse(
|
||||
json.dumps(response_data, ensure_ascii=False),
|
||||
content_type='application/json; charset=utf-8',
|
||||
)
|
||||
|
||||
except BaseException as msg:
|
||||
return HttpResponse(
|
||||
json.dumps({'status': 0, 'error_message': str(msg)}, ensure_ascii=False),
|
||||
content_type='application/json; charset=utf-8',
|
||||
)
|
||||
|
||||
def removeInstall(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
@@ -312,17 +341,63 @@ def removeInstall(request):
|
||||
|
||||
status = data['status']
|
||||
appName = data['appName']
|
||||
version = str(data.get('version', 'latest')).strip() or 'latest'
|
||||
esMajor = str(data.get('esMajor', '8')).strip() or '8'
|
||||
if esMajor not in ('7', '8', '9'):
|
||||
esMajor = '8'
|
||||
rabbitmqStream = str(data.get('rabbitmqStream', '4')).strip() or '4'
|
||||
if rabbitmqStream not in ('3', '4'):
|
||||
rabbitmqStream = '4'
|
||||
confirmAction = bool(data.get('confirmAction', False))
|
||||
|
||||
support = managed_apps_os_support()
|
||||
if not support['supported']:
|
||||
data_ret = {'status': 0, 'error_message': support['reason']}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
|
||||
if status in ('Removing', 'Upgrading') and not confirmAction:
|
||||
data_ret = {'status': 0, 'error_message': 'Action confirmation is required.'}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
|
||||
if appName == 'Elasticsearch':
|
||||
if status == 'Installing':
|
||||
command = '/usr/local/CyberCP/bin/python /usr/local/CyberCP/manageServices/serviceManager.py --function InstallElasticSearch'
|
||||
command = '/usr/local/CyberCP/bin/python /usr/local/CyberCP/manageServices/serviceManager.py --app Elasticsearch --action install --version {0} --esMajor {1}'.format(
|
||||
shlex.quote(version), shlex.quote(esMajor)
|
||||
)
|
||||
elif status == 'Upgrading':
|
||||
command = '/usr/local/CyberCP/bin/python /usr/local/CyberCP/manageServices/serviceManager.py --app Elasticsearch --action upgrade --version {0} --esMajor {1}'.format(
|
||||
shlex.quote(version), shlex.quote(esMajor)
|
||||
)
|
||||
else:
|
||||
command = '/usr/local/CyberCP/bin/python /usr/local/CyberCP/manageServices/serviceManager.py --function RemoveElasticSearch'
|
||||
command = '/usr/local/CyberCP/bin/python /usr/local/CyberCP/manageServices/serviceManager.py --app Elasticsearch --action remove'
|
||||
elif appName == 'Redis':
|
||||
if status == 'Installing':
|
||||
command = '/usr/local/CyberCP/bin/python /usr/local/CyberCP/manageServices/serviceManager.py --function InstallRedis'
|
||||
command = '/usr/local/CyberCP/bin/python /usr/local/CyberCP/manageServices/serviceManager.py --app Redis --action install --version {0}'.format(
|
||||
shlex.quote(version)
|
||||
)
|
||||
elif status == 'Upgrading':
|
||||
command = '/usr/local/CyberCP/bin/python /usr/local/CyberCP/manageServices/serviceManager.py --app Redis --action upgrade --version {0}'.format(
|
||||
shlex.quote(version)
|
||||
)
|
||||
else:
|
||||
command = '/usr/local/CyberCP/bin/python /usr/local/CyberCP/manageServices/serviceManager.py --function RemoveRedis'
|
||||
command = '/usr/local/CyberCP/bin/python /usr/local/CyberCP/manageServices/serviceManager.py --app Redis --action remove'
|
||||
elif appName == 'RabbitMQ':
|
||||
if status == 'Installing':
|
||||
command = '/usr/local/CyberCP/bin/python /usr/local/CyberCP/manageServices/serviceManager.py --app RabbitMQ --action install --version {0} --rabbitmqStream {1}'.format(
|
||||
shlex.quote(version), shlex.quote(rabbitmqStream)
|
||||
)
|
||||
elif status == 'Upgrading':
|
||||
command = '/usr/local/CyberCP/bin/python /usr/local/CyberCP/manageServices/serviceManager.py --app RabbitMQ --action upgrade --version {0} --rabbitmqStream {1}'.format(
|
||||
shlex.quote(version), shlex.quote(rabbitmqStream)
|
||||
)
|
||||
else:
|
||||
command = '/usr/local/CyberCP/bin/python /usr/local/CyberCP/manageServices/serviceManager.py --app RabbitMQ --action remove'
|
||||
else:
|
||||
data_ret = {'status': 0, 'error_message': 'Unknown application selected.'}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
|
||||
ProcessUtilities.popenExecutioner(command)
|
||||
data_ret = {'status': 1}
|
||||
|
||||
209
plogical/pluginMigrationSQL.py
Normal file
209
plogical/pluginMigrationSQL.py
Normal file
@@ -0,0 +1,209 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Plugin install/remove: robust DB migration and teardown for CyberPanel.
|
||||
|
||||
- Prefer Django ``migrate`` / ``migrate zero``.
|
||||
- If the global loader fails or migrate errors, apply ``sqlmigrate`` output and
|
||||
``--fake``, or drop plugin tables and clean ``django_migrations`` on removal.
|
||||
|
||||
Assumes ``manage.py`` / ``CyberCP.settings`` are available under /usr/local/CyberCP.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
|
||||
def _manage_py() -> str:
|
||||
return '/usr/local/CyberCP/manage.py'
|
||||
|
||||
|
||||
def _python_executable() -> str:
|
||||
for candidate in ('/usr/local/CyberCP/bin/python', '/usr/local/CyberCP/bin/python3'):
|
||||
try:
|
||||
if os.path.isfile(candidate) and os.access(candidate, os.X_OK):
|
||||
return candidate
|
||||
except OSError:
|
||||
continue
|
||||
return 'python3'
|
||||
|
||||
|
||||
def _django_setup():
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'CyberCP.settings')
|
||||
import django
|
||||
|
||||
django.setup()
|
||||
|
||||
|
||||
def _db_name() -> str:
|
||||
_django_setup()
|
||||
from django.conf import settings
|
||||
|
||||
return settings.DATABASES['default']['NAME']
|
||||
|
||||
|
||||
def run_manage(args: list, cwd: str | None = None) -> subprocess.CompletedProcess:
|
||||
"""Run manage.py with args (e.g. ['migrate', 'contaboAutoSnapshot', '--noinput'])."""
|
||||
cmd = [_python_executable(), _manage_py()] + args
|
||||
return subprocess.run(
|
||||
cmd,
|
||||
cwd=cwd or '/usr/local/CyberCP',
|
||||
stdin=subprocess.DEVNULL,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=600,
|
||||
)
|
||||
|
||||
|
||||
def list_plugin_migration_names(plugin_name: str) -> list[str]:
|
||||
"""Sorted migration names from disk (e.g. 0001_initial), excluding __init__."""
|
||||
mig_dir = '/usr/local/CyberCP/' + plugin_name + '/migrations'
|
||||
if not os.path.isdir(mig_dir):
|
||||
return []
|
||||
names = []
|
||||
try:
|
||||
for fn in os.listdir(mig_dir):
|
||||
if not fn.endswith('.py') or fn == '__init__.py':
|
||||
continue
|
||||
names.append(fn[:-3])
|
||||
except OSError:
|
||||
return []
|
||||
return sorted(names)
|
||||
|
||||
|
||||
def applied_plugin_migrations(plugin_name: str) -> set[str]:
|
||||
_django_setup()
|
||||
from django.db import connection
|
||||
from django.db.migrations.recorder import MigrationRecorder
|
||||
|
||||
recorder = MigrationRecorder(connection)
|
||||
return {name for app, name in recorder.applied_migrations() if app == plugin_name}
|
||||
|
||||
|
||||
def sqlmigrate_text(plugin_name: str, migration_name: str) -> tuple[int, str]:
|
||||
"""Return (returncode, combined stdout+stderr) for sqlmigrate."""
|
||||
proc = run_manage(['sqlmigrate', plugin_name, migration_name])
|
||||
out = (proc.stdout or '') + (proc.stderr or '')
|
||||
return proc.returncode, out
|
||||
|
||||
|
||||
def _strip_sql_comments(sql: str) -> str:
|
||||
lines = []
|
||||
for line in sql.splitlines():
|
||||
s = line.strip()
|
||||
if s.startswith('--'):
|
||||
continue
|
||||
lines.append(line)
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def _execute_multi_sql_mysql(sql: str) -> tuple[bool, str]:
|
||||
"""Execute multi-statement ``sqlmigrate`` output via ``mariadb`` CLI (socket auth as root)."""
|
||||
sql = _strip_sql_comments(sql).strip()
|
||||
if not sql:
|
||||
return True, ''
|
||||
db = _db_name()
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
['mariadb', db],
|
||||
input=sql.encode('utf-8', errors='replace'),
|
||||
capture_output=True,
|
||||
timeout=300,
|
||||
)
|
||||
err = (proc.stderr or b'').decode('utf-8', errors='replace')[:2000]
|
||||
if proc.returncode != 0:
|
||||
return False, err or 'mariadb exited %s' % proc.returncode
|
||||
return True, ''
|
||||
except FileNotFoundError:
|
||||
return False, 'mariadb client not found; install MariaDB client or fix PATH'
|
||||
except Exception as e:
|
||||
return False, str(e)[:2000]
|
||||
|
||||
|
||||
def apply_pending_migrations_via_sql_and_fake(plugin_name: str, log) -> bool:
|
||||
"""
|
||||
For each on-disk migration not in django_migrations: sqlmigrate + execute, then
|
||||
``migrate <app> <name> --fake`` so each is recorded.
|
||||
"""
|
||||
pending = [m for m in list_plugin_migration_names(plugin_name) if m not in applied_plugin_migrations(plugin_name)]
|
||||
if not pending:
|
||||
log(
|
||||
'SQL fallback: no pending migrations recorded for %s — schema/DB mismatch may remain; '
|
||||
'check migrate errors above.'
|
||||
% plugin_name
|
||||
)
|
||||
return False
|
||||
for mig in pending:
|
||||
rc, out = sqlmigrate_text(plugin_name, mig)
|
||||
if rc != 0:
|
||||
log('sqlmigrate %s %s failed (rc=%s): %s' % (plugin_name, mig, rc, out[:800]))
|
||||
return False
|
||||
ok, err = _execute_multi_sql_mysql(out)
|
||||
if not ok:
|
||||
log('SQL exec failed for %s %s: %s' % (plugin_name, mig, err))
|
||||
return False
|
||||
log('Applied raw SQL for %s %s' % (plugin_name, mig))
|
||||
proc = run_manage(['migrate', plugin_name, mig, '--fake', '--noinput'])
|
||||
if proc.returncode != 0:
|
||||
log(
|
||||
'migrate --fake %s %s failed: %s'
|
||||
% (plugin_name, mig, (proc.stderr or proc.stdout or '')[:800])
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def drop_plugin_tables_and_migration_rows(plugin_name: str, log) -> bool:
|
||||
"""
|
||||
DROP all tables for this app label (prefix ``pluginName_``) and remove
|
||||
django_migrations rows. Uses FOREIGN_KEY_CHECKS=0.
|
||||
"""
|
||||
_django_setup()
|
||||
from django.db import connection
|
||||
|
||||
db = _db_name()
|
||||
prefix_a = plugin_name + '_'
|
||||
prefix_b = plugin_name.lower() + '_'
|
||||
tables = []
|
||||
try:
|
||||
with connection.cursor() as c:
|
||||
c.execute(
|
||||
'SELECT table_name FROM information_schema.tables WHERE table_schema = %s '
|
||||
'AND (table_name LIKE %s OR table_name LIKE %s)',
|
||||
[db, prefix_a + '%', prefix_b + '%'],
|
||||
)
|
||||
tables = [row[0] for row in c.fetchall()]
|
||||
except Exception as e:
|
||||
log('list tables for drop failed: %s' % str(e)[:800])
|
||||
return False
|
||||
if not tables:
|
||||
log('No tables matched prefix for %s; cleaning django_migrations only.' % plugin_name)
|
||||
try:
|
||||
with connection.cursor() as c:
|
||||
c.execute('SET FOREIGN_KEY_CHECKS=0')
|
||||
for t in tables:
|
||||
safe = t.replace('`', '``')
|
||||
c.execute('DROP TABLE IF EXISTS `%s`' % safe)
|
||||
c.execute('SET FOREIGN_KEY_CHECKS=1')
|
||||
c.execute('DELETE FROM django_migrations WHERE app = %s', [plugin_name])
|
||||
log('Dropped %s table(s) for %s and removed django_migrations rows.' % (len(tables), plugin_name))
|
||||
return True
|
||||
except Exception as e:
|
||||
log('drop_plugin_tables failed: %s' % str(e)[:800])
|
||||
return False
|
||||
|
||||
|
||||
def ensure_login_system_migrations_applied(log) -> None:
|
||||
"""
|
||||
Apply loginSystem 0001 if present (graph fix). Safe no-op if already applied.
|
||||
"""
|
||||
mig_path = '/usr/local/CyberCP/loginSystem/migrations/0001_initial.py'
|
||||
if not os.path.isfile(mig_path):
|
||||
return
|
||||
proc = run_manage(['migrate', 'loginSystem', '--noinput'])
|
||||
if proc.returncode != 0:
|
||||
log(
|
||||
'migrate loginSystem exited %s (panels may need manual fix): %s'
|
||||
% (proc.returncode, (proc.stderr or proc.stdout or '')[:600])
|
||||
)
|
||||
@@ -4434,6 +4434,86 @@ class Migration(migrations.Migration):
|
||||
except:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def rabbitMQMigrations():
|
||||
marker_path = '/home/cyberpanel/rabbitmq'
|
||||
rabbitmq_service_files = [
|
||||
'/usr/lib/systemd/system/rabbitmq-server.service',
|
||||
'/lib/systemd/system/rabbitmq-server.service'
|
||||
]
|
||||
rabbitmq_binary_paths = [
|
||||
'/usr/sbin/rabbitmq-server',
|
||||
'/usr/lib/rabbitmq/bin/rabbitmq-server'
|
||||
]
|
||||
|
||||
try:
|
||||
rabbitmq_installed = any(os.path.exists(path) for path in rabbitmq_service_files + rabbitmq_binary_paths)
|
||||
|
||||
if rabbitmq_installed:
|
||||
if not os.path.exists(marker_path):
|
||||
writeToFile = open(marker_path, 'w+')
|
||||
writeToFile.close()
|
||||
Upgrade.stdOut('RabbitMQ detected during upgrade. Marker file created.', 0)
|
||||
|
||||
Upgrade.executioner('systemctl enable rabbitmq-server', 'Enable RabbitMQ service', 0)
|
||||
Upgrade.executioner('systemctl start rabbitmq-server', 'Start RabbitMQ service', 0)
|
||||
else:
|
||||
if os.path.exists(marker_path):
|
||||
os.remove(marker_path)
|
||||
Upgrade.stdOut('RabbitMQ marker removed because service is not installed.', 0)
|
||||
except BaseException as msg:
|
||||
Upgrade.stdOut('RabbitMQ migration failed: ' + str(msg), 0)
|
||||
|
||||
@staticmethod
|
||||
def redisMigrations():
|
||||
marker_path = '/home/cyberpanel/redis'
|
||||
redis_binary = '/usr/bin/redis-server'
|
||||
redis_service_files = [
|
||||
'/usr/lib/systemd/system/redis.service',
|
||||
'/lib/systemd/system/redis.service'
|
||||
]
|
||||
|
||||
try:
|
||||
redis_installed = os.path.exists(redis_binary) or any(os.path.exists(path) for path in redis_service_files)
|
||||
if redis_installed:
|
||||
if not os.path.exists(marker_path):
|
||||
writeToFile = open(marker_path, 'w+')
|
||||
writeToFile.close()
|
||||
Upgrade.stdOut('Redis detected during upgrade. Marker file created.', 0)
|
||||
Upgrade.executioner('systemctl enable redis', 'Enable Redis service', 0)
|
||||
Upgrade.executioner('systemctl start redis', 'Start Redis service', 0)
|
||||
else:
|
||||
if os.path.exists(marker_path):
|
||||
os.remove(marker_path)
|
||||
Upgrade.stdOut('Redis marker removed because service is not installed.', 0)
|
||||
except BaseException as msg:
|
||||
Upgrade.stdOut('Redis migration failed: ' + str(msg), 0)
|
||||
|
||||
@staticmethod
|
||||
def elasticSearchMigrations():
|
||||
marker_path = '/home/cyberpanel/elasticsearch'
|
||||
es_binary = '/usr/share/elasticsearch/bin/elasticsearch'
|
||||
es_service_files = [
|
||||
'/usr/lib/systemd/system/elasticsearch.service',
|
||||
'/lib/systemd/system/elasticsearch.service'
|
||||
]
|
||||
|
||||
try:
|
||||
es_installed = os.path.exists(es_binary) or any(os.path.exists(path) for path in es_service_files)
|
||||
if es_installed:
|
||||
if not os.path.exists(marker_path):
|
||||
writeToFile = open(marker_path, 'w+')
|
||||
writeToFile.close()
|
||||
Upgrade.stdOut('Elasticsearch detected during upgrade. Marker file created.', 0)
|
||||
Upgrade.executioner('systemctl enable elasticsearch', 'Enable Elasticsearch service', 0)
|
||||
Upgrade.executioner('systemctl start elasticsearch', 'Start Elasticsearch service', 0)
|
||||
else:
|
||||
if os.path.exists(marker_path):
|
||||
os.remove(marker_path)
|
||||
Upgrade.stdOut('Elasticsearch marker removed because service is not installed.', 0)
|
||||
except BaseException as msg:
|
||||
Upgrade.stdOut('Elasticsearch migration failed: ' + str(msg), 0)
|
||||
|
||||
@staticmethod
|
||||
def backupCriticalFiles():
|
||||
"""Backup all critical configuration files before upgrade"""
|
||||
@@ -6652,6 +6732,9 @@ slowlog = /var/log/php{version}-fpm-slow.log
|
||||
Upgrade.setupWebmail()
|
||||
Upgrade.setupSieve()
|
||||
Upgrade.enableServices()
|
||||
Upgrade.elasticSearchMigrations()
|
||||
Upgrade.redisMigrations()
|
||||
Upgrade.rabbitMQMigrations()
|
||||
|
||||
# Apply AlmaLinux 9 fixes before other installations
|
||||
Upgrade.fix_almalinux9_mariadb()
|
||||
|
||||
@@ -1378,6 +1378,9 @@
|
||||
<button type="button" class="sort-btn filter-btn" data-filter="active" id="installedFilterBtnActive" onclick="setInstalledFilter('active')" title="{% trans 'Show only active (enabled) plugins' %}">
|
||||
<i class="fas fa-power-off"></i> <span class="filter-btn-label">{% trans "Active only" %}</span>
|
||||
</button>
|
||||
<button type="button" class="sort-btn filter-btn" data-filter="premium" id="installedFilterBtnPremium" onclick="setInstalledFilter('premium')" title="{% trans 'Show only premium (paid) plugins' %}">
|
||||
<i class="fas fa-crown"></i> <span class="filter-btn-label">{% trans "Premium" %}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="installed-sort-row">
|
||||
@@ -1414,7 +1417,7 @@
|
||||
<!-- Grid View -->
|
||||
<div id="gridView" class="plugins-grid">
|
||||
{% for plugin in plugins %}
|
||||
<div class="plugin-card" data-plugin-name="{{ plugin.name }}" data-plugin-desc="{{ plugin.desc }}" data-plugin-type="{{ plugin.type }}" data-modify-date="{{ plugin.modify_date|default:'0000-00-00 00:00:00' }}" data-installed="{{ plugin.installed|yesno:'true,false' }}" data-enabled="{{ plugin.enabled|yesno:'true,false' }}">
|
||||
<div class="plugin-card" data-plugin-name="{{ plugin.name }}" data-plugin-desc="{{ plugin.desc }}" data-plugin-type="{{ plugin.type }}" data-modify-date="{{ plugin.modify_date|default:'0000-00-00 00:00:00' }}" data-installed="{{ plugin.installed|yesno:'true,false' }}" data-enabled="{{ plugin.enabled|yesno:'true,false' }}" data-is-paid="{% if plugin.is_paid|default:False|default_if_none:False %}true{% else %}false{% endif %}">
|
||||
<div class="plugin-header">
|
||||
<div class="plugin-icon">
|
||||
{% if plugin.type|lower == "security" %}
|
||||
@@ -1561,7 +1564,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for plugin in plugins %}
|
||||
<tr data-plugin-name="{{ plugin.name }}" data-plugin-desc="{{ plugin.desc }}" data-plugin-type="{{ plugin.type }}" data-modify-date="{{ plugin.modify_date|default:'0000-00-00 00:00:00' }}" data-installed="{{ plugin.installed|yesno:'true,false' }}" data-enabled="{{ plugin.enabled|yesno:'true,false' }}">
|
||||
<tr data-plugin-name="{{ plugin.name }}" data-plugin-desc="{{ plugin.desc }}" data-plugin-type="{{ plugin.type }}" data-modify-date="{{ plugin.modify_date|default:'0000-00-00 00:00:00' }}" data-installed="{{ plugin.installed|yesno:'true,false' }}" data-enabled="{{ plugin.enabled|yesno:'true,false' }}" data-is-paid="{% if plugin.is_paid|default:False|default_if_none:False %}true{% else %}false{% endif %}">
|
||||
<td>
|
||||
<strong>{{ plugin.name }}</strong>
|
||||
{% if plugin.freshness_badge %}
|
||||
@@ -1871,15 +1874,127 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Cache-busting version: 2026-03-27-v2 - Modify date: browser-local via modify_timestamp + nb-NO style
|
||||
// Cache-busting version: 28.03.2026-v3 - Hash sync for installed filters (#grid?show=premium&sort=…)
|
||||
let storePlugins = [];
|
||||
let currentFilter = 'all';
|
||||
let currentCategory = 'all';
|
||||
let currentSearchQuery = '';
|
||||
let isSettingHash = false; // Flag to prevent infinite loops
|
||||
let currentInstalledSort = 'name-asc'; // name-asc, name-desc, type, date-desc, date-asc
|
||||
let currentInstalledFilter = 'all'; // all, installed, active
|
||||
let currentInstalledFilter = 'all'; // all, installed, active, premium
|
||||
let currentInstalledCategory = 'all'; // all, Utility, Security, ...
|
||||
let applyingInstalledHash = false;
|
||||
let installedSearchHashDebounce = null;
|
||||
|
||||
var INSTALLED_HASH_VALID_VIEWS = ['grid', 'table', 'upgrades', 'store'];
|
||||
var INSTALLED_HASH_SHOW_VALUES = { all: 1, installed: 1, active: 1, premium: 1 };
|
||||
var INSTALLED_HASH_SORT_VALUES = { 'name-asc': 1, 'name-desc': 1, 'type': 1, 'date-desc': 1, 'date-asc': 1 };
|
||||
|
||||
function parsePluginsHashFragment(raw) {
|
||||
raw = (raw || '').trim();
|
||||
var empty = { view: 'grid', show: 'all', sort: 'name-asc', cat: 'all', q: '' };
|
||||
if (!raw) return empty;
|
||||
var qIdx = raw.indexOf('?');
|
||||
var viewPart = (qIdx >= 0 ? raw.slice(0, qIdx) : raw).trim();
|
||||
var qs = qIdx >= 0 ? raw.slice(qIdx + 1) : '';
|
||||
var view = viewPart || 'grid';
|
||||
if (INSTALLED_HASH_VALID_VIEWS.indexOf(view) === -1) {
|
||||
view = 'grid';
|
||||
qs = '';
|
||||
}
|
||||
var params = new URLSearchParams(qs);
|
||||
var show = params.get('show') || 'all';
|
||||
var sort = params.get('sort') || 'name-asc';
|
||||
var cat = params.get('cat') || 'all';
|
||||
var q = params.get('q') || '';
|
||||
if (!INSTALLED_HASH_SHOW_VALUES[show]) show = 'all';
|
||||
if (!INSTALLED_HASH_SORT_VALUES[sort]) sort = 'name-asc';
|
||||
return { view: view, show: show, sort: sort, cat: cat, q: q };
|
||||
}
|
||||
|
||||
function buildPluginsHashString(view) {
|
||||
if (view === 'store' || view === 'upgrades') {
|
||||
return '#' + view;
|
||||
}
|
||||
var parts = [];
|
||||
if (currentInstalledFilter && currentInstalledFilter !== 'all') {
|
||||
parts.push('show=' + encodeURIComponent(currentInstalledFilter));
|
||||
}
|
||||
if (currentInstalledSort && currentInstalledSort !== 'name-asc') {
|
||||
parts.push('sort=' + encodeURIComponent(currentInstalledSort));
|
||||
}
|
||||
if (currentInstalledCategory && currentInstalledCategory !== 'all') {
|
||||
parts.push('cat=' + encodeURIComponent(currentInstalledCategory));
|
||||
}
|
||||
var qEl = document.getElementById('installedPluginSearchInput');
|
||||
var qv = qEl && qEl.value ? String(qEl.value).trim() : '';
|
||||
if (qv) parts.push('q=' + encodeURIComponent(qv));
|
||||
if (parts.length) return '#' + view + '?' + parts.join('&');
|
||||
return '#' + view;
|
||||
}
|
||||
|
||||
function getCurrentInstalledGridOrTableView() {
|
||||
var gv = document.getElementById('gridView');
|
||||
var tv = document.getElementById('tableView');
|
||||
if (gv && gv.style.display === 'grid') return 'grid';
|
||||
if (tv && tv.style.display === 'block') return 'table';
|
||||
return null;
|
||||
}
|
||||
|
||||
function updateInstalledPluginsHashIfNeeded() {
|
||||
if (applyingInstalledHash) return;
|
||||
var v = getCurrentInstalledGridOrTableView();
|
||||
if (!v) return;
|
||||
isSettingHash = true;
|
||||
var h = buildPluginsHashString(v);
|
||||
var newUrl = window.location.pathname + window.location.search + h;
|
||||
if (window.history && window.history.replaceState) {
|
||||
window.history.replaceState(null, '', newUrl);
|
||||
} else {
|
||||
window.location.hash = h.replace(/^#/, '');
|
||||
}
|
||||
setTimeout(function() { isSettingHash = false; }, 100);
|
||||
}
|
||||
|
||||
function scheduleInstalledSearchHashUpdate() {
|
||||
if (installedSearchHashDebounce) clearTimeout(installedSearchHashDebounce);
|
||||
installedSearchHashDebounce = setTimeout(function() {
|
||||
installedSearchHashDebounce = null;
|
||||
updateInstalledPluginsHashIfNeeded();
|
||||
}, 450);
|
||||
}
|
||||
|
||||
function applyInstalledStateFromHash(parsed) {
|
||||
if (!parsed || (parsed.view !== 'grid' && parsed.view !== 'table')) return;
|
||||
applyingInstalledHash = true;
|
||||
try {
|
||||
currentInstalledFilter = parsed.show;
|
||||
currentInstalledSort = parsed.sort;
|
||||
currentInstalledCategory = parsed.cat;
|
||||
var inp = document.getElementById('installedPluginSearchInput');
|
||||
if (inp) {
|
||||
inp.value = parsed.q;
|
||||
var clr = document.getElementById('installedPluginSearchClear');
|
||||
if (clr) clr.style.display = parsed.q ? 'block' : 'none';
|
||||
}
|
||||
var bar = document.getElementById('installedSortFilterBar');
|
||||
if (bar) {
|
||||
bar.querySelectorAll('.filter-btn').forEach(function(btn) {
|
||||
btn.classList.toggle('active', (btn.getAttribute('data-filter') || '') === currentInstalledFilter);
|
||||
});
|
||||
}
|
||||
if (typeof updateInstalledSortButtons === 'function') updateInstalledSortButtons();
|
||||
if (typeof doApplyInstalledSort === 'function') doApplyInstalledSort();
|
||||
document.querySelectorAll('.installed-category-btn').forEach(function(btn) {
|
||||
btn.classList.toggle('active', (btn.getAttribute('data-category') || '') === currentInstalledCategory);
|
||||
});
|
||||
if (typeof filterInstalledPlugins === 'function') filterInstalledPlugins();
|
||||
} catch (e) {
|
||||
console.warn('applyInstalledStateFromHash', e);
|
||||
} finally {
|
||||
applyingInstalledHash = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Get CSRF cookie helper function
|
||||
function getCookie(name) {
|
||||
@@ -1909,17 +2024,13 @@ function toggleView(view, updateHash = true) {
|
||||
// Update URL hash only if explicitly requested (user clicked a button, not initial load)
|
||||
if (updateHash) {
|
||||
isSettingHash = true;
|
||||
const hash = '#' + view;
|
||||
// Use replaceState to update URL - this updates the hash without triggering hashchange event
|
||||
const hash = buildPluginsHashString(view);
|
||||
if (window.history && window.history.replaceState) {
|
||||
// Get current pathname and preserve it, just update the hash
|
||||
const newUrl = window.location.pathname + window.location.search + hash;
|
||||
window.history.replaceState(null, null, newUrl);
|
||||
} else {
|
||||
// Fallback for older browsers - this will trigger hashchange but we have the flag
|
||||
window.location.hash = hash;
|
||||
window.location.hash = hash.replace(/^#/, '');
|
||||
}
|
||||
// Reset flag after a short delay
|
||||
setTimeout(() => { isSettingHash = false; }, 100);
|
||||
}
|
||||
|
||||
@@ -2434,6 +2545,7 @@ function setInstalledFilter(filter) {
|
||||
try {
|
||||
filterInstalledPlugins();
|
||||
} catch (e) { console.warn('setInstalledFilter: filterInstalledPlugins', e); }
|
||||
updateInstalledPluginsHashIfNeeded();
|
||||
}
|
||||
|
||||
function filterInstalledPlugins() {
|
||||
@@ -2446,10 +2558,11 @@ function filterInstalledPlugins() {
|
||||
const noResultsTable = document.getElementById('installedPluginsNoResultsTable');
|
||||
if (!gridView && !tableView) return;
|
||||
var visibleCount = 0;
|
||||
function matchesFilter(installed, enabled) {
|
||||
function matchesFilter(installed, enabled, isPaid) {
|
||||
if (filter === 'all') return true;
|
||||
if (filter === 'installed') return installed === 'true';
|
||||
if (filter === 'active') return installed === 'true' && enabled === 'true';
|
||||
if (filter === 'premium') return isPaid === 'true';
|
||||
return true;
|
||||
}
|
||||
var cat = (typeof currentInstalledCategory !== 'undefined' ? currentInstalledCategory : 'all');
|
||||
@@ -2468,7 +2581,8 @@ function filterInstalledPlugins() {
|
||||
var searchMatch = terms.length === 0 || terms.every(function(term) { return combined.indexOf(term) !== -1; });
|
||||
var installed = card.getAttribute('data-installed') || 'false';
|
||||
var enabled = card.getAttribute('data-enabled') || 'false';
|
||||
var filterMatch = matchesFilter(installed, enabled);
|
||||
var isPaid = card.getAttribute('data-is-paid') || 'false';
|
||||
var filterMatch = matchesFilter(installed, enabled, isPaid);
|
||||
var categoryMatch = matchesCategory(card.getAttribute('data-plugin-type'));
|
||||
var show = searchMatch && filterMatch && categoryMatch;
|
||||
card.style.display = show ? '' : 'none';
|
||||
@@ -2488,7 +2602,8 @@ function filterInstalledPlugins() {
|
||||
var searchMatch = terms.length === 0 || terms.every(function(term) { return combined.indexOf(term) !== -1; });
|
||||
var installed = row.getAttribute('data-installed') || 'false';
|
||||
var enabled = row.getAttribute('data-enabled') || 'false';
|
||||
var filterMatch = matchesFilter(installed, enabled);
|
||||
var isPaid = row.getAttribute('data-is-paid') || 'false';
|
||||
var filterMatch = matchesFilter(installed, enabled, isPaid);
|
||||
var categoryMatch = matchesCategory(row.getAttribute('data-plugin-type'));
|
||||
var show = searchMatch && filterMatch && categoryMatch;
|
||||
row.style.display = show ? '' : 'none';
|
||||
@@ -2514,6 +2629,7 @@ function clearInstalledPluginSearch() {
|
||||
filterInstalledPlugins();
|
||||
input.focus();
|
||||
}
|
||||
updateInstalledPluginsHashIfNeeded();
|
||||
}
|
||||
|
||||
function toggleInstalledSort(field) {
|
||||
@@ -2526,6 +2642,7 @@ function toggleInstalledSort(field) {
|
||||
}
|
||||
updateInstalledSortButtons();
|
||||
doApplyInstalledSort();
|
||||
updateInstalledPluginsHashIfNeeded();
|
||||
}
|
||||
|
||||
function updateInstalledSortButtons() {
|
||||
@@ -2624,6 +2741,7 @@ function filterByCategoryInstalled(category, evt) {
|
||||
});
|
||||
}
|
||||
try { filterInstalledPlugins(); } catch (e) { console.warn('filterByCategoryInstalled', e); }
|
||||
updateInstalledPluginsHashIfNeeded();
|
||||
}
|
||||
|
||||
function upgradePlugin(pluginName, currentVersion, newVersion) {
|
||||
@@ -3490,6 +3608,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
installedSearchClearBtn.style.display = this.value.trim() ? 'block' : 'none';
|
||||
}
|
||||
filterInstalledPlugins();
|
||||
scheduleInstalledSearchHashUpdate();
|
||||
});
|
||||
installedSearchInput.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
@@ -3499,22 +3618,21 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
}
|
||||
|
||||
// Check URL hash for view preference
|
||||
const hash = window.location.hash.substring(1); // Remove #
|
||||
// URL hash: #grid | #table | #store | #upgrades, optional ?show=&sort=&cat=&q= for grid/table
|
||||
const hashRaw = window.location.hash.substring(1);
|
||||
const parsedHash = parsePluginsHashFragment(hashRaw);
|
||||
const validViews = ['grid', 'table', 'upgrades', 'store'];
|
||||
|
||||
// Check if view elements exist before calling toggleView
|
||||
const gridView = document.getElementById('gridView');
|
||||
const tableView = document.getElementById('tableView');
|
||||
const storeView = document.getElementById('storeView');
|
||||
const upgradesViewEl = document.getElementById('upgradesView');
|
||||
|
||||
// Only proceed if all view elements exist (plugins are installed)
|
||||
if (gridView && tableView && storeView) {
|
||||
let initialView = 'grid'; // Default
|
||||
if (validViews.includes(hash)) {
|
||||
initialView = hash;
|
||||
} else {
|
||||
let initialView = 'grid';
|
||||
if (hashRaw.length > 0 && validViews.indexOf(parsedHash.view) !== -1) {
|
||||
initialView = parsedHash.view;
|
||||
} else if (hashRaw.length === 0) {
|
||||
if (gridView.children.length > 0) {
|
||||
initialView = 'grid';
|
||||
} else {
|
||||
@@ -3522,9 +3640,26 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
}
|
||||
|
||||
const hadHash = hash.length > 0;
|
||||
const hadHash = hashRaw.length > 0;
|
||||
try {
|
||||
toggleView(initialView, hadHash);
|
||||
if (initialView === 'grid' || initialView === 'table') {
|
||||
currentInstalledFilter = parsedHash.show;
|
||||
currentInstalledSort = parsedHash.sort;
|
||||
currentInstalledCategory = parsedHash.cat;
|
||||
var insPre = document.getElementById('installedPluginSearchInput');
|
||||
var clrPre = document.getElementById('installedPluginSearchClear');
|
||||
if (insPre) {
|
||||
insPre.value = parsedHash.q;
|
||||
if (clrPre) clrPre.style.display = parsedHash.q ? 'block' : 'none';
|
||||
}
|
||||
}
|
||||
toggleView(initialView, false);
|
||||
if (initialView === 'grid' || initialView === 'table') {
|
||||
applyInstalledStateFromHash(parsedHash);
|
||||
}
|
||||
if (hadHash && (initialView === 'grid' || initialView === 'table' || initialView === 'upgrades' || initialView === 'store')) {
|
||||
updateInstalledPluginsHashIfNeeded();
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('plugins: toggleView on load failed', e);
|
||||
if (storeView) storeView.style.display = 'block';
|
||||
@@ -3549,17 +3684,17 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
// Handle hash changes (back/forward browser buttons)
|
||||
window.addEventListener('hashchange', function() {
|
||||
// Prevent infinite loops when we programmatically set the hash
|
||||
if (isSettingHash) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hash = window.location.hash.substring(1);
|
||||
const validViews = ['grid', 'table', 'store'];
|
||||
|
||||
if (validViews.includes(hash)) {
|
||||
// Don't update hash again since it's already set (user navigated via browser)
|
||||
toggleView(hash, false);
|
||||
const raw = window.location.hash.substring(1);
|
||||
const parsed = parsePluginsHashFragment(raw);
|
||||
var validViews = ['grid', 'table', 'upgrades', 'store'];
|
||||
if (validViews.indexOf(parsed.view) !== -1) {
|
||||
toggleView(parsed.view, false);
|
||||
if (parsed.view === 'grid' || parsed.view === 'table') {
|
||||
applyInstalledStateFromHash(parsed);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.shortcuts import render, redirect
|
||||
from django.http import JsonResponse
|
||||
from django.http import JsonResponse, StreamingHttpResponse
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from plogical.mailUtilities import mailUtilities
|
||||
@@ -2441,6 +2441,7 @@ def plugin_settings_proxy(request, plugin_name):
|
||||
settings_view = getattr(views_mod, candidate, None)
|
||||
if callable(settings_view):
|
||||
response = settings_view(request)
|
||||
response = _inject_plugin_settings_back_bar(response)
|
||||
return _inject_activation_store_hook(response, plugin_name)
|
||||
except ModuleNotFoundError as e:
|
||||
last_err = str(e)
|
||||
@@ -2458,6 +2459,63 @@ def plugin_settings_proxy(request, plugin_name):
|
||||
return HttpResponseNotFound('Plugin not found.')
|
||||
|
||||
|
||||
def _inject_plugin_settings_back_bar(response):
|
||||
"""
|
||||
Add a consistent 'Back to installed Plugins' link at the top of proxied plugin
|
||||
settings HTML so users need not use the sidebar. Only touches HTML responses.
|
||||
"""
|
||||
try:
|
||||
if isinstance(response, StreamingHttpResponse):
|
||||
return response
|
||||
content_type = (response.get('Content-Type', '') or '').lower()
|
||||
if 'text/html' not in content_type:
|
||||
return response
|
||||
if not getattr(response, 'content', None):
|
||||
return response
|
||||
body = response.content.decode('utf-8', errors='ignore')
|
||||
if not body.strip():
|
||||
return response
|
||||
from django.utils.html import escape
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
if 'cp-plugin-settings-back' in body:
|
||||
return response
|
||||
label = escape(str(_('Back to installed Plugins')))
|
||||
# Inside #main-content only: avoids full-viewport strip over the sidebar (baseTemplate uses
|
||||
# #sidebar + #main-content { margin-left: 260px; }).
|
||||
back_html = (
|
||||
'<div id="cp-plugin-settings-back" style="margin:0 0 16px 0;padding:10px 16px;'
|
||||
'background:linear-gradient(90deg,#f1f5f9,#e8eef5);border:1px solid #cbd5e1;border-radius:8px;'
|
||||
'font-size:14px;line-height:1.4;position:relative;max-width:100%;box-sizing:border-box;">'
|
||||
'<a href="/plugins/installed" id="cp-plugin-settings-back-link" '
|
||||
'style="display:inline-flex;align-items:center;gap:8px;color:#1e293b;font-weight:600;'
|
||||
'text-decoration:none;">'
|
||||
'<span aria-hidden="true" style="font-size:1.1em;">←</span><span>' + label + '</span></a></div>'
|
||||
)
|
||||
main_content_m = re.search(
|
||||
r'(<div\s+id\s*=\s*["\']main-content["\'][^>]*>)',
|
||||
body,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
if main_content_m:
|
||||
pos = main_content_m.end()
|
||||
body = body[:pos] + back_html + body[pos:]
|
||||
elif re.search(r'<body[^>]*>', body, flags=re.IGNORECASE):
|
||||
body = re.sub(
|
||||
r'(<body[^>]*>)',
|
||||
r'\1' + back_html,
|
||||
body,
|
||||
count=1,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
else:
|
||||
body = back_html + body
|
||||
response.content = body.encode('utf-8')
|
||||
return response
|
||||
except Exception:
|
||||
return response
|
||||
|
||||
|
||||
def _inject_activation_store_hook(response, plugin_name):
|
||||
"""
|
||||
Tiny safety hook for plugin settings pages:
|
||||
|
||||
@@ -371,6 +371,17 @@ class pluginInstaller:
|
||||
py = pluginInstaller._manage_python_executable()
|
||||
try:
|
||||
os.chdir('/usr/local/CyberCP')
|
||||
try:
|
||||
from plogical import pluginMigrationSQL as _pmig
|
||||
except ImportError:
|
||||
_pmig = None
|
||||
|
||||
def _mig_log(msg):
|
||||
pluginInstaller.stdOut(msg)
|
||||
|
||||
if _pmig is not None:
|
||||
_pmig.ensure_login_system_migrations_applied(_mig_log)
|
||||
|
||||
mk = subprocess.call(
|
||||
[py, manage_py, 'makemigrations', pluginName],
|
||||
stdin=subprocess.DEVNULL,
|
||||
@@ -379,14 +390,59 @@ class pluginInstaller:
|
||||
pluginInstaller.stdOut(
|
||||
'makemigrations %s exited %s (ok if no model changes)' % (pluginName, mk)
|
||||
)
|
||||
mig = subprocess.call(
|
||||
proc = subprocess.run(
|
||||
[py, manage_py, 'migrate', pluginName, '--noinput'],
|
||||
cwd='/usr/local/CyberCP',
|
||||
stdin=subprocess.DEVNULL,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=600,
|
||||
)
|
||||
if mig != 0:
|
||||
if proc.returncode != 0:
|
||||
err_tail = (proc.stderr or proc.stdout or '').strip()[:900]
|
||||
pluginInstaller.stdOut(
|
||||
'migrate %s exited %s — check CyberPanel logs and DB permissions' % (pluginName, mig)
|
||||
'migrate %s exited %s — %s'
|
||||
% (pluginName, proc.returncode, err_tail or 'no stderr')
|
||||
)
|
||||
if _pmig is not None:
|
||||
pluginInstaller.stdOut(
|
||||
'Attempting SQL + migrate --fake fallback for %s..' % pluginName
|
||||
)
|
||||
if _pmig.apply_pending_migrations_via_sql_and_fake(pluginName, _mig_log):
|
||||
pluginInstaller.stdOut('SQL migration fallback succeeded for %s; re-running migrate..' % pluginName)
|
||||
proc2 = subprocess.run(
|
||||
[py, manage_py, 'migrate', pluginName, '--noinput'],
|
||||
cwd='/usr/local/CyberCP',
|
||||
stdin=subprocess.DEVNULL,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=600,
|
||||
)
|
||||
if proc2.returncode != 0:
|
||||
pluginInstaller.stdOut(
|
||||
'migrate %s still failed after SQL fallback (rc=%s): %s'
|
||||
% (
|
||||
pluginName,
|
||||
proc2.returncode,
|
||||
(proc2.stderr or proc2.stdout or '')[:600],
|
||||
)
|
||||
)
|
||||
else:
|
||||
pluginInstaller.stdOut('migrate %s completed after SQL fallback.' % pluginName)
|
||||
else:
|
||||
pluginInstaller.stdOut(
|
||||
'SQL migration fallback failed for %s — check DB user, mariadb client, and logs.'
|
||||
% pluginName
|
||||
)
|
||||
try:
|
||||
from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as _cp_log
|
||||
|
||||
_cp_log.writeToFile(
|
||||
'pluginInstaller.installMigrations %s: migrate rc=%s %s'
|
||||
% (pluginName, proc.returncode, err_tail[:400])
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
try:
|
||||
os.chdir(currentDir)
|
||||
@@ -674,13 +730,53 @@ class pluginInstaller:
|
||||
@staticmethod
|
||||
def removeMigrations(pluginName):
|
||||
currentDir = os.getcwd()
|
||||
os.chdir('/usr/local/CyberCP')
|
||||
py = pluginInstaller._manage_python_executable()
|
||||
subprocess.call(
|
||||
[py, '/usr/local/CyberCP/manage.py', 'migrate', pluginName, 'zero', '--noinput'],
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
os.chdir(currentDir)
|
||||
try:
|
||||
os.chdir('/usr/local/CyberCP')
|
||||
py = pluginInstaller._manage_python_executable()
|
||||
try:
|
||||
from plogical import pluginMigrationSQL as _pmig
|
||||
except ImportError:
|
||||
_pmig = None
|
||||
|
||||
def _mig_log(msg):
|
||||
pluginInstaller.stdOut(msg)
|
||||
|
||||
if _pmig is not None:
|
||||
_pmig.ensure_login_system_migrations_applied(_mig_log)
|
||||
|
||||
proc = subprocess.run(
|
||||
[py, '/usr/local/CyberCP/manage.py', 'migrate', pluginName, 'zero', '--noinput'],
|
||||
cwd='/usr/local/CyberCP',
|
||||
stdin=subprocess.DEVNULL,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=600,
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
err_tail = (proc.stderr or proc.stdout or '').strip()[:900]
|
||||
pluginInstaller.stdOut(
|
||||
'migrate %s zero exited %s — %s'
|
||||
% (pluginName, proc.returncode, err_tail or 'no stderr')
|
||||
)
|
||||
if _pmig is not None:
|
||||
pluginInstaller.stdOut(
|
||||
'Attempting DROP TABLE + django_migrations cleanup for %s..' % pluginName
|
||||
)
|
||||
_pmig.drop_plugin_tables_and_migration_rows(pluginName, _mig_log)
|
||||
try:
|
||||
from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as _cp_log
|
||||
|
||||
_cp_log.writeToFile(
|
||||
'pluginInstaller.removeMigrations %s: zero rc=%s %s'
|
||||
% (pluginName, proc.returncode, err_tail[:400])
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
try:
|
||||
os.chdir(currentDir)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def removePlugin(pluginName):
|
||||
|
||||
@@ -428,10 +428,505 @@ app.controller('pureFTPD', function ($scope, $http, $timeout, $window) {
|
||||
|
||||
app.controller('manageApplications', function ($scope, $http, $timeout, $window) {
|
||||
|
||||
/**
|
||||
* Normalize entries from applicationMeta (strings, numbers, or rare object shapes)
|
||||
* so version pickers never show blank rows. CyberPanel uses {$ ... $} interpolation
|
||||
* in templates; list labels use versionLabel() for consistent display.
|
||||
*/
|
||||
function normalizeVersionToken(v) {
|
||||
if (v === null || v === undefined) {
|
||||
return '';
|
||||
}
|
||||
if (v === 'latest') {
|
||||
return 'latest';
|
||||
}
|
||||
if (typeof v === 'number' && isFinite(v)) {
|
||||
return String(v);
|
||||
}
|
||||
if (angular.isObject(v)) {
|
||||
var o = v;
|
||||
var cand = o.version || o.Version || o.value || o.name || o.ver || o.label;
|
||||
if (cand !== undefined && cand !== null) {
|
||||
return String(cand).trim();
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(o);
|
||||
} catch (ignore) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
return String(v).trim();
|
||||
}
|
||||
|
||||
function sanitizeVersionsArray(vers) {
|
||||
if (!angular.isArray(vers)) {
|
||||
return [];
|
||||
}
|
||||
var out = [];
|
||||
var seen = {};
|
||||
vers.forEach(function (raw) {
|
||||
var t = normalizeVersionToken(raw);
|
||||
if (!t || t === 'latest') {
|
||||
return;
|
||||
}
|
||||
if (!seen[t]) {
|
||||
seen[t] = true;
|
||||
out.push(t);
|
||||
}
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
function versionMatchesRabbitmqStream(ver, stream) {
|
||||
var s = String(stream || '4').trim();
|
||||
var t = normalizeVersionToken(ver);
|
||||
if (!t || t === 'latest') {
|
||||
return false;
|
||||
}
|
||||
var m = /^(\d+)\./.exec(t);
|
||||
return !!(m && m[1] === s);
|
||||
}
|
||||
|
||||
function versionMatchesEsMajor(ver, major) {
|
||||
var mjr = String(major || '8').trim();
|
||||
var t = normalizeVersionToken(ver);
|
||||
if (!t || t === 'latest') {
|
||||
return false;
|
||||
}
|
||||
var m = /^(\d+)\./.exec(t);
|
||||
return !!(m && m[1] === mjr);
|
||||
}
|
||||
|
||||
$scope.versionLabel = function (v) {
|
||||
if (v === 'latest') {
|
||||
return 'latest';
|
||||
}
|
||||
var t = normalizeVersionToken(v);
|
||||
return t || '(unknown)';
|
||||
};
|
||||
|
||||
$scope.versionTrackId = function (idx, v) {
|
||||
return String(idx) + '|' + $scope.versionLabel(v);
|
||||
};
|
||||
|
||||
/* false = long-running install/remove/poll (show spinners); true = idle */
|
||||
$scope.cyberpanelLoading = true;
|
||||
/** Background applicationMeta refresh — separate from cyberpanelLoading so the page does not “freeze” on every modal open. */
|
||||
$scope.appsMetaRefreshing = false;
|
||||
$scope.apps = [
|
||||
{name: 'Elasticsearch', image: '/static/manageServices/images/elastic-search.png'},
|
||||
{name: 'Redis', image: '/static/manageServices/images/redis.png'},
|
||||
{name: 'RabbitMQ', image: '/static/manageServices/images/rabbitmq-logo.svg'}
|
||||
];
|
||||
|
||||
$scope.removeInstall = function (appName, status) {
|
||||
(function mergeMetaBootstrap() {
|
||||
var el = document.getElementById('manageApplicationsMetaBootstrap');
|
||||
if (!el || !el.textContent) {
|
||||
return;
|
||||
}
|
||||
var raw = el.textContent.trim();
|
||||
if (!raw) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var boot = JSON.parse(raw);
|
||||
if (!boot || Number(boot.status) !== 1) {
|
||||
return;
|
||||
}
|
||||
var appMap = {};
|
||||
(boot.apps || []).forEach(function (a) {
|
||||
if (a && a.name) {
|
||||
appMap[a.name] = a;
|
||||
}
|
||||
});
|
||||
$scope.apps = $scope.apps.map(function (baseApp) {
|
||||
var meta = appMap[baseApp.name] || {};
|
||||
var vers = meta.versions;
|
||||
if (!angular.isArray(vers)) {
|
||||
vers = [];
|
||||
}
|
||||
vers = sanitizeVersionsArray(vers);
|
||||
return {
|
||||
name: baseApp.name,
|
||||
image: baseApp.image,
|
||||
installed: !!meta.installed,
|
||||
installedVersion: meta.installedVersion || '',
|
||||
updateAvailable: !!meta.updateAvailable,
|
||||
crossBranchUpdateSuggested: !!meta.crossBranchUpdateSuggested,
|
||||
versions: vers,
|
||||
latestAvailable: meta.latestAvailable || '',
|
||||
latestOverall: meta.latestOverall || '',
|
||||
rabbitmqVersionsHint: meta.rabbitmqVersionsHint || ''
|
||||
};
|
||||
});
|
||||
} catch (ignore) {
|
||||
/* keep bare apps list */
|
||||
}
|
||||
})();
|
||||
|
||||
$scope.selectedVersion = 'latest';
|
||||
/** Row highlight uses $index so only one row looks selected (avoids sticky :focus / repeater quirks). */
|
||||
$scope.selectedVersionRowIndex = 0;
|
||||
$scope.selectedVersions = ['latest'];
|
||||
|
||||
$scope.recalcSelectedVersionRowIndex = function () {
|
||||
var list = $scope.selectedVersions || [];
|
||||
var sel = $scope.selectedVersion;
|
||||
var i = list.indexOf(sel);
|
||||
if (i < 0) {
|
||||
var normSel = normalizeVersionToken(sel);
|
||||
if (normSel) {
|
||||
for (var j = 0; j < list.length; j += 1) {
|
||||
if (normalizeVersionToken(list[j]) === normSel) {
|
||||
i = j;
|
||||
$scope.selectedVersion = list[j];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$scope.selectedVersionRowIndex = (i >= 0) ? i : 0;
|
||||
};
|
||||
|
||||
$scope.selectManagedAppVersion = function (idx, v, $event) {
|
||||
var n = (typeof idx === 'number') ? idx : parseInt(idx, 10);
|
||||
if (!isFinite(n) || n < 0) {
|
||||
n = 0;
|
||||
}
|
||||
$scope.selectedVersionRowIndex = n;
|
||||
$scope.selectedVersion = v;
|
||||
if ($event && $event.target && typeof $event.target.blur === 'function') {
|
||||
$event.target.blur();
|
||||
} else if (typeof document !== 'undefined' && document.activeElement && typeof document.activeElement.blur === 'function') {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
};
|
||||
$scope.selectedEsMajor = '8';
|
||||
$scope.selectedRabbitmqStream = '4';
|
||||
/** RabbitMQ: 4.x is default for new installs (metadata prefetched). Upgrade may require picking stream if version line is unknown. ES major still user-picked before version list loads. */
|
||||
$scope.rabbitmqBranchChosen = false;
|
||||
$scope.esMajorChosen = false;
|
||||
$scope.confirmAction = false;
|
||||
$scope.selectedCurrentVersion = '';
|
||||
|
||||
$scope.chooseRabbitmqStream = function (stream) {
|
||||
var s = String(stream || '4').trim();
|
||||
if (s !== '3' && s !== '4') {
|
||||
s = '4';
|
||||
}
|
||||
$scope.selectedRabbitmqStream = s;
|
||||
$scope.rabbitmqBranchChosen = true;
|
||||
$scope.refreshMeta();
|
||||
};
|
||||
|
||||
$scope.chooseEsMajor = function (major) {
|
||||
var m = String(major || '8').trim();
|
||||
if (m !== '7' && m !== '8' && m !== '9') {
|
||||
m = '8';
|
||||
}
|
||||
$scope.selectedEsMajor = m;
|
||||
$scope.esMajorChosen = true;
|
||||
$scope.refreshMeta();
|
||||
};
|
||||
|
||||
/**
|
||||
* When the install/upgrade modal is open, re-apply version list from latest applicationMeta.
|
||||
* (Page-load meta can be empty for ES if dnf was slow; opening the modal must refetch.)
|
||||
*/
|
||||
$scope.syncModalVersionLists = function () {
|
||||
if (!$scope.appName || ($scope.status !== 'Installing' && $scope.status !== 'Upgrading')) {
|
||||
return;
|
||||
}
|
||||
if ($scope.appName !== 'Elasticsearch' && $scope.appName !== 'Redis' && $scope.appName !== 'RabbitMQ') {
|
||||
return;
|
||||
}
|
||||
if ($scope.appName === 'RabbitMQ' && !$scope.rabbitmqBranchChosen) {
|
||||
$scope.selectedVersions = ['latest'];
|
||||
$scope.selectedVersion = 'latest';
|
||||
$scope.repoShowsOnlyOneStream = false;
|
||||
$scope.recalcSelectedVersionRowIndex();
|
||||
return;
|
||||
}
|
||||
if ($scope.appName === 'Elasticsearch' && !$scope.esMajorChosen) {
|
||||
$scope.selectedVersions = ['latest'];
|
||||
$scope.selectedVersion = 'latest';
|
||||
$scope.repoShowsOnlyOneStream = false;
|
||||
$scope.recalcSelectedVersionRowIndex();
|
||||
return;
|
||||
}
|
||||
var meta = $scope.findAppMeta($scope.appName);
|
||||
var vers = sanitizeVersionsArray((meta && meta.versions) ? meta.versions : []);
|
||||
$scope.selectedVersions = ['latest'].concat(vers);
|
||||
var curRaw = (meta && meta.installedVersion) ? meta.installedVersion : ($scope.selectedCurrentVersion || '');
|
||||
var cur = normalizeVersionToken(curRaw) || String(curRaw || '').trim();
|
||||
if (cur && $scope.selectedVersions.indexOf(cur) === -1) {
|
||||
var allowCur = true;
|
||||
if ($scope.appName === 'RabbitMQ') {
|
||||
allowCur = versionMatchesRabbitmqStream(cur, $scope.selectedRabbitmqStream);
|
||||
} else if ($scope.appName === 'Elasticsearch') {
|
||||
allowCur = versionMatchesEsMajor(cur, $scope.selectedEsMajor);
|
||||
}
|
||||
if (allowCur) {
|
||||
$scope.selectedVersions.push(cur);
|
||||
}
|
||||
}
|
||||
if (cur) {
|
||||
$scope.selectedCurrentVersion = cur;
|
||||
}
|
||||
var prevSel = $scope.selectedVersion;
|
||||
if (prevSel && $scope.selectedVersions.indexOf(prevSel) !== -1) {
|
||||
$scope.selectedVersion = prevSel;
|
||||
} else {
|
||||
$scope.selectedVersion = 'latest';
|
||||
}
|
||||
var realVers = ($scope.selectedVersions || []).filter(function (v) {
|
||||
return v && v !== 'latest';
|
||||
});
|
||||
$scope.repoShowsOnlyOneStream = ($scope.status === 'Upgrading' && realVers.length <= 1);
|
||||
$scope.recalcSelectedVersionRowIndex();
|
||||
};
|
||||
|
||||
$scope.refreshMeta = function () {
|
||||
$scope.appsMetaRefreshing = true;
|
||||
var url = "/manageservices/applicationMeta";
|
||||
var data = {
|
||||
esMajor: $scope.selectedEsMajor,
|
||||
rabbitmqStream: $scope.selectedRabbitmqStream
|
||||
};
|
||||
var config = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=UTF-8',
|
||||
'X-CSRFToken': getCookie('csrftoken')
|
||||
},
|
||||
transformRequest: function (payload) {
|
||||
return angular.toJson(payload);
|
||||
}
|
||||
};
|
||||
|
||||
return $http.post(url, data, config).then(function (response) {
|
||||
$scope.appsMetaRefreshing = false;
|
||||
var payload = response.data;
|
||||
var ok = payload && (payload.status === 1 || payload.status === '1');
|
||||
if (ok) {
|
||||
var appMap = {};
|
||||
(payload.apps || []).forEach(function (app) {
|
||||
appMap[app.name] = app;
|
||||
});
|
||||
var esMetaResp = appMap['Elasticsearch'];
|
||||
var rmqMetaResp = appMap['RabbitMQ'];
|
||||
var respEsMaj = String(esMetaResp && esMetaResp.major != null ? esMetaResp.major : '').trim();
|
||||
if (respEsMaj && respEsMaj !== String($scope.selectedEsMajor || '8').trim()) {
|
||||
return;
|
||||
}
|
||||
var respRmqStream = String(rmqMetaResp && rmqMetaResp.rabbitmqStream != null ? rmqMetaResp.rabbitmqStream : '').trim();
|
||||
if (respRmqStream && respRmqStream !== String($scope.selectedRabbitmqStream || '4').trim()) {
|
||||
return;
|
||||
}
|
||||
$scope.apps = $scope.apps.map(function (baseApp) {
|
||||
var meta = appMap[baseApp.name] || {};
|
||||
var vers = meta.versions;
|
||||
if (!angular.isArray(vers)) {
|
||||
vers = [];
|
||||
}
|
||||
vers = sanitizeVersionsArray(vers);
|
||||
return {
|
||||
name: baseApp.name,
|
||||
image: baseApp.image,
|
||||
installed: !!meta.installed,
|
||||
installedVersion: meta.installedVersion || '',
|
||||
updateAvailable: !!meta.updateAvailable,
|
||||
crossBranchUpdateSuggested: !!meta.crossBranchUpdateSuggested,
|
||||
versions: vers,
|
||||
latestAvailable: meta.latestAvailable || '',
|
||||
latestOverall: meta.latestOverall || '',
|
||||
rabbitmqVersionsHint: meta.rabbitmqVersionsHint || ''
|
||||
};
|
||||
});
|
||||
$scope.syncModalVersionLists();
|
||||
} else {
|
||||
new PNotify({
|
||||
title: 'Operation Failed!',
|
||||
text: (payload && (payload.error_message || payload.errorMessage)) || 'Could not load application metadata.',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
}, function () {
|
||||
$scope.appsMetaRefreshing = false;
|
||||
new PNotify({
|
||||
title: 'Operation Failed!',
|
||||
text: 'Could not connect to server, please refresh this page',
|
||||
type: 'error'
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.prepareAction = function (service, status, bootstrapInstalledVersion) {
|
||||
if (bootstrapInstalledVersion === undefined || bootstrapInstalledVersion === null) {
|
||||
bootstrapInstalledVersion = '';
|
||||
} else {
|
||||
bootstrapInstalledVersion = String(bootstrapInstalledVersion).trim();
|
||||
}
|
||||
$scope.status = status;
|
||||
$scope.appName = service.name;
|
||||
$scope.confirmAction = false;
|
||||
var effectiveInstalled = (service.installedVersion || bootstrapInstalledVersion || '').trim();
|
||||
$scope.selectedCurrentVersion = effectiveInstalled;
|
||||
|
||||
if (service.name === 'RabbitMQ') {
|
||||
if (effectiveInstalled && /^4\./.test(effectiveInstalled)) {
|
||||
$scope.selectedRabbitmqStream = '4';
|
||||
} else if (effectiveInstalled && /^3\./.test(effectiveInstalled)) {
|
||||
$scope.selectedRabbitmqStream = '3';
|
||||
} else if (status === 'Installing') {
|
||||
$scope.selectedRabbitmqStream = '4';
|
||||
}
|
||||
if (status === 'Upgrading' && effectiveInstalled) {
|
||||
if (/^4\./.test(effectiveInstalled) || /^3\./.test(effectiveInstalled)) {
|
||||
$scope.rabbitmqBranchChosen = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (service.name === 'Elasticsearch' && effectiveInstalled) {
|
||||
var iv = effectiveInstalled;
|
||||
if (/^9\./.test(iv)) {
|
||||
$scope.selectedEsMajor = '9';
|
||||
} else if (/^8\./.test(iv)) {
|
||||
$scope.selectedEsMajor = '8';
|
||||
} else if (/^7\./.test(iv)) {
|
||||
$scope.selectedEsMajor = '7';
|
||||
}
|
||||
}
|
||||
|
||||
$scope.selectedVersions = ['latest'];
|
||||
// RabbitMQ upgrade: bootstrap meta is often stream 4; stream follows installed line — do not
|
||||
// reuse service.versions until refreshMeta returns for selectedRabbitmqStream (avoids mismatched list).
|
||||
var deferVersionList = (service.name === 'RabbitMQ' && (!$scope.rabbitmqBranchChosen || status === 'Upgrading'))
|
||||
|| (service.name === 'Elasticsearch' && !$scope.esMajorChosen);
|
||||
if (!deferVersionList) {
|
||||
var svcVers = sanitizeVersionsArray(service.versions || []);
|
||||
if (svcVers.length > 0) {
|
||||
$scope.selectedVersions = ['latest'].concat(svcVers);
|
||||
}
|
||||
var curPick = normalizeVersionToken(effectiveInstalled) || effectiveInstalled;
|
||||
if (curPick && $scope.selectedVersions.indexOf(curPick) === -1) {
|
||||
$scope.selectedVersions.push(curPick);
|
||||
}
|
||||
}
|
||||
$scope.selectedVersion = 'latest';
|
||||
$scope.requestData = '';
|
||||
|
||||
var realVers = ($scope.selectedVersions || []).filter(function (v) {
|
||||
return v && v !== 'latest';
|
||||
});
|
||||
if (deferVersionList) {
|
||||
$scope.repoShowsOnlyOneStream = false;
|
||||
} else {
|
||||
$scope.repoShowsOnlyOneStream = ($scope.status === 'Upgrading' && realVers.length <= 1);
|
||||
}
|
||||
$scope.recalcSelectedVersionRowIndex();
|
||||
};
|
||||
|
||||
$scope.findAppMeta = function (appName) {
|
||||
var found = null;
|
||||
($scope.apps || []).forEach(function (item) {
|
||||
if (item.name === appName) {
|
||||
found = item;
|
||||
}
|
||||
});
|
||||
return found || {};
|
||||
};
|
||||
|
||||
$scope.prepareActionByName = function (appName, status, bootstrapInstalledVersion) {
|
||||
var meta = $scope.findAppMeta(appName);
|
||||
if (!meta.name) {
|
||||
meta = {name: appName, versions: []};
|
||||
}
|
||||
var mver = meta.versions;
|
||||
if (!angular.isArray(mver)) {
|
||||
mver = [];
|
||||
}
|
||||
var merged = {
|
||||
name: meta.name,
|
||||
image: meta.image,
|
||||
installed: meta.installed,
|
||||
installedVersion: meta.installedVersion || '',
|
||||
versions: mver
|
||||
};
|
||||
$scope.prepareAction(merged, status, bootstrapInstalledVersion);
|
||||
};
|
||||
|
||||
/**
|
||||
* Prepare scope then show modal (do not use data-toggle + ng-click — Bootstrap can
|
||||
* open the dialog before Angular runs ng-click, leaving status/appName unset).
|
||||
* For managed apps, wait for applicationMeta so version lists and installedVersion are correct (upgrade vs downgrade).
|
||||
*/
|
||||
$scope.openApplicationsModal = function (appName, status, bootstrapInstalledVersion) {
|
||||
var needMeta = (appName === 'RabbitMQ' || appName === 'Elasticsearch' || appName === 'Redis');
|
||||
var showModal = function () {
|
||||
if (typeof window.jQuery !== 'undefined' && jQuery('#settings').modal) {
|
||||
jQuery('#settings').modal('show');
|
||||
}
|
||||
};
|
||||
if (needMeta) {
|
||||
if (appName === 'RabbitMQ') {
|
||||
if (status === 'Installing') {
|
||||
$scope.selectedRabbitmqStream = '4';
|
||||
$scope.rabbitmqBranchChosen = true;
|
||||
} else {
|
||||
$scope.rabbitmqBranchChosen = false;
|
||||
}
|
||||
}
|
||||
if (appName === 'Elasticsearch') {
|
||||
$scope.esMajorChosen = false;
|
||||
}
|
||||
// RabbitMQ install: default 4.x stream and prefetch metadata. Upgrade: pick stream from installed version.
|
||||
// Elasticsearch: still wait for user major. Redis refreshes on open.
|
||||
$scope.appName = appName;
|
||||
$scope.status = status;
|
||||
$scope.prepareActionByName(appName, status, bootstrapInstalledVersion);
|
||||
showModal();
|
||||
if (appName === 'Redis' || (appName === 'RabbitMQ' && $scope.rabbitmqBranchChosen)) {
|
||||
$timeout(function () {
|
||||
$scope.refreshMeta();
|
||||
}, 0);
|
||||
}
|
||||
} else {
|
||||
$scope.prepareActionByName(appName, status, bootstrapInstalledVersion);
|
||||
showModal();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.runAction = function () {
|
||||
var appName = $scope.appName;
|
||||
var status = $scope.status;
|
||||
if (!appName || !status) {
|
||||
return;
|
||||
}
|
||||
if ((status === 'Removing' || status === 'Upgrading') && !$scope.confirmAction) {
|
||||
new PNotify({
|
||||
title: 'Confirmation Required',
|
||||
text: 'Please confirm this action before proceeding.',
|
||||
type: 'warning'
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (appName === 'RabbitMQ' && (status === 'Installing' || status === 'Upgrading') && !$scope.rabbitmqBranchChosen) {
|
||||
new PNotify({
|
||||
title: 'Stream required',
|
||||
text: 'Choose RabbitMQ 4.x or 3.x above to load versions for that line.',
|
||||
type: 'warning'
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (appName === 'Elasticsearch' && (status === 'Installing' || status === 'Upgrading') && !$scope.esMajorChosen) {
|
||||
new PNotify({
|
||||
title: 'Major version required',
|
||||
text: 'Choose Elasticsearch major (7, 8, or 9) above to load versions for that line.',
|
||||
type: 'warning'
|
||||
});
|
||||
return;
|
||||
}
|
||||
$scope.status = status;
|
||||
$scope.appName = appName;
|
||||
|
||||
@@ -441,7 +936,11 @@ app.controller('manageApplications', function ($scope, $http, $timeout, $window)
|
||||
|
||||
var data = {
|
||||
appName: appName,
|
||||
status: status
|
||||
status: status,
|
||||
version: $scope.selectedVersion || 'latest',
|
||||
esMajor: $scope.selectedEsMajor || '8',
|
||||
rabbitmqStream: $scope.selectedRabbitmqStream || '4',
|
||||
confirmAction: $scope.confirmAction === true
|
||||
};
|
||||
|
||||
var config = {
|
||||
@@ -524,6 +1023,18 @@ app.controller('manageApplications', function ($scope, $http, $timeout, $window)
|
||||
|
||||
}
|
||||
|
||||
// Do not fetch package metadata on page load; it can block workers under DNF load.
|
||||
// Metadata is fetched on-demand when opening install/version-change modals.
|
||||
|
||||
if (typeof window.jQuery !== 'undefined' && jQuery.fn.on) {
|
||||
jQuery('#settings').on('hidden.bs.modal', function () {
|
||||
$scope.$evalAsync(function () {
|
||||
$scope.rabbitmqBranchChosen = false;
|
||||
$scope.esMajorChosen = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
/* Java script code */
|
||||
@@ -974,6 +974,15 @@ app.controller('listContainers', function ($scope, $http) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($scope.dockerUpdateInProgress) {
|
||||
new PNotify({
|
||||
title: 'Update in progress',
|
||||
text: 'Wait until the current update finishes before starting another.',
|
||||
type: 'warning'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// If no new image specified, use current image
|
||||
if (!$scope.newImage) {
|
||||
$scope.newImage = $scope.currentImage;
|
||||
@@ -1000,9 +1009,18 @@ app.controller('listContainers', function ($scope, $http) {
|
||||
history: false
|
||||
}
|
||||
})).get().on('pnotify.confirm', function () {
|
||||
var dockerUpdateNotificationId = null;
|
||||
$scope.dockerUpdateInProgress = true;
|
||||
$('#imageLoading').show();
|
||||
$("#updateContainer").modal("hide");
|
||||
|
||||
if (typeof window.cpDockerUpdateNotifyStart === 'function') {
|
||||
dockerUpdateNotificationId = window.cpDockerUpdateNotifyStart(
|
||||
$scope.updateContainerName,
|
||||
$scope.newImage + ':' + $scope.newTag
|
||||
);
|
||||
}
|
||||
|
||||
url = "/docker/updateContainer";
|
||||
var data = {
|
||||
name: $scope.updateContainerName,
|
||||
@@ -1020,15 +1038,27 @@ app.controller('listContainers', function ($scope, $http) {
|
||||
|
||||
function ListInitialData(response) {
|
||||
console.log(response);
|
||||
$scope.dockerUpdateInProgress = false;
|
||||
$('#imageLoading').hide();
|
||||
|
||||
if (response.data.updateContainerStatus === 1) {
|
||||
var ok = response.data && response.data.updateContainerStatus === 1;
|
||||
var imgLabel = ok
|
||||
? (response.data.new_image || response.data.message || 'Updated')
|
||||
: (response.data && response.data.error_message ? response.data.error_message : 'Update failed');
|
||||
|
||||
if (typeof window.cpDockerUpdateNotifyEnd === 'function' && dockerUpdateNotificationId) {
|
||||
window.cpDockerUpdateNotifyEnd(dockerUpdateNotificationId, ok, imgLabel);
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
new PNotify({
|
||||
title: 'Container Updated Successfully',
|
||||
text: `Container updated to ${response.data.new_image}`,
|
||||
text: 'Container updated to ' + (response.data.new_image || response.data.message || 'new image'),
|
||||
type: 'success'
|
||||
});
|
||||
location.reload();
|
||||
setTimeout(function () {
|
||||
location.reload();
|
||||
}, 2200);
|
||||
} else {
|
||||
new PNotify({
|
||||
title: 'Update Failed',
|
||||
@@ -1039,7 +1069,11 @@ app.controller('listContainers', function ($scope, $http) {
|
||||
}
|
||||
|
||||
function cantLoadInitialData(response) {
|
||||
$scope.dockerUpdateInProgress = false;
|
||||
$('#imageLoading').hide();
|
||||
if (typeof window.cpDockerUpdateNotifyEnd === 'function' && dockerUpdateNotificationId) {
|
||||
window.cpDockerUpdateNotifyEnd(dockerUpdateNotificationId, false, 'Could not connect to server');
|
||||
}
|
||||
new PNotify({
|
||||
title: 'Update Failed',
|
||||
text: 'Could not connect to server',
|
||||
|
||||
@@ -428,10 +428,505 @@ app.controller('pureFTPD', function ($scope, $http, $timeout, $window) {
|
||||
|
||||
app.controller('manageApplications', function ($scope, $http, $timeout, $window) {
|
||||
|
||||
/**
|
||||
* Normalize entries from applicationMeta (strings, numbers, or rare object shapes)
|
||||
* so version pickers never show blank rows. CyberPanel uses {$ ... $} interpolation
|
||||
* in templates; list labels use versionLabel() for consistent display.
|
||||
*/
|
||||
function normalizeVersionToken(v) {
|
||||
if (v === null || v === undefined) {
|
||||
return '';
|
||||
}
|
||||
if (v === 'latest') {
|
||||
return 'latest';
|
||||
}
|
||||
if (typeof v === 'number' && isFinite(v)) {
|
||||
return String(v);
|
||||
}
|
||||
if (angular.isObject(v)) {
|
||||
var o = v;
|
||||
var cand = o.version || o.Version || o.value || o.name || o.ver || o.label;
|
||||
if (cand !== undefined && cand !== null) {
|
||||
return String(cand).trim();
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(o);
|
||||
} catch (ignore) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
return String(v).trim();
|
||||
}
|
||||
|
||||
function sanitizeVersionsArray(vers) {
|
||||
if (!angular.isArray(vers)) {
|
||||
return [];
|
||||
}
|
||||
var out = [];
|
||||
var seen = {};
|
||||
vers.forEach(function (raw) {
|
||||
var t = normalizeVersionToken(raw);
|
||||
if (!t || t === 'latest') {
|
||||
return;
|
||||
}
|
||||
if (!seen[t]) {
|
||||
seen[t] = true;
|
||||
out.push(t);
|
||||
}
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
function versionMatchesRabbitmqStream(ver, stream) {
|
||||
var s = String(stream || '4').trim();
|
||||
var t = normalizeVersionToken(ver);
|
||||
if (!t || t === 'latest') {
|
||||
return false;
|
||||
}
|
||||
var m = /^(\d+)\./.exec(t);
|
||||
return !!(m && m[1] === s);
|
||||
}
|
||||
|
||||
function versionMatchesEsMajor(ver, major) {
|
||||
var mjr = String(major || '8').trim();
|
||||
var t = normalizeVersionToken(ver);
|
||||
if (!t || t === 'latest') {
|
||||
return false;
|
||||
}
|
||||
var m = /^(\d+)\./.exec(t);
|
||||
return !!(m && m[1] === mjr);
|
||||
}
|
||||
|
||||
$scope.versionLabel = function (v) {
|
||||
if (v === 'latest') {
|
||||
return 'latest';
|
||||
}
|
||||
var t = normalizeVersionToken(v);
|
||||
return t || '(unknown)';
|
||||
};
|
||||
|
||||
$scope.versionTrackId = function (idx, v) {
|
||||
return String(idx) + '|' + $scope.versionLabel(v);
|
||||
};
|
||||
|
||||
/* false = long-running install/remove/poll (show spinners); true = idle */
|
||||
$scope.cyberpanelLoading = true;
|
||||
/** Background applicationMeta refresh — separate from cyberpanelLoading so the page does not “freeze” on every modal open. */
|
||||
$scope.appsMetaRefreshing = false;
|
||||
$scope.apps = [
|
||||
{name: 'Elasticsearch', image: '/static/manageServices/images/elastic-search.png'},
|
||||
{name: 'Redis', image: '/static/manageServices/images/redis.png'},
|
||||
{name: 'RabbitMQ', image: '/static/manageServices/images/rabbitmq-logo.svg'}
|
||||
];
|
||||
|
||||
$scope.removeInstall = function (appName, status) {
|
||||
(function mergeMetaBootstrap() {
|
||||
var el = document.getElementById('manageApplicationsMetaBootstrap');
|
||||
if (!el || !el.textContent) {
|
||||
return;
|
||||
}
|
||||
var raw = el.textContent.trim();
|
||||
if (!raw) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var boot = JSON.parse(raw);
|
||||
if (!boot || Number(boot.status) !== 1) {
|
||||
return;
|
||||
}
|
||||
var appMap = {};
|
||||
(boot.apps || []).forEach(function (a) {
|
||||
if (a && a.name) {
|
||||
appMap[a.name] = a;
|
||||
}
|
||||
});
|
||||
$scope.apps = $scope.apps.map(function (baseApp) {
|
||||
var meta = appMap[baseApp.name] || {};
|
||||
var vers = meta.versions;
|
||||
if (!angular.isArray(vers)) {
|
||||
vers = [];
|
||||
}
|
||||
vers = sanitizeVersionsArray(vers);
|
||||
return {
|
||||
name: baseApp.name,
|
||||
image: baseApp.image,
|
||||
installed: !!meta.installed,
|
||||
installedVersion: meta.installedVersion || '',
|
||||
updateAvailable: !!meta.updateAvailable,
|
||||
crossBranchUpdateSuggested: !!meta.crossBranchUpdateSuggested,
|
||||
versions: vers,
|
||||
latestAvailable: meta.latestAvailable || '',
|
||||
latestOverall: meta.latestOverall || '',
|
||||
rabbitmqVersionsHint: meta.rabbitmqVersionsHint || ''
|
||||
};
|
||||
});
|
||||
} catch (ignore) {
|
||||
/* keep bare apps list */
|
||||
}
|
||||
})();
|
||||
|
||||
$scope.selectedVersion = 'latest';
|
||||
/** Row highlight uses $index so only one row looks selected (avoids sticky :focus / repeater quirks). */
|
||||
$scope.selectedVersionRowIndex = 0;
|
||||
$scope.selectedVersions = ['latest'];
|
||||
|
||||
$scope.recalcSelectedVersionRowIndex = function () {
|
||||
var list = $scope.selectedVersions || [];
|
||||
var sel = $scope.selectedVersion;
|
||||
var i = list.indexOf(sel);
|
||||
if (i < 0) {
|
||||
var normSel = normalizeVersionToken(sel);
|
||||
if (normSel) {
|
||||
for (var j = 0; j < list.length; j += 1) {
|
||||
if (normalizeVersionToken(list[j]) === normSel) {
|
||||
i = j;
|
||||
$scope.selectedVersion = list[j];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$scope.selectedVersionRowIndex = (i >= 0) ? i : 0;
|
||||
};
|
||||
|
||||
$scope.selectManagedAppVersion = function (idx, v, $event) {
|
||||
var n = (typeof idx === 'number') ? idx : parseInt(idx, 10);
|
||||
if (!isFinite(n) || n < 0) {
|
||||
n = 0;
|
||||
}
|
||||
$scope.selectedVersionRowIndex = n;
|
||||
$scope.selectedVersion = v;
|
||||
if ($event && $event.target && typeof $event.target.blur === 'function') {
|
||||
$event.target.blur();
|
||||
} else if (typeof document !== 'undefined' && document.activeElement && typeof document.activeElement.blur === 'function') {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
};
|
||||
$scope.selectedEsMajor = '8';
|
||||
$scope.selectedRabbitmqStream = '4';
|
||||
/** RabbitMQ: 4.x is default for new installs (metadata prefetched). Upgrade may require picking stream if version line is unknown. ES major still user-picked before version list loads. */
|
||||
$scope.rabbitmqBranchChosen = false;
|
||||
$scope.esMajorChosen = false;
|
||||
$scope.confirmAction = false;
|
||||
$scope.selectedCurrentVersion = '';
|
||||
|
||||
$scope.chooseRabbitmqStream = function (stream) {
|
||||
var s = String(stream || '4').trim();
|
||||
if (s !== '3' && s !== '4') {
|
||||
s = '4';
|
||||
}
|
||||
$scope.selectedRabbitmqStream = s;
|
||||
$scope.rabbitmqBranchChosen = true;
|
||||
$scope.refreshMeta();
|
||||
};
|
||||
|
||||
$scope.chooseEsMajor = function (major) {
|
||||
var m = String(major || '8').trim();
|
||||
if (m !== '7' && m !== '8' && m !== '9') {
|
||||
m = '8';
|
||||
}
|
||||
$scope.selectedEsMajor = m;
|
||||
$scope.esMajorChosen = true;
|
||||
$scope.refreshMeta();
|
||||
};
|
||||
|
||||
/**
|
||||
* When the install/upgrade modal is open, re-apply version list from latest applicationMeta.
|
||||
* (Page-load meta can be empty for ES if dnf was slow; opening the modal must refetch.)
|
||||
*/
|
||||
$scope.syncModalVersionLists = function () {
|
||||
if (!$scope.appName || ($scope.status !== 'Installing' && $scope.status !== 'Upgrading')) {
|
||||
return;
|
||||
}
|
||||
if ($scope.appName !== 'Elasticsearch' && $scope.appName !== 'Redis' && $scope.appName !== 'RabbitMQ') {
|
||||
return;
|
||||
}
|
||||
if ($scope.appName === 'RabbitMQ' && !$scope.rabbitmqBranchChosen) {
|
||||
$scope.selectedVersions = ['latest'];
|
||||
$scope.selectedVersion = 'latest';
|
||||
$scope.repoShowsOnlyOneStream = false;
|
||||
$scope.recalcSelectedVersionRowIndex();
|
||||
return;
|
||||
}
|
||||
if ($scope.appName === 'Elasticsearch' && !$scope.esMajorChosen) {
|
||||
$scope.selectedVersions = ['latest'];
|
||||
$scope.selectedVersion = 'latest';
|
||||
$scope.repoShowsOnlyOneStream = false;
|
||||
$scope.recalcSelectedVersionRowIndex();
|
||||
return;
|
||||
}
|
||||
var meta = $scope.findAppMeta($scope.appName);
|
||||
var vers = sanitizeVersionsArray((meta && meta.versions) ? meta.versions : []);
|
||||
$scope.selectedVersions = ['latest'].concat(vers);
|
||||
var curRaw = (meta && meta.installedVersion) ? meta.installedVersion : ($scope.selectedCurrentVersion || '');
|
||||
var cur = normalizeVersionToken(curRaw) || String(curRaw || '').trim();
|
||||
if (cur && $scope.selectedVersions.indexOf(cur) === -1) {
|
||||
var allowCur = true;
|
||||
if ($scope.appName === 'RabbitMQ') {
|
||||
allowCur = versionMatchesRabbitmqStream(cur, $scope.selectedRabbitmqStream);
|
||||
} else if ($scope.appName === 'Elasticsearch') {
|
||||
allowCur = versionMatchesEsMajor(cur, $scope.selectedEsMajor);
|
||||
}
|
||||
if (allowCur) {
|
||||
$scope.selectedVersions.push(cur);
|
||||
}
|
||||
}
|
||||
if (cur) {
|
||||
$scope.selectedCurrentVersion = cur;
|
||||
}
|
||||
var prevSel = $scope.selectedVersion;
|
||||
if (prevSel && $scope.selectedVersions.indexOf(prevSel) !== -1) {
|
||||
$scope.selectedVersion = prevSel;
|
||||
} else {
|
||||
$scope.selectedVersion = 'latest';
|
||||
}
|
||||
var realVers = ($scope.selectedVersions || []).filter(function (v) {
|
||||
return v && v !== 'latest';
|
||||
});
|
||||
$scope.repoShowsOnlyOneStream = ($scope.status === 'Upgrading' && realVers.length <= 1);
|
||||
$scope.recalcSelectedVersionRowIndex();
|
||||
};
|
||||
|
||||
$scope.refreshMeta = function () {
|
||||
$scope.appsMetaRefreshing = true;
|
||||
var url = "/manageservices/applicationMeta";
|
||||
var data = {
|
||||
esMajor: $scope.selectedEsMajor,
|
||||
rabbitmqStream: $scope.selectedRabbitmqStream
|
||||
};
|
||||
var config = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=UTF-8',
|
||||
'X-CSRFToken': getCookie('csrftoken')
|
||||
},
|
||||
transformRequest: function (payload) {
|
||||
return angular.toJson(payload);
|
||||
}
|
||||
};
|
||||
|
||||
return $http.post(url, data, config).then(function (response) {
|
||||
$scope.appsMetaRefreshing = false;
|
||||
var payload = response.data;
|
||||
var ok = payload && (payload.status === 1 || payload.status === '1');
|
||||
if (ok) {
|
||||
var appMap = {};
|
||||
(payload.apps || []).forEach(function (app) {
|
||||
appMap[app.name] = app;
|
||||
});
|
||||
var esMetaResp = appMap['Elasticsearch'];
|
||||
var rmqMetaResp = appMap['RabbitMQ'];
|
||||
var respEsMaj = String(esMetaResp && esMetaResp.major != null ? esMetaResp.major : '').trim();
|
||||
if (respEsMaj && respEsMaj !== String($scope.selectedEsMajor || '8').trim()) {
|
||||
return;
|
||||
}
|
||||
var respRmqStream = String(rmqMetaResp && rmqMetaResp.rabbitmqStream != null ? rmqMetaResp.rabbitmqStream : '').trim();
|
||||
if (respRmqStream && respRmqStream !== String($scope.selectedRabbitmqStream || '4').trim()) {
|
||||
return;
|
||||
}
|
||||
$scope.apps = $scope.apps.map(function (baseApp) {
|
||||
var meta = appMap[baseApp.name] || {};
|
||||
var vers = meta.versions;
|
||||
if (!angular.isArray(vers)) {
|
||||
vers = [];
|
||||
}
|
||||
vers = sanitizeVersionsArray(vers);
|
||||
return {
|
||||
name: baseApp.name,
|
||||
image: baseApp.image,
|
||||
installed: !!meta.installed,
|
||||
installedVersion: meta.installedVersion || '',
|
||||
updateAvailable: !!meta.updateAvailable,
|
||||
crossBranchUpdateSuggested: !!meta.crossBranchUpdateSuggested,
|
||||
versions: vers,
|
||||
latestAvailable: meta.latestAvailable || '',
|
||||
latestOverall: meta.latestOverall || '',
|
||||
rabbitmqVersionsHint: meta.rabbitmqVersionsHint || ''
|
||||
};
|
||||
});
|
||||
$scope.syncModalVersionLists();
|
||||
} else {
|
||||
new PNotify({
|
||||
title: 'Operation Failed!',
|
||||
text: (payload && (payload.error_message || payload.errorMessage)) || 'Could not load application metadata.',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
}, function () {
|
||||
$scope.appsMetaRefreshing = false;
|
||||
new PNotify({
|
||||
title: 'Operation Failed!',
|
||||
text: 'Could not connect to server, please refresh this page',
|
||||
type: 'error'
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.prepareAction = function (service, status, bootstrapInstalledVersion) {
|
||||
if (bootstrapInstalledVersion === undefined || bootstrapInstalledVersion === null) {
|
||||
bootstrapInstalledVersion = '';
|
||||
} else {
|
||||
bootstrapInstalledVersion = String(bootstrapInstalledVersion).trim();
|
||||
}
|
||||
$scope.status = status;
|
||||
$scope.appName = service.name;
|
||||
$scope.confirmAction = false;
|
||||
var effectiveInstalled = (service.installedVersion || bootstrapInstalledVersion || '').trim();
|
||||
$scope.selectedCurrentVersion = effectiveInstalled;
|
||||
|
||||
if (service.name === 'RabbitMQ') {
|
||||
if (effectiveInstalled && /^4\./.test(effectiveInstalled)) {
|
||||
$scope.selectedRabbitmqStream = '4';
|
||||
} else if (effectiveInstalled && /^3\./.test(effectiveInstalled)) {
|
||||
$scope.selectedRabbitmqStream = '3';
|
||||
} else if (status === 'Installing') {
|
||||
$scope.selectedRabbitmqStream = '4';
|
||||
}
|
||||
if (status === 'Upgrading' && effectiveInstalled) {
|
||||
if (/^4\./.test(effectiveInstalled) || /^3\./.test(effectiveInstalled)) {
|
||||
$scope.rabbitmqBranchChosen = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (service.name === 'Elasticsearch' && effectiveInstalled) {
|
||||
var iv = effectiveInstalled;
|
||||
if (/^9\./.test(iv)) {
|
||||
$scope.selectedEsMajor = '9';
|
||||
} else if (/^8\./.test(iv)) {
|
||||
$scope.selectedEsMajor = '8';
|
||||
} else if (/^7\./.test(iv)) {
|
||||
$scope.selectedEsMajor = '7';
|
||||
}
|
||||
}
|
||||
|
||||
$scope.selectedVersions = ['latest'];
|
||||
// RabbitMQ upgrade: bootstrap meta is often stream 4; stream follows installed line — do not
|
||||
// reuse service.versions until refreshMeta returns for selectedRabbitmqStream (avoids mismatched list).
|
||||
var deferVersionList = (service.name === 'RabbitMQ' && (!$scope.rabbitmqBranchChosen || status === 'Upgrading'))
|
||||
|| (service.name === 'Elasticsearch' && !$scope.esMajorChosen);
|
||||
if (!deferVersionList) {
|
||||
var svcVers = sanitizeVersionsArray(service.versions || []);
|
||||
if (svcVers.length > 0) {
|
||||
$scope.selectedVersions = ['latest'].concat(svcVers);
|
||||
}
|
||||
var curPick = normalizeVersionToken(effectiveInstalled) || effectiveInstalled;
|
||||
if (curPick && $scope.selectedVersions.indexOf(curPick) === -1) {
|
||||
$scope.selectedVersions.push(curPick);
|
||||
}
|
||||
}
|
||||
$scope.selectedVersion = 'latest';
|
||||
$scope.requestData = '';
|
||||
|
||||
var realVers = ($scope.selectedVersions || []).filter(function (v) {
|
||||
return v && v !== 'latest';
|
||||
});
|
||||
if (deferVersionList) {
|
||||
$scope.repoShowsOnlyOneStream = false;
|
||||
} else {
|
||||
$scope.repoShowsOnlyOneStream = ($scope.status === 'Upgrading' && realVers.length <= 1);
|
||||
}
|
||||
$scope.recalcSelectedVersionRowIndex();
|
||||
};
|
||||
|
||||
$scope.findAppMeta = function (appName) {
|
||||
var found = null;
|
||||
($scope.apps || []).forEach(function (item) {
|
||||
if (item.name === appName) {
|
||||
found = item;
|
||||
}
|
||||
});
|
||||
return found || {};
|
||||
};
|
||||
|
||||
$scope.prepareActionByName = function (appName, status, bootstrapInstalledVersion) {
|
||||
var meta = $scope.findAppMeta(appName);
|
||||
if (!meta.name) {
|
||||
meta = {name: appName, versions: []};
|
||||
}
|
||||
var mver = meta.versions;
|
||||
if (!angular.isArray(mver)) {
|
||||
mver = [];
|
||||
}
|
||||
var merged = {
|
||||
name: meta.name,
|
||||
image: meta.image,
|
||||
installed: meta.installed,
|
||||
installedVersion: meta.installedVersion || '',
|
||||
versions: mver
|
||||
};
|
||||
$scope.prepareAction(merged, status, bootstrapInstalledVersion);
|
||||
};
|
||||
|
||||
/**
|
||||
* Prepare scope then show modal (do not use data-toggle + ng-click — Bootstrap can
|
||||
* open the dialog before Angular runs ng-click, leaving status/appName unset).
|
||||
* For managed apps, wait for applicationMeta so version lists and installedVersion are correct (upgrade vs downgrade).
|
||||
*/
|
||||
$scope.openApplicationsModal = function (appName, status, bootstrapInstalledVersion) {
|
||||
var needMeta = (appName === 'RabbitMQ' || appName === 'Elasticsearch' || appName === 'Redis');
|
||||
var showModal = function () {
|
||||
if (typeof window.jQuery !== 'undefined' && jQuery('#settings').modal) {
|
||||
jQuery('#settings').modal('show');
|
||||
}
|
||||
};
|
||||
if (needMeta) {
|
||||
if (appName === 'RabbitMQ') {
|
||||
if (status === 'Installing') {
|
||||
$scope.selectedRabbitmqStream = '4';
|
||||
$scope.rabbitmqBranchChosen = true;
|
||||
} else {
|
||||
$scope.rabbitmqBranchChosen = false;
|
||||
}
|
||||
}
|
||||
if (appName === 'Elasticsearch') {
|
||||
$scope.esMajorChosen = false;
|
||||
}
|
||||
// RabbitMQ install: default 4.x stream and prefetch metadata. Upgrade: pick stream from installed version.
|
||||
// Elasticsearch: still wait for user major. Redis refreshes on open.
|
||||
$scope.appName = appName;
|
||||
$scope.status = status;
|
||||
$scope.prepareActionByName(appName, status, bootstrapInstalledVersion);
|
||||
showModal();
|
||||
if (appName === 'Redis' || (appName === 'RabbitMQ' && $scope.rabbitmqBranchChosen)) {
|
||||
$timeout(function () {
|
||||
$scope.refreshMeta();
|
||||
}, 0);
|
||||
}
|
||||
} else {
|
||||
$scope.prepareActionByName(appName, status, bootstrapInstalledVersion);
|
||||
showModal();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.runAction = function () {
|
||||
var appName = $scope.appName;
|
||||
var status = $scope.status;
|
||||
if (!appName || !status) {
|
||||
return;
|
||||
}
|
||||
if ((status === 'Removing' || status === 'Upgrading') && !$scope.confirmAction) {
|
||||
new PNotify({
|
||||
title: 'Confirmation Required',
|
||||
text: 'Please confirm this action before proceeding.',
|
||||
type: 'warning'
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (appName === 'RabbitMQ' && (status === 'Installing' || status === 'Upgrading') && !$scope.rabbitmqBranchChosen) {
|
||||
new PNotify({
|
||||
title: 'Stream required',
|
||||
text: 'Choose RabbitMQ 4.x or 3.x above to load versions for that line.',
|
||||
type: 'warning'
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (appName === 'Elasticsearch' && (status === 'Installing' || status === 'Upgrading') && !$scope.esMajorChosen) {
|
||||
new PNotify({
|
||||
title: 'Major version required',
|
||||
text: 'Choose Elasticsearch major (7, 8, or 9) above to load versions for that line.',
|
||||
type: 'warning'
|
||||
});
|
||||
return;
|
||||
}
|
||||
$scope.status = status;
|
||||
$scope.appName = appName;
|
||||
|
||||
@@ -441,7 +936,11 @@ app.controller('manageApplications', function ($scope, $http, $timeout, $window)
|
||||
|
||||
var data = {
|
||||
appName: appName,
|
||||
status: status
|
||||
status: status,
|
||||
version: $scope.selectedVersion || 'latest',
|
||||
esMajor: $scope.selectedEsMajor || '8',
|
||||
rabbitmqStream: $scope.selectedRabbitmqStream || '4',
|
||||
confirmAction: $scope.confirmAction === true
|
||||
};
|
||||
|
||||
var config = {
|
||||
@@ -524,6 +1023,18 @@ app.controller('manageApplications', function ($scope, $http, $timeout, $window)
|
||||
|
||||
}
|
||||
|
||||
// Do not fetch package metadata on page load; it can block workers under DNF load.
|
||||
// Metadata is fetched on-demand when opening install/version-change modals.
|
||||
|
||||
if (typeof window.jQuery !== 'undefined' && jQuery.fn.on) {
|
||||
jQuery('#settings').on('hidden.bs.modal', function () {
|
||||
$scope.$evalAsync(function () {
|
||||
$scope.rabbitmqBranchChosen = false;
|
||||
$scope.esMajorChosen = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
/* Java script code */
|
||||
@@ -848,9 +848,20 @@ app.controller('createACLCTRL', function ($scope, $http) {
|
||||
|
||||
var url = "/users/createACLFunc";
|
||||
|
||||
var aclNameTrimmed = ($scope.aclName !== undefined && $scope.aclName !== null) ? String($scope.aclName).trim() : '';
|
||||
if (!aclNameTrimmed) {
|
||||
$scope.aclLoading = true;
|
||||
safePNotify({
|
||||
title: 'Error!',
|
||||
text: 'Please enter a name for this ACL.',
|
||||
type: 'error'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var data = {
|
||||
|
||||
aclName: $scope.aclName,
|
||||
aclName: aclNameTrimmed,
|
||||
makeAdmin: $scope.makeAdmin,
|
||||
|
||||
//
|
||||
@@ -943,10 +954,10 @@ app.controller('createACLCTRL', function ($scope, $http) {
|
||||
type: 'success'
|
||||
});
|
||||
} else {
|
||||
|
||||
var errText = (response.data && (response.data.errorMessage || response.data.error_message)) ? (response.data.errorMessage || response.data.error_message) : 'Unknown error';
|
||||
safePNotify({
|
||||
title: 'Error!',
|
||||
text: response.data.errorMessage,
|
||||
text: errText,
|
||||
type: 'error'
|
||||
});
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
from django.shortcuts import render, redirect
|
||||
from django.http import HttpResponse
|
||||
from django.utils.translation import gettext as _
|
||||
from django.db import models
|
||||
from django.db.utils import IntegrityError, ProgrammingError, OperationalError
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
@@ -765,9 +766,17 @@ def createACLFunc(request):
|
||||
if currentACL['admin'] == 1:
|
||||
data = json.loads(request.body)
|
||||
|
||||
acl_name_raw = data.get('aclName', '') or ''
|
||||
acl_name = str(acl_name_raw).strip()
|
||||
if not acl_name:
|
||||
msg = str(_('Please enter a name for this ACL.'))
|
||||
err_body = {'status': 0, 'error_message': msg, 'errorMessage': msg}
|
||||
return HttpResponse(json.dumps(err_body), content_type='application/json')
|
||||
data['aclName'] = acl_name
|
||||
|
||||
## Version Management
|
||||
|
||||
if data['makeAdmin']:
|
||||
if data.get('makeAdmin'):
|
||||
data['adminStatus'] = 1
|
||||
else:
|
||||
data['adminStatus'] = 0
|
||||
|
||||
@@ -1,322 +0,0 @@
|
||||
{% extends "baseTemplate/index.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}
|
||||
Security Management - CyberPanel
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">
|
||||
<i class="fas fa-shield-alt"></i>
|
||||
Security Management
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Security Alerts Section -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="alert alert-warning">
|
||||
<h5><i class="fas fa-exclamation-triangle"></i> Security Alerts</h5>
|
||||
<p>Monitor and manage security threats detected by the system.</p>
|
||||
<button class="btn btn-warning" onclick="refreshSecurityAlerts()">
|
||||
<i class="fas fa-sync"></i> Refresh Alerts
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Security Alerts List -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4>Recent Security Alerts</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="securityAlertsContainer">
|
||||
<!-- Alerts will be loaded here -->
|
||||
<div class="text-center">
|
||||
<i class="fas fa-spinner fa-spin"></i> Loading security alerts...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Blocked IPs Management -->
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4>Blocked IP Addresses</h4>
|
||||
<button class="btn btn-success btn-sm" onclick="refreshBlockedIPs()">
|
||||
<i class="fas fa-sync"></i> Refresh
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped" id="blockedIPsTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>IP Address</th>
|
||||
<th>Blocked At</th>
|
||||
<th>Reason</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="blockedIPsTableBody">
|
||||
<tr>
|
||||
<td colspan="4" class="text-center">
|
||||
<i class="fas fa-spinner fa-spin"></i> Loading...
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Manual IP Blocking -->
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4>Manual IP Blocking</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="blockIPForm">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="ipAddress">IP Address</label>
|
||||
<input type="text" class="form-control" id="ipAddress" name="ip_address" placeholder="192.168.1.100" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="blockReason">Reason</label>
|
||||
<input type="text" class="form-control" id="blockReason" name="reason" placeholder="Suspicious activity" value="Manual block via CyberPanel">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-danger">
|
||||
<i class="fas fa-ban"></i> Block IP Address
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Sample security alerts data (in a real implementation, this would come from the backend)
|
||||
const sampleAlerts = [
|
||||
{
|
||||
id: 1,
|
||||
type: 'brute_force',
|
||||
ip: '129.212.176.254',
|
||||
attempts: 85,
|
||||
severity: 'HIGH',
|
||||
timestamp: '2024-01-15 14:30:25',
|
||||
description: 'IP address 129.212.176.254 has made 85 failed password attempts. This indicates a potential brute force attack.'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: 'brute_force',
|
||||
ip: '177.10.47.186',
|
||||
attempts: 10,
|
||||
severity: 'HIGH',
|
||||
timestamp: '2024-01-15 14:25:10',
|
||||
description: 'IP address 177.10.47.186 has made 10 failed password attempts. This indicates a potential brute force attack.'
|
||||
}
|
||||
];
|
||||
|
||||
function refreshSecurityAlerts() {
|
||||
const container = document.getElementById('securityAlertsContainer');
|
||||
|
||||
if (sampleAlerts.length === 0) {
|
||||
container.innerHTML = '<div class="alert alert-info">No security alerts found.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
sampleAlerts.forEach(alert => {
|
||||
const severityClass = alert.severity === 'HIGH' ? 'danger' : alert.severity === 'MEDIUM' ? 'warning' : 'info';
|
||||
|
||||
html += `
|
||||
<div class="alert alert-${severityClass} mb-3">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<h6 class="alert-heading">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
${alert.type.replace('_', ' ').toUpperCase()} Attack Detected
|
||||
</h6>
|
||||
<p class="mb-2">${alert.description}</p>
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<strong>IP Address:</strong> ${alert.ip}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<strong>Failed Attempts:</strong> ${alert.attempts}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<strong>Attack Type:</strong> Brute Force
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<strong>Time:</strong> ${alert.timestamp}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<span class="badge badge-${severityClass}">${alert.severity}</span>
|
||||
<div class="mt-2">
|
||||
<button class="btn btn-sm btn-danger" onclick="blockIP('${alert.ip}', 'Brute force attack - ${alert.attempts} attempts')">
|
||||
<i class="fas fa-ban"></i> Block IP
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
function blockIP(ipAddress, reason) {
|
||||
if (!confirm(`Are you sure you want to block IP address ${ipAddress}?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = {
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}',
|
||||
'ip_address': ipAddress,
|
||||
'reason': reason
|
||||
};
|
||||
|
||||
$.post('{% url "blockIPAddress" %}', formData, function(data) {
|
||||
if (data.status === 1) {
|
||||
showNotification('success', data.message);
|
||||
refreshBlockedIPs();
|
||||
refreshSecurityAlerts(); // Refresh to remove the alert or update its status
|
||||
} else {
|
||||
showNotification('error', data.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function unblockIP(ipAddress) {
|
||||
if (!confirm(`Are you sure you want to unblock IP address ${ipAddress}?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = {
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}',
|
||||
'ip_address': ipAddress
|
||||
};
|
||||
|
||||
$.post('{% url "unblockIPAddress" %}', formData, function(data) {
|
||||
if (data.status === 1) {
|
||||
showNotification('success', data.message);
|
||||
refreshBlockedIPs();
|
||||
} else {
|
||||
showNotification('error', data.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function refreshBlockedIPs() {
|
||||
$.post('{% url "getBlockedIPs" %}', {
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
||||
}, function(data) {
|
||||
if (data.status === 1) {
|
||||
displayBlockedIPs(data.blocked_ips);
|
||||
} else {
|
||||
showNotification('error', data.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function displayBlockedIPs(blockedIPs) {
|
||||
const tbody = document.getElementById('blockedIPsTableBody');
|
||||
|
||||
if (blockedIPs.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="4" class="text-center">No blocked IP addresses found</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
blockedIPs.forEach(ip => {
|
||||
html += `
|
||||
<tr>
|
||||
<td>${ip}</td>
|
||||
<td>N/A</td>
|
||||
<td>Blocked via CyberPanel</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-warning" onclick="unblockIP('${ip}')">
|
||||
<i class="fas fa-unlock"></i> Unblock
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
|
||||
tbody.innerHTML = html;
|
||||
}
|
||||
|
||||
function showNotification(type, message) {
|
||||
const alertClass = type === 'success' ? 'alert-success' : 'alert-danger';
|
||||
const icon = type === 'success' ? 'fa-check-circle' : 'fa-exclamation-circle';
|
||||
|
||||
const notification = `
|
||||
<div class="alert ${alertClass} alert-dismissible fade show" role="alert">
|
||||
<i class="fas ${icon}"></i> ${message}
|
||||
<button type="button" class="close" data-dismiss="alert">
|
||||
<span>×</span>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
$('.card-body').prepend(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
$('.alert').fadeOut();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Handle manual IP blocking form
|
||||
$(document).ready(function() {
|
||||
$('#blockIPForm').on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const ipAddress = $('#ipAddress').val();
|
||||
const reason = $('#blockReason').val();
|
||||
|
||||
if (!ipAddress) {
|
||||
showNotification('error', 'Please enter an IP address');
|
||||
return;
|
||||
}
|
||||
|
||||
blockIP(ipAddress, reason);
|
||||
$('#blockIPForm')[0].reset();
|
||||
});
|
||||
|
||||
// Load initial data
|
||||
refreshSecurityAlerts();
|
||||
refreshBlockedIPs();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -215,13 +215,6 @@ urlpatterns = [
|
||||
path('getBandwidthResetLogs', views.getBandwidthResetLogs, name='getBandwidthResetLogs'),
|
||||
path('scheduleBandwidthReset', views.scheduleBandwidthReset, name='scheduleBandwidthReset'),
|
||||
|
||||
# Security Management
|
||||
path('securityManagement', views.securityManagementPage, name='securityManagementPage'),
|
||||
|
||||
# IP Blocking
|
||||
path('blockIPAddress', views.blockIPAddress, name='blockIPAddress'),
|
||||
path('unblockIPAddress', views.unblockIPAddress, name='unblockIPAddress'),
|
||||
path('getBlockedIPs', views.getBlockedIPs, name='getBlockedIPs'),
|
||||
path('checkIPStatus', views.checkIPStatus, name='checkIPStatus'),
|
||||
|
||||
# Catch all for domains (must be last)
|
||||
|
||||
@@ -2242,15 +2242,6 @@ def bandwidthManagementPage(request):
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
def securityManagementPage(request):
|
||||
"""Render the Security Management page."""
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
proc = httpProc(request, 'websiteFunctions/securityManagement.html', {}, 'admin')
|
||||
return proc.render()
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
def getFTPQuotaStatus(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
@@ -2308,31 +2299,6 @@ def scheduleBandwidthReset(request):
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
# IP Blocking Views
|
||||
def blockIPAddress(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
wm = WebsiteManager()
|
||||
return wm.blockIPAddress(userID, request.POST)
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
def unblockIPAddress(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
wm = WebsiteManager()
|
||||
return wm.unblockIPAddress(userID, request.POST)
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
def getBlockedIPs(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
wm = WebsiteManager()
|
||||
return wm.getBlockedIPs(userID, request.POST)
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
def checkIPStatus(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
|
||||
@@ -9322,141 +9322,6 @@ StrictHostKeyChecking no
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
|
||||
def blockIPAddress(self, userID=None, data=None):
|
||||
"""
|
||||
Block an IP address
|
||||
"""
|
||||
try:
|
||||
currentACL = ACLManager.loadedACL(userID)
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
|
||||
# Check if user has permission
|
||||
if not (currentACL.get('admin', 0) == 1):
|
||||
return ACLManager.loadErrorJson('status', 0)
|
||||
|
||||
ip_address = data.get('ip_address')
|
||||
reason = data.get('reason', 'Manual block via CyberPanel')
|
||||
|
||||
if not ip_address:
|
||||
data_ret = {
|
||||
'status': 0,
|
||||
'message': 'IP address is required'
|
||||
}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
|
||||
# Import firewall utilities
|
||||
from plogical.firewallUtilities import FirewallUtilities
|
||||
|
||||
# Block the IP
|
||||
success, message = FirewallUtilities.blockIP(ip_address, reason)
|
||||
|
||||
if success:
|
||||
data_ret = {
|
||||
'status': 1,
|
||||
'message': message
|
||||
}
|
||||
else:
|
||||
data_ret = {
|
||||
'status': 0,
|
||||
'message': message
|
||||
}
|
||||
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
|
||||
except Exception as e:
|
||||
data_ret = {
|
||||
'status': 0,
|
||||
'message': f'Error blocking IP: {str(e)}'
|
||||
}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
|
||||
def unblockIPAddress(self, userID=None, data=None):
|
||||
"""
|
||||
Unblock an IP address
|
||||
"""
|
||||
try:
|
||||
currentACL = ACLManager.loadedACL(userID)
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
|
||||
# Check if user has permission
|
||||
if not (currentACL.get('admin', 0) == 1):
|
||||
return ACLManager.loadErrorJson('status', 0)
|
||||
|
||||
ip_address = data.get('ip_address')
|
||||
|
||||
if not ip_address:
|
||||
data_ret = {
|
||||
'status': 0,
|
||||
'message': 'IP address is required'
|
||||
}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
|
||||
# Import firewall utilities
|
||||
from plogical.firewallUtilities import FirewallUtilities
|
||||
|
||||
# Unblock the IP
|
||||
success, message = FirewallUtilities.unblockIP(ip_address)
|
||||
|
||||
if success:
|
||||
data_ret = {
|
||||
'status': 1,
|
||||
'message': message
|
||||
}
|
||||
else:
|
||||
data_ret = {
|
||||
'status': 0,
|
||||
'message': message
|
||||
}
|
||||
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
|
||||
except Exception as e:
|
||||
data_ret = {
|
||||
'status': 0,
|
||||
'message': f'Error unblocking IP: {str(e)}'
|
||||
}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
|
||||
def getBlockedIPs(self, userID=None, data=None):
|
||||
"""
|
||||
Get list of blocked IP addresses
|
||||
"""
|
||||
try:
|
||||
currentACL = ACLManager.loadedACL(userID)
|
||||
admin = Administrator.objects.get(pk=userID)
|
||||
|
||||
# Check if user has permission
|
||||
if not (currentACL.get('admin', 0) == 1):
|
||||
return ACLManager.loadErrorJson('status', 0)
|
||||
|
||||
# Import firewall utilities
|
||||
from plogical.firewallUtilities import FirewallUtilities
|
||||
|
||||
# Get blocked IPs
|
||||
blocked_ips = FirewallUtilities.getBlockedIPs()
|
||||
|
||||
data_ret = {
|
||||
'status': 1,
|
||||
'blocked_ips': blocked_ips
|
||||
}
|
||||
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
|
||||
except Exception as e:
|
||||
data_ret = {
|
||||
'status': 0,
|
||||
'message': f'Error getting blocked IPs: {str(e)}'
|
||||
}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
|
||||
def checkIPStatus(self, userID=None, data=None):
|
||||
"""
|
||||
Check if an IP is blocked
|
||||
|
||||
Reference in New Issue
Block a user