Implement Docker network management features: Add endpoints for retrieving and creating Docker networks, and update container management to support network configuration and port mapping updates. Enhance UI for network selection and port editing in the container management interface. Update database schema to include network-related fields.

https://github.com/usmannasir/cyberpanel/issues/923
This commit is contained in:
Master3395
2025-09-21 21:14:34 +02:00
parent cc07f12017
commit 09c9d67536
9 changed files with 1200 additions and 12 deletions

View File

@@ -213,8 +213,8 @@ class ContainerManager(multi.Thread):
proc = httpProc(request, template, data, 'admin')
return proc.render()
except Exception as e:
secure_log_error(e, \'container_operation\')
return HttpResponse(\'Operation failed\')
secure_log_error(e, 'container_operation')
return HttpResponse('Operation failed')
def listContainers(self, request=None, userID=None, data=None):
client = docker.from_env()
@@ -333,8 +333,8 @@ class ContainerManager(multi.Thread):
return HttpResponse(json_data)
except Exception as e:
secure_log_error(e, \'containerLogStatus\')
data_ret = secure_error_response(e, \'Failed to containerLogStatus\')
secure_log_error(e, 'containerLogStatus')
data_ret = secure_error_response(e, 'Failed to containerLogStatus')
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
@@ -417,6 +417,22 @@ class ContainerManager(multi.Thread):
if isinstance(volume, dict) and 'src' in volume and 'dest' in volume:
volumes[volume['src']] = {'bind': volume['dest'], 'mode': 'rw'}
# Network configuration
network = data.get('network', 'bridge') # Default to bridge network
network_mode = data.get('network_mode', 'bridge')
# Extra options support (like --add-host)
extra_hosts = {}
extra_options = data.get('extraOptions', {})
if extra_options:
for option, value in extra_options.items():
if option == 'add_host' and value:
# Parse --add-host entries (format: hostname:ip)
for host_entry in value.split(','):
if ':' in host_entry:
hostname, ip = host_entry.strip().split(':', 1)
extra_hosts[hostname.strip()] = ip.strip()
## Create Configurations
admin = Administrator.objects.get(userName=dockerOwner)
@@ -426,7 +442,16 @@ class ContainerManager(multi.Thread):
'ports': portConfig,
'publish_all_ports': True,
'environment': envDict,
'volumes': volumes}
'volumes': volumes,
'network_mode': network_mode}
# Add network configuration
if network != 'bridge' or network_mode == 'bridge':
containerArgs['network'] = network
# Add extra hosts if specified
if extra_hosts:
containerArgs['extra_hosts'] = extra_hosts
containerArgs['mem_limit'] = memory * 1048576; # Converts MB to bytes ( 0 * x = 0 for unlimited memory)
@@ -467,6 +492,9 @@ class ContainerManager(multi.Thread):
image=image,
memory=memory,
ports=json.dumps(portConfig),
network=network,
network_mode=network_mode,
extra_options=json.dumps(extra_options),
volumes=json.dumps(volumes),
env=json.dumps(envDict),
cid=container.id)
@@ -2420,3 +2448,187 @@ class ContainerManager(multi.Thread):
data_ret = {'status': 0, 'error_message': str(msg)}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
def getDockerNetworks(self, userID=None):
"""
Get list of all Docker networks
"""
try:
admin = Administrator.objects.get(pk=userID)
if admin.acl.adminStatus != 1:
return ACLManager.loadError()
client = docker.from_env()
# Get all networks
networks = client.networks.list()
network_list = []
for network in networks:
network_info = {
'id': network.id,
'name': network.name,
'driver': network.attrs.get('Driver', 'unknown'),
'scope': network.attrs.get('Scope', 'local'),
'created': network.attrs.get('Created', ''),
'containers': len(network.attrs.get('Containers', {})),
'ipam': network.attrs.get('IPAM', {}),
'labels': network.attrs.get('Labels', {})
}
network_list.append(network_info)
data_ret = {
'status': 1,
'error_message': 'None',
'networks': network_list
}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except Exception as msg:
logging.CyberCPLogFileWriter.writeToFile(str(msg) + ' [ContainerManager.getDockerNetworks]')
data_ret = {'status': 0, 'error_message': str(msg)}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
def createDockerNetwork(self, userID=None, data=None):
"""
Create a new Docker network
"""
try:
admin = Administrator.objects.get(pk=userID)
if admin.acl.adminStatus != 1:
return ACLManager.loadError()
client = docker.from_env()
name = data.get('name')
driver = data.get('driver', 'bridge')
subnet = data.get('subnet', '')
gateway = data.get('gateway', '')
ip_range = data.get('ip_range', '')
if not name:
data_ret = {'status': 0, 'error_message': 'Network name is required'}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
# Prepare IPAM configuration
ipam_config = []
if subnet:
ipam_entry = {'subnet': subnet}
if gateway:
ipam_entry['gateway'] = gateway
if ip_range:
ipam_entry['ip_range'] = ip_range
ipam_config.append(ipam_entry)
ipam = {'driver': 'default', 'config': ipam_config} if ipam_config else None
# Create the network
network = client.networks.create(
name=name,
driver=driver,
ipam=ipam
)
data_ret = {
'status': 1,
'error_message': 'None',
'network_id': network.id,
'message': f'Network {name} created successfully'
}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except Exception as msg:
logging.CyberCPLogFileWriter.writeToFile(str(msg) + ' [ContainerManager.createDockerNetwork]')
data_ret = {'status': 0, 'error_message': str(msg)}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
def updateContainerPorts(self, userID=None, data=None):
"""
Update port mappings for an existing container
"""
try:
admin = Administrator.objects.get(pk=userID)
if admin.acl.adminStatus != 1:
return ACLManager.loadError()
client = docker.from_env()
container_name = data.get('name')
new_ports = data.get('ports', {})
if not container_name:
data_ret = {'status': 0, 'error_message': 'Container name is required'}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
# Get the container
try:
container = client.containers.get(container_name)
except docker.errors.NotFound:
data_ret = {'status': 0, 'error_message': f'Container {container_name} not found'}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
# Check if container is running
if container.status != 'running':
data_ret = {'status': 0, 'error_message': 'Container must be running to update port mappings'}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
# Get current container configuration
container_config = container.attrs['Config']
host_config = container.attrs['HostConfig']
# Update port bindings
port_bindings = {}
for container_port, host_port in new_ports.items():
if host_port: # Only add if host port is specified
port_bindings[container_port] = host_port
# Stop the container
container.stop(timeout=10)
# Create new container with updated port configuration
new_container = client.containers.create(
image=container_config['Image'],
name=f"{container_name}_temp",
ports=list(new_ports.keys()),
host_config=client.api.create_host_config(port_bindings=port_bindings),
environment=container_config.get('Env', []),
volumes=host_config.get('Binds', []),
detach=True
)
# Remove old container and rename new one
container.remove()
new_container.rename(container_name)
# Start the updated container
new_container.start()
# Update database record if it exists
try:
db_container = Containers.objects.get(name=container_name)
db_container.ports = json.dumps(new_ports)
db_container.save()
except Containers.DoesNotExist:
pass # Container not in database, that's okay
data_ret = {
'status': 1,
'error_message': 'None',
'message': f'Port mappings updated for container {container_name}'
}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except Exception as msg:
logging.CyberCPLogFileWriter.writeToFile(str(msg) + ' [ContainerManager.updateContainerPorts]')
data_ret = {'status': 0, 'error_message': str(msg)}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)

View File

@@ -16,3 +16,6 @@ class Containers(models.Model):
volumes = models.TextField(default="{}")
env = models.TextField(default="{}")
startOnReboot = models.IntegerField(default=0)
network = models.CharField(max_length=100, default='bridge')
network_mode = models.CharField(max_length=50, default='bridge')
extra_options = models.TextField(default="{}")

View File

@@ -128,6 +128,33 @@ app.controller('runContainer', function ($scope, $http) {
// Advanced Environment Variable Mode
$scope.advancedEnvMode = false;
// Network configuration
$scope.selectedNetwork = 'bridge';
$scope.networkMode = 'bridge';
$scope.extraHosts = '';
$scope.availableNetworks = [];
// Load available Docker networks
$scope.loadAvailableNetworks = function() {
var url = "/docker/getDockerNetworks";
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, {}, config).then(function(response) {
if (response.data.status === 1) {
$scope.availableNetworks = response.data.networks;
}
}, function(error) {
console.error('Failed to load networks:', error);
});
};
// Initialize networks on page load
$scope.loadAvailableNetworks();
// Helper function to generate Docker Compose YAML
$scope.generateDockerComposeYml = function(containerInfo) {
var yml = 'version: \'3.8\'\n\n';
@@ -622,8 +649,12 @@ app.controller('runContainer', function ($scope, $http) {
image: image,
envList: finalEnvList,
volList: $scope.volList,
advancedEnvMode: $scope.advancedEnvMode
advancedEnvMode: $scope.advancedEnvMode,
network: $scope.selectedNetwork,
network_mode: $scope.networkMode,
extraOptions: {
add_host: $scope.extraHosts
}
};
try {
@@ -2137,6 +2168,82 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
$("#commandModal").modal("show");
};
// Port editing functionality
$scope.showPortEditModal = function() {
// Initialize current ports from container data
$scope.currentPorts = {};
if ($scope.ports) {
for (var iport in $scope.ports) {
var eport = $scope.ports[iport];
if (eport && eport.length > 0) {
$scope.currentPorts[iport] = eport[0].HostPort;
}
}
}
$("#portEditModal").modal("show");
};
$scope.addNewPortMapping = function() {
var containerPort = prompt('Enter container port (e.g., 80/tcp):');
if (containerPort) {
$scope.currentPorts[containerPort] = '';
$scope.$apply();
}
};
$scope.removePortMapping = function(containerPort) {
if (confirm('Are you sure you want to remove this port mapping?')) {
delete $scope.currentPorts[containerPort];
}
};
$scope.updatePortMappings = function() {
$("#portEditLoading").show();
$scope.updatingPorts = true;
var url = "/docker/updateContainerPorts";
var data = {
name: $scope.cName,
ports: $scope.currentPorts
};
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(function(response) {
$("#portEditLoading").hide();
$scope.updatingPorts = false;
if (response.data.status === 1) {
$("#portEditModal").modal("hide");
// Refresh container status and ports
$scope.refreshContainerInfo();
new PNotify({
title: 'Success',
text: 'Port mappings updated successfully',
type: 'success'
});
} else {
new PNotify({
title: 'Error',
text: 'Failed to update port mappings: ' + response.data.error_message,
type: 'error'
});
}
}, function(error) {
$("#portEditLoading").hide();
$scope.updatingPorts = false;
new PNotify({
title: 'Error',
text: 'Error updating port mappings: ' + error.data.error_message,
type: 'error'
});
});
};
$scope.executeCommand = function() {
if (!$scope.commandToExecute.trim()) {
new PNotify({

View File

@@ -0,0 +1,621 @@
{% extends "baseTemplate/index.html" %}
{% load i18n %}
{% block title %}{% trans "Docker Network Management" %}{% endblock %}
{% block content %}
{% load static %}
{% get_current_language as LANGUAGE_CODE %}
<style>
.modern-container {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.page-header {
text-align: center;
margin-bottom: 3rem;
padding: 3rem 0;
background: linear-gradient(135deg, var(--bg-hover, #f8f9ff) 0%, var(--bg-gradient, #f0f1ff) 100%);
border-radius: 20px;
animation: fadeInDown 0.5s ease-out;
position: relative;
overflow: hidden;
}
.page-header::before {
content: '';
position: absolute;
top: -50%;
right: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle at 70% 30%, var(--accent-shadow-light, rgba(91, 95, 207, 0.15)) 0%, transparent 50%);
animation: rotate 30s linear infinite;
}
.header-content {
position: relative;
z-index: 1;
}
.page-title {
font-size: 2.5rem;
font-weight: 700;
color: var(--text-primary, #1e293b);
margin-bottom: 1rem;
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
}
.network-icon {
width: 60px;
height: 60px;
background: var(--bg-secondary, white);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px var(--shadow-medium, rgba(0,0,0,0.1));
}
.main-card {
background: var(--bg-secondary, white);
border-radius: 16px;
box-shadow: 0 1px 3px var(--shadow-light, rgba(0,0,0,0.05)), 0 10px 40px var(--shadow-color, rgba(0,0,0,0.08));
border: 1px solid var(--border-color, #e8e9ff);
overflow: hidden;
margin-bottom: 2rem;
animation: fadeInUp 0.5s ease-out;
}
.card-header {
background: linear-gradient(135deg, var(--bg-hover, #f8f9ff) 0%, var(--bg-gradient, #f0f1ff) 100%);
padding: 1.5rem 2rem;
border-bottom: 1px solid var(--border-color, #e8e9ff);
display: flex;
justify-content: space-between;
align-items: center;
}
.card-title {
font-size: 1.25rem;
font-weight: 600;
color: var(--text-primary, #1e293b);
margin: 0;
display: flex;
align-items: center;
gap: 0.75rem;
}
.card-body {
padding: 2rem;
}
.btn {
padding: 0.5rem 1rem;
border-radius: 8px;
font-weight: 500;
font-size: 0.875rem;
cursor: pointer;
transition: all 0.3s ease;
border: none;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.btn-primary {
background: var(--accent-color, #5b5fcf);
color: var(--bg-secondary, white);
}
.btn-primary:hover {
background: var(--accent-hover, #4547a9);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(91, 95, 207, 0.3);
}
.btn-success {
background: var(--success-bg, #d1fae5);
color: var(--success-text, #065f46);
border: 1px solid #a7f3d0;
}
.btn-success:hover {
background: #a7f3d0;
transform: translateY(-2px);
}
.btn-danger {
background: #fee2e2;
color: var(--danger-color, #ef4444);
border: 1px solid #fecaca;
}
.btn-danger:hover {
background: #fecaca;
transform: translateY(-2px);
}
.network-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 1.5rem;
}
.network-card {
background: var(--bg-secondary, white);
border: 1px solid var(--border-color, #e8e9ff);
border-radius: 12px;
padding: 1.5rem;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.network-card::before {
content: '';
position: absolute;
top: 0;
right: 0;
width: 100px;
height: 100px;
background: linear-gradient(135deg, var(--accent-focus, rgba(91, 95, 207, 0.1)) 0%, transparent 100%);
border-radius: 0 0 0 100%;
}
.network-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px var(--accent-shadow-light, rgba(91, 95, 207, 0.15));
border-color: var(--accent-color, #5b5fcf);
}
.network-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1rem;
position: relative;
z-index: 1;
}
.network-name {
font-size: 1.125rem;
font-weight: 600;
color: var(--text-primary, #1e293b);
margin: 0;
}
.network-driver {
background: var(--accent-bg, #e0e7ff);
color: var(--accent-color, #5b5fcf);
padding: 0.25rem 0.75rem;
border-radius: 6px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
}
.network-info {
position: relative;
z-index: 1;
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
font-size: 0.875rem;
}
.info-label {
color: var(--text-secondary, #64748b);
font-weight: 500;
}
.info-value {
color: var(--text-primary, #1e293b);
font-weight: 600;
}
.network-actions {
margin-top: 1rem;
display: flex;
gap: 0.5rem;
position: relative;
z-index: 1;
}
.loading-spinner {
width: 20px;
height: 20px;
border: 3px solid #e8e9ff;
border-top-color: var(--accent-color, #5b5fcf);
border-radius: 50%;
animation: spin 1s linear infinite;
display: inline-block;
margin-left: 1rem;
}
.empty-state {
text-align: center;
padding: 3rem 2rem;
color: var(--text-secondary, #64748b);
}
.empty-state i {
font-size: 3rem;
margin-bottom: 1rem;
opacity: 0.5;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes fadeInDown {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 768px) {
.network-grid {
grid-template-columns: 1fr;
}
.card-header {
flex-direction: column;
gap: 1rem;
text-align: center;
}
}
</style>
<div class="modern-container" ng-controller="manageNetworks">
<div class="page-header">
<div class="header-content">
<h1 class="page-title">
<div class="network-icon">
<i class="fas fa-network-wired" style="color: var(--accent-color, #5b5fcf); font-size: 1.75rem;"></i>
</div>
{% trans "Docker Network Management" %}
</h1>
<p style="color: var(--text-secondary, #64748b); font-size: 1.125rem; margin: 0;">
{% trans "Manage Docker networks for container connectivity" %}
</p>
</div>
</div>
<!-- Networks Overview -->
<div class="main-card">
<div class="card-header">
<h2 class="card-title">
<i class="fas fa-list"></i>
{% trans "Available Networks" %}
<img id="networkLoading" src="/static/images/loading.gif" style="display: none;" class="loading-spinner">
</h2>
<div>
<button class="btn btn-primary" ng-click="refreshNetworks()" ng-disabled="loading">
<i class="fas fa-sync"></i>
{% trans "Refresh" %}
</button>
<button class="btn btn-success" ng-click="showCreateNetworkModal()">
<i class="fas fa-plus"></i>
{% trans "Create Network" %}
</button>
</div>
</div>
<div class="card-body">
<div ng-show="loading" class="text-center" style="padding: 2rem;">
<div class="loading-spinner" style="width: 40px; height: 40px; margin: 0 auto;"></div>
<p style="margin-top: 1rem; color: var(--text-secondary, #64748b);">{% trans "Loading networks..." %}</p>
</div>
<div ng-show="!loading && networks.length === 0" class="empty-state">
<i class="fas fa-network-wired"></i>
<h3>{% trans "No Networks Found" %}</h3>
<p>{% trans "Create your first Docker network to get started." %}</p>
<button class="btn btn-primary" ng-click="showCreateNetworkModal()">
<i class="fas fa-plus"></i>
{% trans "Create Network" %}
</button>
</div>
<div ng-show="!loading && networks.length > 0" class="network-grid">
<div ng-repeat="network in networks" class="network-card">
<div class="network-header">
<h3 class="network-name">{$ network.name $}</h3>
<span class="network-driver">{$ network.driver $}</span>
</div>
<div class="network-info">
<div class="info-item">
<span class="info-label">{% trans "ID" %}</span>
<span class="info-value">{$ network.id | limitTo: 12 $}...</span>
</div>
<div class="info-item">
<span class="info-label">{% trans "Scope" %}</span>
<span class="info-value">{$ network.scope $}</span>
</div>
<div class="info-item">
<span class="info-label">{% trans "Containers" %}</span>
<span class="info-value">{$ network.containers $}</span>
</div>
<div class="info-item" ng-if="network.ipam.config.length > 0">
<span class="info-label">{% trans "Subnet" %}</span>
<span class="info-value">{$ network.ipam.config[0].subnet $}</span>
</div>
<div class="info-item" ng-if="network.ipam.config.length > 0 && network.ipam.config[0].gateway">
<span class="info-label">{% trans "Gateway" %}</span>
<span class="info-value">{$ network.ipam.config[0].gateway $}</span>
</div>
</div>
<div class="network-actions">
<button class="btn btn-danger btn-sm" ng-click="removeNetwork(network)"
ng-disabled="network.containers > 0"
title="{% trans 'Cannot remove network with running containers' %}">
<i class="fas fa-trash"></i>
{% trans "Remove" %}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Create Network Modal -->
<div id="createNetworkModal" 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-plus" style="margin-right: 0.5rem;"></i>
{% trans "Create Docker Network" %}
</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">
<form name="createNetworkForm" class="form-horizontal">
<div class="form-group">
<label class="col-sm-3 control-label">{% trans "Network Name" %}</label>
<div class="col-sm-9">
<input type="text" class="form-control" ng-model="newNetwork.name"
placeholder="{% trans 'e.g., my-custom-network' %}" required>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">{% trans "Driver" %}</label>
<div class="col-sm-9">
<select class="form-control" ng-model="newNetwork.driver">
<option value="bridge">{% trans "Bridge" %}</option>
<option value="overlay">{% trans "Overlay" %}</option>
<option value="macvlan">{% trans "MacVLAN" %}</option>
<option value="ipvlan">{% trans "IPvLAN" %}</option>
</select>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">{% trans "Subnet" %}</label>
<div class="col-sm-9">
<input type="text" class="form-control" ng-model="newNetwork.subnet"
placeholder="{% trans 'e.g., 172.20.0.0/16' %}">
<small class="help-block">{% trans "Optional: Specify a custom subnet for the network" %}</small>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">{% trans "Gateway" %}</label>
<div class="col-sm-9">
<input type="text" class="form-control" ng-model="newNetwork.gateway"
placeholder="{% trans 'e.g., 172.20.0.1' %}">
<small class="help-block">{% trans "Optional: Specify a custom gateway IP" %}</small>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">{% trans "IP Range" %}</label>
<div class="col-sm-9">
<input type="text" class="form-control" ng-model="newNetwork.ip_range"
placeholder="{% trans 'e.g., 172.20.0.0/24' %}">
<small class="help-block">{% trans "Optional: Specify an IP range for container IPs" %}</small>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<img id="createNetworkLoading" src="/static/images/loading.gif" style="display: none;" class="loading-spinner">
<button type="button" class="btn btn-primary" ng-disabled="creatingNetwork" ng-click="createNetwork()">
<i class="fas fa-plus"></i> {% trans "Create Network" %}
</button>
<button type="button" class="btn btn-default" data-dismiss="modal">
<i class="fas fa-times"></i> {% trans "Cancel" %}
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block footer_scripts %}
<script src="{% static 'dockerManager/dockerManager.js' %}"></script>
<script>
// Network management controller
app.controller('manageNetworks', function ($scope, $http) {
$scope.networks = [];
$scope.loading = true;
$scope.creatingNetwork = false;
$scope.newNetwork = {
name: '',
driver: 'bridge',
subnet: '',
gateway: '',
ip_range: ''
};
// Load networks
$scope.loadNetworks = function() {
$scope.loading = true;
var url = "/docker/getDockerNetworks";
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, {}, config).then(function(response) {
$scope.loading = false;
if (response.data.status === 1) {
$scope.networks = response.data.networks;
} else {
new PNotify({
title: 'Error',
text: 'Failed to load networks: ' + response.data.error_message,
type: 'error'
});
}
}, function(error) {
$scope.loading = false;
new PNotify({
title: 'Error',
text: 'Error loading networks: ' + error.data.error_message,
type: 'error'
});
});
};
// Refresh networks
$scope.refreshNetworks = function() {
$scope.loadNetworks();
};
// Show create network modal
$scope.showCreateNetworkModal = function() {
$scope.newNetwork = {
name: '',
driver: 'bridge',
subnet: '',
gateway: '',
ip_range: ''
};
$('#createNetworkModal').modal('show');
};
// Create network
$scope.createNetwork = function() {
if (!$scope.newNetwork.name.trim()) {
new PNotify({
title: 'Error',
text: 'Network name is required',
type: 'error'
});
return;
}
$('#createNetworkLoading').show();
$scope.creatingNetwork = true;
var url = "/docker/createDockerNetwork";
var data = {
name: $scope.newNetwork.name,
driver: $scope.newNetwork.driver,
subnet: $scope.newNetwork.subnet,
gateway: $scope.newNetwork.gateway,
ip_range: $scope.newNetwork.ip_range
};
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(function(response) {
$('#createNetworkLoading').hide();
$scope.creatingNetwork = false;
if (response.data.status === 1) {
$('#createNetworkModal').modal('hide');
$scope.loadNetworks();
new PNotify({
title: 'Success',
text: 'Network created successfully',
type: 'success'
});
} else {
new PNotify({
title: 'Error',
text: 'Failed to create network: ' + response.data.error_message,
type: 'error'
});
}
}, function(error) {
$('#createNetworkLoading').hide();
$scope.creatingNetwork = false;
new PNotify({
title: 'Error',
text: 'Error creating network: ' + error.data.error_message,
type: 'error'
});
});
};
// Remove network
$scope.removeNetwork = function(network) {
if (network.containers > 0) {
new PNotify({
title: 'Error',
text: 'Cannot remove network with running containers',
type: 'error'
});
return;
}
if (confirm('Are you sure you want to remove network "' + network.name + '"?')) {
// Implementation for removing network would go here
new PNotify({
title: 'Info',
text: 'Network removal not implemented in this demo',
type: 'info'
});
}
};
// Initialize
$scope.loadNetworks();
});
</script>
{% endblock %}

View File

@@ -755,12 +755,76 @@
</div>
</div>
<!-- Network Configuration Section -->
<div class="form-section">
<div class="section-header">
<div class="section-icon">
<i class="fas fa-network-wired"></i>
</div>
<div>
<h2 class="section-title">{% trans "Network Configuration" %}</h2>
<p class="section-subtitle">{% trans "Configure network settings and extra options for the container" %}</p>
</div>
</div>
<div class="form-row">
<label class="form-label">
{% trans "Network" %}
<div class="tooltip">
<i class="fas fa-question-circle tooltip-icon"></i>
<div class="tooltip-content">
{% trans "Select the Docker network for the container" %}
</div>
</div>
</label>
<select class="form-control" ng-model="selectedNetwork" ng-init="selectedNetwork='bridge'">
<option value="bridge">{% trans "Default Bridge" %}</option>
<option value="host">{% trans "Host Network" %}</option>
<option value="none">{% trans "No Network" %}</option>
<option ng-repeat="network in availableNetworks" value="{$ network.name $}">
{$ network.name $} ({$ network.driver $})
</option>
</select>
</div>
<div class="form-row">
<label class="form-label">
{% trans "Extra Hosts" %}
<div class="tooltip">
<i class="fas fa-question-circle tooltip-icon"></i>
<div class="tooltip-content">
{% trans "Add custom host entries (e.g., host.docker.internal:host-gateway)" %}
</div>
</div>
</label>
<input type="text" class="form-control" ng-model="extraHosts"
placeholder="{% trans 'host.docker.internal:host-gateway, example.com:1.2.3.4' %}">
</div>
<div class="form-row">
<label class="form-label">
{% trans "Network Mode" %}
<div class="tooltip">
<i class="fas fa-question-circle tooltip-icon"></i>
<div class="tooltip-content">
{% trans "Override network mode (bridge, host, none, or custom network)" %}
</div>
</div>
</label>
<select class="form-control" ng-model="networkMode" ng-init="networkMode='bridge'">
<option value="bridge">{% trans "Bridge" %}</option>
<option value="host">{% trans "Host" %}</option>
<option value="none">{% trans "None" %}</option>
</select>
</div>
</div>
<!-- Port Configuration Section -->
{% if portConfig %}
<div class="form-section">
<div class="section-header">
<div class="section-icon">
<i class="fas fa-network-wired"></i>
<i class="fas fa-plug"></i>
</div>
<div>
<h2 class="section-title">{% trans "Port Mapping" %}</h2>

View File

@@ -761,6 +761,11 @@
<i class="fas fa-code action-icon" style="color: #10b981;"></i>
<div class="action-text">{% trans "Run Command" %}</div>
</div>
<div class="action-btn" ng-click="showPortEditModal()" ng-disabled="status!='running'">
<i class="fas fa-edit action-icon" style="color: #8b5cf6;"></i>
<div class="action-text">{% trans "Edit Ports" %}</div>
</div>
</div>
</div>
</div>
@@ -1239,6 +1244,78 @@
</div>
</div>
</div>
<!-- Port Editing Modal -->
<div id="portEditModal" 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-edit" style="margin-right: 0.5rem;"></i>
{% trans "Edit Port Mappings" %}
</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="alert alert-info">
<i class="fas fa-info-circle"></i>
<strong>{% trans "Important:" %}</strong> {% trans "Editing port mappings will temporarily stop and recreate the container. Any unsaved data in the container may be lost." %}
</div>
<div class="form-group">
<label class="control-label">
<i class="fas fa-plug" style="margin-right: 0.5rem;"></i>
{% trans "Port Mappings" %}
</label>
<div class="port-mapping-container">
<div ng-repeat="(containerPort, hostPort) in currentPorts" class="port-mapping-row" style="display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem; padding: 1rem; background: #f8f9fa; border-radius: 8px;">
<div style="flex: 1;">
<label style="font-weight: 600; color: #495057; margin-bottom: 0.25rem;">{% trans "Container Port" %}</label>
<input type="text" class="form-control" ng-model="containerPort" disabled style="font-family: monospace;">
</div>
<div style="color: #007bff; font-size: 1.25rem;">
<i class="fas fa-arrow-right"></i>
</div>
<div style="flex: 1;">
<label style="font-weight: 600; color: #495057; margin-bottom: 0.25rem;">{% trans "Host Port" %}</label>
<input type="number" class="form-control" ng-model="currentPorts[containerPort]"
placeholder="{% trans 'e.g., 8080' %}" min="1024" max="65535">
</div>
<div style="display: flex; align-items: center; height: 38px;">
<button type="button" class="btn btn-danger btn-sm" ng-click="removePortMapping(containerPort)"
ng-show="Object.keys(currentPorts).length > 1" title="{% trans 'Remove port mapping' %}">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
<div ng-show="Object.keys(currentPorts).length === 0" class="text-center text-muted" style="padding: 2rem;">
<i class="fas fa-info-circle fa-2x" style="margin-bottom: 1rem;"></i>
<p>{% trans "No port mappings configured" %}</p>
</div>
</div>
</div>
<div class="form-group">
<button type="button" class="btn btn-primary" ng-click="addNewPortMapping()">
<i class="fas fa-plus"></i>
{% trans "Add Port Mapping" %}
</button>
</div>
</div>
<div class="modal-footer">
<img id="portEditLoading" src="/static/images/loading.gif" style="display: none;" class="loading-spinner">
<button type="button" class="btn btn-primary" ng-disabled="updatingPorts" ng-click="updatePortMappings()">
<i class="fas fa-save"></i> {% trans "Update Port Mappings" %}
</button>
<button type="button" class="btn btn-default" data-dismiss="modal">
<i class="fas fa-times"></i> {% trans "Cancel" %}
</button>
</div>
</div>
</div>
</div>
</div>
<style>

View File

@@ -27,6 +27,11 @@ urlpatterns = [
re_path(r'^getImageHistory$', views.getImageHistory, name='getImageHistory'),
re_path(r'^removeImage$', views.removeImage, name='removeImage'),
re_path(r'^pullImage$', views.pullImage, name='pullImage'),
# Network management endpoints
re_path(r'^getDockerNetworks$', views.getDockerNetworks, name='getDockerNetworks'),
re_path(r'^createDockerNetwork$', views.createDockerNetwork, name='createDockerNetwork'),
re_path(r'^updateContainerPorts$', views.updateContainerPorts, name='updateContainerPorts'),
re_path(r'^manageNetworks$', views.manageNetworks, name='manageNetworks'),
re_path(r'^updateContainer$', views.updateContainer, name='updateContainer'),
re_path(r'^listContainers$', views.listContainers, name='listContainers'),
re_path(r'^deleteContainerWithData$', views.deleteContainerWithData, name='deleteContainerWithData'),

View File

@@ -766,3 +766,86 @@ def listContainers(request):
return coreResult
except KeyError:
return redirect(loadLoginPage)
@preDockerRun
def getDockerNetworks(request):
"""
Get list of all Docker networks
"""
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] == 1:
pass
else:
return ACLManager.loadErrorJson()
cm = ContainerManager()
coreResult = cm.getDockerNetworks(userID)
return coreResult
except KeyError:
return redirect(loadLoginPage)
@preDockerRun
def createDockerNetwork(request):
"""
Create a new Docker network
"""
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] == 1:
pass
else:
return ACLManager.loadErrorJson()
cm = ContainerManager()
coreResult = cm.createDockerNetwork(userID, json.loads(request.body))
return coreResult
except KeyError:
return redirect(loadLoginPage)
@preDockerRun
def updateContainerPorts(request):
"""
Update port mappings for an existing container
"""
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] == 1:
pass
else:
return ACLManager.loadErrorJson()
cm = ContainerManager()
coreResult = cm.updateContainerPorts(userID, json.loads(request.body))
return coreResult
except KeyError:
return redirect(loadLoginPage)
@preDockerRun
def manageNetworks(request):
"""
Display the network management page
"""
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] == 1:
pass
else:
return ACLManager.loadError()
template = 'dockerManager/manageNetworks.html'
proc = httpProc(request, template, {}, 'admin')
return proc.render()
except KeyError:
return redirect(loadLoginPage)

View File

@@ -2204,6 +2204,22 @@ CREATE TABLE `websiteFunctions_backupsv2` (`id` integer AUTO_INCREMENT NOT NULL
except:
pass
# Add new fields for network configuration and extra options
try:
cursor.execute('ALTER TABLE dockerManager_containers ADD network VARCHAR(100) DEFAULT "bridge"')
except:
pass
try:
cursor.execute('ALTER TABLE dockerManager_containers ADD network_mode VARCHAR(50) DEFAULT "bridge"')
except:
pass
try:
cursor.execute('ALTER TABLE dockerManager_containers ADD extra_options LONGTEXT DEFAULT "{}"')
except:
pass
try:
connection.close()
except: