improve docs, fix links, and add container console support

This update improves documentation clarity, fixes broken documentation links, and introduces a new feature that allows you to access and use the console directly inside containers from CyberPanel.
This commit is contained in:
Master3395
2025-09-05 01:14:04 +02:00
parent d8d3865816
commit 05c441bd2b
11 changed files with 574 additions and 100 deletions

View File

@@ -1319,4 +1319,97 @@ class ContainerManager(multi.Thread):
except BaseException as msg:
data_ret = {'removeImageStatus': 0, 'error_message': str(msg)}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
def executeContainerCommand(self, userID=None, data=None):
"""
Execute a command inside a running Docker container
"""
try:
name = data['name']
command = data['command']
# Check if container is registered in database or unlisted
if Containers.objects.filter(name=name).exists():
if ACLManager.checkContainerOwnership(name, userID) != 1:
return ACLManager.loadErrorJson('commandStatus', 0)
client = docker.from_env()
dockerAPI = docker.APIClient()
try:
container = client.containers.get(name)
except docker.errors.NotFound as err:
data_ret = {'commandStatus': 0, 'error_message': 'Container does not exist'}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except:
data_ret = {'commandStatus': 0, 'error_message': 'Unknown error'}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
# Check if container is running
if container.status != 'running':
data_ret = {'commandStatus': 0, 'error_message': 'Container must be running to execute commands'}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
try:
# Execute command in container
# Split command into parts for proper execution
import shlex
command_parts = shlex.split(command)
# Execute command with proper shell
exec_result = container.exec_run(
command_parts,
stdout=True,
stderr=True,
stdin=False,
tty=False,
privileged=False,
user='',
detach=False,
demux=False,
workdir=None,
environment=None
)
# Get output and exit code
output = exec_result.output.decode('utf-8') if exec_result.output else ''
exit_code = exec_result.exit_code
# Format the response
if exit_code == 0:
data_ret = {
'commandStatus': 1,
'error_message': 'None',
'output': output,
'exit_code': exit_code,
'command': command
}
else:
data_ret = {
'commandStatus': 1,
'error_message': 'Command executed with non-zero exit code',
'output': output,
'exit_code': exit_code,
'command': command
}
json_data = json.dumps(data_ret, ensure_ascii=False)
return HttpResponse(json_data)
except docker.errors.APIError as err:
data_ret = {'commandStatus': 0, 'error_message': f'Docker API error: {str(err)}'}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except Exception as err:
data_ret = {'commandStatus': 0, 'error_message': f'Execution error: {str(err)}'}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except BaseException as msg:
data_ret = {'commandStatus': 0, 'error_message': str(msg)}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)

View File

@@ -976,6 +976,101 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
}
};
// Command execution functionality
$scope.commandToExecute = '';
$scope.executingCommand = false;
$scope.commandOutput = null;
$scope.commandHistory = [];
$scope.showCommandModal = function() {
$scope.commandToExecute = '';
$scope.commandOutput = null;
$("#commandModal").modal("show");
};
$scope.executeCommand = function() {
if (!$scope.commandToExecute.trim()) {
new PNotify({
title: 'Error',
text: 'Please enter a command to execute',
type: 'error'
});
return;
}
$scope.executingCommand = true;
$scope.commandOutput = null;
url = "/docker/executeContainerCommand";
var data = {
name: $scope.cName,
command: $scope.commandToExecute.trim()
};
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(ListInitialData, cantLoadInitialData);
function ListInitialData(response) {
console.log(response);
$scope.executingCommand = false;
if (response.data.commandStatus === 1) {
$scope.commandOutput = {
command: response.data.command,
output: response.data.output,
exit_code: response.data.exit_code
};
// Add to command history
$scope.commandHistory.unshift({
command: response.data.command,
timestamp: new Date()
});
// Keep only last 10 commands
if ($scope.commandHistory.length > 10) {
$scope.commandHistory = $scope.commandHistory.slice(0, 10);
}
// Show success notification
new PNotify({
title: 'Command Executed',
text: 'Command completed with exit code: ' + response.data.exit_code,
type: response.data.exit_code === 0 ? 'success' : 'warning'
});
}
else {
new PNotify({
title: 'Command Execution Failed',
text: response.data.error_message,
type: 'error'
});
}
}
function cantLoadInitialData(response) {
$scope.executingCommand = false;
new PNotify({
title: 'Command Execution Failed',
text: 'Could not connect to server',
type: 'error'
});
}
};
$scope.selectCommand = function(command) {
$scope.commandToExecute = command;
};
$scope.clearOutput = function() {
$scope.commandOutput = null;
};
});

View File

@@ -756,6 +756,11 @@
<i class="fas fa-terminal action-icon" style="color: #ec4899;"></i>
<div class="action-text">{% trans "Processes" %}</div>
</div>
<div class="action-btn" ng-click="showCommandModal()" ng-disabled="status!='running'">
<i class="fas fa-code action-icon" style="color: #10b981;"></i>
<div class="action-text">{% trans "Run Command" %}</div>
</div>
</div>
</div>
</div>
@@ -957,6 +962,94 @@
</div>
</div>
</div>
<!-- Command Execution Modal -->
<div id="commandModal" class="modal fade" role="dialog">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">
<i class="fas fa-code" style="margin-right: 0.5rem;"></i>
{% trans "Execute Command" %}
</h4>
<button type="button" class="close" data-dismiss="modal"
style="font-size: 1.5rem; background: transparent; border: none;">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="commandInput" class="control-label">
<i class="fas fa-terminal" style="margin-right: 0.5rem;"></i>
{% trans "Command to execute" %}
</label>
<div class="input-group">
<input type="text"
id="commandInput"
class="form-control"
ng-model="commandToExecute"
placeholder="Enter command (e.g., ls -la, ps aux, whoami, env)"
ng-keyup="$event.keyCode === 13 && executeCommand()"
style="font-family: 'Courier New', monospace;">
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button" ng-click="executeCommand()" ng-disabled="!commandToExecute || executingCommand">
<i class="fas fa-play" ng-hide="executingCommand"></i>
<i class="fas fa-spinner fa-spin" ng-show="executingCommand"></i>
{% trans "Execute" %}
</button>
</div>
</div>
<small class="form-text text-muted">
{% trans "Commands will be executed inside the running container. Use proper shell syntax." %}
</small>
</div>
<!-- Command History -->
<div class="form-group" ng-show="commandHistory.length > 0">
<label class="control-label">
<i class="fas fa-history" style="margin-right: 0.5rem;"></i>
{% trans "Command History" %}
</label>
<div class="command-history">
<div class="history-item"
ng-repeat="cmd in commandHistory track by $index"
ng-click="selectCommand(cmd.command)"
style="cursor: pointer; padding: 0.25rem 0.5rem; margin: 0.125rem 0; background: #f8f9fa; border-radius: 4px; border-left: 3px solid #007bff;">
<code style="font-size: 0.875rem;">{{ cmd.command }}</code>
<small class="text-muted" style="float: right;">{{ cmd.timestamp | date:'short' }}</small>
</div>
</div>
</div>
<!-- Output Display -->
<div class="form-group" ng-show="commandOutput">
<label class="control-label">
<i class="fas fa-terminal" style="margin-right: 0.5rem;"></i>
{% trans "Command Output" %}
</label>
<div class="terminal-output" style="background: #1a202c; color: #e2e8f0; padding: 1rem; border-radius: 8px; font-family: 'Courier New', monospace; font-size: 0.875rem; max-height: 300px; overflow-y: auto; white-space: pre-wrap;">
<div ng-show="commandOutput.exit_code !== undefined" style="margin-bottom: 0.5rem;">
<span style="color: #68d391;">$</span> <span style="color: #fbb6ce;">{{ commandOutput.command }}</span>
<span style="color: #a0aec0; margin-left: 1rem;">(exit code: {{ commandOutput.exit_code }})</span>
</div>
<div ng-bind="commandOutput.output"></div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" ng-click="executeCommand()" ng-disabled="!commandToExecute || executingCommand">
<i class="fas fa-play" ng-hide="executingCommand"></i>
<i class="fas fa-spinner fa-spin" ng-show="executingCommand"></i>
{% trans "Execute Command" %}
</button>
<button type="button" class="btn btn-secondary" ng-click="clearOutput()" ng-disabled="!commandOutput">
<i class="fas fa-trash"></i> {% trans "Clear Output" %}
</button>
<button type="button" class="btn btn-default" data-dismiss="modal">
<i class="fas fa-times"></i> {% trans "Close" %}
</button>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -36,6 +36,7 @@ urlpatterns = [
path('recreateappcontainer', views.recreateappcontainer, name='recreateappcontainer'),
path('RestartContainerAPP', views.RestartContainerAPP, name='RestartContainerAPP'),
path('StopContainerAPP', views.StopContainerAPP, name='StopContainerAPP'),
path('executeContainerCommand', views.executeContainerCommand, name='executeContainerCommand'),
# Docker Container Actions
path('startContainer', startContainer, name='startContainer'),

View File

@@ -537,6 +537,24 @@ def StopContainerAPP(request):
cm = ContainerManager()
coreResult = cm.StopContainerAPP(userID, json.loads(request.body))
return coreResult
except KeyError:
return redirect(loadLoginPage)
@preDockerRun
def executeContainerCommand(request):
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] == 1:
pass
else:
return ACLManager.loadErrorJson()
cm = ContainerManager()
coreResult = cm.executeContainerCommand(userID, json.loads(request.body))
return coreResult
except KeyError:
return redirect(loadLoginPage)