Skip to content

Commit cfa4860

Browse files
committed
feat(hzforge): implement upgrade-trac subcommand + _upgrade_trac_env
Real implementation of the per-env upgrade orchestrator stubbed in the earlier "stub _upgrade_trac_env" commit. Scope today: items 1 & 2 from the stub's docstring (legacy macro cleanup + trac.ini sanity). Items 3 & 4 (Trac 1.6 DB schema walk db29 -> db45 + Py2-plugin quarantine across that walk) still gate on a Py3.11 + Trac 1.6 install. New helpers: - _macros_universal_installed(): probes `python2 -c 'import hubzero_macros'` and python3 likewise; True iff either succeeds. - _upgrade_trac_env(env_path): for each `<env>/plugins/{image,link}.py`, move to `.disabled` via _rename() if the universal hubzero_macros is installed (otherwise warn and leave in place). Parses <env>/conf/trac.ini and warns on any `[components]` entry starting with `image.` or `link.` (case preserved via optionxform=str -- the [components] keys are module/class paths and are case-sensitive). - cmd_upgrade_trac(): enumerates envs (positional ENV args or default = all envs with conf/trac.ini), runs _upgrade_trac_env per env, and lets apply_changes() do a graceful httpd reload if anything moved. New CLI: `hzforge upgrade-trac [ENV ...]` -- runs the per-env upgrade. main() collects args.envs (comma-joined supported like services). 7 new pytest cases (now 32 total). Discovered while testing on the live host: ALL 66 trac envs carry the legacy image.py/link.py copies (the hubzero-forge installtool's drop-in template fired for every env, not just the 5 the earlier ls sample showed). The Stage 2 work, when it lands, will append DB-schema-walk + plugin- quarantine logic to _upgrade_trac_env -- the docstring of the today- implemented portion sketches the planned shape.
1 parent 1ab7d36 commit cfa4860

2 files changed

Lines changed: 188 additions & 29 deletions

File tree

hzforge.py

Lines changed: 103 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"""
3939

4040
import argparse
41+
import configparser
4142
import grp
4243
import os
4344
import pwd
@@ -1305,36 +1306,96 @@ def repair():
13051306
doctor()
13061307

13071308

1309+
def _macros_universal_installed():
1310+
"""True iff the universal `hubzero_macros` plugin is importable by some
1311+
Python available on this host (Py2 today; Py3 after Stage 2). When yes,
1312+
per-env legacy `image.py`/`link.py` copies are safe to disable because
1313+
the universal plugin keeps serving the same `[[image …]]` / `[[link …]]`
1314+
macros."""
1315+
for py in ("python2", "python3"):
1316+
if _ok([py, "-c", "import hubzero_macros"]):
1317+
return True
1318+
return False
1319+
1320+
13081321
def _upgrade_trac_env(env_path):
1309-
"""STUB -- per-env Trac upgrade tasks (not yet implemented).
1310-
1311-
When this lands it will own bringing a single Trac env up to current
1312-
expectations. Planned scope:
1313-
1314-
1. **Legacy macro cleanup.** Remove the stale per-env macros at
1315-
`<env>/plugins/{image,link}.py` (forge dropped these into envs at
1316-
tool-create time; the universal `hubzero_macros` plugin replaces them
1317-
system-wide and the per-env copies become redundant once it is
1318-
pip-installed).
1319-
2. **Plugin installation/config verification.** Confirm the expected
1320-
universal Trac plugins (`hubzeroplugin` from `hubzero-trac-mysqlauthz`,
1321-
`hubzero_macros`) are discoverable via `pkg_resources.iter_entry_points`
1322-
at the current pinned versions, and that `<env>/conf/trac.ini` does
1323-
not have stale `[components]` enable/disable lines pointing at removed
1324-
per-env modules.
1325-
3. **(Stage 2) DB schema walk** db29 -> db45 against the per-env SQLite
1326-
via a headless Py3 Trac 1.6 (`Environment(env_path).needs_upgrade()`
1327-
then `DatabaseManager(env).upgrade(45)`, then `WikiAdmin._do_upgrade()`),
1328-
with a `cp -a <env> <env>.bak-<ts>` backup beforehand.
1329-
4. **(Stage 2) Plugin quarantine across the schema walk** -- move
1330-
Py2-only eggs aside before Trac 1.6 boots over the env (component
1331-
discovery would otherwise crash on `import ConfigParser`/`<>`), then
1332-
restore the Py3-ported wheels afterwards.
1333-
1334-
Today this function is a no-op placeholder; it will be wired to a
1335-
`hzforge upgrade-trac` subcommand when the scope above is implemented.
1336-
"""
1337-
pass # TODO: implement per the docstring above
1322+
"""Per-env Trac upgrade: legacy macro cleanup + trac.ini sanity.
1323+
1324+
Today (items 1 & 2 of the original scope):
1325+
* Move any `<env>/plugins/{image,link}.py` left over from forge's
1326+
`installtool` to `<file>.disabled` -- BUT only when the universal
1327+
`hubzero_macros` plugin is installed (a Python on the host can
1328+
`import hubzero_macros`), so the env keeps serving those macros.
1329+
Otherwise just warn and leave them in place.
1330+
* Read `<env>/conf/trac.ini` and warn on any `[components]` entry
1331+
starting with `image.` or `link.` -- those mask the universal
1332+
plugin's `hubzero_macros.image`/`.link` modules and want cleanup.
1333+
1334+
Stage 2 (DB schema walk db29 -> db45, Py2-plugin quarantine across the
1335+
walk) is still pending; it gates on a Py3.11 + Trac 1.6 install."""
1336+
name = os.path.basename(env_path)
1337+
step(f"Upgrade trac env: {name}")
1338+
if not os.path.isdir(env_path):
1339+
warn(f" {env_path}: not a directory")
1340+
return
1341+
1342+
universal_ok = _macros_universal_installed()
1343+
plugins_dir = os.path.join(env_path, "plugins")
1344+
if os.path.isdir(plugins_dir):
1345+
for legacy in ("image.py", "link.py"):
1346+
src = os.path.join(plugins_dir, legacy)
1347+
if not os.path.isfile(src):
1348+
continue
1349+
if universal_ok:
1350+
_rename(src, src + ".disabled")
1351+
else:
1352+
warn(f" legacy {plugins_dir}/{legacy} present; install "
1353+
"hubzero-trac-macros first, then re-run upgrade-trac")
1354+
1355+
trac_ini = os.path.join(env_path, "conf", "trac.ini")
1356+
if os.path.isfile(trac_ini):
1357+
try:
1358+
cp = configparser.RawConfigParser()
1359+
cp.optionxform = str # `[components]` keys are case-sensitive
1360+
cp.read(trac_ini)
1361+
if cp.has_section("components"):
1362+
for opt, val in cp.items("components"):
1363+
if opt.startswith(("image.", "link.")):
1364+
warn(f" {trac_ini} [components] {opt} = {val} "
1365+
"(legacy per-env macro reference; safe to remove)")
1366+
except (configparser.Error, OSError) as e:
1367+
warn(f" could not read {trac_ini}: {e}")
1368+
log(" done")
1369+
1370+
1371+
def cmd_upgrade_trac():
1372+
"""`hzforge upgrade-trac [env ...]` -- run _upgrade_trac_env() per env.
1373+
With no positional ENV args, runs across every env under /opt/trac/tools/
1374+
(a subdir containing conf/trac.ini)."""
1375+
tools_dir = OPT["trac_tools"][0]
1376+
if not os.path.isdir(tools_dir):
1377+
die(f"trac not configured (no {tools_dir})")
1378+
all_envs = sorted(
1379+
d for d in os.listdir(tools_dir)
1380+
if os.path.isdir(os.path.join(tools_dir, d))
1381+
and os.path.isfile(os.path.join(tools_dir, d, "conf", "trac.ini"))
1382+
)
1383+
requested = list(getattr(ARGS, "envs", None) or [])
1384+
if requested:
1385+
bad = [e for e in requested if e not in all_envs]
1386+
if bad:
1387+
die(f"unknown env(s): {', '.join(bad)} "
1388+
f"(have: {', '.join(all_envs) or '(none)'})")
1389+
targets = requested
1390+
else:
1391+
targets = all_envs
1392+
step(f"Upgrade {len(targets)} trac env(s): {','.join(targets) or '(none)'}")
1393+
for env_name in targets:
1394+
_upgrade_trac_env(os.path.join(tools_dir, env_name))
1395+
# Anything _rename()d set CTX.config_changed; apply_changes() does the
1396+
# graceful httpd reload so the mod_wsgi daemon picks up the per-env
1397+
# plugin changes. (No-op if nothing was actually moved.)
1398+
apply_changes()
13381399

13391400

13401401
# ---------------------------------------------------------------------------- #
@@ -1383,6 +1444,10 @@ def build_parser():
13831444
help="create throwaway projects and verify each service serves")
13841445
pt.add_argument("services", nargs="*", metavar="SERVICE",
13851446
help=svc_help + " to test; default: all configured")
1447+
pug = sub.add_parser("upgrade-trac", parents=[common],
1448+
help="per-env Trac upgrade (legacy macro cleanup + trac.ini sanity)")
1449+
pug.add_argument("envs", nargs="*", metavar="ENV",
1450+
help="trac envs to upgrade (under /opt/trac/tools); default: all configured")
13861451
return p
13871452

13881453

@@ -1417,6 +1482,13 @@ def main():
14171482
if args.command == "uninstall" and not services:
14181483
die("uninstall needs at least one service: " + " ".join(ALL_SERVICES))
14191484
args.services = services
1485+
# envs (positional on upgrade-trac); allow comma-joined like services.
1486+
# Not validated against ALL_SERVICES -- envs are arbitrary subdirs of
1487+
# /opt/trac/tools/ and the existence check happens in cmd_upgrade_trac().
1488+
envs = []
1489+
for tok in (getattr(args, "envs", None) or []):
1490+
envs += [e for e in tok.split(",") if e]
1491+
args.envs = envs
14201492
ARGS = args
14211493

14221494
print("=" * 72)
@@ -1437,6 +1509,8 @@ def main():
14371509
repair()
14381510
elif args.command == "test":
14391511
cmd_test()
1512+
elif args.command == "upgrade-trac":
1513+
cmd_upgrade_trac()
14401514

14411515
step("Done")
14421516
for n in CTX.notes:

tests/test_hzforge.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,3 +179,88 @@ def test_ldap_bindpw_inline_warns(monkeypatch):
179179
def test_trac_spec_exact_version(spec, expected, monkeypatch):
180180
monkeypatch.setattr(hz, "ARGS", make_args(trac_spec=spec), raising=False)
181181
assert hz._trac_spec_exact_version() == expected
182+
183+
184+
# --- upgrade-trac: _macros_universal_installed + _upgrade_trac_env ---
185+
186+
def test_macros_universal_installed_true_if_any_python_can_import(monkeypatch):
187+
monkeypatch.setattr(hz, "_ok", lambda cmd: cmd[0] == "python2")
188+
assert hz._macros_universal_installed() is True
189+
190+
191+
def test_macros_universal_installed_false_when_neither_python_can_import(monkeypatch):
192+
monkeypatch.setattr(hz, "_ok", lambda cmd: False)
193+
assert hz._macros_universal_installed() is False
194+
195+
196+
def test_upgrade_trac_env_warns_about_legacy_macros_when_universal_missing(tmp_path, monkeypatch):
197+
monkeypatch.setattr(hz, "CTX", hz.Ctx(False), raising=False)
198+
monkeypatch.setattr(hz, "_macros_universal_installed", lambda: False)
199+
env = tmp_path / "histogram"
200+
plugins = env / "plugins"
201+
plugins.mkdir(parents=True)
202+
(plugins / "image.py").write_text("# legacy image macro")
203+
(plugins / "link.py").write_text("# legacy link macro")
204+
hz._upgrade_trac_env(str(env))
205+
# universal missing -> files NOT moved
206+
assert (plugins / "image.py").is_file()
207+
assert (plugins / "link.py").is_file()
208+
# ... but warnings name both, and reference the hubzero-trac-macros install:
209+
notes_text = "\n".join(hz.CTX.notes)
210+
assert "image.py" in notes_text
211+
assert "link.py" in notes_text
212+
assert "install hubzero-trac-macros" in notes_text
213+
214+
215+
def test_upgrade_trac_env_disables_legacy_macros_when_universal_present(tmp_path, monkeypatch):
216+
monkeypatch.setattr(hz, "CTX", hz.Ctx(False), raising=False)
217+
monkeypatch.setattr(hz, "_macros_universal_installed", lambda: True)
218+
env = tmp_path / "wiki"
219+
plugins = env / "plugins"
220+
plugins.mkdir(parents=True)
221+
(plugins / "image.py").write_text("# legacy image macro")
222+
(plugins / "link.py").write_text("# legacy link macro")
223+
hz._upgrade_trac_env(str(env))
224+
# originals moved aside -> .disabled siblings remain
225+
assert not (plugins / "image.py").exists()
226+
assert (plugins / "image.py.disabled").is_file()
227+
assert not (plugins / "link.py").exists()
228+
assert (plugins / "link.py.disabled").is_file()
229+
230+
231+
def test_upgrade_trac_env_dry_run_does_not_move_files(tmp_path, monkeypatch):
232+
monkeypatch.setattr(hz, "CTX", hz.Ctx(True), raising=False) # dry=True
233+
monkeypatch.setattr(hz, "_macros_universal_installed", lambda: True)
234+
env = tmp_path / "x"
235+
plugins = env / "plugins"
236+
plugins.mkdir(parents=True)
237+
(plugins / "image.py").write_text("# legacy")
238+
hz._upgrade_trac_env(str(env))
239+
assert (plugins / "image.py").is_file() # still here
240+
assert not (plugins / "image.py.disabled").exists() # not moved
241+
242+
243+
def test_upgrade_trac_env_warns_about_stale_components_in_trac_ini(tmp_path, monkeypatch):
244+
monkeypatch.setattr(hz, "CTX", hz.Ctx(False), raising=False)
245+
monkeypatch.setattr(hz, "_macros_universal_installed", lambda: True)
246+
env = tmp_path / "histogram"
247+
conf = env / "conf"
248+
conf.mkdir(parents=True)
249+
(conf / "trac.ini").write_text(
250+
"[components]\n"
251+
"image.* = enabled\n"
252+
"link.linkMacro = enabled\n"
253+
"trac.* = enabled\n"
254+
)
255+
hz._upgrade_trac_env(str(env))
256+
notes_text = "\n".join(hz.CTX.notes)
257+
assert "image.*" in notes_text
258+
assert "link.linkMacro" in notes_text
259+
assert "trac.*" not in notes_text # non-legacy entry not flagged
260+
261+
262+
def test_upgrade_trac_env_handles_missing_dir(tmp_path, monkeypatch):
263+
monkeypatch.setattr(hz, "CTX", hz.Ctx(False), raising=False)
264+
monkeypatch.setattr(hz, "_macros_universal_installed", lambda: False)
265+
hz._upgrade_trac_env(str(tmp_path / "nonexistent"))
266+
assert any("not a directory" in n for n in hz.CTX.notes)

0 commit comments

Comments
 (0)