ndev-stack 0.3.12__tar.gz → 0.3.13__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.12 → ndev_stack-0.3.13}/PKG-INFO +1 -1
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/__init__.py +1 -1
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/linux/commands/doctor.py +16 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/linux/commands/setup.py +6 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/linux/commands/vhost.py +2 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/linux/runtime/nginx_helper.py +108 -4
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/linux/tui.py +141 -60
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev_stack.egg-info/PKG-INFO +1 -1
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/pyproject.toml +1 -1
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/tests/test_opensuse_compat.py +77 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/README.md +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/__main__.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/cli.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/common/__init__.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/common/config.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/common/constants.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/common/distro.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/common/github.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/common/logger.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/common/manifest.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/common/modules/__init__.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/common/modules/base.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/common/modules/builtin/mailpit/manifest.json +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/common/modules/builtin/mailpit/module.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/common/modules/builtin/mongodb/manifest.json +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/common/modules/builtin/mongodb/module.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/common/modules/builtin/postgres/manifest.json +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/common/modules/builtin/postgres/module.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/common/modules/builtin/redis/manifest.json +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/common/modules/builtin/redis/module.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/common/modules/manager.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/common/utils.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/linux/__init__.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/linux/chroot/manager.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/linux/chroot/packages.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/linux/chroot/shell.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/linux/cli.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/linux/commands/available.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/linux/commands/clean.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/linux/commands/ctl.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/linux/commands/current.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/linux/commands/db.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/linux/commands/grok.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/linux/commands/install.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/linux/commands/list.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/linux/commands/logs.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/linux/commands/mailpit.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/linux/commands/modules.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/linux/commands/mongodb.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/linux/commands/postgres.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/linux/commands/reload.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/linux/commands/restart.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/linux/commands/start.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/linux/commands/status.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/linux/commands/stop.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/linux/commands/uninstall.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/linux/commands/update.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/linux/commands/upgrade.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/linux/commands/use.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/linux/php/builder.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/linux/php/downloader.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/linux/php/extensions.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/linux/php/installer.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/linux/php/resolver.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/linux/php/templates.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/linux/runtime/fpm.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/linux/runtime/mailpit.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/linux/runtime/pma.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/linux/runtime/process.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/linux/runtime/services.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/linux/runtime/sockets.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/linux/runtime/upgrade.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/main.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/tui.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/win/__init__.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/win/bin/shim.exe +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/win/cli.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/win/commands/__init__.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/win/core/__init__.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/win/core/db.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/win/core/elevate.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/win/core/ext.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/win/core/fcgi.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/win/core/grok.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/win/core/logs.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/win/core/mailpit.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/win/core/mkcert.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/win/core/paths.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/win/core/php.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/win/core/pma.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/win/core/redis_core.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/win/core/services.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/win/core/setup.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/win/core/shim.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/win/core/upgrade.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/win/core/vhost.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/win/templates/vhost.conf.tmpl +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/win/templates/vhost_ssl.conf.tmpl +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev/win/tui.py +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev_stack.egg-info/SOURCES.txt +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev_stack.egg-info/dependency_links.txt +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev_stack.egg-info/entry_points.txt +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev_stack.egg-info/requires.txt +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/ndev_stack.egg-info/top_level.txt +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/setup.cfg +0 -0
- {ndev_stack-0.3.12 → ndev_stack-0.3.13}/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.13
|
|
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
|
|
@@ -74,6 +74,22 @@ def doctor_cmd():
|
|
|
74
74
|
else:
|
|
75
75
|
table.add_row("mkcert Local SSL", "[yellow]NOT INSTALLED[/yellow]", "Optional for SSL. Run 'ndev upgrade mkcert' to install.")
|
|
76
76
|
|
|
77
|
+
try:
|
|
78
|
+
from ndev.linux.runtime.nginx_helper import is_selinux_enforcing, is_selinux_httpd_permissive
|
|
79
|
+
if is_selinux_enforcing():
|
|
80
|
+
if is_selinux_httpd_permissive():
|
|
81
|
+
table.add_row("SELinux Web Server Access", "[green]OK[/green]", "Enforcing mode, httpd_t is permissive")
|
|
82
|
+
else:
|
|
83
|
+
table.add_row(
|
|
84
|
+
"SELinux Web Server Access",
|
|
85
|
+
"[yellow]RESTRICTED[/yellow]",
|
|
86
|
+
"Enforcing. Nginx cannot access ~/Sites or FastCGI sockets. Run: sudo /usr/sbin/semanage permissive -a httpd_t"
|
|
87
|
+
)
|
|
88
|
+
else:
|
|
89
|
+
table.add_row("SELinux Web Server Access", "[green]OK[/green]", "SELinux is permissive or disabled")
|
|
90
|
+
except Exception:
|
|
91
|
+
pass
|
|
92
|
+
|
|
77
93
|
if NDEV_DIR.exists():
|
|
78
94
|
table.add_row("Layout Directory (~/.ndev)", "[green]OK[/green]", f"Exists at {NDEV_DIR}")
|
|
79
95
|
else:
|
|
@@ -71,6 +71,12 @@ def setup_cmd():
|
|
|
71
71
|
except Exception:
|
|
72
72
|
pass
|
|
73
73
|
|
|
74
|
+
try:
|
|
75
|
+
from ndev.linux.runtime.nginx_helper import ensure_selinux_httpd_permissions
|
|
76
|
+
ensure_selinux_httpd_permissions()
|
|
77
|
+
except Exception:
|
|
78
|
+
pass
|
|
79
|
+
|
|
74
80
|
console.print("[bold green]MariaDB, Nginx, and Bubblewrap installed and configured successfully![/bold green]\n")
|
|
75
81
|
else:
|
|
76
82
|
# 1. Update Package Lists
|
|
@@ -321,6 +321,7 @@ def vhost_cmd(
|
|
|
321
321
|
ensure_nginx_sites_enabled_included,
|
|
322
322
|
install_nginx_ssl_cert,
|
|
323
323
|
ensure_docroot_accessible,
|
|
324
|
+
ensure_selinux_httpd_permissions,
|
|
324
325
|
add_to_hosts_privileged,
|
|
325
326
|
reload_nginx,
|
|
326
327
|
)
|
|
@@ -350,6 +351,7 @@ def vhost_cmd(
|
|
|
350
351
|
logger.warning(f"Could not install SSL certificate to /etc/nginx/ssl: {e}")
|
|
351
352
|
|
|
352
353
|
ensure_docroot_accessible(root_path)
|
|
354
|
+
ensure_selinux_httpd_permissions()
|
|
353
355
|
|
|
354
356
|
conf_file = vhost_dir / f"{domain}.conf"
|
|
355
357
|
config_template = generate_vhost_config(
|
|
@@ -66,6 +66,96 @@ def check_sudo_needs_password() -> bool:
|
|
|
66
66
|
return True
|
|
67
67
|
|
|
68
68
|
|
|
69
|
+
def run_privileged_command(
|
|
70
|
+
cmd: list[str],
|
|
71
|
+
password: Optional[str] = None,
|
|
72
|
+
check: bool = True,
|
|
73
|
+
timeout: int = 60,
|
|
74
|
+
capture_output: bool = True
|
|
75
|
+
) -> subprocess.CompletedProcess:
|
|
76
|
+
"""
|
|
77
|
+
Run a command with elevated privileges using sudo -n if non-root.
|
|
78
|
+
Validates sudo password first if provided to refresh the sudo credential timestamp.
|
|
79
|
+
"""
|
|
80
|
+
full_cmd = list(cmd)
|
|
81
|
+
if os.geteuid() != 0:
|
|
82
|
+
if password:
|
|
83
|
+
validate_sudo_password(password)
|
|
84
|
+
full_cmd = ["sudo", "-n"] + full_cmd
|
|
85
|
+
return subprocess.run(
|
|
86
|
+
full_cmd,
|
|
87
|
+
check=check,
|
|
88
|
+
timeout=timeout,
|
|
89
|
+
capture_output=capture_output,
|
|
90
|
+
text=True
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def is_selinux_enforcing() -> bool:
|
|
95
|
+
"""Check if SELinux is present and in Enforcing mode."""
|
|
96
|
+
enforce_file = Path("/sys/fs/selinux/enforce")
|
|
97
|
+
if enforce_file.exists():
|
|
98
|
+
try:
|
|
99
|
+
return enforce_file.read_text().strip() == "1"
|
|
100
|
+
except Exception:
|
|
101
|
+
pass
|
|
102
|
+
getenforce = shutil.which("getenforce") or "/usr/sbin/getenforce"
|
|
103
|
+
if Path(getenforce).exists():
|
|
104
|
+
try:
|
|
105
|
+
res = subprocess.run([getenforce], capture_output=True, text=True, timeout=2)
|
|
106
|
+
return res.stdout.strip().lower() == "enforcing"
|
|
107
|
+
except Exception:
|
|
108
|
+
pass
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def is_selinux_httpd_permissive() -> bool:
|
|
113
|
+
"""Check if httpd_t is in SELinux permissive types."""
|
|
114
|
+
if not is_selinux_enforcing():
|
|
115
|
+
return True
|
|
116
|
+
seinfo = shutil.which("seinfo") or "/usr/bin/seinfo"
|
|
117
|
+
if Path(seinfo).exists():
|
|
118
|
+
try:
|
|
119
|
+
res = subprocess.run([seinfo, "--permissive"], capture_output=True, text=True, timeout=3)
|
|
120
|
+
return "httpd_t" in res.stdout
|
|
121
|
+
except Exception:
|
|
122
|
+
pass
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def ensure_selinux_httpd_permissions(password: Optional[str] = None) -> bool:
|
|
127
|
+
"""
|
|
128
|
+
Ensure SELinux allows Nginx (httpd_t) to access user document roots and FastCGI sockets.
|
|
129
|
+
If SELinux is enforcing and httpd_t is not permissive, add httpd_t to permissive types
|
|
130
|
+
via semanage and enable relevant SELinux booleans.
|
|
131
|
+
"""
|
|
132
|
+
if not is_selinux_enforcing() or is_selinux_httpd_permissive():
|
|
133
|
+
return True
|
|
134
|
+
|
|
135
|
+
success = False
|
|
136
|
+
semanage = shutil.which("semanage") or "/usr/sbin/semanage"
|
|
137
|
+
if Path(semanage).exists():
|
|
138
|
+
try:
|
|
139
|
+
run_privileged_command([semanage, "permissive", "-a", "httpd_t"], password=password, check=False)
|
|
140
|
+
success = is_selinux_httpd_permissive()
|
|
141
|
+
except Exception as e:
|
|
142
|
+
logger.warning(f"Failed to set httpd_t permissive via semanage: {e}")
|
|
143
|
+
|
|
144
|
+
# Also enable booleans as supplementary fallback
|
|
145
|
+
setsebool = shutil.which("setsebool") or "/usr/sbin/setsebool"
|
|
146
|
+
if Path(setsebool).exists():
|
|
147
|
+
try:
|
|
148
|
+
run_privileged_command(
|
|
149
|
+
[setsebool, "-P", "httpd_read_user_content=1", "httpd_enable_homedirs=1", "httpd_can_network_connect=1"],
|
|
150
|
+
password=password,
|
|
151
|
+
check=False
|
|
152
|
+
)
|
|
153
|
+
except Exception:
|
|
154
|
+
pass
|
|
155
|
+
|
|
156
|
+
return success or is_selinux_httpd_permissive()
|
|
157
|
+
|
|
158
|
+
|
|
69
159
|
def write_privileged_file(target: Path, content: str, password: Optional[str] = None) -> None:
|
|
70
160
|
"""
|
|
71
161
|
Write content to a file, using sudo tee if current process lacks permissions.
|
|
@@ -527,10 +617,24 @@ def ensure_docroot_accessible(root_path: Path) -> None:
|
|
|
527
617
|
try:
|
|
528
618
|
resolved_root = root_path.resolve()
|
|
529
619
|
resolved_home = home.resolve()
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
620
|
+
|
|
621
|
+
# Traverse every directory from root_path up to home and ensure +x (0o001)
|
|
622
|
+
curr = resolved_root
|
|
623
|
+
while curr != curr.parent:
|
|
624
|
+
try:
|
|
625
|
+
st = curr.stat()
|
|
626
|
+
if not (st.st_mode & 0o001):
|
|
627
|
+
os.chmod(str(curr), st.st_mode | 0o001)
|
|
628
|
+
except Exception:
|
|
629
|
+
pass
|
|
630
|
+
if curr == resolved_home:
|
|
631
|
+
break
|
|
632
|
+
curr = curr.parent
|
|
633
|
+
|
|
634
|
+
# Also ensure home directory itself has +x
|
|
635
|
+
st = resolved_home.stat()
|
|
636
|
+
if not (st.st_mode & 0o001):
|
|
637
|
+
os.chmod(str(resolved_home), st.st_mode | 0o001)
|
|
534
638
|
except Exception:
|
|
535
639
|
pass
|
|
536
640
|
|
|
@@ -12,7 +12,7 @@ import subprocess
|
|
|
12
12
|
import threading
|
|
13
13
|
import webbrowser
|
|
14
14
|
from pathlib import Path
|
|
15
|
-
from typing import Any, Dict, List, Optional, Tuple
|
|
15
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
16
16
|
|
|
17
17
|
from rich.text import Text
|
|
18
18
|
from textual import on, work
|
|
@@ -265,6 +265,12 @@ class SudoPasswordModal(ModalScreen[Optional[str]]):
|
|
|
265
265
|
yield Button("Authenticate", variant="primary", id="btn-auth-confirm")
|
|
266
266
|
yield Button("Cancel", variant="default", id="btn-auth-cancel")
|
|
267
267
|
|
|
268
|
+
def on_mount(self) -> None:
|
|
269
|
+
try:
|
|
270
|
+
self.query_one("#input-sudo-pass", Input).focus()
|
|
271
|
+
except Exception:
|
|
272
|
+
pass
|
|
273
|
+
|
|
268
274
|
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
269
275
|
self.query_one("#btn-auth-confirm", Button).press()
|
|
270
276
|
|
|
@@ -327,6 +333,12 @@ class CreateVhostModal(ModalScreen[Optional[dict]]):
|
|
|
327
333
|
yield Button("Create VHost", variant="success", id="btn-modal-create")
|
|
328
334
|
yield Button("Cancel", variant="default", id="btn-modal-cancel")
|
|
329
335
|
|
|
336
|
+
def on_mount(self) -> None:
|
|
337
|
+
try:
|
|
338
|
+
self.query_one("#input-vhost-domain", Input).focus()
|
|
339
|
+
except Exception:
|
|
340
|
+
pass
|
|
341
|
+
|
|
330
342
|
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
331
343
|
self.query_one("#btn-modal-create", Button).press()
|
|
332
344
|
|
|
@@ -516,6 +528,8 @@ class NdevDashboard(App):
|
|
|
516
528
|
|
|
517
529
|
def _needs_sudo(self) -> bool:
|
|
518
530
|
"""Check if elevated privileges are needed and require a password prompt."""
|
|
531
|
+
if os.geteuid() == 0:
|
|
532
|
+
return False
|
|
519
533
|
from ndev.linux.runtime.nginx_helper import check_sudo_needs_password, validate_sudo_password
|
|
520
534
|
if not check_sudo_needs_password():
|
|
521
535
|
return False
|
|
@@ -523,6 +537,27 @@ class NdevDashboard(App):
|
|
|
523
537
|
return False
|
|
524
538
|
return True
|
|
525
539
|
|
|
540
|
+
def ensure_sudo(self, action_name: str, callback: Callable[[Optional[str]], None]) -> None:
|
|
541
|
+
"""
|
|
542
|
+
Ensure sudo access is ready before executing a privileged action.
|
|
543
|
+
If sudo requires password and we don't have a valid cached password,
|
|
544
|
+
present SudoPasswordModal. Once authenticated, cache password and invoke callback(password).
|
|
545
|
+
If cancelled, notify user and abort action.
|
|
546
|
+
If sudo does not require a password, invoke callback(self._sudo_password) immediately.
|
|
547
|
+
"""
|
|
548
|
+
if not self._needs_sudo():
|
|
549
|
+
callback(self._sudo_password)
|
|
550
|
+
return
|
|
551
|
+
|
|
552
|
+
def _on_pwd(pwd: Optional[str]) -> None:
|
|
553
|
+
if pwd:
|
|
554
|
+
self._sudo_password = pwd
|
|
555
|
+
callback(pwd)
|
|
556
|
+
else:
|
|
557
|
+
self.notify("Administrator authentication cancelled.", severity="warning")
|
|
558
|
+
|
|
559
|
+
self.push_screen(SudoPasswordModal(action_name), _on_pwd)
|
|
560
|
+
|
|
526
561
|
def compose(self) -> ComposeResult:
|
|
527
562
|
yield Header(show_clock=True)
|
|
528
563
|
|
|
@@ -938,19 +973,29 @@ class NdevDashboard(App):
|
|
|
938
973
|
|
|
939
974
|
# ── ACTION WORKERS ────────────────────────────────────────────────────────
|
|
940
975
|
|
|
941
|
-
|
|
942
|
-
async def action_start_all(self) -> None:
|
|
976
|
+
def action_start_all(self) -> None:
|
|
943
977
|
"""Start all services and FastCGI pools."""
|
|
978
|
+
self.ensure_sudo("Starting all services", self._start_all_worker)
|
|
979
|
+
|
|
980
|
+
@work(exclusive=True)
|
|
981
|
+
async def _start_all_worker(self, password: Optional[str] = None) -> None:
|
|
944
982
|
self.log_message("[bold blue]Starting all services...[/bold blue]")
|
|
945
983
|
self.notify("Starting all web services...", severity="information")
|
|
946
984
|
|
|
947
985
|
def _do() -> None:
|
|
986
|
+
from ndev.linux.runtime.nginx_helper import run_privileged_command
|
|
948
987
|
# 1. Nginx
|
|
949
988
|
if shutil.which("systemctl"):
|
|
950
|
-
|
|
989
|
+
try:
|
|
990
|
+
run_privileged_command(["systemctl", "start", "nginx"], password=password, check=False)
|
|
991
|
+
except Exception as e:
|
|
992
|
+
self.log_message(f"[yellow]Nginx start notice: {e}[/yellow]")
|
|
951
993
|
# 2. MariaDB
|
|
952
994
|
if shutil.which("systemctl"):
|
|
953
|
-
|
|
995
|
+
try:
|
|
996
|
+
run_privileged_command(["systemctl", "start", "mariadb"], password=password, check=False)
|
|
997
|
+
except Exception as e:
|
|
998
|
+
self.log_message(f"[yellow]MariaDB start notice: {e}[/yellow]")
|
|
954
999
|
# 3. phpMyAdmin
|
|
955
1000
|
try:
|
|
956
1001
|
start_pma()
|
|
@@ -980,16 +1025,26 @@ class NdevDashboard(App):
|
|
|
980
1025
|
self.notify("All services started.", severity="information")
|
|
981
1026
|
self._refresh_all_data(full_rebuild=False)
|
|
982
1027
|
|
|
983
|
-
|
|
984
|
-
async def action_stop_all(self) -> None:
|
|
1028
|
+
def action_stop_all(self) -> None:
|
|
985
1029
|
"""Stop all running services and FastCGI pools."""
|
|
1030
|
+
self.ensure_sudo("Stopping all services", self._stop_all_worker)
|
|
1031
|
+
|
|
1032
|
+
@work(exclusive=True)
|
|
1033
|
+
async def _stop_all_worker(self, password: Optional[str] = None) -> None:
|
|
986
1034
|
self.log_message("[bold blue]Stopping all services...[/bold blue]")
|
|
987
1035
|
self.notify("Stopping all web services...", severity="information")
|
|
988
1036
|
|
|
989
1037
|
def _do() -> None:
|
|
1038
|
+
from ndev.linux.runtime.nginx_helper import run_privileged_command
|
|
990
1039
|
if shutil.which("systemctl"):
|
|
991
|
-
|
|
992
|
-
|
|
1040
|
+
try:
|
|
1041
|
+
run_privileged_command(["systemctl", "stop", "nginx"], password=password, check=False)
|
|
1042
|
+
except Exception:
|
|
1043
|
+
pass
|
|
1044
|
+
try:
|
|
1045
|
+
run_privileged_command(["systemctl", "stop", "mariadb"], password=password, check=False)
|
|
1046
|
+
except Exception:
|
|
1047
|
+
pass
|
|
993
1048
|
stop_pma()
|
|
994
1049
|
try:
|
|
995
1050
|
from ndev.common.modules import get_module_manager
|
|
@@ -1007,16 +1062,26 @@ class NdevDashboard(App):
|
|
|
1007
1062
|
self.notify("All services stopped.", severity="information")
|
|
1008
1063
|
self._refresh_all_data(full_rebuild=False)
|
|
1009
1064
|
|
|
1010
|
-
|
|
1011
|
-
async def action_restart_all(self) -> None:
|
|
1065
|
+
def action_restart_all(self) -> None:
|
|
1012
1066
|
"""Restart all services."""
|
|
1067
|
+
self.ensure_sudo("Restarting all services", self._restart_all_worker)
|
|
1068
|
+
|
|
1069
|
+
@work(exclusive=True)
|
|
1070
|
+
async def _restart_all_worker(self, password: Optional[str] = None) -> None:
|
|
1013
1071
|
self.log_message("[bold blue]Restarting all services...[/bold blue]")
|
|
1014
1072
|
self.notify("Restarting all web services...", severity="information")
|
|
1015
1073
|
|
|
1016
1074
|
def _do() -> None:
|
|
1075
|
+
from ndev.linux.runtime.nginx_helper import run_privileged_command
|
|
1017
1076
|
if shutil.which("systemctl"):
|
|
1018
|
-
|
|
1019
|
-
|
|
1077
|
+
try:
|
|
1078
|
+
run_privileged_command(["systemctl", "restart", "nginx"], password=password, check=False)
|
|
1079
|
+
except Exception:
|
|
1080
|
+
pass
|
|
1081
|
+
try:
|
|
1082
|
+
run_privileged_command(["systemctl", "restart", "mariadb"], password=password, check=False)
|
|
1083
|
+
except Exception:
|
|
1084
|
+
pass
|
|
1020
1085
|
restart_pma()
|
|
1021
1086
|
try:
|
|
1022
1087
|
from ndev.common.modules import get_module_manager
|
|
@@ -1034,29 +1099,25 @@ class NdevDashboard(App):
|
|
|
1034
1099
|
self.notify("All services restarted.", severity="information")
|
|
1035
1100
|
self._refresh_all_data(full_rebuild=False)
|
|
1036
1101
|
|
|
1037
|
-
|
|
1038
|
-
async def action_reload_nginx(self) -> None:
|
|
1102
|
+
def action_reload_nginx(self) -> None:
|
|
1039
1103
|
"""Reload Nginx configuration."""
|
|
1104
|
+
self.ensure_sudo("Reloading Nginx", self._reload_nginx_worker)
|
|
1105
|
+
|
|
1106
|
+
@work(exclusive=True)
|
|
1107
|
+
async def _reload_nginx_worker(self, password: Optional[str] = None) -> None:
|
|
1040
1108
|
self.log_message("[bold blue]Reloading Nginx configuration...[/bold blue]")
|
|
1041
1109
|
|
|
1042
|
-
def _do_reload(
|
|
1110
|
+
def _do_reload() -> bool:
|
|
1043
1111
|
from ndev.linux.runtime.nginx_helper import reload_nginx
|
|
1044
|
-
|
|
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")
|
|
1112
|
+
return reload_nginx(password=password)
|
|
1051
1113
|
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
asyncio.create_task(asyncio.to_thread(_do_reload, pwd))
|
|
1057
|
-
self.push_screen(SudoPasswordModal("Reloading Nginx"), _on_pwd)
|
|
1114
|
+
ok = await asyncio.to_thread(_do_reload)
|
|
1115
|
+
if ok:
|
|
1116
|
+
self.log_message("[bold green]✓ Nginx configuration reloaded successfully.[/bold green]")
|
|
1117
|
+
self.notify("Nginx reloaded.", severity="information")
|
|
1058
1118
|
else:
|
|
1059
|
-
|
|
1119
|
+
self.log_message("[bold red]✗ Failed to reload Nginx.[/bold red]")
|
|
1120
|
+
self.notify("Nginx reload failed.", severity="error")
|
|
1060
1121
|
|
|
1061
1122
|
@work(exclusive=True)
|
|
1062
1123
|
async def action_check_upgrades(self) -> None:
|
|
@@ -1187,7 +1248,14 @@ class NdevDashboard(App):
|
|
|
1187
1248
|
pwd = res.get("password") or self._sudo_password
|
|
1188
1249
|
if pwd:
|
|
1189
1250
|
self._sudo_password = pwd
|
|
1190
|
-
|
|
1251
|
+
domain = res["domain"]
|
|
1252
|
+
root = res["root"]
|
|
1253
|
+
php_ver = res["php"]
|
|
1254
|
+
ssl = res["ssl"]
|
|
1255
|
+
self.ensure_sudo(
|
|
1256
|
+
f"Creating virtual host '{domain}'",
|
|
1257
|
+
lambda valid_pwd: self._create_vhost_worker(domain, root, php_ver, ssl, password=valid_pwd)
|
|
1258
|
+
)
|
|
1191
1259
|
|
|
1192
1260
|
self.push_screen(CreateVhostModal(installed, curr, needs_sudo=self._needs_sudo()), _on_modal_result)
|
|
1193
1261
|
|
|
@@ -1205,6 +1273,7 @@ class NdevDashboard(App):
|
|
|
1205
1273
|
ensure_nginx_sites_enabled_included,
|
|
1206
1274
|
install_nginx_ssl_cert,
|
|
1207
1275
|
ensure_docroot_accessible,
|
|
1276
|
+
ensure_selinux_httpd_permissions,
|
|
1208
1277
|
write_privileged_file,
|
|
1209
1278
|
symlink_privileged,
|
|
1210
1279
|
add_to_hosts_privileged,
|
|
@@ -1233,6 +1302,7 @@ class NdevDashboard(App):
|
|
|
1233
1302
|
logger.warning(f"Could not install SSL certificate into /etc/nginx/ssl: {e}")
|
|
1234
1303
|
|
|
1235
1304
|
ensure_docroot_accessible(Path(root))
|
|
1305
|
+
ensure_selinux_httpd_permissions(password=password)
|
|
1236
1306
|
|
|
1237
1307
|
mm = php_ver.replace(".", "")[:2] if "." in php_ver else php_ver
|
|
1238
1308
|
selected_sock = NDEV_DIR / "run" / f"php{mm}.sock"
|
|
@@ -1280,20 +1350,12 @@ class NdevDashboard(App):
|
|
|
1280
1350
|
row_key = vhost_table.coordinate_to_cell_key((vhost_table.cursor_row, 0)).row_key
|
|
1281
1351
|
domain = str(row_key.value)
|
|
1282
1352
|
|
|
1283
|
-
def _do_delete(pwd: Optional[str] = None) -> None:
|
|
1284
|
-
if pwd:
|
|
1285
|
-
self._sudo_password = pwd
|
|
1286
|
-
self._delete_vhost_worker(domain, password=pwd or self._sudo_password)
|
|
1287
|
-
|
|
1288
1353
|
def _on_confirm(confirmed: bool) -> None:
|
|
1289
1354
|
if confirmed:
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
)
|
|
1295
|
-
else:
|
|
1296
|
-
_do_delete(self._sudo_password)
|
|
1355
|
+
self.ensure_sudo(
|
|
1356
|
+
f"Deleting virtual host '{domain}'",
|
|
1357
|
+
lambda pwd: self._delete_vhost_worker(domain, password=pwd)
|
|
1358
|
+
)
|
|
1297
1359
|
|
|
1298
1360
|
msg = f"Are you sure you want to delete virtual host '{domain}'?\n\nThis will remove its Nginx configuration, SSL certificates, and hosts entry."
|
|
1299
1361
|
self.push_screen(ConfirmActionModal("Delete Virtual Host", msg, confirm_label="Delete VHost", variant="error"), _on_confirm)
|
|
@@ -1315,8 +1377,6 @@ class NdevDashboard(App):
|
|
|
1315
1377
|
except Exception as e:
|
|
1316
1378
|
self.log_message(f"[bold red]✗ Failed to delete virtual host {domain}: {e}[/bold red]")
|
|
1317
1379
|
self.notify(f"Delete failed: {e}", severity="error")
|
|
1318
|
-
self.log_message(f"[bold red]✗ Failed to delete virtual host {domain}: {e}[/bold red]")
|
|
1319
|
-
self.notify(f"Delete failed: {e}", severity="error")
|
|
1320
1380
|
|
|
1321
1381
|
def action_install_php(self) -> None:
|
|
1322
1382
|
"""Open modal dialog to install a new PHP runtime."""
|
|
@@ -1385,7 +1445,7 @@ class NdevDashboard(App):
|
|
|
1385
1445
|
self.notify(f"Uninstall failed: {e}", severity="error")
|
|
1386
1446
|
|
|
1387
1447
|
def _handle_selected_service_install(self) -> None:
|
|
1388
|
-
"""Install or setup the selected service (PMA, Mailpit, Nginx, MariaDB)."""
|
|
1448
|
+
"""Install or setup the selected service (PMA, Mailpit, Nginx, MariaDB, Redis)."""
|
|
1389
1449
|
svc_table = self.query_one("#table-services", DataTable)
|
|
1390
1450
|
if svc_table.cursor_row is not None and svc_table.row_count > 0:
|
|
1391
1451
|
row_key = svc_table.coordinate_to_cell_key((svc_table.cursor_row, 0)).row_key
|
|
@@ -1396,34 +1456,41 @@ class NdevDashboard(App):
|
|
|
1396
1456
|
self.notify("Please select a service row from the table.", severity="warning")
|
|
1397
1457
|
return
|
|
1398
1458
|
|
|
1399
|
-
|
|
1459
|
+
if key in ["nginx", "mariadb", "redis"]:
|
|
1460
|
+
self.ensure_sudo(
|
|
1461
|
+
f"Install {key}",
|
|
1462
|
+
lambda pwd: self._install_service_worker(key, password=pwd)
|
|
1463
|
+
)
|
|
1464
|
+
else:
|
|
1465
|
+
self._install_service_worker(key)
|
|
1400
1466
|
|
|
1401
1467
|
@work(exclusive=True)
|
|
1402
|
-
async def _install_service_worker(self, key: str) -> None:
|
|
1468
|
+
async def _install_service_worker(self, key: str, password: Optional[str] = None) -> None:
|
|
1403
1469
|
self.query_one(TabbedContent).active = "tab-logs"
|
|
1404
1470
|
self.log_message(f"[bold blue]Installing/setting up component '{key}'...[/bold blue]")
|
|
1405
1471
|
self.notify(f"Installing {key}...", severity="information")
|
|
1406
1472
|
|
|
1407
1473
|
def _do() -> None:
|
|
1474
|
+
from ndev.linux.runtime.nginx_helper import run_privileged_command
|
|
1408
1475
|
if key == "pma":
|
|
1409
1476
|
setup_pma()
|
|
1410
1477
|
elif key == "mailpit":
|
|
1411
1478
|
setup_mailpit()
|
|
1412
1479
|
elif key == "redis":
|
|
1413
1480
|
if shutil.which("zypper"):
|
|
1414
|
-
|
|
1481
|
+
run_privileged_command(["zypper", "--non-interactive", "install", "-y", "redis"], password=password, timeout=300)
|
|
1415
1482
|
elif shutil.which("apt") or shutil.which("apt-get"):
|
|
1416
|
-
|
|
1483
|
+
run_privileged_command(["apt-get", "install", "-y", "redis-server"], password=password, timeout=300)
|
|
1417
1484
|
elif key == "nginx":
|
|
1418
1485
|
if shutil.which("zypper"):
|
|
1419
|
-
|
|
1486
|
+
run_privileged_command(["zypper", "--non-interactive", "install", "-y", "nginx"], password=password, timeout=300)
|
|
1420
1487
|
elif shutil.which("apt") or shutil.which("apt-get"):
|
|
1421
|
-
|
|
1488
|
+
run_privileged_command(["apt-get", "install", "-y", "nginx"], password=password, timeout=300)
|
|
1422
1489
|
elif key == "mariadb":
|
|
1423
1490
|
if shutil.which("zypper"):
|
|
1424
|
-
|
|
1491
|
+
run_privileged_command(["zypper", "--non-interactive", "install", "-y", "mariadb", "mariadb-client"], password=password, timeout=300)
|
|
1425
1492
|
elif shutil.which("apt") or shutil.which("apt-get"):
|
|
1426
|
-
|
|
1493
|
+
run_privileged_command(["apt-get", "install", "-y", "mariadb-server"], password=password, timeout=300)
|
|
1427
1494
|
|
|
1428
1495
|
elif key.startswith("mod:"):
|
|
1429
1496
|
mod_name = key.split(":", 1)[1]
|
|
@@ -1522,8 +1589,7 @@ class NdevDashboard(App):
|
|
|
1522
1589
|
if row_key:
|
|
1523
1590
|
self._switch_php_version(row_key)
|
|
1524
1591
|
|
|
1525
|
-
|
|
1526
|
-
async def _handle_selected_service_action(self, action: str) -> None:
|
|
1592
|
+
def _handle_selected_service_action(self, action: str) -> None:
|
|
1527
1593
|
"""Contextually start, stop, or restart the service highlighted in the table."""
|
|
1528
1594
|
svc_table = self.query_one("#table-services", DataTable)
|
|
1529
1595
|
if svc_table.cursor_row is not None and svc_table.row_count > 0:
|
|
@@ -1535,20 +1601,35 @@ class NdevDashboard(App):
|
|
|
1535
1601
|
self.notify("Please select a service row from the table.", severity="warning")
|
|
1536
1602
|
return
|
|
1537
1603
|
|
|
1604
|
+
if key in ["nginx", "mariadb"]:
|
|
1605
|
+
self.ensure_sudo(
|
|
1606
|
+
f"{action.capitalize()} {key}",
|
|
1607
|
+
lambda pwd: self._service_action_worker(key, action, password=pwd)
|
|
1608
|
+
)
|
|
1609
|
+
else:
|
|
1610
|
+
self._service_action_worker(key, action)
|
|
1611
|
+
|
|
1612
|
+
@work(exclusive=True)
|
|
1613
|
+
async def _service_action_worker(self, key: str, action: str, password: Optional[str] = None) -> None:
|
|
1538
1614
|
self.log_message(f"[bold blue]Executing '{action}' on {key}...[/bold blue]")
|
|
1539
1615
|
|
|
1540
1616
|
def _do() -> None:
|
|
1617
|
+
from ndev.linux.runtime.nginx_helper import run_privileged_command
|
|
1541
1618
|
if key == "nginx":
|
|
1542
1619
|
if shutil.which("systemctl"):
|
|
1543
|
-
|
|
1620
|
+
run_privileged_command(["systemctl", action, "nginx"], password=password)
|
|
1544
1621
|
else:
|
|
1545
|
-
|
|
1622
|
+
run_privileged_command(["service", "nginx", action], password=password)
|
|
1546
1623
|
|
|
1547
1624
|
elif key == "mariadb":
|
|
1625
|
+
svc_name = "mariadb"
|
|
1548
1626
|
if shutil.which("systemctl"):
|
|
1549
|
-
subprocess.run(["
|
|
1627
|
+
chk = subprocess.run(["systemctl", "status", "mariadb"], capture_output=True)
|
|
1628
|
+
if chk.returncode == 4:
|
|
1629
|
+
svc_name = "mysql"
|
|
1630
|
+
run_privileged_command(["systemctl", action, svc_name], password=password)
|
|
1550
1631
|
else:
|
|
1551
|
-
|
|
1632
|
+
run_privileged_command(["service", svc_name, action], password=password)
|
|
1552
1633
|
|
|
1553
1634
|
elif key == "pma":
|
|
1554
1635
|
if action == "start":
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ndev-stack
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.13
|
|
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.13"
|
|
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"
|
|
@@ -4,6 +4,7 @@ Includes tests for distro detection, caching, Nginx vhost management, service di
|
|
|
4
4
|
log resolution, and process permission handling.
|
|
5
5
|
"""
|
|
6
6
|
import os
|
|
7
|
+
import subprocess
|
|
7
8
|
import unittest
|
|
8
9
|
from unittest.mock import patch, MagicMock
|
|
9
10
|
from pathlib import Path
|
|
@@ -331,6 +332,52 @@ class TestNginxHelper(unittest.TestCase):
|
|
|
331
332
|
self.assertTrue(removed)
|
|
332
333
|
self.assertFalse(conf_file.exists())
|
|
333
334
|
|
|
335
|
+
def test_run_privileged_command(self):
|
|
336
|
+
with patch("subprocess.run") as mock_run:
|
|
337
|
+
mock_run.return_value = subprocess.CompletedProcess(args=["systemctl", "status"], returncode=0)
|
|
338
|
+
with patch("os.geteuid", return_value=1000):
|
|
339
|
+
with patch("ndev.linux.runtime.nginx_helper.validate_sudo_password") as mock_val:
|
|
340
|
+
nginx_helper.run_privileged_command(["systemctl", "status", "nginx"], password="secret")
|
|
341
|
+
mock_val.assert_called_once_with("secret")
|
|
342
|
+
mock_run.assert_called_once()
|
|
343
|
+
self.assertEqual(mock_run.call_args[0][0], ["sudo", "-n", "systemctl", "status", "nginx"])
|
|
344
|
+
|
|
345
|
+
def test_is_selinux_enforcing_and_permissive(self):
|
|
346
|
+
with patch.object(Path, "exists", return_value=True):
|
|
347
|
+
with patch.object(Path, "read_text", return_value="1\n"):
|
|
348
|
+
self.assertTrue(nginx_helper.is_selinux_enforcing())
|
|
349
|
+
|
|
350
|
+
with patch("ndev.linux.runtime.nginx_helper.is_selinux_enforcing", return_value=True):
|
|
351
|
+
with patch("shutil.which", return_value="/usr/bin/seinfo"):
|
|
352
|
+
with patch("subprocess.run") as mock_run:
|
|
353
|
+
mock_run.return_value = subprocess.CompletedProcess(args=[], returncode=0, stdout="Permissive Types: 2\n httpd_t\n xrdp_t\n")
|
|
354
|
+
self.assertTrue(nginx_helper.is_selinux_httpd_permissive())
|
|
355
|
+
|
|
356
|
+
mock_run.return_value = subprocess.CompletedProcess(args=[], returncode=0, stdout="Permissive Types: 1\n xrdp_t\n")
|
|
357
|
+
self.assertFalse(nginx_helper.is_selinux_httpd_permissive())
|
|
358
|
+
|
|
359
|
+
def test_ensure_selinux_httpd_permissions(self):
|
|
360
|
+
with patch("ndev.linux.runtime.nginx_helper.is_selinux_enforcing", return_value=True):
|
|
361
|
+
with patch("ndev.linux.runtime.nginx_helper.is_selinux_httpd_permissive", side_effect=[False, True]):
|
|
362
|
+
with patch("ndev.linux.runtime.nginx_helper.run_privileged_command") as mock_priv:
|
|
363
|
+
res = nginx_helper.ensure_selinux_httpd_permissions(password="pwd")
|
|
364
|
+
self.assertTrue(res)
|
|
365
|
+
mock_priv.assert_called()
|
|
366
|
+
|
|
367
|
+
def test_ensure_docroot_accessible_ancestors(self):
|
|
368
|
+
home_dir = self.test_dir / "userhome"
|
|
369
|
+
sub_dir = home_dir / "opt" / "Sites" / "app" / "public"
|
|
370
|
+
sub_dir.mkdir(parents=True)
|
|
371
|
+
# Remove other execute
|
|
372
|
+
os.chmod(home_dir, 0o700)
|
|
373
|
+
os.chmod(home_dir / "opt", 0o700)
|
|
374
|
+
|
|
375
|
+
with patch("pathlib.Path.home", return_value=home_dir):
|
|
376
|
+
with patch.dict(os.environ, {}, clear=True):
|
|
377
|
+
nginx_helper.ensure_docroot_accessible(sub_dir)
|
|
378
|
+
self.assertTrue(bool(home_dir.stat().st_mode & 0o001))
|
|
379
|
+
self.assertTrue(bool((home_dir / "opt").stat().st_mode & 0o001))
|
|
380
|
+
|
|
334
381
|
|
|
335
382
|
class TestSocketsAndProcess(unittest.TestCase):
|
|
336
383
|
def test_get_major_minor_safe(self):
|
|
@@ -502,5 +549,35 @@ class TestTuiSelectEventHandling(unittest.TestCase):
|
|
|
502
549
|
app._switch_php_version.assert_called_once_with("8.4.27")
|
|
503
550
|
|
|
504
551
|
|
|
552
|
+
class TestTuiEnsureSudo(unittest.TestCase):
|
|
553
|
+
def test_ensure_sudo_when_not_needed(self):
|
|
554
|
+
from ndev.linux.tui import NdevDashboard
|
|
555
|
+
app = NdevDashboard()
|
|
556
|
+
app._needs_sudo = MagicMock(return_value=False)
|
|
557
|
+
app._sudo_password = "cached_password"
|
|
558
|
+
|
|
559
|
+
callback = MagicMock()
|
|
560
|
+
app.ensure_sudo("Test Action", callback)
|
|
561
|
+
callback.assert_called_once_with("cached_password")
|
|
562
|
+
|
|
563
|
+
def test_ensure_sudo_prompts_modal_when_needed(self):
|
|
564
|
+
from ndev.linux.tui import NdevDashboard
|
|
565
|
+
app = NdevDashboard()
|
|
566
|
+
app._needs_sudo = MagicMock(return_value=True)
|
|
567
|
+
app.push_screen = MagicMock()
|
|
568
|
+
|
|
569
|
+
callback = MagicMock()
|
|
570
|
+
app.ensure_sudo("Privileged Action", callback)
|
|
571
|
+
|
|
572
|
+
app.push_screen.assert_called_once()
|
|
573
|
+
modal, on_dismiss = app.push_screen.call_args[0]
|
|
574
|
+
self.assertEqual(modal.action_name, "Privileged Action")
|
|
575
|
+
|
|
576
|
+
# Simulate user providing password
|
|
577
|
+
on_dismiss("new_password")
|
|
578
|
+
self.assertEqual(app._sudo_password, "new_password")
|
|
579
|
+
callback.assert_called_once_with("new_password")
|
|
580
|
+
|
|
581
|
+
|
|
505
582
|
if __name__ == "__main__":
|
|
506
583
|
unittest.main()
|
|
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
|