Skip to content

Commit 63b00f5

Browse files
authored
Merge pull request #280 from ThePorgs/dev
Release 5.1.3
2 parents b33c9b2 + a6a9234 commit 63b00f5

20 files changed

+151
-87
lines changed

.github/workflows/sub_testing.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ jobs:
3333
strategy:
3434
fail-fast: false
3535
matrix:
36-
version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
36+
version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
3737
os: [win32, linux, darwin]
3838
steps:
3939
- uses: actions/checkout@main

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
If you'd like to contributing to this project, everything you need to know is at https://docs.exegol.com/contribute/intro
1+
If you'd like to contribute to this project, everything you need to know is at https://docs.exegol.com/contribute/intro

exegol-images

exegol/config/ConstantConfig.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import os
22
from pathlib import Path
33

4-
__version__ = "5.1.2"
4+
__version__ = "5.1.3"
55

66

77
class ConstantConfig:

exegol/console/ExegolPrompt.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,11 @@ async def Acknowledge(message: str) -> None:
3232
"""Quick function to format rich Confirmation and options on every exegol interaction"""
3333
formatted_question = f"[bold blue][>][/bold blue] {message} [bright_magenta][Press ENTER to acknowledge][/bright_magenta]"
3434
async with ConsoleLock:
35-
rich.prompt.Prompt.ask(
36-
formatted_question,
37-
show_choices=False,
38-
show_default=False,
39-
console=console)
35+
try:
36+
rich.prompt.Prompt.ask(
37+
formatted_question,
38+
show_choices=False,
39+
show_default=False,
40+
console=console)
41+
except ValueError:
42+
raise EOFError

exegol/console/TUI.py

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -280,12 +280,16 @@ async def selectFromTable(cls,
280280
data: Sequence[SelectableInterface],
281281
object_type: Optional[Type] = None,
282282
default: Optional[str] = None,
283-
allow_None: bool = False,
284-
conflict_mode: bool = False) -> Union[SelectableInterface, str]:
283+
allow_none: bool = False,
284+
conflict_mode: bool = False,
285+
multiple: bool = False) -> Union[SelectableInterface, str, Sequence[SelectableInterface], Sequence[str]]:
285286
"""Return an object (implementing SelectableInterface) selected by the user
286287
Return a str when allow_none is true and no object have been selected
287288
Raise IndexError of the data list is empty.
288-
Set conflict_mode to override the key in order to select a specific object with duplicate name"""
289+
Set allow_none to allow the user to select no object and return a str
290+
Set conflict_mode to override the key in order to select a specific object with duplicate name
291+
Set multiple to add * option to select multiple objects
292+
"""
289293
cls.__isInteractionAllowed()
290294
# Check if there is at least one object in the list
291295
if len(data) == 0:
@@ -308,8 +312,10 @@ async def selectFromTable(cls,
308312
# If no default have been supplied, using the first one
309313
if default is None:
310314
default = choices[0]
315+
if multiple:
316+
choices.append("*")
311317
# When allow_none is enabled, disabling choices restriction
312-
if allow_None:
318+
if allow_none:
313319
choices_select: Optional[List[str]] = None
314320
logger.info(
315321
f"You can use a name that does not already exist to {action} a new {object_name}"
@@ -322,6 +328,8 @@ async def selectFromTable(cls,
322328
default=default,
323329
choices=choices_select,
324330
show_choices=False)
331+
if choice == "*":
332+
return list(data)
325333
if conflict_mode:
326334
# In conflict mode, choice are only index number offset by 1
327335
return data[int(choice) - 1]
@@ -332,8 +340,8 @@ async def selectFromTable(cls,
332340
return match[0]
333341
elif len(match) > 1:
334342
logger.error(f"Conflict detected ! Multiple {object_name} have the same name, please select the intended one.")
335-
return await cls.selectFromTable(match, object_type, default=None, allow_None=False, conflict_mode=True)
336-
if allow_None:
343+
return await cls.selectFromTable(match, object_type, default=None, allow_none=False, conflict_mode=True, multiple=multiple)
344+
if allow_none:
337345
if await ExegolRich.Confirm(
338346
f"No {object_name} is available under this name, do you want to {action} it?",
339347
default=True):
@@ -361,9 +369,14 @@ async def multipleSelectFromTable(cls,
361369
else:
362370
object_subject = "object"
363371
while True:
364-
selected = cast(SelectableInterface, await cls.selectFromTable(pool, object_type, default))
365-
result.append(selected)
366-
pool.remove(selected)
372+
selected = cast(Union[SelectableInterface, Sequence[SelectableInterface]], await cls.selectFromTable(pool, object_type, default, multiple=True))
373+
if isinstance(selected, SelectableInterface):
374+
result.append(selected)
375+
pool.remove(selected)
376+
else:
377+
result.extend(selected)
378+
for i in selected:
379+
pool.remove(i)
367380
if len(pool) == 0:
368381
return result
369382
elif not await ExegolRich.Confirm(f"Do you want to select another {object_subject}?", default=False):

exegol/manager/ExegolManager.py

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,10 @@ async def info(cls) -> None:
7676
@classmethod
7777
async def start(cls) -> None:
7878
"""Create and/or start an exegol container to finally spawn an interactive shell"""
79-
logger.info("Starting exegol")
8079
# Check if the first positional parameter have been supplied
8180
cls.__interactive_mode = not bool(ParametersManager().containertag)
8281
if not cls.__interactive_mode:
83-
logger.info("Arguments supplied with the command, skipping interactive mode")
82+
logger.info("Skipping interactive mode (arguments supplied)")
8483
container = await cls.__loadOrCreateContainer()
8584
assert container is not None and type(container) is ExegolContainer
8685
if not container.isNew():
@@ -113,7 +112,6 @@ async def upgrade(cls) -> None:
113112
async def exec(cls) -> None:
114113
"""Create and/or start an exegol container to execute a specific command.
115114
The execution can be seen in console output or be relayed in the background as a daemon."""
116-
logger.info("Starting exegol")
117115
if ParametersManager().tmp:
118116
container = await cls.__createTmpContainer(ParametersManager().selector)
119117
if not ParametersManager().daemon:
@@ -129,7 +127,7 @@ async def exec(cls) -> None:
129127
@classmethod
130128
async def stop(cls) -> None:
131129
"""Stop an exegol container"""
132-
logger.info("Stopping exegol")
130+
logger.info("Stopping container(s)")
133131
container = await cls.__loadOrCreateContainer(multiple=True, must_exist=True, filters=[ExegolContainer.Filters.STARTED])
134132
assert container is not None and type(container) is list
135133
for c in container:
@@ -165,7 +163,7 @@ async def build(cls) -> None:
165163
async def update(cls) -> None:
166164
"""Update python wrapper (git installation required) and Pull a docker exegol image"""
167165
if ParametersManager().offline_mode:
168-
logger.critical("It's not possible to update Exegol in offline mode. Please retry later with an internet connection.")
166+
logger.critical("Exegol cannot be updated without Internet access. Skipping.")
169167
if not ParametersManager().skip_git:
170168
await UpdateManager.updateWrapper()
171169
await UpdateManager.updateResources()
@@ -175,7 +173,7 @@ async def update(cls) -> None:
175173
@classmethod
176174
async def uninstall(cls) -> None:
177175
"""Remove an exegol image"""
178-
logger.info("Uninstalling an exegol image")
176+
logger.info("Uninstalling image")
179177
# Set log level to verbose in order to show every image installed including the outdated.
180178
if not logger.isEnabledFor(ExeLog.VERBOSE):
181179
logger.setLevel(ExeLog.VERBOSE)
@@ -195,7 +193,7 @@ async def uninstall(cls) -> None:
195193
@classmethod
196194
async def remove(cls) -> None:
197195
"""Remove an exegol container"""
198-
logger.info("Removing an exegol container")
196+
logger.info("Removing container(s)")
199197
containers = await cls.__loadOrCreateContainer(multiple=True, must_exist=True)
200198
assert type(containers) is list
201199
if len(containers) == 0:
@@ -335,7 +333,7 @@ async def __loadOrInstallImage(cls,
335333
else:
336334
# Interactive (TUI) image selection
337335
image_selection = cast(Union[Optional[ExegolImage], List[ExegolImage]],
338-
await cls.__interactiveSelection(ExegolImage, image_list, multiple, must_exist))
336+
await cls.__interactiveSelection(ExegolImage, image_list, multiple))
339337
else:
340338
# Select image by tag name (non-interactive)
341339
if multiple:
@@ -463,7 +461,7 @@ async def __loadOrCreateContainer(cls,
463461
else:
464462
# Interactive container selection
465463
cls.__container = cast(Union[Optional[ExegolContainer], List[ExegolContainer]],
466-
await cls.__interactiveSelection(ExegolContainer, container_list, multiple, must_exist))
464+
await cls.__interactiveSelection(ExegolContainer, container_list, multiple))
467465
else:
468466
# Try to find the corresponding container
469467
if multiple:
@@ -491,9 +489,9 @@ async def __loadOrCreateContainer(cls,
491489
# Create container
492490
if must_exist:
493491
if container_tag is not None:
494-
logger.warning(f"The container named '{container_tag}' has not been found")
492+
logger.warning(f"Container '{container_tag}' has not been found")
495493
return [] if multiple else None
496-
logger.info(f"Creating new container named '{container_tag}'")
494+
logger.info(f"Creating new container '{container_tag}'")
497495
return await cls.__createContainer(container_tag)
498496
assert cls.__container is not None
499497
return cast(Union[Optional[ExegolContainer], List[ExegolContainer]], cls.__container)
@@ -502,16 +500,15 @@ async def __loadOrCreateContainer(cls,
502500
async def __interactiveSelection(cls,
503501
object_type: Type[Union[ExegolImage, ExegolContainer]],
504502
object_list: Sequence[SelectableInterface],
505-
multiple: bool = False,
506-
must_exist: bool = False) -> \
503+
multiple: bool = False) -> \
507504
Union[Optional[ExegolImage], Optional[ExegolContainer], Sequence[ExegolImage], Sequence[ExegolContainer]]:
508505
"""Interactive object selection process, depending on object_type.
509506
object_type can be ExegolImage or ExegolContainer."""
510-
user_selection: Union[SelectableInterface, Sequence[SelectableInterface], str]
507+
user_selection: Union[SelectableInterface, Sequence[SelectableInterface], Sequence[str], str]
511508
if multiple:
512509
user_selection = await ExegolTUI.multipleSelectFromTable(object_list, object_type=object_type)
513510
else:
514-
user_selection = await ExegolTUI.selectFromTable(object_list, object_type=object_type, allow_None=object_type is ExegolContainer)
511+
user_selection = await ExegolTUI.selectFromTable(object_list, object_type=object_type, allow_none=object_type is ExegolContainer)
515512
# Check if the user has chosen an existing object
516513
if type(user_selection) is str:
517514
# Otherwise, create a new object with the supplied name
@@ -523,19 +520,19 @@ async def __interactiveSelection(cls,
523520
async def __createContainer(cls, name: Optional[str]) -> ExegolContainer:
524521
"""Create an ExegolContainer"""
525522
if name is None:
526-
name = await ExegolRich.Ask("Enter the name of your new exegol container", default="default")
523+
name = await ExegolRich.Ask("Enter new container name", default="default")
527524
logger.verbose("Configuring new exegol container")
528525
# Create exegol config
529526
image: Optional[ExegolImage] = cast(ExegolImage, await cls.__loadOrInstallImage(show_custom=True))
530527
assert image is not None # load or install return an image
531528
if name is None:
532-
name = await ExegolRich.Ask("Enter the name of your new exegol container", default="default")
529+
name = await ExegolRich.Ask("Enter new container name", default="default")
533530
model = await ExegolContainerTemplate.newContainer(name, image, hostname=ParametersManager().hostname)
534531

535532
# Recap
536533
await ExegolTUI.printContainerRecap(model)
537534
if cls.__interactive_mode:
538-
if not model.image.isUpToDate() and \
535+
if not model.image.isUpToDate() and "Unknown" not in model.image.getStatus() and \
539536
await ExegolRich.Confirm("Do you want to [green]update[/green] the selected image?", False):
540537
image = await UpdateManager.updateImage(model.image.getName())
541538
if image is not None:

exegol/manager/UpdateManager.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import re
22
from pathlib import Path
3-
from typing import Optional, Dict, cast, Tuple, Sequence
3+
from typing import Optional, Dict, cast, Tuple, Sequence, List
44

55
from exegol.config.ConstantConfig import ConstantConfig
66
from exegol.config.DataCache import DataCache
@@ -30,20 +30,20 @@ async def updateImage(cls, tag: Optional[str] = None, install_mode: bool = False
3030
if image_args is not None and tag is None:
3131
tag = image_args
3232
if tag is None:
33-
all_images = await DockerUtils().listImages()
33+
all_images: List[ExegolImage] = await DockerUtils().listImages()
3434
# Filter for updatable images
3535
if install_mode:
36-
available_images = [i for i in all_images if not i.isLocked()]
36+
available_images = [i for i in all_images if not i.isLocked() and not i.isLocal()]
3737
else:
38-
available_images = [i for i in all_images if i.isInstall() and not i.isUpToDate() and not i.isLocked()]
38+
available_images = [i for i in all_images if i.isInstall() and not i.isLocal() and not i.isUpToDate() and not i.isLocked()]
3939
if len(available_images) == 0:
4040
logger.success("All images already installed are up to date!")
4141
return None
4242
try:
4343
# Interactive selection
4444
selected_image = await ExegolTUI.selectFromTable(available_images,
4545
object_type=ExegolImage,
46-
allow_None=False)
46+
allow_none=False)
4747
except IndexError:
4848
# No images are available
4949
if install_mode:

exegol/model/ContainerConfig.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ def __init__(self, container: Optional[Container] = None, container_name: Option
126126
self.__workspace_dedicated_path: Optional[str] = None
127127
self.__disable_workspace: bool = False
128128
self.__container_entrypoint: List[str] = self.__default_entrypoint
129-
self.__vpn_path: Optional[Union[Path, PurePath]] = None
129+
self.__vpn_path: Optional[Path] = None
130130
self.__shell_logging: bool = False
131131
# Entrypoint features
132132
self.legacy_entrypoint: bool = True
@@ -770,10 +770,6 @@ async def enableVPN(self, config_path: Optional[Union[str, PurePath]] = None, ap
770770
self.__vpn_mode = "ovpn"
771771
self.__vpn_parameters = await self.__prepareOpenVpnVolumes(vpn_path, skip_conf_checks=apply_only)
772772
elif vpn_path.is_file() and vpn_path.suffix == ".conf":
773-
if not SessionHandler().pro_feature_access():
774-
logger.error("WireGuard VPN support is exclusive to Pro/Enterprise users. Coming soon to Community.")
775-
self.__disableVPN()
776-
raise InteractiveError
777773
# Wireguard config
778774
self.__addSysctl("net.ipv4.conf.all.src_valid_mark", "1")
779775
self.__vpn_mode = "wgconf"
@@ -1616,6 +1612,10 @@ def getTextFeatures(self, verbose: bool = False) -> str:
16161612
return "[i][bright_black]Default configuration[/bright_black][/i]"
16171613
return result
16181614

1615+
def getVpnConfigPath(self) -> Optional[Path]:
1616+
"""Get VPN Config path"""
1617+
return self.__vpn_path
1618+
16191619
def getVpnName(self) -> str:
16201620
"""Get VPN Config name"""
16211621
if self.__vpn_path is None:

0 commit comments

Comments
 (0)