Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exegol 4.2.5 #175

Merged
merged 15 commits into from
Aug 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
<a target="_blank" rel="noopener noreferrer" href="https://www.blackhat.com/asia-23/arsenal/schedule/#exegol-professional-hacking-setup-30815" title="Schedule">
<img alt="Black Hat Asia 2023" src="https://img.shields.io/badge/Black%20Hat%20Arsenal-Asia%202023-blueviolet">
</a>
<a target="_blank" rel="noopener noreferrer" href="https://www.blackhat.com/us-23/arsenal/schedule/#exegol-professional-hacking-setup-31711" title="Schedule">
<img alt="Black Hat USA 2023" src="https://img.shields.io/badge/Black%20Hat%20Arsenal-USA%202023-blueviolet">
</a>
<br><br>
<a target="_blank" rel="noopener noreferrer" href="https://discord.gg/cXThyp7D6P" title="Join us on Discord"><img src="https://raw.githubusercontent.com/ThePorgs/Exegol-docs/main/.assets/discord_join_us.png" width="150" alt="Join us on Discord"></a>
<br><br>
Expand Down
2 changes: 1 addition & 1 deletion exegol-docker-build
2 changes: 1 addition & 1 deletion exegol/config/ConstantConfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
class ConstantConfig:
"""Constant parameters information"""
# Exegol Version
version: str = "4.2.4"
version: str = "4.2.5"

# Exegol documentation link
documentation: str = "https://exegol.rtfd.io/"
Expand Down
2 changes: 1 addition & 1 deletion exegol/config/DataCache.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
class DataCache(DataFileUtils, metaclass=MetaSingleton):
"""This class allows loading cached information defined configurations

Exemple of data:
Example of data:
{
wrapper: {
update: {
Expand Down
2 changes: 1 addition & 1 deletion exegol/console/TUI.py
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,7 @@ def printContainerRecap(cls, container: ExegolContainerTemplate):
container_info_header += f" - v.{container.image.getImageVersion()}"
if "Unknown" not in container.image.getStatus():
container_info_header += f" ({container.image.getStatus(include_version=False)})"
if container.image.getArch() != EnvInfo.arch or logger.isEnabledFor(ExeLog.VERBOSE):
if container.image.getArch().split('/')[0] != EnvInfo.arch or logger.isEnabledFor(ExeLog.VERBOSE):
color = ConsoleFormat.getArchColor(container.image.getArch())
container_info_header += f" [{color}]({container.image.getArch()})[/{color}]"
recap.add_column(container_info_header)
Expand Down
7 changes: 5 additions & 2 deletions exegol/manager/UpdateManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ def __updateGit(gitUtils: GitUtils) -> bool:
if current_branch is None:
logger.warning("HEAD is detached. Please checkout to an existing branch.")
current_branch = "unknown"
if logger.isEnabledFor(ExeLog.VERBOSE) or current_branch not in ["master", "main"]:
if logger.isEnabledFor(ExeLog.VERBOSE):
available_branches = gitUtils.listBranch()
# Ask to checkout only if there is more than one branch available
if len(available_branches) > 1:
Expand Down Expand Up @@ -289,7 +289,10 @@ def isUpdateTag(cls) -> bool:

@classmethod
def display_latest_version(cls) -> str:
return f"[blue]v{DataCache().get_wrapper_data().last_version}[/blue]"
last_version = DataCache().get_wrapper_data().last_version
if len(last_version) == 8 and '.' not in last_version:
return f"[bright_black]\[{last_version}][/bright_black]"
return f"[blue]v{last_version}[/blue]"

@classmethod
def __untagUpdateAvailable(cls, current_version: Optional[str] = None):
Expand Down
13 changes: 11 additions & 2 deletions exegol/model/ContainerConfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -588,10 +588,19 @@ def prepareShare(self, share_name: str):
# Skip default volume workspace if disabled
return
else:
# Add shared-data-volumes private workspace bind volume
# Add dedicated private workspace bind volume
volume_path = str(UserConfig().private_volume_path.joinpath(share_name))
self.addVolume(volume_path, '/workspace', enable_sticky_group=True)

def rollback_preparation(self, share_name: str):
"""Undo preparation in case of container creation failure"""
if self.__workspace_custom_path is None and not self.__disable_workspace:
# Remove dedicated workspace volume
logger.info("Rollback: removing dedicated workspace directory")
directory_path = UserConfig().private_volume_path.joinpath(share_name)
if directory_path.is_dir():
directory_path.rmdir()

def setNetworkMode(self, host_mode: Optional[bool]):
"""Set container's network mode, true for host, false for bridge"""
if host_mode is None:
Expand Down Expand Up @@ -852,7 +861,7 @@ def removeVolume(self, host_path: Optional[str] = None, container_path: Optional
"""Remove a volume from the container configuration (Only before container creation)"""
if host_path is None and container_path is None:
# This is a dev problem
raise ReferenceError('At least one parameter must be set')
raise ValueError('At least one parameter must be set')
for i in range(len(self.__mounts)):
# For each Mount object compare the host_path if supplied or the container_path si supplied
if host_path is not None and self.__mounts[i].get("Source") == host_path:
Expand Down
3 changes: 3 additions & 0 deletions exegol/model/ExegolContainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,9 @@ def __removeVolume(self):
list_files = []
else:
return
except FileNotFoundError:
logger.debug("This workspace has already been removed.")
return
try:
if len(list_files) > 0:
# Directory is not empty
Expand Down
8 changes: 8 additions & 0 deletions exegol/model/ExegolContainerTemplate.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from rich.prompt import Prompt

from exegol.config.EnvInfo import EnvInfo
from exegol.model.ContainerConfig import ContainerConfig
from exegol.model.ExegolImage import ExegolImage

Expand All @@ -14,6 +15,9 @@ def __init__(self, name: Optional[str], config: ContainerConfig, image: ExegolIm
if name is None:
name = Prompt.ask("[bold blue][?][/bold blue] Enter the name of your new exegol container", default="default")
assert name is not None
if (EnvInfo.isWindowsHost() or EnvInfo.isMacHost()) and not name.startswith("exegol-"):
# Force container as lowercase because the filesystem of windows / mac are case-insensitive => https://github.com/ThePorgs/Exegol/issues/167
name = name.lower()
self.container_name: str = name if name.startswith("exegol-") else f'exegol-{name}'
self.name: str = name.replace('exegol-', '')
if hostname:
Expand All @@ -31,6 +35,10 @@ def prepare(self):
"""Prepare the model before creating the docker container"""
self.config.prepareShare(self.name)

def rollback(self):
"""Rollback change in case of container creation fail."""
self.config.rollback_preparation(self.name)

def getDisplayName(self) -> str:
"""Getter of the container's name for TUI purpose"""
if self.container_name != self.hostname:
Expand Down
2 changes: 1 addition & 1 deletion exegol/model/ExegolImage.py
Original file line number Diff line number Diff line change
Expand Up @@ -616,7 +616,7 @@ def getName(self) -> str:
def getDisplayName(self) -> str:
"""Image's display name getter"""
result = self.__alt_name if self.__alt_name else self.__name
if self.getArch() != ParametersManager().arch or logger.isEnabledFor(ExeLog.VERBOSE):
if self.getArch().split('/')[0] != ParametersManager().arch or logger.isEnabledFor(ExeLog.VERBOSE):
color = ConsoleFormat.getArchColor(self.getArch())
result += f" [{color}]({self.getArch()})[/{color}]"
return result
Expand Down
10 changes: 6 additions & 4 deletions exegol/utils/DataFileUtils.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,18 @@ def _build_file_content(self) -> str:
This fonction build the default file content. Called when the file doesn't exist yet or have been upgrade and need to be updated.
:return:
"""
raise NotImplementedError(
f"The '_build_default_file' method hasn't been implemented in the '{self.__class__}' class.")
raise NotImplementedError(f"The '_build_default_file' method hasn't been implemented in the '{self.__class__}' class.")

def _create_config_file(self):
"""
Create or overwrite the file content to the default / current value depending on the '_build_default_file' that must be redefined in child class.
:return:
"""
with open(self._file_path, 'w') as file:
file.write(self._build_file_content())
try:
with open(self._file_path, 'w') as file:
file.write(self._build_file_content())
except PermissionError as e:
logger.critical(f"Unable to open the file '{self._file_path}' ({e}). Please fix your file permissions or run exegol with the correct rights.")

def _parse_config(self):
data: Dict = {}
Expand Down
21 changes: 20 additions & 1 deletion exegol/utils/DockerUtils.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,20 @@ def createContainer(cls, model: ExegolContainerTemplate, temporary: bool = False
auto_remove=temporary,
working_dir=model.config.getWorkingDir())
except APIError as err:
logger.error(err.explanation.decode('utf-8') if type(err.explanation) is bytes else err.explanation)
message = err.explanation.decode('utf-8').replace('[', '\\[') if type(err.explanation) is bytes else err.explanation
message = message.replace('[', '\\[')
logger.error(message)
logger.debug(err)
model.rollback()
try:
container = cls.__client.containers.list(all=True, filters={"name": model.container_name})
if container is not None and len(container) > 0:
for c in container:
if c.name == model.container_name: # Search for exact match
container[0].remove()
logger.debug("Container removed")
except Exception:
pass
logger.critical("Error while creating exegol container. Exiting.")
# Not reachable, critical logging will exit
return # type: ignore
Expand All @@ -151,6 +163,13 @@ def getContainer(cls, tag: str) -> ExegolContainer:
return # type: ignore
# Check if there is at least 1 result. If no container was found, raise ObjectNotFound.
if container is None or len(container) == 0:
# Handle case-insensitive OS
if EnvInfo.isWindowsHost() or EnvInfo.isMacHost():
# First try to fetch the container as-is (for retroactive support with old container with uppercase characters)
# If the user's input didn't match any container, try to force the name in lowercase if not already tried
lowered_tag = tag.lower()
if lowered_tag != tag:
return cls.getContainer(lowered_tag)
raise ObjectNotFound
# Filter results with exact name matching
for c in container:
Expand Down
4 changes: 4 additions & 0 deletions exegol/utils/GuiUtils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import os
import shutil
import subprocess
import sys
import time
from pathlib import Path
from typing import Optional
Expand Down Expand Up @@ -92,6 +93,9 @@ def __macGuiChecks(cls) -> bool:
# Notify user to change configuration
logger.error("XQuartz does not allow network connections. "
"You need to manually change the configuration to 'Allow connections from network clients'")
# Add sys.platform check to exclude windows env (fix for mypy static code analysis)
if sys.platform != "win32" and os.getuid() == 0:
logger.warning("You are running exegol as [red]root[/red]! The root user cannot check in the user context whether XQuartz is properly configured or not.")
return False

# Check if XQuartz is started, check is dir exist and if there is at least one socket
Expand Down
6 changes: 3 additions & 3 deletions exegol/utils/WebUtils.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def getRemoteVersion(cls, tag: str) -> Optional[str]:
response = cls.__runRequest(url, service_name="Docker Registry", headers=manifest_headers, method="GET")
version: Optional[str] = None
if response is not None and response.status_code == 200:
data = json.loads(response.text)
data = json.loads(response.content.decode("utf-8"))
# Parse metadata of the current image from v1 schema
metadata = json.loads(data.get("history", [])[0]['v1Compatibility'])
# Find version label and extract data
Expand All @@ -106,7 +106,7 @@ def runJsonRequest(cls, url: str, service_name: str, headers: Optional[Dict] = N
return None
data = cls.__runRequest(url, service_name, headers, method, data, retry_count)
if data is not None and data.status_code == 200:
data = json.loads(data.text)
data = json.loads(data.content.decode("utf-8"))
elif data is not None:
logger.error(f"Error during web request to {service_name} ({data.status_code}) on {url}")
if data.status_code == 404 and service_name == "Dockerhub":
Expand All @@ -127,7 +127,7 @@ def __runRequest(cls, url: str, service_name: str, headers: Optional[Dict] = Non
response = requests.request(method=method, url=url, timeout=(5, 10), verify=ParametersManager().verify, headers=headers, data=data)
return response
except requests.exceptions.HTTPError as e:
logger.error(f"Response error: {e.response.text}")
logger.error(f"Response error: {e.response.content.decode('utf-8')}")
except requests.exceptions.ConnectionError as err:
logger.debug(f"Error: {err}")
error_re = re.search(r"\[Errno [-\d]+]\s?([^']*)('\))+\)*", str(err))
Expand Down
2 changes: 1 addition & 1 deletion tests/test_exegol.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@


def test_version():
assert __version__ == '4.2.4'
assert __version__ == '4.2.5'