gui4aws 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gui4aws/__about__.py +25 -0
- gui4aws/__init__.py +11 -0
- gui4aws/__main__.py +6 -0
- gui4aws/app.py +121 -0
- gui4aws/cli.py +194 -0
- gui4aws/config.py +139 -0
- gui4aws/demo_resources/__init__.py +79 -0
- gui4aws/demo_resources/_common.py +11 -0
- gui4aws/demo_resources/aurora.py +89 -0
- gui4aws/demo_resources/backup.py +167 -0
- gui4aws/demo_resources/compute.py +97 -0
- gui4aws/demo_resources/ecs.py +113 -0
- gui4aws/demo_resources/iam_resources.py +77 -0
- gui4aws/demo_resources/kms.py +51 -0
- gui4aws/demo_resources/messaging.py +58 -0
- gui4aws/demo_resources/monitoring.py +136 -0
- gui4aws/demo_resources/networking.py +121 -0
- gui4aws/demo_resources/secrets.py +71 -0
- gui4aws/demo_resources/storage.py +81 -0
- gui4aws/execution/__init__.py +17 -0
- gui4aws/execution/action_cache.py +200 -0
- gui4aws/execution/action_history.py +141 -0
- gui4aws/execution/aws_cli_executor.py +208 -0
- gui4aws/execution/boto3_executor.py +201 -0
- gui4aws/execution/endpoint_config.py +52 -0
- gui4aws/execution/execution_mode.py +14 -0
- gui4aws/execution/script_generator.py +208 -0
- gui4aws/gui/__init__.py +5 -0
- gui4aws/gui/action_dialog.py +355 -0
- gui4aws/gui/action_form.py +106 -0
- gui4aws/gui/cdk_dialog.py +783 -0
- gui4aws/gui/confirmation_dialog.py +136 -0
- gui4aws/gui/detail_tree.py +66 -0
- gui4aws/gui/diagnostic_panel.py +415 -0
- gui4aws/gui/filter_bar.py +242 -0
- gui4aws/gui/jmespath_designer_dialog.py +658 -0
- gui4aws/gui/json_viewer_dialog.py +216 -0
- gui4aws/gui/main_panel.py +515 -0
- gui4aws/gui/main_window.py +1233 -0
- gui4aws/gui/output_panel.py +189 -0
- gui4aws/gui/resource_table.py +119 -0
- gui4aws/gui/review_dialog.py +196 -0
- gui4aws/gui/script_editor_panel.py +194 -0
- gui4aws/gui/script_viewer.py +57 -0
- gui4aws/gui/server_manager_mixin.py +249 -0
- gui4aws/gui/sidebar.py +76 -0
- gui4aws/gui/sql_runner_dialog.py +396 -0
- gui4aws/gui/status_bar.py +51 -0
- gui4aws/gui/terraform_dialog.py +68 -0
- gui4aws/gui/toolbar.py +214 -0
- gui4aws/gui/window_helpers.py +279 -0
- gui4aws/gui/worker.py +146 -0
- gui4aws/logging_config.py +58 -0
- gui4aws/models.py +300 -0
- gui4aws/moto_server.py +241 -0
- gui4aws/py.typed +0 -0
- gui4aws/robotocore_server.py +345 -0
- gui4aws/services/__init__.py +5 -0
- gui4aws/services/athena/__init__.py +5 -0
- gui4aws/services/athena/actions.py +278 -0
- gui4aws/services/athena/models.py +34 -0
- gui4aws/services/athena/service.py +69 -0
- gui4aws/services/athena/views.py +95 -0
- gui4aws/services/aurora/__init__.py +9 -0
- gui4aws/services/aurora/actions.py +1072 -0
- gui4aws/services/aurora/models.py +95 -0
- gui4aws/services/aurora/service.py +202 -0
- gui4aws/services/aurora/views.py +183 -0
- gui4aws/services/backup/__init__.py +13 -0
- gui4aws/services/backup/actions.py +601 -0
- gui4aws/services/backup/models.py +93 -0
- gui4aws/services/backup/service.py +154 -0
- gui4aws/services/backup/views.py +151 -0
- gui4aws/services/cloudformation/__init__.py +0 -0
- gui4aws/services/cloudformation/actions.py +285 -0
- gui4aws/services/cloudformation/models.py +19 -0
- gui4aws/services/cloudformation/service.py +92 -0
- gui4aws/services/cloudformation/views.py +35 -0
- gui4aws/services/cloudwatch/__init__.py +0 -0
- gui4aws/services/cloudwatch/actions.py +303 -0
- gui4aws/services/cloudwatch/models.py +49 -0
- gui4aws/services/cloudwatch/service.py +81 -0
- gui4aws/services/cloudwatch/views.py +99 -0
- gui4aws/services/ecs/__init__.py +5 -0
- gui4aws/services/ecs/actions.py +760 -0
- gui4aws/services/ecs/models.py +62 -0
- gui4aws/services/ecs/service.py +185 -0
- gui4aws/services/ecs/views.py +180 -0
- gui4aws/services/iam/__init__.py +0 -0
- gui4aws/services/iam/actions.py +368 -0
- gui4aws/services/iam/models.py +55 -0
- gui4aws/services/iam/service.py +129 -0
- gui4aws/services/iam/views.py +81 -0
- gui4aws/services/kms/__init__.py +5 -0
- gui4aws/services/kms/actions.py +388 -0
- gui4aws/services/kms/models.py +49 -0
- gui4aws/services/kms/service.py +87 -0
- gui4aws/services/kms/views.py +103 -0
- gui4aws/services/lambdas/__init__.py +0 -0
- gui4aws/services/lambdas/actions.py +243 -0
- gui4aws/services/lambdas/models.py +22 -0
- gui4aws/services/lambdas/service.py +53 -0
- gui4aws/services/lambdas/views.py +48 -0
- gui4aws/services/networking/__init__.py +5 -0
- gui4aws/services/networking/actions.py +545 -0
- gui4aws/services/networking/models.py +87 -0
- gui4aws/services/networking/service.py +136 -0
- gui4aws/services/networking/views.py +203 -0
- gui4aws/services/s3/__init__.py +0 -0
- gui4aws/services/s3/actions.py +215 -0
- gui4aws/services/s3/models.py +27 -0
- gui4aws/services/s3/service.py +72 -0
- gui4aws/services/s3/views.py +49 -0
- gui4aws/services/secrets/__init__.py +5 -0
- gui4aws/services/secrets/actions.py +264 -0
- gui4aws/services/secrets/models.py +22 -0
- gui4aws/services/secrets/service.py +73 -0
- gui4aws/services/secrets/views.py +47 -0
- gui4aws/services/service_registry.py +144 -0
- gui4aws/services/ses/__init__.py +0 -0
- gui4aws/services/ses/actions.py +128 -0
- gui4aws/services/ses/models.py +25 -0
- gui4aws/services/ses/service.py +46 -0
- gui4aws/services/ses/views.py +44 -0
- gui4aws/services/sns/__init__.py +0 -0
- gui4aws/services/sns/actions.py +183 -0
- gui4aws/services/sns/models.py +27 -0
- gui4aws/services/sns/service.py +66 -0
- gui4aws/services/sns/views.py +41 -0
- gui4aws/services/sqs/__init__.py +0 -0
- gui4aws/services/sqs/actions.py +243 -0
- gui4aws/services/sqs/models.py +18 -0
- gui4aws/services/sqs/service.py +59 -0
- gui4aws/services/sqs/views.py +40 -0
- gui4aws/services/ssm/__init__.py +5 -0
- gui4aws/services/ssm/actions.py +223 -0
- gui4aws/services/ssm/models.py +20 -0
- gui4aws/services/ssm/service.py +93 -0
- gui4aws/services/ssm/views.py +49 -0
- gui4aws/sql_runner/__init__.py +1 -0
- gui4aws/sql_runner/connection.py +243 -0
- gui4aws/testing/__init__.py +7 -0
- gui4aws/testing/moto_support.py +30 -0
- gui4aws-0.0.1.dist-info/METADATA +94 -0
- gui4aws-0.0.1.dist-info/RECORD +148 -0
- gui4aws-0.0.1.dist-info/WHEEL +4 -0
- gui4aws-0.0.1.dist-info/entry_points.txt +2 -0
- gui4aws-0.0.1.dist-info/licenses/LICENSE +21 -0
gui4aws/__about__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Metadata for gui4aws."""
|
|
2
|
+
|
|
3
|
+
__all__ = [
|
|
4
|
+
"__credits__",
|
|
5
|
+
"__dependencies__",
|
|
6
|
+
"__description__",
|
|
7
|
+
"__keywords__",
|
|
8
|
+
"__license__",
|
|
9
|
+
"__readme__",
|
|
10
|
+
"__requires_python__",
|
|
11
|
+
"__status__",
|
|
12
|
+
"__title__",
|
|
13
|
+
"__version__",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
__title__ = "gui4aws"
|
|
17
|
+
__version__ = "0.0.1"
|
|
18
|
+
__description__ = "Tkinter GUI for AWS"
|
|
19
|
+
__readme__ = "README.md"
|
|
20
|
+
__credits__ = [{"name": "Matthew Martin", "email": "matthewdeanmartin@gmail.com"}]
|
|
21
|
+
__keywords__ = ["gui4aws", "aws", "tkinter", "gui"]
|
|
22
|
+
__license__ = "MIT"
|
|
23
|
+
__requires_python__ = ">=3.10"
|
|
24
|
+
__status__ = "3 - Alpha"
|
|
25
|
+
__dependencies__ = ["boto3>=1.35.0", "keyring>=25.0.0", "tomli>=2.4.1 ; python_full_version < '3.11'"]
|
gui4aws/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""gui4aws: A thoughtful Tkinter GUI for AWS.
|
|
2
|
+
|
|
3
|
+
This package provides a desktop application for browsing and managing AWS resources
|
|
4
|
+
with a focus on safety, transparency, and developer productivity. It supports
|
|
5
|
+
both boto3 and AWS CLI execution modes, and includes built-in support for
|
|
6
|
+
local emulation via Moto and Robotocore.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from gui4aws.__about__ import __version__
|
|
10
|
+
|
|
11
|
+
__all__ = ["__version__"]
|
gui4aws/__main__.py
ADDED
gui4aws/app.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""AppContext: the single source of truth for runtime selections.
|
|
2
|
+
|
|
3
|
+
The GUI binds widget values to fields on AppContext and asks AppContext for the right executor.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from gui4aws.execution.action_cache import ActionCache
|
|
13
|
+
from gui4aws.execution.action_history import ActionHistory
|
|
14
|
+
from gui4aws.execution.aws_cli_executor import AwsCliExecutor
|
|
15
|
+
from gui4aws.execution.boto3_executor import Boto3Executor
|
|
16
|
+
from gui4aws.execution.endpoint_config import EndpointConfig, EndpointMode
|
|
17
|
+
from gui4aws.execution.execution_mode import ExecutionMode
|
|
18
|
+
from gui4aws.services.service_registry import ServiceRegistry, default_registry
|
|
19
|
+
|
|
20
|
+
__all__ = ["AWS_PARTITIONS", "AppContext"]
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
# AWS partitions with their default region for region enumeration.
|
|
25
|
+
# Keys are partition names; values are a (default_region, display_label) tuple.
|
|
26
|
+
AWS_PARTITIONS: dict[str, tuple[str, str]] = {
|
|
27
|
+
"aws": ("us-east-1", "AWS Standard"),
|
|
28
|
+
"aws-us-gov": ("us-gov-west-1", "AWS GovCloud (US)"),
|
|
29
|
+
"aws-cn": ("cn-north-1", "AWS China"),
|
|
30
|
+
"aws-iso": ("us-iso-east-1", "AWS ISO"),
|
|
31
|
+
"aws-iso-b": ("us-isob-east-1", "AWS ISO-B"),
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class AppContext:
|
|
37
|
+
"""Holds profile/region/mode/endpoint plus the action history and registry."""
|
|
38
|
+
|
|
39
|
+
profile_name: str | None = None
|
|
40
|
+
region_name: str = "us-east-1"
|
|
41
|
+
partition: str = "aws"
|
|
42
|
+
mode: ExecutionMode = ExecutionMode.BOTO3
|
|
43
|
+
endpoint_config: EndpointConfig = field(default_factory=EndpointConfig)
|
|
44
|
+
history: ActionHistory = field(default_factory=ActionHistory)
|
|
45
|
+
registry: ServiceRegistry = field(default_factory=default_registry)
|
|
46
|
+
action_cache: ActionCache = field(default_factory=ActionCache)
|
|
47
|
+
|
|
48
|
+
def set_mode(self, mode: ExecutionMode) -> None:
|
|
49
|
+
"""Change execution mode (AWS CLI vs boto3)."""
|
|
50
|
+
logger.info("execution mode -> %s", mode)
|
|
51
|
+
self.mode = mode
|
|
52
|
+
self.action_cache.clear()
|
|
53
|
+
|
|
54
|
+
def set_region(self, region_name: str) -> None:
|
|
55
|
+
"""Change region; resource lists should refresh after this."""
|
|
56
|
+
logger.info("region -> %s", region_name)
|
|
57
|
+
self.region_name = region_name
|
|
58
|
+
self.action_cache.clear()
|
|
59
|
+
|
|
60
|
+
def set_partition(self, partition: str) -> None:
|
|
61
|
+
"""Change AWS partition (aws, aws-us-gov, aws-cn, aws-iso, aws-iso-b)."""
|
|
62
|
+
logger.info("partition -> %s", partition)
|
|
63
|
+
self.partition = partition
|
|
64
|
+
self.action_cache.clear()
|
|
65
|
+
|
|
66
|
+
def set_profile(self, profile_name: str | None) -> None:
|
|
67
|
+
"""Change AWS profile (None means rely on environment)."""
|
|
68
|
+
logger.info("profile -> %s", profile_name)
|
|
69
|
+
self.profile_name = profile_name
|
|
70
|
+
self.action_cache.clear()
|
|
71
|
+
|
|
72
|
+
def set_endpoint(self, mode: EndpointMode, endpoint_url: str | None = None) -> None:
|
|
73
|
+
"""Change endpoint mode + URL."""
|
|
74
|
+
self.endpoint_config = EndpointConfig.for_mode(mode, endpoint_url)
|
|
75
|
+
logger.info("endpoint -> %s url=%s", mode, endpoint_url)
|
|
76
|
+
self.action_cache.clear()
|
|
77
|
+
|
|
78
|
+
def boto3_executor(self) -> Boto3Executor:
|
|
79
|
+
"""Build a fresh Boto3Executor that reflects current selections."""
|
|
80
|
+
return Boto3Executor(
|
|
81
|
+
profile_name=self.profile_name,
|
|
82
|
+
region_name=self.region_name,
|
|
83
|
+
endpoint_config=self.endpoint_config,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def aws_cli_executor(self) -> AwsCliExecutor:
|
|
87
|
+
"""Build a fresh AwsCliExecutor that reflects current selections."""
|
|
88
|
+
return AwsCliExecutor(
|
|
89
|
+
profile_name=self.profile_name,
|
|
90
|
+
region_name=self.region_name,
|
|
91
|
+
endpoint_config=self.endpoint_config,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def execute(self, action: Any, inputs: dict[str, str]) -> Any:
|
|
95
|
+
"""Dispatch to whichever executor matches the current mode."""
|
|
96
|
+
cache_key = self.action_cache.build_key(
|
|
97
|
+
action,
|
|
98
|
+
inputs,
|
|
99
|
+
mode=self.mode,
|
|
100
|
+
profile_name=self.profile_name,
|
|
101
|
+
region_name=self.region_name,
|
|
102
|
+
endpoint_config=self.endpoint_config,
|
|
103
|
+
)
|
|
104
|
+
cached = self.action_cache.get(cache_key)
|
|
105
|
+
if cached is not None:
|
|
106
|
+
return cached
|
|
107
|
+
result: Any
|
|
108
|
+
if self.mode is ExecutionMode.BOTO3:
|
|
109
|
+
result = self.boto3_executor().execute(action, inputs)
|
|
110
|
+
else:
|
|
111
|
+
result = self.aws_cli_executor().execute(action, inputs)
|
|
112
|
+
if self.action_cache.should_cache(action, result):
|
|
113
|
+
self.action_cache.put(cache_key, result)
|
|
114
|
+
return result
|
|
115
|
+
|
|
116
|
+
def invalidate_read_cache(self, service_id: str | None = None) -> None:
|
|
117
|
+
"""Drop cached read-only results globally or for one service."""
|
|
118
|
+
if service_id is None:
|
|
119
|
+
self.action_cache.clear()
|
|
120
|
+
return
|
|
121
|
+
self.action_cache.invalidate_service(service_id)
|
gui4aws/cli.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""Command-line entry point for gui4aws."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import logging
|
|
7
|
+
import shutil
|
|
8
|
+
import sys
|
|
9
|
+
from collections.abc import Sequence
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import boto3
|
|
13
|
+
import botocore
|
|
14
|
+
|
|
15
|
+
from gui4aws.__about__ import __version__
|
|
16
|
+
from gui4aws.app import AppContext
|
|
17
|
+
from gui4aws.execution.endpoint_config import EndpointConfig, EndpointMode
|
|
18
|
+
from gui4aws.execution.execution_mode import ExecutionMode
|
|
19
|
+
from gui4aws.logging_config import configure_logging
|
|
20
|
+
|
|
21
|
+
__all__ = ["build_parser", "main"]
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
27
|
+
"""Construct the argparse parser."""
|
|
28
|
+
parser = argparse.ArgumentParser(prog="gui4aws", description="Tkinter GUI for AWS")
|
|
29
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
30
|
+
parser.add_argument("--log-level", default="INFO", help="DEBUG, INFO, WARNING, ERROR")
|
|
31
|
+
parser.add_argument("--log-file", type=Path, default=None, help="optional file to also log to")
|
|
32
|
+
|
|
33
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
34
|
+
|
|
35
|
+
gui_parser = subparsers.add_parser("gui", help="launch the GUI")
|
|
36
|
+
add_common_runtime_options(gui_parser)
|
|
37
|
+
|
|
38
|
+
doctor_parser = subparsers.add_parser("doctor", help="environment diagnostics")
|
|
39
|
+
doctor_parser.add_argument("--check-aws-cli", action="store_true", help="report aws CLI presence")
|
|
40
|
+
doctor_parser.add_argument("--check-boto3", action="store_true", help="report boto3 version")
|
|
41
|
+
doctor_parser.add_argument("--check-docker", action="store_true", help="report docker presence")
|
|
42
|
+
|
|
43
|
+
subparsers.add_parser("list-profiles", help="list available AWS profiles")
|
|
44
|
+
list_regions_parser = subparsers.add_parser("list-regions", help="list known AWS regions")
|
|
45
|
+
from gui4aws.app import AWS_PARTITIONS
|
|
46
|
+
|
|
47
|
+
list_regions_parser.add_argument(
|
|
48
|
+
"--partition",
|
|
49
|
+
default="aws",
|
|
50
|
+
choices=list(AWS_PARTITIONS.keys()),
|
|
51
|
+
help="AWS partition to list regions for (default: aws)",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
return parser
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def add_common_runtime_options(parser: argparse.ArgumentParser) -> None:
|
|
58
|
+
"""Add the runtime-selection options shared by ``gui`` (and future subcommands)."""
|
|
59
|
+
from gui4aws.app import AWS_PARTITIONS
|
|
60
|
+
|
|
61
|
+
parser.add_argument("--profile", default=None, help="AWS profile name")
|
|
62
|
+
parser.add_argument("--region", default="us-east-1", help="AWS region")
|
|
63
|
+
parser.add_argument(
|
|
64
|
+
"--partition",
|
|
65
|
+
default="aws",
|
|
66
|
+
choices=list(AWS_PARTITIONS.keys()),
|
|
67
|
+
help="AWS partition (aws, aws-us-gov, aws-cn, aws-iso, aws-iso-b)",
|
|
68
|
+
)
|
|
69
|
+
parser.add_argument(
|
|
70
|
+
"--mode",
|
|
71
|
+
default=ExecutionMode.BOTO3.value,
|
|
72
|
+
choices=[m.value for m in ExecutionMode],
|
|
73
|
+
help="execution mode",
|
|
74
|
+
)
|
|
75
|
+
parser.add_argument(
|
|
76
|
+
"--endpoint-mode",
|
|
77
|
+
default=EndpointMode.AWS.value,
|
|
78
|
+
choices=[m.value for m in EndpointMode],
|
|
79
|
+
help="endpoint mode",
|
|
80
|
+
)
|
|
81
|
+
parser.add_argument("--endpoint-url", default=None, help="endpoint URL (required for --endpoint-mode custom)")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
85
|
+
"""Entry point for the ``gui4aws`` command."""
|
|
86
|
+
parser = build_parser()
|
|
87
|
+
args = parser.parse_args(argv)
|
|
88
|
+
configure_logging(args.log_level, args.log_file)
|
|
89
|
+
|
|
90
|
+
if args.command in (None, "gui"):
|
|
91
|
+
return run_gui(args)
|
|
92
|
+
if args.command == "doctor":
|
|
93
|
+
return run_doctor(args)
|
|
94
|
+
if args.command == "list-profiles":
|
|
95
|
+
return run_list_profiles()
|
|
96
|
+
if args.command == "list-regions":
|
|
97
|
+
return run_list_regions(args)
|
|
98
|
+
parser.error(f"unknown command: {args.command}")
|
|
99
|
+
return 2
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def run_gui(args: argparse.Namespace) -> int:
|
|
103
|
+
"""Launch the GUI window."""
|
|
104
|
+
from gui4aws.config import load_config
|
|
105
|
+
from gui4aws.gui.main_window import create_main_window
|
|
106
|
+
|
|
107
|
+
saved = load_config()
|
|
108
|
+
|
|
109
|
+
# CLI args take precedence over saved config; config takes precedence over defaults.
|
|
110
|
+
cli_profile = getattr(args, "profile", None)
|
|
111
|
+
cli_region = getattr(args, "region", None)
|
|
112
|
+
cli_partition = getattr(args, "partition", None)
|
|
113
|
+
|
|
114
|
+
profile = cli_profile or (saved.default_profile or None)
|
|
115
|
+
partition = cli_partition or saved.default_partition or "aws"
|
|
116
|
+
region = cli_region or saved.default_region or "us-east-1"
|
|
117
|
+
|
|
118
|
+
context = AppContext(
|
|
119
|
+
profile_name=profile,
|
|
120
|
+
region_name=region,
|
|
121
|
+
partition=partition,
|
|
122
|
+
mode=ExecutionMode(getattr(args, "mode", saved.default_mode or ExecutionMode.BOTO3.value)),
|
|
123
|
+
endpoint_config=EndpointConfig.for_mode(
|
|
124
|
+
EndpointMode(getattr(args, "endpoint_mode", saved.default_endpoint_mode or EndpointMode.AWS.value)),
|
|
125
|
+
getattr(args, "endpoint_url", None),
|
|
126
|
+
),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
profiles = available_profiles()
|
|
130
|
+
regions = available_regions(partition=partition)
|
|
131
|
+
window = create_main_window(context, profiles=profiles, regions=regions)
|
|
132
|
+
window.run()
|
|
133
|
+
return 0
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def run_doctor(args: argparse.Namespace) -> int:
|
|
137
|
+
"""Print environment diagnostics."""
|
|
138
|
+
print(f"gui4aws version: {__version__}")
|
|
139
|
+
print(f"python: {sys.version.split()[0]}")
|
|
140
|
+
if args.check_boto3 or not any([args.check_aws_cli, args.check_docker]):
|
|
141
|
+
print(f"boto3: {boto3.__version__}")
|
|
142
|
+
print(f"botocore: {botocore.__version__}")
|
|
143
|
+
if args.check_aws_cli or not any([args.check_boto3, args.check_docker]):
|
|
144
|
+
aws_path = shutil.which("aws")
|
|
145
|
+
print(f"aws CLI: {aws_path or 'not found on PATH'}")
|
|
146
|
+
if args.check_docker:
|
|
147
|
+
docker_path = shutil.which("docker")
|
|
148
|
+
print(f"docker: {docker_path or 'not found on PATH'}")
|
|
149
|
+
try:
|
|
150
|
+
import moto # pylint: disable=unused-import # noqa: F401
|
|
151
|
+
|
|
152
|
+
print("moto: importable")
|
|
153
|
+
except ImportError:
|
|
154
|
+
print("moto: not importable (install dev deps to use moto-backed tests)")
|
|
155
|
+
profiles = available_profiles()
|
|
156
|
+
print(f"profiles: {', '.join(profiles) if profiles else '(none configured)'}")
|
|
157
|
+
return 0
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def run_list_profiles() -> int:
|
|
161
|
+
"""Print available AWS profiles, one per line."""
|
|
162
|
+
for profile in available_profiles():
|
|
163
|
+
print(profile)
|
|
164
|
+
return 0
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def run_list_regions(args: argparse.Namespace | None = None) -> int:
|
|
168
|
+
"""Print known AWS regions, one per line."""
|
|
169
|
+
partition = getattr(args, "partition", "aws") if args else "aws"
|
|
170
|
+
for region in available_regions(partition=partition):
|
|
171
|
+
print(region)
|
|
172
|
+
return 0
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def available_profiles() -> list[str]:
|
|
176
|
+
"""Best-effort profile list from boto3's session."""
|
|
177
|
+
try:
|
|
178
|
+
session = boto3.Session()
|
|
179
|
+
return list(session.available_profiles)
|
|
180
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
181
|
+
return []
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def available_regions(service: str = "ec2", partition: str = "aws") -> list[str]:
|
|
185
|
+
"""Known regions for ``service`` in ``partition`` (EC2 is the most-supported default)."""
|
|
186
|
+
try:
|
|
187
|
+
session = boto3.Session()
|
|
188
|
+
return sorted(session.get_available_regions(service, partition_name=partition))
|
|
189
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
190
|
+
return []
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
if __name__ == "__main__":
|
|
194
|
+
raise SystemExit(main())
|
gui4aws/config.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""User configuration: load/save preferences in a per-OS config dir.
|
|
2
|
+
|
|
3
|
+
We hand-roll a minimal TOML writer to stay stdlib-only on the write path. Reading uses
|
|
4
|
+
``tomllib`` (Python 3.11+). For Python 3.10 we fall back to JSON.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
__all__ = ["AppConfig", "config_path", "load_config", "save_config"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class AppConfig:
|
|
21
|
+
"""User-visible app config."""
|
|
22
|
+
|
|
23
|
+
default_profile: str = ""
|
|
24
|
+
default_region: str = "us-east-1"
|
|
25
|
+
default_partition: str = "aws"
|
|
26
|
+
default_mode: str = "boto3"
|
|
27
|
+
default_endpoint_mode: str = "aws"
|
|
28
|
+
window_width: int = 1400
|
|
29
|
+
window_height: int = 900
|
|
30
|
+
history_enabled: bool = True
|
|
31
|
+
history_max_entries: int = 500
|
|
32
|
+
local_endpoints: dict[str, str] = field(default_factory=dict)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def config_path() -> Path:
|
|
36
|
+
"""Return the OS-appropriate config file path."""
|
|
37
|
+
if sys.platform.startswith("win"):
|
|
38
|
+
base = Path(os.environ.get("APPDATA", str(Path.home() / "AppData" / "Roaming")))
|
|
39
|
+
return base / "gui4aws" / "config.toml"
|
|
40
|
+
if sys.platform == "darwin":
|
|
41
|
+
return Path.home() / "Library" / "Application Support" / "gui4aws" / "config.toml"
|
|
42
|
+
xdg = os.environ.get("XDG_CONFIG_HOME")
|
|
43
|
+
base = Path(xdg) if xdg else Path.home() / ".config"
|
|
44
|
+
return base / "gui4aws" / "config.toml"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def load_config(path: Path | None = None) -> AppConfig:
|
|
48
|
+
"""Load config from disk, returning defaults if no file exists."""
|
|
49
|
+
if path is None:
|
|
50
|
+
path = config_path()
|
|
51
|
+
if not path.exists():
|
|
52
|
+
return AppConfig()
|
|
53
|
+
raw = path.read_text(encoding="utf-8")
|
|
54
|
+
data: dict[str, Any]
|
|
55
|
+
if path.suffix == ".json":
|
|
56
|
+
data = json.loads(raw)
|
|
57
|
+
else:
|
|
58
|
+
try:
|
|
59
|
+
if sys.version_info >= (3, 11):
|
|
60
|
+
import tomllib
|
|
61
|
+
else:
|
|
62
|
+
import tomli as tomllib
|
|
63
|
+
|
|
64
|
+
data = tomllib.loads(raw)
|
|
65
|
+
except ImportError: # pragma: no cover
|
|
66
|
+
# Fallback if neither is available (should not happen with our deps)
|
|
67
|
+
data = json.loads(raw)
|
|
68
|
+
return apply(data)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def save_config(config: AppConfig, path: Path | None = None) -> None:
|
|
72
|
+
"""Save config to disk in TOML."""
|
|
73
|
+
if path is None:
|
|
74
|
+
path = config_path()
|
|
75
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
76
|
+
path.write_text(render_toml(config), encoding="utf-8")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def apply(data: dict[str, Any]) -> AppConfig:
|
|
80
|
+
"""Map a raw dictionary (from TOML/JSON) onto an AppConfig instance."""
|
|
81
|
+
config = AppConfig()
|
|
82
|
+
for key in (
|
|
83
|
+
"default_profile",
|
|
84
|
+
"default_region",
|
|
85
|
+
"default_partition",
|
|
86
|
+
"default_mode",
|
|
87
|
+
"default_endpoint_mode",
|
|
88
|
+
):
|
|
89
|
+
if key in data:
|
|
90
|
+
setattr(config, key, str(data[key]))
|
|
91
|
+
window = data.get("window", {})
|
|
92
|
+
if isinstance(window, dict):
|
|
93
|
+
if "width" in window:
|
|
94
|
+
config.window_width = int(window["width"])
|
|
95
|
+
if "height" in window:
|
|
96
|
+
config.window_height = int(window["height"])
|
|
97
|
+
history = data.get("history", {})
|
|
98
|
+
if isinstance(history, dict):
|
|
99
|
+
if "enabled" in history:
|
|
100
|
+
config.history_enabled = bool(history["enabled"])
|
|
101
|
+
if "max_entries" in history:
|
|
102
|
+
config.history_max_entries = int(history["max_entries"])
|
|
103
|
+
endpoints = data.get("local_endpoints", {})
|
|
104
|
+
if isinstance(endpoints, dict):
|
|
105
|
+
for service, value in endpoints.items():
|
|
106
|
+
if isinstance(value, dict) and "endpoint_url" in value:
|
|
107
|
+
config.local_endpoints[str(service)] = str(value["endpoint_url"])
|
|
108
|
+
elif isinstance(value, str):
|
|
109
|
+
config.local_endpoints[str(service)] = value
|
|
110
|
+
return config
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def render_toml(config: AppConfig) -> str:
|
|
114
|
+
"""Serialize AppConfig to a TOML string."""
|
|
115
|
+
lines = [
|
|
116
|
+
f'default_profile = "{config.default_profile}"',
|
|
117
|
+
f'default_region = "{config.default_region}"',
|
|
118
|
+
f'default_partition = "{config.default_partition}"',
|
|
119
|
+
f'default_mode = "{config.default_mode}"',
|
|
120
|
+
f'default_endpoint_mode = "{config.default_endpoint_mode}"',
|
|
121
|
+
"",
|
|
122
|
+
"[window]",
|
|
123
|
+
f"width = {config.window_width}",
|
|
124
|
+
f"height = {config.window_height}",
|
|
125
|
+
"",
|
|
126
|
+
"[history]",
|
|
127
|
+
f"enabled = {'true' if config.history_enabled else 'false'}",
|
|
128
|
+
f"max_entries = {config.history_max_entries}",
|
|
129
|
+
]
|
|
130
|
+
for service, url in sorted(config.local_endpoints.items()):
|
|
131
|
+
lines.extend(
|
|
132
|
+
[
|
|
133
|
+
"",
|
|
134
|
+
f"[local_endpoints.{service}]",
|
|
135
|
+
f'endpoint_url = "{url}"',
|
|
136
|
+
]
|
|
137
|
+
)
|
|
138
|
+
lines.append("")
|
|
139
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Seed demo AWS resources so the GUI has something to browse immediately.
|
|
2
|
+
|
|
3
|
+
All resources are tagged with ``gui4aws:demo = true`` and given descriptive names so
|
|
4
|
+
users can immediately distinguish demo data from real infrastructure.
|
|
5
|
+
|
|
6
|
+
This package only writes resources; it never deletes. Call :func:`seed_demo_resources`
|
|
7
|
+
with a boto3 session (or endpoint_url for moto server mode) to create the assets.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from gui4aws.demo_resources.aurora import seed_aurora
|
|
16
|
+
from gui4aws.demo_resources.backup import seed_backup
|
|
17
|
+
from gui4aws.demo_resources.compute import seed_lambda
|
|
18
|
+
from gui4aws.demo_resources.ecs import seed_ecs
|
|
19
|
+
from gui4aws.demo_resources.iam_resources import seed_iam_extras
|
|
20
|
+
from gui4aws.demo_resources.kms import seed_kms
|
|
21
|
+
from gui4aws.demo_resources.messaging import seed_ses, seed_sns
|
|
22
|
+
from gui4aws.demo_resources.monitoring import seed_cloudformation, seed_cloudwatch
|
|
23
|
+
from gui4aws.demo_resources.networking import seed_networking
|
|
24
|
+
from gui4aws.demo_resources.secrets import seed_secrets, seed_ssm
|
|
25
|
+
from gui4aws.demo_resources.storage import seed_s3, seed_sqs
|
|
26
|
+
|
|
27
|
+
__all__ = ["seed_demo_resources"]
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def seed_demo_resources(
|
|
33
|
+
*,
|
|
34
|
+
region_name: str = "us-east-1",
|
|
35
|
+
endpoint_url: str | None = None,
|
|
36
|
+
profile_name: str | None = None,
|
|
37
|
+
is_robotocore: bool = False,
|
|
38
|
+
) -> dict[str, list[str]]:
|
|
39
|
+
"""Create demo resources and return a report of what was created.
|
|
40
|
+
|
|
41
|
+
``is_robotocore`` controls whether richer demo data (backup jobs, restore jobs,
|
|
42
|
+
ECS services with tasks, etc.) is seeded. Robotocore has broader API coverage
|
|
43
|
+
than Moto for these resource types.
|
|
44
|
+
|
|
45
|
+
Returns a dict mapping resource type to list of identifiers.
|
|
46
|
+
"""
|
|
47
|
+
import boto3
|
|
48
|
+
|
|
49
|
+
session: Any
|
|
50
|
+
if profile_name:
|
|
51
|
+
session = boto3.Session(profile_name=profile_name, region_name=region_name)
|
|
52
|
+
else:
|
|
53
|
+
session = boto3.Session(region_name=region_name)
|
|
54
|
+
|
|
55
|
+
def client(service: str) -> Any:
|
|
56
|
+
if endpoint_url:
|
|
57
|
+
return session.client(service, endpoint_url=endpoint_url)
|
|
58
|
+
return session.client(service)
|
|
59
|
+
|
|
60
|
+
created: dict[str, list[str]] = {}
|
|
61
|
+
|
|
62
|
+
rds_client = client("rds")
|
|
63
|
+
created.update(seed_aurora(rds_client))
|
|
64
|
+
created.update(seed_backup(client("backup"), rds_client, extended=is_robotocore))
|
|
65
|
+
created.update(seed_networking(client("ec2"), client("elbv2")))
|
|
66
|
+
created.update(seed_ecs(client("ecs"), extended=is_robotocore))
|
|
67
|
+
created.update(seed_secrets(client("secretsmanager")))
|
|
68
|
+
created.update(seed_ssm(client("ssm")))
|
|
69
|
+
created.update(seed_kms(client("kms")))
|
|
70
|
+
created.update(seed_s3(client("s3")))
|
|
71
|
+
created.update(seed_sqs(client("sqs")))
|
|
72
|
+
created.update(seed_lambda(client("lambda"), client("iam")))
|
|
73
|
+
created.update(seed_cloudwatch(client("cloudwatch"), client("logs")))
|
|
74
|
+
created.update(seed_cloudformation(client("cloudformation")))
|
|
75
|
+
created.update(seed_sns(client("sns")))
|
|
76
|
+
created.update(seed_ses(client("ses")))
|
|
77
|
+
created.update(seed_iam_extras(client("iam")))
|
|
78
|
+
|
|
79
|
+
return created
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Shared constants and helpers used across demo resource seeders."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
DEMO_TAG = {"Key": "gui4aws:demo", "Value": "true"}
|
|
6
|
+
DEMO_DESC_TAG_KEY = "Description"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def tags(*extra: dict[str, str]) -> list[dict[str, str]]:
|
|
10
|
+
"""Return a tag list with the standard demo tag plus any extras."""
|
|
11
|
+
return [DEMO_TAG, *extra]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Seed demo Aurora clusters, instances, and snapshots."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from gui4aws.demo_resources._common import DEMO_DESC_TAG_KEY, tags
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def seed_aurora(rds: Any) -> dict[str, list[str]]:
|
|
14
|
+
"""Seed demo Aurora clusters, instances, and snapshots.
|
|
15
|
+
|
|
16
|
+
Creates both MySQL and PostgreSQL clusters with associated instances and a sample
|
|
17
|
+
snapshot to demonstrate RDS resource browsing and management in the GUI.
|
|
18
|
+
"""
|
|
19
|
+
created: dict[str, list[str]] = {"aurora_clusters": [], "aurora_instances": [], "aurora_snapshots": []}
|
|
20
|
+
|
|
21
|
+
clusters = [
|
|
22
|
+
{
|
|
23
|
+
"DBClusterIdentifier": "demo-aurora-mysql-prod",
|
|
24
|
+
"Engine": "aurora-mysql",
|
|
25
|
+
"MasterUsername": "admin",
|
|
26
|
+
"MasterUserPassword": "DemoPass123!",
|
|
27
|
+
"Tags": tags(
|
|
28
|
+
{"Key": "Name", "Value": "demo-aurora-mysql-prod"},
|
|
29
|
+
{"Key": DEMO_DESC_TAG_KEY, "Value": "Demo Aurora MySQL cluster (production-like)"},
|
|
30
|
+
),
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"DBClusterIdentifier": "demo-aurora-pg-analytics",
|
|
34
|
+
"Engine": "aurora-postgresql",
|
|
35
|
+
"MasterUsername": "postgres",
|
|
36
|
+
"MasterUserPassword": "DemoPass123!",
|
|
37
|
+
"Tags": tags(
|
|
38
|
+
{"Key": "Name", "Value": "demo-aurora-pg-analytics"},
|
|
39
|
+
{"Key": DEMO_DESC_TAG_KEY, "Value": "Demo Aurora PostgreSQL cluster (analytics)"},
|
|
40
|
+
),
|
|
41
|
+
},
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
for spec in clusters:
|
|
45
|
+
cluster_id = str(spec["DBClusterIdentifier"])
|
|
46
|
+
engine = str(spec["Engine"])
|
|
47
|
+
try:
|
|
48
|
+
rds.create_db_cluster(**spec)
|
|
49
|
+
logger.info("created Aurora cluster %s", cluster_id)
|
|
50
|
+
created["aurora_clusters"].append(cluster_id)
|
|
51
|
+
except Exception as exc: # pylint: disable=broad-exception-caught
|
|
52
|
+
logger.warning("skipped cluster %s: %s", cluster_id, exc)
|
|
53
|
+
continue
|
|
54
|
+
|
|
55
|
+
instance_id = f"{cluster_id}-instance-1"
|
|
56
|
+
try:
|
|
57
|
+
rds.create_db_instance(
|
|
58
|
+
DBInstanceIdentifier=instance_id,
|
|
59
|
+
DBInstanceClass="db.t3.medium",
|
|
60
|
+
Engine=engine,
|
|
61
|
+
DBClusterIdentifier=cluster_id,
|
|
62
|
+
Tags=tags(
|
|
63
|
+
{"Key": "Name", "Value": instance_id},
|
|
64
|
+
{"Key": DEMO_DESC_TAG_KEY, "Value": f"Demo instance for {cluster_id}"},
|
|
65
|
+
),
|
|
66
|
+
)
|
|
67
|
+
logger.info("created Aurora instance %s", instance_id)
|
|
68
|
+
created["aurora_instances"].append(instance_id)
|
|
69
|
+
except Exception as exc: # pylint: disable=broad-exception-caught
|
|
70
|
+
logger.warning("skipped instance %s: %s", instance_id, exc)
|
|
71
|
+
|
|
72
|
+
if created["aurora_clusters"]:
|
|
73
|
+
source = created["aurora_clusters"][0]
|
|
74
|
+
snap_id = f"{source}-demo-snapshot"
|
|
75
|
+
try:
|
|
76
|
+
rds.create_db_cluster_snapshot(
|
|
77
|
+
DBClusterIdentifier=source,
|
|
78
|
+
DBClusterSnapshotIdentifier=snap_id,
|
|
79
|
+
Tags=tags(
|
|
80
|
+
{"Key": "Name", "Value": snap_id},
|
|
81
|
+
{"Key": DEMO_DESC_TAG_KEY, "Value": "Demo snapshot — safe to restore"},
|
|
82
|
+
),
|
|
83
|
+
)
|
|
84
|
+
logger.info("created Aurora snapshot %s", snap_id)
|
|
85
|
+
created["aurora_snapshots"].append(snap_id)
|
|
86
|
+
except Exception as exc: # pylint: disable=broad-exception-caught
|
|
87
|
+
logger.warning("skipped snapshot %s: %s", snap_id, exc)
|
|
88
|
+
|
|
89
|
+
return created
|