Skip to content

Commit

Permalink
Merge pull request #22 from need4swede/v1.0.7-5
Browse files Browse the repository at this point in the history
V1.0.8
  • Loading branch information
need4swede authored Jul 14, 2024
2 parents 0ffdecf + 32e3730 commit 90bb4f5
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 64 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 🚢 Portall - Port Management System

![Version](https://img.shields.io/badge/version-1.0.7-blue.svg)
![Version](https://img.shields.io/badge/version-1.0.8-blue.svg)
![License](https://img.shields.io/badge/license-MIT-green.svg)
![Docker](https://img.shields.io/badge/docker-ready-brightgreen.svg)

Expand Down
5 changes: 5 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# <a href="https://github.com/need4swede/portall/releases/tag/v1.0.8" target="_blank">v1.0.8</a>
## Changed:
### Overhauled Docker-Compose Imports
Complete rewrite of how docker-compose data is imported to make the import logic more versititle.

# <a href="https://github.com/need4swede/portall/releases/tag/v1.0.7" target="_blank">v1.0.7</a>
## Added:
### Sorting
Expand Down
2 changes: 1 addition & 1 deletion templates/settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ <h2 class="section-title mb-4">About Portall</h2>
<h3 class="card-title">Version Info</h3>
<ul class="list-unstyled">
<li><span class="info-label">Version:</span> {{ version }}</li>
<li><span class="info-label">Released:</span> July 13, 2024</li>
<li><span class="info-label">Released:</span> July 14, 2024</li>
<li><span class="info-label">Github:</span> <a href="https://github.com/need4swede/Portall"
target="_blank">Portall Repository</a></li>
</ul>
Expand Down
199 changes: 137 additions & 62 deletions utils/routes/imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
# Standard Imports
import json # For parsing JSON data
import re # For regular expressions
import yaml # For parsing YAML data

# External Imports
from flask import Blueprint # For creating a blueprint
Expand All @@ -18,7 +17,7 @@
# Create the blueprint
imports_bp = Blueprint('imports', __name__)

## Import Route ##
# Import to Database

@imports_bp.route('/import', methods=['GET', 'POST'])
def import_data():
Expand All @@ -32,69 +31,75 @@ def import_data():
The function supports importing from Caddyfile, JSON, and Docker-Compose formats.
It checks for existing entries in the database to avoid duplicates and
provides a summary of added and skipped entries.
The order is reset for each unique IP address.
Returns:
For GET: Rendered HTML template
For POST: JSON response indicating success or failure of the import,
including counts of added and skipped entries.
including counts of added and skipped entries.
"""
if request.method == 'POST':
# Extract import type and file content from the form data
import_type = request.form.get('import_type')
file_content = request.form.get('file_content')

# Determine the appropriate import function based on the file type
if import_type == 'Caddyfile':
imported_data = import_caddyfile(file_content)
elif import_type == 'JSON':
imported_data = import_json(file_content)
elif import_type == 'Docker-Compose':
imported_data = import_docker_compose(file_content)
else:
# Return an error response for unsupported import types
return jsonify({'success': False, 'message': 'Unsupported import type'}), 400

# Initialize counters for added and skipped entries
added_count = 0
skipped_count = 0
ip_order_map = {} # To keep track of the current order for each IP

# Process each item in the imported data
# Group imported data by IP address
grouped_data = {}
for item in imported_data:
# Check if the entry already exists in the database
existing_port = Port.query.filter_by(
ip_address=item['ip'],
port_number=item['port'],
port_protocol=item['port_protocol']
).first()

if existing_port is None:
# If the entry doesn't exist, create a new Port object
port = Port(
ip = item['ip']
if ip not in grouped_data:
grouped_data[ip] = []
grouped_data[ip].append(item)

for ip, items in grouped_data.items():
# Get the maximum order for this IP
max_order = db.session.query(db.func.max(Port.order)).filter(Port.ip_address == ip).scalar()
current_order = max_order if max_order is not None else -1

for item in items:
existing_port = Port.query.filter_by(
ip_address=item['ip'],
nickname=item['nickname'] if item['nickname'] is not None else None,
port_number=item['port'],
description=item['description'],
port_protocol=item['port_protocol']
)
# Add the new port to the database session
db.session.add(port)
added_count += 1
else:
# If the entry already exists, skip it
skipped_count += 1

# Commit all changes to the database
).first()

if existing_port is None:
current_order += 1
port = Port(
ip_address=item['ip'],
nickname=item['nickname'] if item['nickname'] is not None else None,
port_number=item['port'],
description=item['description'],
port_protocol=item['port_protocol'],
order=current_order
)
db.session.add(port)
added_count += 1
else:
skipped_count += 1

db.session.commit()

# Return a success response with summary of the import operation
return jsonify({
'success': True,
'message': f'Imported {added_count} entries, skipped {skipped_count} existing entries'
})

# For GET requests, render the import template
return render_template('import.html', theme=session.get('theme', 'light'))
## Import Types ##

# Import Types

def import_caddyfile(content):
"""
Expand Down Expand Up @@ -130,51 +135,109 @@ def import_caddyfile(content):
'nickname': None,
'port': int(port),
'description': current_domain,
'port_protocol': 'TCP' # Assume TCP
'port_protocol': 'TCP' # Assume TCP
})

return entries

def import_docker_compose(content):
try:
data = yaml.load(content, Loader=yaml.FullLoader)
entries = []
def parse_docker_compose(content):
"""
Parse Docker Compose file content and extract service information.
Args:
content (str): The content of the Docker Compose file
if isinstance(data, dict):
services = data.get('services', {})
Returns:
dict: A dictionary with service names as keys and lists of (port, protocol) tuples as values
"""
result = {}
current_service = None
current_image = None
in_services = False
in_ports = False
indent_level = 0

def add_port(image, port, protocol):
image_parts = image.split('/')
image_name = image_parts[-1].split(':')[0]
if image_name not in result:
result[image_name] = []
result[image_name].append((port, protocol))

for service_name, service_config in services.items():
if any(db in service_name.lower() for db in ['db', 'database', 'mysql', 'postgres', 'mariadb', 'mailhog']):
continue
lines = content.split('\n')
for line in lines:
original_line = line
line = line.strip()
current_indent = len(original_line) - len(original_line.lstrip())

if line.startswith('services:'):
in_services = True
indent_level = current_indent
continue

if in_services and current_indent == indent_level + 2:
if ':' in line and not line.startswith('-'):
current_service = line.split(':')[0].strip()
current_image = None
in_ports = False

if in_services and current_indent == indent_level + 4:
if line.startswith('image:'):
current_image = line.split('image:')[1].strip()
if line.startswith('ports:'):
in_ports = True
continue

if in_ports and current_indent == indent_level + 6:
if line.startswith('-'):
port_mapping = line.split('-')[1].strip().strip('"').strip("'")
if ':' in port_mapping:
host_port = port_mapping.split(':')[0]
protocol = 'UDP' if '/udp' in port_mapping else 'TCP'
host_port = host_port.split('/')[0] # Remove any protocol specification from the port
if current_image:
add_port(current_image, host_port, protocol)
elif in_ports and current_indent <= indent_level + 4:
in_ports = False

return result

ports = service_config.get('ports', [])
image = service_config.get('image', '')
def import_docker_compose(content):
"""
Parse a Docker Compose file and extract port information.
for port_mapping in ports:
if isinstance(port_mapping, str):
try:
parsed_port, protocol = parse_port_and_protocol(port_mapping)
This function processes Docker Compose file content, extracting service names,
ports, and protocols.
description = image if image else service_name
Args:
content (str): The content of the Docker Compose file
entries.append({
'ip': '127.0.0.1',
'nickname': None,
'port': parsed_port,
'description': description,
'port_protocol': protocol
})
Returns:
list: A list of dictionaries containing extracted port information
print(f"Added entry: IP: 127.0.0.1, Port: {parsed_port}, Protocol: {protocol}, Description: {description}")
Raises:
ValueError: If there's an error parsing the Docker Compose file
"""
try:
parsed_data = parse_docker_compose(content)

except ValueError as e:
print(f"Warning: {str(e)} for service {service_name}")
entries = []
for image, ports in parsed_data.items():
for port, protocol in ports:
entry = {
"ip": "127.0.0.1",
"nickname": None,
"port": int(port),
"description": image,
"port_protocol": protocol
}
entries.append(entry)

print(f"Total entries found: {len(entries)}")
return entries

except yaml.YAMLError as e:
raise ValueError(f"Invalid Docker-Compose YAML format: {str(e)}")
except Exception as e:
raise ValueError(f"Error parsing Docker-Compose file: {str(e)}")

def import_json(content):
"""
Expand Down Expand Up @@ -207,6 +270,8 @@ def import_json(content):
except json.JSONDecodeError:
raise ValueError("Invalid JSON format")

# Import Helpers

def parse_port_and_protocol(port_value):
"""
Parse a port value and protocol, handling direct integers, environment variable expressions,
Expand Down Expand Up @@ -254,4 +319,14 @@ def parse_port_and_protocol(port_value):
return int(port_value), protocol

# If we can't parse it, raise an error
raise ValueError(f"Unable to parse port value: {port_value}")
raise ValueError(f"Unable to parse port value: {port_value}")

def get_max_order():
"""
Retrieve the maximum order value from the Port table.
Returns:
int: The maximum order value, or -1 if the table is empty.
"""
max_order = db.session.query(db.func.max(Port.order)).scalar()
return max_order if max_order is not None else -1

0 comments on commit 90bb4f5

Please sign in to comment.