ndev-stack 0.3.9__tar.gz → 0.3.10__tar.gz
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.
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/PKG-INFO +1 -1
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/__init__.py +1 -1
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/linux/runtime/nginx_helper.py +70 -21
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/linux/tui.py +126 -24
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev_stack.egg-info/PKG-INFO +1 -1
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/pyproject.toml +1 -1
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/tests/test_opensuse_compat.py +22 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/README.md +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/__main__.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/cli.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/common/__init__.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/common/config.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/common/constants.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/common/distro.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/common/github.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/common/logger.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/common/manifest.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/common/modules/__init__.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/common/modules/base.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/common/modules/builtin/mailpit/manifest.json +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/common/modules/builtin/mailpit/module.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/common/modules/builtin/mongodb/manifest.json +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/common/modules/builtin/mongodb/module.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/common/modules/builtin/postgres/manifest.json +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/common/modules/builtin/postgres/module.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/common/modules/builtin/redis/manifest.json +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/common/modules/builtin/redis/module.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/common/modules/manager.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/common/utils.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/linux/__init__.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/linux/chroot/manager.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/linux/chroot/packages.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/linux/chroot/shell.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/linux/cli.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/linux/commands/available.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/linux/commands/clean.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/linux/commands/ctl.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/linux/commands/current.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/linux/commands/db.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/linux/commands/doctor.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/linux/commands/grok.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/linux/commands/install.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/linux/commands/list.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/linux/commands/logs.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/linux/commands/mailpit.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/linux/commands/modules.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/linux/commands/mongodb.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/linux/commands/postgres.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/linux/commands/reload.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/linux/commands/restart.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/linux/commands/setup.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/linux/commands/start.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/linux/commands/status.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/linux/commands/stop.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/linux/commands/uninstall.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/linux/commands/update.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/linux/commands/upgrade.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/linux/commands/use.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/linux/commands/vhost.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/linux/php/builder.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/linux/php/downloader.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/linux/php/extensions.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/linux/php/installer.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/linux/php/resolver.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/linux/php/templates.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/linux/runtime/fpm.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/linux/runtime/mailpit.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/linux/runtime/pma.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/linux/runtime/process.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/linux/runtime/services.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/linux/runtime/sockets.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/linux/runtime/upgrade.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/main.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/tui.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/win/__init__.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/win/bin/shim.exe +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/win/cli.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/win/commands/__init__.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/win/core/__init__.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/win/core/db.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/win/core/elevate.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/win/core/ext.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/win/core/fcgi.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/win/core/grok.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/win/core/logs.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/win/core/mailpit.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/win/core/mkcert.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/win/core/paths.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/win/core/php.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/win/core/pma.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/win/core/redis_core.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/win/core/services.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/win/core/setup.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/win/core/shim.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/win/core/upgrade.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/win/core/vhost.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/win/templates/vhost.conf.tmpl +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/win/templates/vhost_ssl.conf.tmpl +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev/win/tui.py +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev_stack.egg-info/SOURCES.txt +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev_stack.egg-info/dependency_links.txt +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev_stack.egg-info/entry_points.txt +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev_stack.egg-info/requires.txt +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/ndev_stack.egg-info/top_level.txt +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/setup.cfg +0 -0
- {ndev_stack-0.3.9 → ndev_stack-0.3.10}/tests/test_shim.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ndev-stack
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.10
|
|
4
4
|
Summary: Cross-platform developer stack manager for PHP, FastCGI/PHP-FPM, MariaDB, Redis, PostgreSQL, MongoDB, Nginx, SSL, and Mailpit on Linux and Windows
|
|
5
5
|
Author-email: Ankit Karan <ankit.karan99@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/ankitkaran99/ndev
|
|
@@ -32,7 +32,41 @@ def is_ipv6_supported() -> bool:
|
|
|
32
32
|
return False
|
|
33
33
|
|
|
34
34
|
|
|
35
|
-
def
|
|
35
|
+
def validate_sudo_password(password: str) -> bool:
|
|
36
|
+
"""
|
|
37
|
+
Validate sudo password using 'sudo -v -S' and refresh sudo credential timestamp.
|
|
38
|
+
Returns True if password is valid, False otherwise.
|
|
39
|
+
"""
|
|
40
|
+
if not password:
|
|
41
|
+
return False
|
|
42
|
+
try:
|
|
43
|
+
proc = subprocess.run(
|
|
44
|
+
["sudo", "-v", "-S"],
|
|
45
|
+
input=f"{password}\n",
|
|
46
|
+
text=True,
|
|
47
|
+
capture_output=True,
|
|
48
|
+
timeout=5
|
|
49
|
+
)
|
|
50
|
+
return proc.returncode == 0
|
|
51
|
+
except Exception:
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def check_sudo_needs_password() -> bool:
|
|
56
|
+
"""
|
|
57
|
+
Check if sudo commands currently require a password prompt.
|
|
58
|
+
Returns True if sudo requires password, False if root or timestamp is active.
|
|
59
|
+
"""
|
|
60
|
+
if os.geteuid() == 0:
|
|
61
|
+
return False
|
|
62
|
+
try:
|
|
63
|
+
res = subprocess.run(["sudo", "-n", "true"], capture_output=True, timeout=3)
|
|
64
|
+
return res.returncode != 0
|
|
65
|
+
except Exception:
|
|
66
|
+
return True
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def write_privileged_file(target: Path, content: str, password: Optional[str] = None) -> None:
|
|
36
70
|
"""
|
|
37
71
|
Write content to a file, using sudo tee if current process lacks permissions.
|
|
38
72
|
Ensures parent directories exist.
|
|
@@ -50,10 +84,14 @@ def write_privileged_file(target: Path, content: str) -> None:
|
|
|
50
84
|
except (PermissionError, OSError):
|
|
51
85
|
pass
|
|
52
86
|
|
|
53
|
-
#
|
|
54
|
-
|
|
87
|
+
# If password supplied, ensure sudo credential timestamp is active
|
|
88
|
+
if password:
|
|
89
|
+
validate_sudo_password(password)
|
|
90
|
+
|
|
91
|
+
# Fallback to non-interactive sudo
|
|
92
|
+
subprocess.run(["sudo", "-n", "mkdir", "-p", str(target.parent)], check=True)
|
|
55
93
|
subprocess.run(
|
|
56
|
-
["sudo", "tee", str(target)],
|
|
94
|
+
["sudo", "-n", "tee", str(target)],
|
|
57
95
|
input=content,
|
|
58
96
|
text=True,
|
|
59
97
|
capture_output=True,
|
|
@@ -61,7 +99,7 @@ def write_privileged_file(target: Path, content: str) -> None:
|
|
|
61
99
|
)
|
|
62
100
|
|
|
63
101
|
|
|
64
|
-
def symlink_privileged(target: Path, link: Path) -> None:
|
|
102
|
+
def symlink_privileged(target: Path, link: Path, password: Optional[str] = None) -> None:
|
|
65
103
|
"""
|
|
66
104
|
Create a symbolic link, using sudo if current process lacks permissions.
|
|
67
105
|
Ensures parent directory exists and removes any existing destination link/file.
|
|
@@ -81,11 +119,14 @@ def symlink_privileged(target: Path, link: Path) -> None:
|
|
|
81
119
|
except (PermissionError, OSError):
|
|
82
120
|
pass
|
|
83
121
|
|
|
84
|
-
|
|
85
|
-
|
|
122
|
+
if password:
|
|
123
|
+
validate_sudo_password(password)
|
|
86
124
|
|
|
125
|
+
subprocess.run(["sudo", "-n", "mkdir", "-p", str(link.parent)], check=True)
|
|
126
|
+
subprocess.run(["sudo", "-n", "ln", "-sf", str(target_resolved), str(link)], check=True)
|
|
87
127
|
|
|
88
|
-
|
|
128
|
+
|
|
129
|
+
def unlink_privileged(path: Path, password: Optional[str] = None) -> None:
|
|
89
130
|
"""
|
|
90
131
|
Unlink a file or symlink, falling back to sudo if needed.
|
|
91
132
|
"""
|
|
@@ -102,10 +143,13 @@ def unlink_privileged(path: Path) -> None:
|
|
|
102
143
|
except (PermissionError, OSError):
|
|
103
144
|
pass
|
|
104
145
|
|
|
105
|
-
|
|
146
|
+
if password:
|
|
147
|
+
validate_sudo_password(password)
|
|
148
|
+
|
|
149
|
+
subprocess.run(["sudo", "-n", "rm", "-f", str(path)], check=True)
|
|
106
150
|
|
|
107
151
|
|
|
108
|
-
def add_to_hosts_privileged(domain: str, ip: str = "127.0.0.1") -> None:
|
|
152
|
+
def add_to_hosts_privileged(domain: str, ip: str = "127.0.0.1", password: Optional[str] = None) -> None:
|
|
109
153
|
"""
|
|
110
154
|
Safely append a domain mapping to /etc/hosts if not already present.
|
|
111
155
|
"""
|
|
@@ -139,9 +183,12 @@ def add_to_hosts_privileged(domain: str, ip: str = "127.0.0.1") -> None:
|
|
|
139
183
|
except (PermissionError, OSError):
|
|
140
184
|
pass
|
|
141
185
|
|
|
186
|
+
if password:
|
|
187
|
+
validate_sudo_password(password)
|
|
188
|
+
|
|
142
189
|
try:
|
|
143
190
|
subprocess.run(
|
|
144
|
-
["sudo", "tee", "-a", str(hosts_file)],
|
|
191
|
+
["sudo", "-n", "tee", "-a", str(hosts_file)],
|
|
145
192
|
input=line_to_add,
|
|
146
193
|
text=True,
|
|
147
194
|
capture_output=True,
|
|
@@ -151,7 +198,7 @@ def add_to_hosts_privileged(domain: str, ip: str = "127.0.0.1") -> None:
|
|
|
151
198
|
logger.warning(f"Could not update /etc/hosts via sudo: {e}")
|
|
152
199
|
|
|
153
200
|
|
|
154
|
-
def remove_from_hosts_privileged(domain: str) -> None:
|
|
201
|
+
def remove_from_hosts_privileged(domain: str, password: Optional[str] = None) -> None:
|
|
155
202
|
"""
|
|
156
203
|
Remove domain entry from /etc/hosts safely.
|
|
157
204
|
"""
|
|
@@ -177,7 +224,7 @@ def remove_from_hosts_privileged(domain: str) -> None:
|
|
|
177
224
|
|
|
178
225
|
if modified:
|
|
179
226
|
new_content = "\n".join(new_lines) + "\n"
|
|
180
|
-
write_privileged_file(hosts_file, new_content)
|
|
227
|
+
write_privileged_file(hosts_file, new_content, password=password)
|
|
181
228
|
except Exception as e:
|
|
182
229
|
logger.warning(f"Could not remove {domain} from /etc/hosts: {e}")
|
|
183
230
|
|
|
@@ -223,7 +270,7 @@ def get_nginx_vhost_dirs() -> Tuple[Path, Optional[Path]]:
|
|
|
223
270
|
return fallback, None
|
|
224
271
|
|
|
225
272
|
|
|
226
|
-
def ensure_nginx_sites_enabled_included() -> None:
|
|
273
|
+
def ensure_nginx_sites_enabled_included(password: Optional[str] = None) -> None:
|
|
227
274
|
"""
|
|
228
275
|
Ensure Nginx includes virtual hosts from /etc/nginx/sites-enabled.
|
|
229
276
|
If /etc/nginx/nginx.conf already includes sites-enabled, no change is made.
|
|
@@ -245,7 +292,7 @@ def ensure_nginx_sites_enabled_included() -> None:
|
|
|
245
292
|
)
|
|
246
293
|
try:
|
|
247
294
|
if not bridge_conf.exists() or bridge_conf.read_text(encoding="utf-8", errors="ignore") != bridge_content:
|
|
248
|
-
write_privileged_file(bridge_conf, bridge_content)
|
|
295
|
+
write_privileged_file(bridge_conf, bridge_content, password=password)
|
|
249
296
|
except Exception as e:
|
|
250
297
|
logger.debug(f"Could not create sites-enabled bridge configuration: {e}")
|
|
251
298
|
|
|
@@ -377,7 +424,7 @@ server {{
|
|
|
377
424
|
"""
|
|
378
425
|
|
|
379
426
|
|
|
380
|
-
def reload_nginx() -> bool:
|
|
427
|
+
def reload_nginx(password: Optional[str] = None) -> bool:
|
|
381
428
|
"""
|
|
382
429
|
Reload Nginx configuration safely using available init system or nginx CLI.
|
|
383
430
|
"""
|
|
@@ -395,7 +442,9 @@ def reload_nginx() -> bool:
|
|
|
395
442
|
return False
|
|
396
443
|
|
|
397
444
|
if os.geteuid() != 0:
|
|
398
|
-
|
|
445
|
+
if password:
|
|
446
|
+
validate_sudo_password(password)
|
|
447
|
+
cmd = ["sudo", "-n"] + cmd
|
|
399
448
|
|
|
400
449
|
try:
|
|
401
450
|
res = subprocess.run(cmd, capture_output=True, timeout=10)
|
|
@@ -451,7 +500,7 @@ def get_vhost(domain: str) -> Optional[Dict[str, Any]]:
|
|
|
451
500
|
return None
|
|
452
501
|
|
|
453
502
|
|
|
454
|
-
def remove_vhost(domain: str) -> bool:
|
|
503
|
+
def remove_vhost(domain: str, password: Optional[str] = None) -> bool:
|
|
455
504
|
"""Remove vhost configuration file(s), symlinks, and /etc/hosts entry."""
|
|
456
505
|
clean_domain = re.sub(r"^https?://", "", domain.strip().lower()).rstrip("/")
|
|
457
506
|
removed = False
|
|
@@ -459,7 +508,7 @@ def remove_vhost(domain: str) -> bool:
|
|
|
459
508
|
conf_file = sdir / f"{clean_domain}.conf"
|
|
460
509
|
if conf_file.exists() or conf_file.is_symlink():
|
|
461
510
|
try:
|
|
462
|
-
unlink_privileged(conf_file)
|
|
511
|
+
unlink_privileged(conf_file, password=password)
|
|
463
512
|
removed = True
|
|
464
513
|
except Exception as e:
|
|
465
514
|
logger.warning(f"Could not remove {conf_file}: {e}")
|
|
@@ -474,9 +523,9 @@ def remove_vhost(domain: str) -> bool:
|
|
|
474
523
|
pass
|
|
475
524
|
|
|
476
525
|
# Remove from /etc/hosts
|
|
477
|
-
remove_from_hosts_privileged(clean_domain)
|
|
526
|
+
remove_from_hosts_privileged(clean_domain, password=password)
|
|
478
527
|
|
|
479
528
|
# Reload nginx
|
|
480
|
-
reload_nginx()
|
|
529
|
+
reload_nginx(password=password)
|
|
481
530
|
|
|
482
531
|
return removed
|
|
@@ -248,13 +248,53 @@ def _pick_directory_os(initial_path: Optional[str] = None) -> Optional[str]:
|
|
|
248
248
|
return None
|
|
249
249
|
|
|
250
250
|
|
|
251
|
+
class SudoPasswordModal(ModalScreen[Optional[str]]):
|
|
252
|
+
"""Modal dialog to prompt for administrator (sudo) password when needed."""
|
|
253
|
+
|
|
254
|
+
def __init__(self, action_name: str = "This operation") -> None:
|
|
255
|
+
super().__init__()
|
|
256
|
+
self.action_name = action_name
|
|
257
|
+
|
|
258
|
+
def compose(self) -> ComposeResult:
|
|
259
|
+
with Vertical(id="modal-dialog"):
|
|
260
|
+
yield Label("🔐 Administrator Authentication", id="modal-title")
|
|
261
|
+
yield Label(f"{self.action_name} requires administrator privileges.", classes="modal-label")
|
|
262
|
+
yield Label("Enter your sudo password:", classes="modal-label")
|
|
263
|
+
yield Input(password=True, placeholder="Password", id="input-sudo-pass", classes="modal-input")
|
|
264
|
+
with Horizontal(id="modal-buttons"):
|
|
265
|
+
yield Button("Authenticate", variant="primary", id="btn-auth-confirm")
|
|
266
|
+
yield Button("Cancel", variant="default", id="btn-auth-cancel")
|
|
267
|
+
|
|
268
|
+
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
269
|
+
self.query_one("#btn-auth-confirm", Button).press()
|
|
270
|
+
|
|
271
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
272
|
+
if event.button.id == "btn-auth-confirm":
|
|
273
|
+
pwd_input = self.query_one("#input-sudo-pass", Input)
|
|
274
|
+
pwd = pwd_input.value
|
|
275
|
+
if not pwd:
|
|
276
|
+
self.notify("Password is required.", severity="error")
|
|
277
|
+
pwd_input.focus()
|
|
278
|
+
return
|
|
279
|
+
from ndev.linux.runtime.nginx_helper import validate_sudo_password
|
|
280
|
+
if not validate_sudo_password(pwd):
|
|
281
|
+
self.notify("Incorrect administrator password. Please try again.", severity="error")
|
|
282
|
+
pwd_input.value = ""
|
|
283
|
+
pwd_input.focus()
|
|
284
|
+
return
|
|
285
|
+
self.dismiss(pwd)
|
|
286
|
+
else:
|
|
287
|
+
self.dismiss(None)
|
|
288
|
+
|
|
289
|
+
|
|
251
290
|
class CreateVhostModal(ModalScreen[Optional[dict]]):
|
|
252
291
|
"""Modal dialog to create a new virtual host with local SSL on Linux."""
|
|
253
292
|
|
|
254
|
-
def __init__(self, installed_phps: list[str], default_php: Optional[str] = None) -> None:
|
|
293
|
+
def __init__(self, installed_phps: list[str], default_php: Optional[str] = None, needs_sudo: bool = False) -> None:
|
|
255
294
|
super().__init__()
|
|
256
295
|
self.installed_phps = installed_phps
|
|
257
296
|
self.default_php = default_php or (installed_phps[0] if installed_phps else "8.4")
|
|
297
|
+
self.needs_sudo = needs_sudo
|
|
258
298
|
self._user_edited_root = False
|
|
259
299
|
|
|
260
300
|
def compose(self) -> ComposeResult:
|
|
@@ -273,10 +313,23 @@ class CreateVhostModal(ModalScreen[Optional[dict]]):
|
|
|
273
313
|
yield Label("PHP Version:", classes="modal-label")
|
|
274
314
|
yield Select(php_options, value=initial_php, id="select-vhost-php", classes="modal-input")
|
|
275
315
|
yield Checkbox("Enable Local SSL (HTTPS with mkcert)", value=True, id="chk-vhost-ssl")
|
|
316
|
+
|
|
317
|
+
if self.needs_sudo:
|
|
318
|
+
yield Label("🔐 Administrator (sudo) Password:", classes="modal-label")
|
|
319
|
+
yield Input(
|
|
320
|
+
placeholder="Enter sudo password for /etc/nginx & /etc/hosts",
|
|
321
|
+
password=True,
|
|
322
|
+
id="input-vhost-password",
|
|
323
|
+
classes="modal-input"
|
|
324
|
+
)
|
|
325
|
+
|
|
276
326
|
with Horizontal(id="modal-buttons"):
|
|
277
327
|
yield Button("Create VHost", variant="success", id="btn-modal-create")
|
|
278
328
|
yield Button("Cancel", variant="default", id="btn-modal-cancel")
|
|
279
329
|
|
|
330
|
+
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
331
|
+
self.query_one("#btn-modal-create", Button).press()
|
|
332
|
+
|
|
280
333
|
def on_input_changed(self, event: Input.Changed) -> None:
|
|
281
334
|
if event.input.id == "input-vhost-domain":
|
|
282
335
|
domain = event.value.strip()
|
|
@@ -318,11 +371,27 @@ class CreateVhostModal(ModalScreen[Optional[dict]]):
|
|
|
318
371
|
self.notify("Document root directory is required.", severity="error")
|
|
319
372
|
return
|
|
320
373
|
|
|
374
|
+
password = None
|
|
375
|
+
if self.needs_sudo:
|
|
376
|
+
pwd_input = self.query_one("#input-vhost-password", Input)
|
|
377
|
+
password = pwd_input.value
|
|
378
|
+
if not password:
|
|
379
|
+
self.notify("Administrator (sudo) password is required.", severity="error")
|
|
380
|
+
pwd_input.focus()
|
|
381
|
+
return
|
|
382
|
+
from ndev.linux.runtime.nginx_helper import validate_sudo_password
|
|
383
|
+
if not validate_sudo_password(password):
|
|
384
|
+
self.notify("Incorrect administrator password. Please try again.", severity="error")
|
|
385
|
+
pwd_input.value = ""
|
|
386
|
+
pwd_input.focus()
|
|
387
|
+
return
|
|
388
|
+
|
|
321
389
|
self.dismiss({
|
|
322
390
|
"domain": domain,
|
|
323
391
|
"root": root,
|
|
324
392
|
"php": php_ver,
|
|
325
393
|
"ssl": bool(ssl),
|
|
394
|
+
"password": password,
|
|
326
395
|
})
|
|
327
396
|
else:
|
|
328
397
|
self.dismiss(None)
|
|
@@ -443,6 +512,16 @@ class NdevDashboard(App):
|
|
|
443
512
|
self._updating_selects = False
|
|
444
513
|
self._available_logs: Dict[str, Path] = {}
|
|
445
514
|
self._selected_service_key: Optional[str] = None
|
|
515
|
+
self._sudo_password: Optional[str] = None
|
|
516
|
+
|
|
517
|
+
def _needs_sudo(self) -> bool:
|
|
518
|
+
"""Check if elevated privileges are needed and require a password prompt."""
|
|
519
|
+
from ndev.linux.runtime.nginx_helper import check_sudo_needs_password, validate_sudo_password
|
|
520
|
+
if not check_sudo_needs_password():
|
|
521
|
+
return False
|
|
522
|
+
if self._sudo_password and validate_sudo_password(self._sudo_password):
|
|
523
|
+
return False
|
|
524
|
+
return True
|
|
446
525
|
|
|
447
526
|
def compose(self) -> ComposeResult:
|
|
448
527
|
yield Header(show_clock=True)
|
|
@@ -959,18 +1038,25 @@ class NdevDashboard(App):
|
|
|
959
1038
|
async def action_reload_nginx(self) -> None:
|
|
960
1039
|
"""Reload Nginx configuration."""
|
|
961
1040
|
self.log_message("[bold blue]Reloading Nginx configuration...[/bold blue]")
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1041
|
+
|
|
1042
|
+
def _do_reload(pwd: Optional[str] = None) -> None:
|
|
1043
|
+
from ndev.linux.runtime.nginx_helper import reload_nginx
|
|
1044
|
+
ok = reload_nginx(password=pwd or self._sudo_password)
|
|
1045
|
+
if ok:
|
|
1046
|
+
self.log_message("[bold green]✓ Nginx configuration reloaded successfully.[/bold green]")
|
|
1047
|
+
self.notify("Nginx reloaded.", severity="information")
|
|
1048
|
+
else:
|
|
1049
|
+
self.log_message("[bold red]✗ Failed to reload Nginx.[/bold red]")
|
|
1050
|
+
self.notify("Nginx reload failed.", severity="error")
|
|
1051
|
+
|
|
1052
|
+
if self._needs_sudo():
|
|
1053
|
+
def _on_pwd(pwd: Optional[str]) -> None:
|
|
1054
|
+
if pwd:
|
|
1055
|
+
self._sudo_password = pwd
|
|
1056
|
+
asyncio.create_task(asyncio.to_thread(_do_reload, pwd))
|
|
1057
|
+
self.push_screen(SudoPasswordModal("Reloading Nginx"), _on_pwd)
|
|
1058
|
+
else:
|
|
1059
|
+
await asyncio.to_thread(_do_reload, self._sudo_password)
|
|
974
1060
|
|
|
975
1061
|
@work(exclusive=True)
|
|
976
1062
|
async def action_check_upgrades(self) -> None:
|
|
@@ -1098,12 +1184,15 @@ class NdevDashboard(App):
|
|
|
1098
1184
|
|
|
1099
1185
|
def _on_modal_result(res: Optional[dict]) -> None:
|
|
1100
1186
|
if res:
|
|
1101
|
-
|
|
1187
|
+
pwd = res.get("password") or self._sudo_password
|
|
1188
|
+
if pwd:
|
|
1189
|
+
self._sudo_password = pwd
|
|
1190
|
+
self._create_vhost_worker(res["domain"], res["root"], res["php"], res["ssl"], password=pwd)
|
|
1102
1191
|
|
|
1103
|
-
self.push_screen(CreateVhostModal(installed, curr), _on_modal_result)
|
|
1192
|
+
self.push_screen(CreateVhostModal(installed, curr, needs_sudo=self._needs_sudo()), _on_modal_result)
|
|
1104
1193
|
|
|
1105
1194
|
@work(exclusive=True)
|
|
1106
|
-
async def _create_vhost_worker(self, domain: str, root: str, php_ver: str, ssl: bool) -> None:
|
|
1195
|
+
async def _create_vhost_worker(self, domain: str, root: str, php_ver: str, ssl: bool, password: Optional[str] = None) -> None:
|
|
1107
1196
|
self.query_one(TabbedContent).active = "tab-logs"
|
|
1108
1197
|
self.log_message(f"[bold blue]Creating virtual host '{domain}' (PHP {php_ver}, SSL={ssl})...[/bold blue]")
|
|
1109
1198
|
self.notify(f"Creating vhost {domain}...", severity="information")
|
|
@@ -1122,7 +1211,7 @@ class NdevDashboard(App):
|
|
|
1122
1211
|
|
|
1123
1212
|
vhost_dir, enabled_dir = get_nginx_vhost_dirs()
|
|
1124
1213
|
if enabled_dir:
|
|
1125
|
-
ensure_nginx_sites_enabled_included()
|
|
1214
|
+
ensure_nginx_sites_enabled_included(password=password)
|
|
1126
1215
|
|
|
1127
1216
|
conf_file = vhost_dir / f"{domain}.conf"
|
|
1128
1217
|
|
|
@@ -1142,16 +1231,16 @@ class NdevDashboard(App):
|
|
|
1142
1231
|
cert_path=cert_path,
|
|
1143
1232
|
key_path=key_path,
|
|
1144
1233
|
)
|
|
1145
|
-
write_privileged_file(conf_file, tpl)
|
|
1234
|
+
write_privileged_file(conf_file, tpl, password=password)
|
|
1146
1235
|
if enabled_dir and enabled_dir != vhost_dir:
|
|
1147
1236
|
link = enabled_dir / f"{domain}.conf"
|
|
1148
|
-
symlink_privileged(conf_file, link)
|
|
1237
|
+
symlink_privileged(conf_file, link, password=password)
|
|
1149
1238
|
|
|
1150
1239
|
# Add /etc/hosts entry
|
|
1151
|
-
add_to_hosts_privileged(domain)
|
|
1240
|
+
add_to_hosts_privileged(domain, password=password)
|
|
1152
1241
|
|
|
1153
1242
|
# Reload Nginx
|
|
1154
|
-
reload_nginx()
|
|
1243
|
+
reload_nginx(password=password)
|
|
1155
1244
|
return str(conf_file)
|
|
1156
1245
|
|
|
1157
1246
|
try:
|
|
@@ -1177,21 +1266,32 @@ class NdevDashboard(App):
|
|
|
1177
1266
|
row_key = vhost_table.coordinate_to_cell_key((vhost_table.cursor_row, 0)).row_key
|
|
1178
1267
|
domain = str(row_key.value)
|
|
1179
1268
|
|
|
1269
|
+
def _do_delete(pwd: Optional[str] = None) -> None:
|
|
1270
|
+
if pwd:
|
|
1271
|
+
self._sudo_password = pwd
|
|
1272
|
+
self._delete_vhost_worker(domain, password=pwd or self._sudo_password)
|
|
1273
|
+
|
|
1180
1274
|
def _on_confirm(confirmed: bool) -> None:
|
|
1181
1275
|
if confirmed:
|
|
1182
|
-
self.
|
|
1276
|
+
if self._needs_sudo():
|
|
1277
|
+
self.push_screen(
|
|
1278
|
+
SudoPasswordModal(f"Deleting virtual host '{domain}'"),
|
|
1279
|
+
lambda pwd: _do_delete(pwd) if pwd else None
|
|
1280
|
+
)
|
|
1281
|
+
else:
|
|
1282
|
+
_do_delete(self._sudo_password)
|
|
1183
1283
|
|
|
1184
1284
|
msg = f"Are you sure you want to delete virtual host '{domain}'?\n\nThis will remove its Nginx configuration, SSL certificates, and hosts entry."
|
|
1185
1285
|
self.push_screen(ConfirmActionModal("Delete Virtual Host", msg, confirm_label="Delete VHost", variant="error"), _on_confirm)
|
|
1186
1286
|
|
|
1187
1287
|
@work(exclusive=True)
|
|
1188
|
-
async def _delete_vhost_worker(self, domain: str) -> None:
|
|
1288
|
+
async def _delete_vhost_worker(self, domain: str, password: Optional[str] = None) -> None:
|
|
1189
1289
|
self.log_message(f"[bold yellow]Deleting virtual host '{domain}'...[/bold yellow]")
|
|
1190
1290
|
self.notify(f"Deleting vhost {domain}...", severity="information")
|
|
1191
1291
|
|
|
1192
1292
|
def _do() -> None:
|
|
1193
1293
|
from ndev.linux.runtime.nginx_helper import remove_vhost
|
|
1194
|
-
remove_vhost(domain)
|
|
1294
|
+
remove_vhost(domain, password=password)
|
|
1195
1295
|
|
|
1196
1296
|
try:
|
|
1197
1297
|
await asyncio.to_thread(_do)
|
|
@@ -1201,6 +1301,8 @@ class NdevDashboard(App):
|
|
|
1201
1301
|
except Exception as e:
|
|
1202
1302
|
self.log_message(f"[bold red]✗ Failed to delete virtual host {domain}: {e}[/bold red]")
|
|
1203
1303
|
self.notify(f"Delete failed: {e}", severity="error")
|
|
1304
|
+
self.log_message(f"[bold red]✗ Failed to delete virtual host {domain}: {e}[/bold red]")
|
|
1305
|
+
self.notify(f"Delete failed: {e}", severity="error")
|
|
1204
1306
|
|
|
1205
1307
|
def action_install_php(self) -> None:
|
|
1206
1308
|
"""Open modal dialog to install a new PHP runtime."""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ndev-stack
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.10
|
|
4
4
|
Summary: Cross-platform developer stack manager for PHP, FastCGI/PHP-FPM, MariaDB, Redis, PostgreSQL, MongoDB, Nginx, SSL, and Mailpit on Linux and Windows
|
|
5
5
|
Author-email: Ankit Karan <ankit.karan99@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/ankitkaran99/ndev
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ndev-stack"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.10"
|
|
8
8
|
description = "Cross-platform developer stack manager for PHP, FastCGI/PHP-FPM, MariaDB, Redis, PostgreSQL, MongoDB, Nginx, SSL, and Mailpit on Linux and Windows"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -233,6 +233,28 @@ class TestNginxHelper(unittest.TestCase):
|
|
|
233
233
|
self.assertFalse(link_target.exists())
|
|
234
234
|
self.assertFalse(link_target.is_symlink())
|
|
235
235
|
|
|
236
|
+
def test_validate_sudo_password(self):
|
|
237
|
+
with patch("subprocess.run") as mock_run:
|
|
238
|
+
mock_run.return_value = unittest.mock.MagicMock(returncode=0)
|
|
239
|
+
self.assertTrue(nginx_helper.validate_sudo_password("valid_pwd"))
|
|
240
|
+
|
|
241
|
+
mock_run.return_value = unittest.mock.MagicMock(returncode=1)
|
|
242
|
+
self.assertFalse(nginx_helper.validate_sudo_password("invalid_pwd"))
|
|
243
|
+
|
|
244
|
+
self.assertFalse(nginx_helper.validate_sudo_password(""))
|
|
245
|
+
|
|
246
|
+
def test_check_sudo_needs_password(self):
|
|
247
|
+
with patch("os.geteuid", return_value=0):
|
|
248
|
+
self.assertFalse(nginx_helper.check_sudo_needs_password())
|
|
249
|
+
|
|
250
|
+
with patch("os.geteuid", return_value=1000):
|
|
251
|
+
with patch("subprocess.run") as mock_run:
|
|
252
|
+
mock_run.return_value = unittest.mock.MagicMock(returncode=0)
|
|
253
|
+
self.assertFalse(nginx_helper.check_sudo_needs_password())
|
|
254
|
+
|
|
255
|
+
mock_run.return_value = unittest.mock.MagicMock(returncode=1)
|
|
256
|
+
self.assertTrue(nginx_helper.check_sudo_needs_password())
|
|
257
|
+
|
|
236
258
|
def test_fastcgi_snippet_opensuse(self):
|
|
237
259
|
with patch.object(Path, "exists", autospec=True) as mock_exists:
|
|
238
260
|
def side_effect_exists(p):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|