1 parent
da4f047c37
commit
f677ec82cf
21 files changed
+1746
-1226
No files matched your search
Vendored
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"workbench.colorCustomizations": {
|
||||
"activityBar.activeBackground": "#2f7c47",
|
||||
"activityBar.background": "#2f7c47",
|
||||
"activityBar.foreground": "#e7e7e7",
|
||||
"activityBar.inactiveForeground": "#e7e7e799",
|
||||
"activityBarBadge.background": "#422c74",
|
||||
"activityBarBadge.foreground": "#e7e7e7",
|
||||
"activityBarTop.activeBackground": "#2f7c47",
|
||||
"activityBarTop.background": "#2f7c47",
|
||||
"activityBarTop.foreground": "#e7e7e7",
|
||||
"activityBarTop.inactiveForeground": "#e7e7e799",
|
||||
"commandCenter.border": "#e7e7e799",
|
||||
"commandCenter.foreground": "#e7e7e7",
|
||||
"sash.hoverBorder": "#2f7c47",
|
||||
"statusBar.background": "#215732",
|
||||
"statusBar.debuggingBackground": "#215732",
|
||||
"statusBar.debuggingForeground": "#e7e7e7",
|
||||
"statusBar.foreground": "#e7e7e7",
|
||||
"statusBarItem.hoverBackground": "#2f7c47",
|
||||
"statusBarItem.remoteBackground": "#1b122e",
|
||||
"statusBarItem.remoteForeground": "#e7e7e7",
|
||||
"titleBar.activeBackground": "#215732",
|
||||
"titleBar.activeForeground": "#e7e7e7",
|
||||
"titleBar.inactiveBackground": "#21573299",
|
||||
"titleBar.inactiveForeground": "#e7e7e799"
|
||||
},
|
||||
"peacock.remoteColor": "#215732"
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
+232
@@ -19,6 +19,7 @@ to quit.
|
||||
- [Window tiling (gTile)](#window-tiling-gtile)
|
||||
- [Compose key — accents & Greek letters](#compose-key--accents--greek-letters)
|
||||
- [Screen & session behavior](#screen--session-behavior)
|
||||
- [Scripts in this directory](#scripts-in-this-directory)
|
||||
|
||||
(Links above are for viewing this file in an editor/browser that
|
||||
supports Markdown anchors, e.g. VS Code or GitHub/GitLab — the
|
||||
@@ -132,3 +133,234 @@ missing or accidentally edited); defaults come from
|
||||
for the session (`kerberos_ticket_renew.sh`).
|
||||
- The local pixi Python env in `~/bpy` is kept in sync with the
|
||||
shared env in the background (`pixi_local_sync.sh`).
|
||||
|
||||
|
||||
<!-- BEGIN SCRIPT INDEX (generated by list_bin_commands) -->
|
||||
|
||||
## Scripts in this directory
|
||||
|
||||
Every script in `/sf/bernina/bin`, grouped by topic. Generated by `list_bin_commands` from the actual directory contents -- re-run it after adding/removing scripts to keep this in sync.
|
||||
|
||||
### Cheat sheet & help
|
||||
|
||||
| Command | Description |
|
||||
|---|---|
|
||||
| `cheatsheet` | Show this cheat sheet in a terminal (glow + less, full detail) |
|
||||
| `cheatsheet_overlay` | Show this cheat sheet as a floating overlay panel (bound to Super+F1) |
|
||||
| `cheatsheet_to_shortcuts_json` | Regenerate the overlay's JSON from this file's headings/tables |
|
||||
| `check_kerberos_renewal` | Diagnose why the Kerberos ticket renewer might be renewing/prompting too often |
|
||||
| `useful_commands` | Show the older, plain-text useful-commands reference |
|
||||
|
||||
### EPICS / eco environment
|
||||
|
||||
| Command | Description |
|
||||
|---|---|
|
||||
| `eco` | Start eco, the beamline's Python control environment |
|
||||
| `eco-dev` | Start eco from the development checkout (symlink into the eco repo) |
|
||||
| `eco-in` | Start eco inside a chosen environment (arg, default bpy-default) |
|
||||
| `eco-notlazy` | Start eco with lazy-loading disabled (default env bernina37) |
|
||||
| `eco-status-server` | Start/stop/check the eco status server (namespace mode) |
|
||||
| `eco-status-server-install-user-service` | Install the eco status server as a systemd --user service |
|
||||
| `eco-test` | Start eco against the sfb312 test micromamba environment |
|
||||
| `eco_bams_bpms` | Start eco configured for BAM/BPM work |
|
||||
| `eco_historic` | Start eco against historic/legacy config |
|
||||
| `eco_mm_env` | Start eco in a chosen micromamba environment |
|
||||
| `eco_new` | Start eco using bpy-default.sh directly |
|
||||
| `eco_noenv` | Start eco's startup.py directly, no environment activation step |
|
||||
| `eco_old` | Start eco against the older sfb312 micromamba environment |
|
||||
| `eco_roman` | Start eco configured for the bernina_robot setup |
|
||||
| `ecotest` | Start eco in the jungfrau_client conda environment |
|
||||
|
||||
### Python, conda & pixi environments
|
||||
|
||||
| Command | Description |
|
||||
|---|---|
|
||||
| `anaconda_bernina_env` | Conda init hook for the bernina anaconda env (source this) |
|
||||
| `anaconda_env` | Activate a named anaconda environment |
|
||||
| `anaconda_manual` | Show the anaconda environment manual (rendered markdown) |
|
||||
| `anaconda_testenv` | Activate the anaconda test environment |
|
||||
| `bpy` | Activate a named bpy (pixi) environment |
|
||||
| `bpy-default.sh` | Set PATH for the default shared bpy312 pixi environment (source this) |
|
||||
| `bpy-dev.sh` | Set PATH for the bpy312-dev pixi environment (source this) |
|
||||
| `bpy-env` | Activate the Bernina pixi Python env -- local per-host mirror if in sync, else shared (source this) |
|
||||
| `bpy-select` | Interactively pick/switch the active bpy pixi environment |
|
||||
| `micromamba` | micromamba package manager |
|
||||
| `mm_bernina` | Micromamba shell-init hook for bernina (source this) |
|
||||
| `mm_bernina_env` | Activate a named micromamba environment |
|
||||
| `pixi` | pixi package manager |
|
||||
| `pixi_local_sync.sh` | Keep the local per-host pixi env mirror in sync (runs in background at login) |
|
||||
| `pixi_rebuild_local.sh` | Rebuild the bernina pixi env in an isolated local sandbox, for testing |
|
||||
|
||||
### Jupyter & notebooks
|
||||
|
||||
| Command | Description |
|
||||
|---|---|
|
||||
| `bernina-jupyter` | Start JupyterLab in the bernina-scripts config dir (bpy env) |
|
||||
| `ipylab` | Start an ipylab session in a chosen anaconda environment |
|
||||
| `jupyter-notebook-on-ra` | Open a Jupyter Notebook on a remote-access host, for a given user |
|
||||
| `jupyterlab-on-ra` | Open JupyterLab on a remote-access host, for a given user |
|
||||
|
||||
### Browsers
|
||||
|
||||
| Command | Description |
|
||||
|---|---|
|
||||
| `chrome` | Open Chrome with a per-host profile |
|
||||
| `chromeAs` | Open another user's Chrome profile from your own desktop (older variant) |
|
||||
| `chrome_as` | Open another user's Chrome profile from your own desktop |
|
||||
| `firefox` | Open Firefox with a per-host profile |
|
||||
| `firefox-create-hostprofile` | Create a fresh per-host Firefox profile |
|
||||
| `firefox-hostprofile` | Open Firefox with the per-host profile (alt entrypoint) |
|
||||
| `firefox_as` | Open another user's default Firefox profile from your own desktop |
|
||||
| `firefox_default` | Default Firefox profile launcher (symlink to firefox_shared_profile) |
|
||||
| `firefox_shared_profile` | Open Firefox using a shared/synced profile |
|
||||
| `google-chrome` | Open Chrome with a per-host profile (alt entrypoint) |
|
||||
|
||||
### Displays & control panels
|
||||
|
||||
| Command | Description |
|
||||
|---|---|
|
||||
| `Aramis` | Open the Aramis overview display (mydm/medm) |
|
||||
| `alarms_caqtdm` | Open the EPICS alarms caQtDM panel |
|
||||
| `aramis-home` | Open the Aramis photonics overview caQtDM panel |
|
||||
| `bernina-home` | Open the esbMaster caQtDM panel |
|
||||
| `cams_sf` | Open the SwissFEL cameras caQtDM panel |
|
||||
| `diffractometer-config` | Open the diffractometer motor IOC configuration tool |
|
||||
| `motion-home` | Open the ZamofingMaster motion caQtDM panel |
|
||||
| `screen_panel` | Open the SF controls screen panel |
|
||||
| `screenpanel` | Open the screen panel (uses screenpanel-sf if available) |
|
||||
| `sf_archiver_panel` | Open the SwissFEL archiver viewer panel |
|
||||
| `strip_chart` | Open the PShell strip chart viewer |
|
||||
| `swissfelStatus` | Open the SwissFEL OP overview caQtDM panel |
|
||||
| `thc_angles` | Open the THC angles bokeh app in Firefox |
|
||||
| `thc_stripchart` | Open the THC temperature strip chart |
|
||||
| `timetool_online_plot` | Open the timetool online plot (voila notebook) |
|
||||
|
||||
### Cameras
|
||||
|
||||
| Command | Description |
|
||||
|---|---|
|
||||
| `bernina-cam-mobile1` | View the mobile1 camera feed (VLC) |
|
||||
| `bernina-cam-n` | View the north camera feed (VLC) |
|
||||
| `bernina-cam-s` | View the south camera feed (VLC) |
|
||||
| `bernina-cam-w` | View the west camera feed (VLC) |
|
||||
| `bernina-cam-wideangle1` | View wide-angle camera 1 feed (VLC) |
|
||||
| `bernina-cam-wideangle2` | View wide-angle camera 2 feed (VLC) |
|
||||
| `bernina-cam-wideangle3` | View wide-angle camera 3 feed (VLC) |
|
||||
| `bernina-cam-wideangle4` | View wide-angle camera 4 feed (VLC) |
|
||||
| `bernina-cams-fix` | Launch the fixed multi-camera mosaic view (VLC) |
|
||||
| `monitoring_cameras` | Open the monitoring cameras panel (pshell-based) |
|
||||
| `record_camera` | Record a camera feed to file |
|
||||
|
||||
### FEL & laser control
|
||||
|
||||
| Command | Description |
|
||||
|---|---|
|
||||
| `feloff` | Stop/block the FEL beam and announce it |
|
||||
| `felon` | Open/enable the FEL beam and announce it |
|
||||
| `laseroff` | Block the laser shutter and announce it |
|
||||
| `laseron` | Open the laser shutter and announce it |
|
||||
|
||||
### pshell & robot control
|
||||
|
||||
| Command | Description |
|
||||
|---|---|
|
||||
| `pshell_be` | Start the PShell Workbench for the main Bernina beamline environment |
|
||||
| `pshell_correlation` | Open the correlation panel |
|
||||
| `pshell_default` | Default pshell entrypoint (symlink to pshell_be) |
|
||||
| `robot_server` | Start the PShell Workbench for the Bernina robot -- needs a new binary, /opt/gfa/pshell is gone (see chat history: /sf/controls/bin/pshell -wb is the likely replacement, blocked on a dataFormat=null config issue) |
|
||||
|
||||
### Desktop & window management
|
||||
|
||||
| Command | Description |
|
||||
|---|---|
|
||||
| `code` | Launch VS Code |
|
||||
| `darkscreens` | Blank/turn off the screens (xset dpms) |
|
||||
| `flameshot` | Take a screenshot |
|
||||
| `gnome-terminal_as` | Open a GNOME Terminal as another user |
|
||||
| `launcher` | Main Bernina application launcher |
|
||||
| `launcher_dev` | Application launcher (dev/legacy python 3.5 environment) |
|
||||
| `launcher_lca` | Open the launcher on the machine/LCA network, for a given user |
|
||||
| `launcher_legacy` | Legacy application launcher (python 3.5 environment) |
|
||||
| `rdpWater` | RDP into the WaterLab Windows machine |
|
||||
| `remoteDesktop` | RDP into an arbitrary PSI Windows host |
|
||||
| `sf-launcher` | Open the launcher on the sf network, for a given user |
|
||||
| `termAs` | Open a terminal as another user |
|
||||
| `terminalAs` | Open a terminal as another user (alt entrypoint) |
|
||||
| `window_arrange.py` | Arrange/tile windows helper script |
|
||||
| `winterm` | Open a Windows-side terminal session |
|
||||
|
||||
### Applications & viewers
|
||||
|
||||
| Command | Description |
|
||||
|---|---|
|
||||
| `hdfview` | Open HDFView (HDF5 file viewer) |
|
||||
| `jungfrau_console` | Open the Jungfrau detector console |
|
||||
| `ncdu` | Disk usage analyzer |
|
||||
| `paraview` | Open ParaView |
|
||||
| `pycharm` | Open PyCharm Community |
|
||||
| `pylote` | Open Pylote |
|
||||
| `vesta` | Open VESTA (crystal structure viewer) |
|
||||
|
||||
### Login session & keybinding setup
|
||||
|
||||
| Command | Description |
|
||||
|---|---|
|
||||
| `create-links-in-home` | Force-apply PSI's Partially Shared Home symlinks now |
|
||||
| `gogh` | Gogh terminal color-scheme installer (legacy -- upstream download is broken, see install_terminal_profiles) |
|
||||
| `install_gtile` | Install/update and enable the gTile gnome-shell extension (runs at login) |
|
||||
| `install_terminal_profiles` | Install Solarized/Spacedust GNOME Terminal profiles, set Spacedust default (runs at login) |
|
||||
| `kerberos_ticket_renew.sh` | Keep the Kerberos ticket cache alive for the login session (runs in background) |
|
||||
| `migrate_to_network_drive.sh` | Move a local dotfile onto PSI's Partially Shared Home and register it for sync |
|
||||
| `set_gnome_keyboard_shortcut` | Register one GNOME custom keyboard shortcut (used by startup_bernina_terminal) |
|
||||
| `startup_bernina_terminal` | Full per-login setup for a Bernina console -- runs automatically at login |
|
||||
| `sync_personal_config.sh` | Seed .ipython/.condarc onto PSI's Partially Shared Home for cross-host sync |
|
||||
|
||||
### gopass secrets & git
|
||||
|
||||
| Command | Description |
|
||||
|---|---|
|
||||
| `git-push` | git push, forcing the origin remote to SSH first |
|
||||
| `gopass` | Bernina gopass secrets store CLI |
|
||||
| `gopass.py` | Minimal Python interface to the Bernina gopass store |
|
||||
| `gopass_bernina_service_setup.sh` | One-shot setup: give a service account (gac-bernina) access to the gopass store |
|
||||
| `gopass_bernina_setup.sh` | One-shot onboarding for a human PSI account joining the gopass store |
|
||||
|
||||
### Personal dirs & data
|
||||
|
||||
| Command | Description |
|
||||
|---|---|
|
||||
| `create_data_symlinks` | (Re)create the ~/bernina_data convenience symlink |
|
||||
| `create_personal_dir.sh` | Create/verify a personal working directory under /sf/bernina/config/personal/ (your username) |
|
||||
| `name2pgroups` | Look up p-group(s) for a given user name |
|
||||
| `pgroup2name` | Look up the user name for a given p-group |
|
||||
| `quotas-bernina` | Show Bernina's storage quota usage |
|
||||
|
||||
### Beamline status & monitoring
|
||||
|
||||
| Command | Description |
|
||||
|---|---|
|
||||
| `chiller_flowrate` | Query the JF16 chiller flowrate over telnet |
|
||||
| `feedbacks_bernina` | Open the undulator/feedbacks control tool, for a given user |
|
||||
| `get_bernina_PVs_alive` | Check whether a set of Bernina PVs are alive |
|
||||
| `go2daq` | SSH to the SwissFEL DAQ host (sf-daq-3) |
|
||||
| `ioc-control-bernina` | Open the IOC process monitor (PBMonitor) |
|
||||
| `ioc-finder` | Find/locate an EPICS IOC |
|
||||
| `is_control` | Check whether a given hostname is a primary controls terminal |
|
||||
| `phonebook` | Show the Bernina phone list |
|
||||
| `remaining-slurm-job-time` | Show remaining time for a SLURM job |
|
||||
| `restart_service` | Restart a systemd service as the dbe account (sudo) |
|
||||
| `run-and-position.py` | Run-and-position helper script |
|
||||
| `saythis` | Speak given text aloud |
|
||||
| `set_bs_pvs` | Configure the SLAAR21 beam-synchronous PVs |
|
||||
| `slurm_resources` | Show SLURM partition resource usage |
|
||||
| `undulator-pointing` | Open the undulator pointing control tool, for a given user |
|
||||
|
||||
### Desktop entries (.desktop files)
|
||||
|
||||
| Command | Description |
|
||||
|---|---|
|
||||
| `Bernina.desktop` | Bernina launcher desktop icon (runs launcher) |
|
||||
| `bernifox.desktop` | Bernina-branded Firefox launcher (per-host profile), set as default browser |
|
||||
| `kerberos-ticket-renew.desktop` | Autostart entry for the Kerberos ticket renewer |
|
||||
|
||||
<!-- END SCRIPT INDEX -->
|
||||
@@ -95,8 +95,9 @@ def build_shortcuts(header, rows):
|
||||
if row[0]
|
||||
]
|
||||
|
||||
# Shortcut | Action [| Runs] (everything else: app shortcuts, gTile, primary-controls)
|
||||
if ncols >= 2 and header[0].lower().startswith("shortcut"):
|
||||
# Shortcut | Action [| Runs] (app shortcuts, gTile, primary-controls)
|
||||
# Command | Description (the bin/ script index)
|
||||
if ncols >= 2 and header[0].lower().startswith(("shortcut", "command")):
|
||||
return [
|
||||
{"key": row[0], "description": row[1]}
|
||||
for row in rows
|
||||
|
||||
+15
-28
@@ -1,11 +1,11 @@
|
||||
# Personal directories on `/sf/bernina`
|
||||
|
||||
`/sf/bernina/config/personal/<username>` is the Bernina beamline
|
||||
convention for a per-user directory on real, git-friendly NFS storage —
|
||||
as opposed to `~/network_drives`, which is SMB and can't hold a git
|
||||
repository at all (see `migrate_to_network_drive.md`). It's the location
|
||||
the gopass guide (`gopass.md`) uses for a personal gopass clone that's
|
||||
automatically the same on every Bernina host.
|
||||
`/sf/bernina/config/personal/<username>` is the Bernina convention for a
|
||||
per-user directory on real, git-friendly NFS storage — unlike
|
||||
`~/network_drives` (SMB), which can't hold a git repo (see
|
||||
`migrate_to_network_drive.md`). The gopass guide (`gopass.md`) uses it for
|
||||
a personal store clone that's automatically the same on every Bernina
|
||||
host.
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -14,29 +14,16 @@ create_personal_dir.sh # create/verify your own directory
|
||||
create_personal_dir.sh someuser # provision on someone else's behalf (needs root, or ask them to self-serve)
|
||||
```
|
||||
|
||||
Run it again any time — it's safe to re-run, and only changes anything
|
||||
that doesn't already match.
|
||||
Safe to re-run — only changes what doesn't already match.
|
||||
|
||||
## What it does
|
||||
|
||||
1. **Checks the shared parent directory (`/sf/bernina/config/personal/`)
|
||||
allows self-service.** It needs to be group-writable (so anyone in
|
||||
`unx-sf_bernina_bs` can create their own subdirectory) with the sticky
|
||||
bit set (so nobody can rename or delete someone *else's* directory —
|
||||
the same principle `/tmp` uses). If it isn't set up that way yet and
|
||||
you own it, the script offers to fix it (`chmod 3775`,
|
||||
`chgrp unx-sf_bernina_bs`) — this only needs doing once, ever, since
|
||||
1. Makes sure the shared parent (`/sf/bernina/config/personal/`) is
|
||||
group-writable with the sticky bit set — like `/tmp`: anyone in
|
||||
`unx-sf_bernina_bs` can add their own subdirectory, but nobody can
|
||||
touch anyone else's. Offers to fix it (`chmod 3775`) if you own it and
|
||||
it isn't set up that way yet; only ever needs doing once, since
|
||||
`/sf/bernina` is one NFS export shared by every Bernina host.
|
||||
2. **Creates (or verifies) `<username>`'s own subdirectory**, locked to
|
||||
`2700` — owner-only, nobody else (not even other team members) can
|
||||
list or read into it, despite the parent being browsable.
|
||||
|
||||
## Why this layout
|
||||
|
||||
- Parent `rwxrwxr-x` + sticky: anyone can see who has a personal
|
||||
directory and add their own; nobody can touch anyone else's entry.
|
||||
- Each user's own directory `rwx------`: totally private once inside.
|
||||
|
||||
This is the standard "shared drop-in directory, private subdirectories"
|
||||
pattern — structurally the same idea as `/tmp`, scoped to the beamline
|
||||
group instead of the whole system.
|
||||
2. Creates (or verifies) `<username>`'s own subdirectory, locked to
|
||||
`2700` — owner-only, nobody else can list or read into it even though
|
||||
the parent is browsable.
|
||||
Executable
+183
@@ -0,0 +1,183 @@
|
||||
#!/bin/bash
|
||||
# eco status server (namespace mode) - start/stop/status wrapper.
|
||||
#
|
||||
# Canonical copy lives in the eco repo at eco/status_server/bin/; the copy in
|
||||
# /sf/bernina/bin is installed from there (see install-user-service).
|
||||
#
|
||||
# Runs in the FOREGROUND by default, which is what systemd's Type=simple
|
||||
# wants and what the accompanying user unit calls. Use `-b` to detach for
|
||||
# interactive use.
|
||||
#
|
||||
# Everything is overridable by environment, so the same script serves a
|
||||
# personal checkout, the shared checkout and the systemd unit unchanged:
|
||||
#
|
||||
# ECO_STATUS_SERVER_CHECKOUT eco checkout to run (prepended to PYTHONPATH)
|
||||
# ECO_STATUS_SERVER_CONFIG server config JSON
|
||||
# ECO_STATUS_SERVER_PYTHON interpreter (needs eco's full dependency set)
|
||||
# ECO_STATUS_SERVER_URL what `status`/`stop` talk to
|
||||
# ECO_STATUS_SERVER_LOG log file used by -b
|
||||
#
|
||||
# A file at /sf/bernina/config/eco_status_server/env (or
|
||||
# $ECO_STATUS_SERVER_ENV) is sourced first if it exists, so a site can set
|
||||
# those in one place without editing this script.
|
||||
set -euo pipefail
|
||||
|
||||
ENV_FILE="${ECO_STATUS_SERVER_ENV:-/sf/bernina/config/eco_status_server/env}"
|
||||
# shellcheck source=/dev/null
|
||||
[ -r "$ENV_FILE" ] && . "$ENV_FILE"
|
||||
|
||||
CHECKOUT="${ECO_STATUS_SERVER_CHECKOUT:-/sf/bernina/code/gac-bernina/eco}"
|
||||
CONFIG="${ECO_STATUS_SERVER_CONFIG:-/sf/bernina/config/eco_status_server/bernina_namespace.json}"
|
||||
PYTHON="${ECO_STATUS_SERVER_PYTHON:-/sf/bernina/applications/python/.pixi/envs/bpy312/bin/python}"
|
||||
URL="${ECO_STATUS_SERVER_URL:-http://$(hostname -s):8091}"
|
||||
LOG="${ECO_STATUS_SERVER_LOG:-$HOME/.eco/status_server_$(hostname -s).log}"
|
||||
|
||||
# Matches only the running server, never this script or an ssh command line
|
||||
# that mentions it (the bracket keeps the pattern from matching itself).
|
||||
PGREP_PATTERN='[e]co\.status_server --mode'
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
usage: $(basename "$0") <command> [options]
|
||||
|
||||
start [-b] [-- <extra args>] run the server (foreground; -b to detach)
|
||||
stop stop the running server on this host
|
||||
restart stop, then start detached
|
||||
status query $URL/health
|
||||
wait [timeout] block until the server reports ready
|
||||
logs [-f] show the detached-mode log
|
||||
config print the resolved configuration
|
||||
|
||||
Environment (current values):
|
||||
ECO_STATUS_SERVER_CHECKOUT=$CHECKOUT
|
||||
ECO_STATUS_SERVER_CONFIG=$CONFIG
|
||||
ECO_STATUS_SERVER_PYTHON=$PYTHON
|
||||
ECO_STATUS_SERVER_URL=$URL
|
||||
ECO_STATUS_SERVER_LOG=$LOG
|
||||
EOF
|
||||
}
|
||||
|
||||
die() { echo "$(basename "$0"): $*" >&2; exit 1; }
|
||||
|
||||
check_prereqs() {
|
||||
[ -d "$CHECKOUT/eco" ] || die "no eco checkout at $CHECKOUT (set ECO_STATUS_SERVER_CHECKOUT)"
|
||||
# Not fatal, but almost always the explanation when half the namespace
|
||||
# fails to initialize: without the gateway list, Channel Access falls
|
||||
# back to the local broadcast domain. A systemd unit inherits nothing,
|
||||
# so this is the first thing to check there.
|
||||
if [ -z "${EPICS_CA_ADDR_LIST:-}" ]; then
|
||||
echo "$(basename "$0"): warning: EPICS_CA_ADDR_LIST is not set;" >&2
|
||||
echo " most components will fail to connect. See the EnvironmentFile" >&2
|
||||
echo " written by eco-status-server-install-user-service." >&2
|
||||
fi
|
||||
[ -r "$CONFIG" ] || die "no server config at $CONFIG (set ECO_STATUS_SERVER_CONFIG)"
|
||||
[ -x "$PYTHON" ] || die "no interpreter at $PYTHON (set ECO_STATUS_SERVER_PYTHON)"
|
||||
}
|
||||
|
||||
server_pid() { pgrep -f "$PGREP_PATTERN" | head -1; }
|
||||
|
||||
cmd_start() {
|
||||
local background=0
|
||||
if [ "${1:-}" = "-b" ] || [ "${1:-}" = "--background" ]; then background=1; shift; fi
|
||||
[ "${1:-}" = "--" ] && shift
|
||||
check_prereqs
|
||||
local pid; pid="$(server_pid || true)"
|
||||
[ -n "$pid" ] && die "already running (pid $pid); use 'restart' or 'stop' first"
|
||||
|
||||
export PYTHONPATH="$CHECKOUT${PYTHONPATH:+:$PYTHONPATH}"
|
||||
if [ "$background" -eq 1 ]; then
|
||||
mkdir -p "$(dirname "$LOG")"
|
||||
# </dev/null matters: several eco components prompt for credentials
|
||||
# on stdin during __init__, and a headless server must get EOF
|
||||
# rather than block forever waiting for an answer nobody will type.
|
||||
nohup "$PYTHON" -u -m eco.status_server --mode namespace \
|
||||
--config "$CONFIG" "$@" </dev/null >>"$LOG" 2>&1 &
|
||||
echo "started pid $! -> $LOG"
|
||||
echo "the namespace init takes minutes; watch it with: $(basename "$0") wait"
|
||||
else
|
||||
exec "$PYTHON" -u -m eco.status_server --mode namespace \
|
||||
--config "$CONFIG" "$@" </dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_stop() {
|
||||
local pid; pid="$(server_pid || true)"
|
||||
[ -z "$pid" ] && { echo "not running"; return 0; }
|
||||
kill "$pid"
|
||||
for _ in $(seq 1 30); do
|
||||
sleep 1
|
||||
pgrep -f "$PGREP_PATTERN" >/dev/null || { echo "stopped (was pid $pid)"; return 0; }
|
||||
done
|
||||
die "pid $pid did not exit after 30 s"
|
||||
}
|
||||
|
||||
cmd_status() {
|
||||
local pid; pid="$(server_pid || true)"
|
||||
if [ -n "$pid" ]; then echo "process: running, pid $pid"; else echo "process: not running on $(hostname -s)"; fi
|
||||
# urllib rather than curl|python: the health formatting is a real
|
||||
# program, and quoting it through a shell pipeline is how it silently
|
||||
# turns into "no answer" the first time someone edits it.
|
||||
# `|| true`: a server that is not up yet is a normal state for this
|
||||
# command, and under `set -e` a non-zero exit here would abort the
|
||||
# `wait` loop that calls it.
|
||||
"$PYTHON" - "$URL" <<'PYEOF' || true
|
||||
import json, sys, urllib.request
|
||||
|
||||
url = sys.argv[1].rstrip("/") + "/health"
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=15) as r:
|
||||
h = json.load(r)
|
||||
except Exception as exc:
|
||||
print("health: no answer from %s (%s)" % (url, exc))
|
||||
raise SystemExit(1)
|
||||
|
||||
print("state: {state} (generation {generation}, up {up:.1f} min)".format(
|
||||
state=h["state"], generation=h["generation"], up=h["uptime_s"] / 60))
|
||||
print("init: {i}/{t} components, {f} failed".format(
|
||||
i=h["n_initialized"], t=h["n_target_names"], f=h["n_failed"]))
|
||||
print("serving: {d} status detectors, {m} monitorable".format(
|
||||
d=h["n_direct_read"], m=h.get("n_monitorable")))
|
||||
print("process: {r:.0f} MB, {t} threads, {c:.0f} s cpu".format(
|
||||
r=h.get("rss_mb") or 0, t=h.get("n_threads"), c=h.get("cpu_seconds") or 0))
|
||||
if h.get("failed_names"):
|
||||
print("failed: " + ", ".join(h["failed_names"]))
|
||||
for rec in h.get("recordings", []):
|
||||
if rec.get("running"):
|
||||
print("recording {id}: {u} updates, {s} stored".format(
|
||||
id=rec["recording_id"], u=rec["n_updates"], s=rec["n_stored"]))
|
||||
PYEOF
|
||||
}
|
||||
|
||||
is_ready() {
|
||||
"$PYTHON" - "$URL" <<'PYEOF' >/dev/null 2>&1
|
||||
import json, sys, urllib.request
|
||||
with urllib.request.urlopen(sys.argv[1].rstrip("/") + "/health", timeout=15) as r:
|
||||
raise SystemExit(0 if json.load(r).get("ready") else 1)
|
||||
PYEOF
|
||||
}
|
||||
|
||||
cmd_wait() {
|
||||
# split, not `local a=.. b=$((a))`: under `set -u` bash evaluates the
|
||||
# arithmetic before the first assignment is visible, and the command
|
||||
# fails with "timeout: unbound variable".
|
||||
local timeout="${1:-1800}"
|
||||
local end=$((SECONDS + timeout))
|
||||
while [ $SECONDS -lt $end ]; do
|
||||
if is_ready; then cmd_status; return 0; fi
|
||||
cmd_status | sed -n '2p'
|
||||
sleep 15
|
||||
done
|
||||
die "not ready after ${timeout}s"
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
start) shift; cmd_start "$@" ;;
|
||||
stop) cmd_stop ;;
|
||||
restart) cmd_stop; shift; cmd_start -b "$@" ;;
|
||||
status) cmd_status ;;
|
||||
wait) shift; cmd_wait "$@" ;;
|
||||
logs) shift; [ "${1:-}" = "-f" ] && tail -f "$LOG" || tail -n 100 "$LOG" ;;
|
||||
config) usage ;;
|
||||
""|-h|--help|help) usage ;;
|
||||
*) usage; exit 2 ;;
|
||||
esac
|
||||
Executable
+122
@@ -0,0 +1,122 @@
|
||||
#!/bin/bash
|
||||
# Install the eco status server as a *user* systemd service on this host.
|
||||
#
|
||||
# User-level (`systemctl --user`) rather than system-level on purpose: it
|
||||
# needs no root, and the service then runs as the invoking account - which
|
||||
# matters twice for this server. It writes status.json into the pgroup's
|
||||
# res/ tree, so it needs that account's pgroup membership; and several
|
||||
# bernina components authenticate to auxiliary machines over SSH during
|
||||
# __init__, so it needs that account's SSH credentials.
|
||||
#
|
||||
# The unit just calls `eco-status-server start` in the foreground, so the
|
||||
# interactive command and the service run exactly the same thing.
|
||||
set -euo pipefail
|
||||
|
||||
SELF_DIR="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)"
|
||||
LAUNCHER="${ECO_STATUS_SERVER_LAUNCHER:-$SELF_DIR/eco-status-server}"
|
||||
UNIT_DIR="$HOME/.config/systemd/user"
|
||||
UNIT="$UNIT_DIR/eco-status-server.service"
|
||||
ENV_FILE="${ECO_STATUS_SERVER_ENV:-$HOME/.config/eco-status-server.env}"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
usage: $(basename "$0") [--launcher PATH] [--env-file PATH] [--force]
|
||||
|
||||
Writes:
|
||||
$UNIT
|
||||
and, if it does not exist yet, a template environment file at
|
||||
$ENV_FILE
|
||||
|
||||
Then prints the systemctl commands to enable and start it.
|
||||
EOF
|
||||
}
|
||||
|
||||
FORCE=0
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--launcher) LAUNCHER="$2"; shift 2 ;;
|
||||
--env-file) ENV_FILE="$2"; shift 2 ;;
|
||||
--force) FORCE=1; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "unknown option: $1" >&2; usage; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[ -x "$LAUNCHER" ] || { echo "launcher not executable: $LAUNCHER" >&2; exit 1; }
|
||||
|
||||
mkdir -p "$UNIT_DIR" "$(dirname "$ENV_FILE")"
|
||||
|
||||
if [ ! -e "$ENV_FILE" ]; then
|
||||
{
|
||||
echo "# Environment for the eco status server (systemd EnvironmentFile"
|
||||
echo "# syntax: KEY=value, no export, no shell expansion). Edit to taste."
|
||||
echo "ECO_STATUS_SERVER_CHECKOUT=${ECO_STATUS_SERVER_CHECKOUT:-/sf/bernina/code/gac-bernina/eco}"
|
||||
echo "ECO_STATUS_SERVER_CONFIG=${ECO_STATUS_SERVER_CONFIG:-/sf/bernina/config/eco_status_server/bernina_namespace.json}"
|
||||
echo "ECO_STATUS_SERVER_PYTHON=${ECO_STATUS_SERVER_PYTHON:-/sf/bernina/applications/python/.pixi/envs/bpy312/bin/python}"
|
||||
echo
|
||||
echo "# Channel Access settings, captured from the shell that ran the"
|
||||
echo "# installer. A systemd user service inherits almost nothing, and"
|
||||
echo "# without EPICS_CA_ADDR_LIST the server talks to the local"
|
||||
echo "# broadcast domain instead of the beamline gateway: measured on"
|
||||
echo "# saresb-cons-04, that is the difference between 77 of 87"
|
||||
echo "# components initializing and 49 of 87."
|
||||
env | grep '^EPICS_' | sort || true
|
||||
} > "$ENV_FILE"
|
||||
echo "wrote template $ENV_FILE - check it before starting the service"
|
||||
if ! grep -q '^EPICS_CA_ADDR_LIST=' "$ENV_FILE"; then
|
||||
echo
|
||||
echo "WARNING: EPICS_CA_ADDR_LIST was not set in this shell, so it is" >&2
|
||||
echo " not in $ENV_FILE either. Re-run this from a shell where" >&2
|
||||
echo " Channel Access works, or add it by hand - otherwise the" >&2
|
||||
echo " service will start but most components will not connect." >&2
|
||||
fi
|
||||
else
|
||||
echo "keeping existing $ENV_FILE"
|
||||
fi
|
||||
|
||||
if [ -e "$UNIT" ] && [ "$FORCE" -eq 0 ]; then
|
||||
echo "$UNIT already exists; re-run with --force to overwrite" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cat > "$UNIT" <<EOF
|
||||
[Unit]
|
||||
Description=eco status/monitor server (namespace mode)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
EnvironmentFile=$ENV_FILE
|
||||
ExecStart=$LAUNCHER start
|
||||
# The port is bound immediately and /health answers from the first second;
|
||||
# namespace.init_all() then runs on a background thread for several minutes.
|
||||
# So "started" here means "answering /health with state=initializing", and
|
||||
# clients wait for state=ready themselves - no long TimeoutStartSec needed.
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
# A restart that keeps failing should back off rather than reinitialize the
|
||||
# whole namespace in a tight loop.
|
||||
StartLimitIntervalSec=600
|
||||
StartLimitBurst=5
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
EOF
|
||||
|
||||
echo "wrote $UNIT"
|
||||
cat <<EOF
|
||||
|
||||
Next steps:
|
||||
|
||||
# let the service keep running when you are not logged in
|
||||
loginctl enable-linger $USER
|
||||
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable --now eco-status-server
|
||||
systemctl --user status eco-status-server
|
||||
journalctl --user -u eco-status-server -f
|
||||
|
||||
# kill -USR1 <mainpid> dumps every thread's stack into the journal -
|
||||
# the way to see which component a stuck initialization is sitting in.
|
||||
EOF
|
||||
@@ -11,4 +11,4 @@ FLAMESHOT_DIR="/sf/bernina/applications/Flameshot/Flameshot-12.1.0-extracted"
|
||||
LC_ALL=C \
|
||||
QT_QPA_PLATFORM_PLUGIN_PATH=/usr/lib64/qt5/plugins/platforms \
|
||||
LD_LIBRARY_PATH=/usr/lib64 \
|
||||
"$FLAMESHOT_DIR/usr/bin/flameshot" "$@"
|
||||
"$FLAMESHOT_DIR/usr/bin/flameshot" "$@" "&"
|
||||
+140
-55
@@ -2,28 +2,33 @@
|
||||
#
|
||||
# gopass_bernina_service_setup.sh — make a *service/automation* account's
|
||||
# (e.g. gac-bernina) access to the "bernina" gopass store independent of
|
||||
# any particular host's $HOME. See gopass.md §11.
|
||||
# any particular host's $HOME. See gopass.md.
|
||||
#
|
||||
# Why this exists, and why it's different from gopass_bernina_setup.sh
|
||||
# (the script for human accounts): a human account's $HOME is made to look
|
||||
# the same on every PSI Linux host via "Partially Shared Home" (specific
|
||||
# files symlinked/copied in from ~/network_drives at login - see
|
||||
# migrate_to_network_drive.md). A service account may not have that, and
|
||||
# even if it did, ~/network_drives itself can't hold a git repo (confirmed,
|
||||
# gopass.md §10.3) - so a shared $HOME for this kind of account is a
|
||||
# single point of failure the moment it isn't actually shared anymore.
|
||||
# (the script for human accounts): this account needs a passphrase-less
|
||||
# GPG key (unattended use) and a repo-scoped SSH Deploy Key instead of a
|
||||
# personal SSH key - everything else (storage backend, the network_drives
|
||||
# chmod workaround) is the same idea as the human script.
|
||||
#
|
||||
# The fix: put EVERYTHING this account needs for gopass - GPG keyring, SSH
|
||||
# deploy key, gopass's own config, and the store clone itself - on
|
||||
# /sf/bernina (confirmed git-safe NFS4, gopass.md §10.2), and repoint the
|
||||
# relevant tools at it with three environment variables (all individually
|
||||
# confirmed to work: GOPASS_HOMEDIR relocates gopass's config/mounts,
|
||||
# GNUPGHOME relocates GPG's keyring, GIT_SSH_COMMAND controls which key
|
||||
# gopass's own git-based sync uses - gopass shells out to the real `git`
|
||||
# binary, so this is respected). Those three lines live in one file on
|
||||
# /sf/bernina; each host then only needs ONE line in its own LOCAL
|
||||
# ~/.bashrc sourcing it - a one-time, per-host, low-maintenance step,
|
||||
# unlike trying to keep a whole $HOME in sync.
|
||||
# Storage backend: prefers ~/network_drives (SMB) - the account's GPG
|
||||
# keyring, gopass config, and store clone all end up there. Falls back to
|
||||
# /sf/bernina/config/personal/<account> (NFS4) only if network_drives
|
||||
# isn't reachable AND the account has group access to create a directory
|
||||
# there (unlike a human's /sf/bernina/config/personal, this needs
|
||||
# `unx-sf_bernina_bs` group membership, which a service account often
|
||||
# doesn't have - confirmed for gac-bernina: `create_personal_dir.sh`
|
||||
# fails with "Permission denied"). Check which of the two actually works
|
||||
# for THIS account before relying on either - see gopass.md.
|
||||
#
|
||||
# THE WORKAROUND THIS SCRIPT USES for network_drives (confirmed by hand -
|
||||
# see gopass.md): network_drives (SMB) rejects the chmod calls that
|
||||
# `git init`/`git clone`/`git config`/`gpg --gen-key`/`ssh-keygen` make
|
||||
# internally, so those must never run directly there. An
|
||||
# ALREADY-initialized repo, keyring, or key pair, copied on with `cp -r`
|
||||
# (rsync also works on some shares but confirmed to fail outright on at
|
||||
# least one - see copy_tree() below), keeps working normally afterward.
|
||||
# So: do the actual init/clone/keygen in a local scratch directory first,
|
||||
# then copy the result onto network_drives and use it from there.
|
||||
#
|
||||
# Run this AS the service account (e.g. via `become <account>`, or however
|
||||
# you get a shell as it). Safe to re-run - every step checks current state
|
||||
@@ -37,19 +42,37 @@
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||
cat <<'USAGE'
|
||||
gopass_bernina_service_setup.sh [account-email]
|
||||
|
||||
For: the non-interactive SERVICE/AUTOMATION account (e.g. gac-bernina) -
|
||||
NOT a human. This account can READ secrets (its GPG key is authorized as
|
||||
a recipient) but does not need write/admin access - see gopass.md for the
|
||||
3-role split (admin / normal user / gac-bernina). Its git access is a
|
||||
repo-scoped Deploy Key, which should be added READ-ONLY.
|
||||
|
||||
Run this AS the service account. Puts everything (passphrase-less GPG
|
||||
key, SSH deploy key, gopass config, store clone) under
|
||||
~/network_drives/home/gopass/ if reachable, else
|
||||
/sf/bernina/config/personal/<account>/ - same auto-detection as
|
||||
gopass_bernina_setup.sh, the human onboarding script (use that one
|
||||
instead for a human, including the admin).
|
||||
|
||||
Note: /sf/bernina/config/personal needs unx-sf_bernina_bs group
|
||||
membership, which a service account often doesn't have - if
|
||||
create_personal_dir.sh fails with "Permission denied" for this account,
|
||||
network_drives is your only option; check it's reachable on every host
|
||||
this account needs to run from first.
|
||||
USAGE
|
||||
exit 0
|
||||
fi
|
||||
|
||||
GITEA_REMOTE="git@gitea.psi.ch:Bernina/bernina-secrets.git"
|
||||
STORE_ALIAS="bernina"
|
||||
PERSONAL_ROOT="/sf/bernina/config/personal"
|
||||
BIN_DIR="/sf/bernina/bin"
|
||||
|
||||
ACCOUNT="$(whoami)"
|
||||
EMAIL="${1:-${ACCOUNT}@psi.ch}"
|
||||
PERSONAL_DIR="/sf/bernina/config/personal/${ACCOUNT}"
|
||||
GOPASS_HOME_DIR="${PERSONAL_DIR}/gopass-home"
|
||||
GNUPG_DIR="${PERSONAL_DIR}/gnupg"
|
||||
SSH_DIR="${PERSONAL_DIR}/ssh"
|
||||
STORE_DIR="${PERSONAL_DIR}/gopass-${STORE_ALIAS}"
|
||||
ENV_FILE="${PERSONAL_DIR}/env.sh"
|
||||
|
||||
log() { echo "==> $*"; }
|
||||
warn() { echo "!! $*" >&2; }
|
||||
ask() {
|
||||
@@ -57,36 +80,72 @@ ask() {
|
||||
read -r -p "$1 [y/N] " reply
|
||||
[[ "$reply" =~ ^[Yy]$ ]]
|
||||
}
|
||||
copy_tree() {
|
||||
# `rsync -rlt` (no -p/-a - permission preservation doesn't work on
|
||||
# network_drives) works on some network_drives/SMB shares but
|
||||
# confirmed to fail on at least one (gac-bernina's: exit 3, "Errors
|
||||
# selecting input/output files, dirs", zero output even with -v and
|
||||
# stderr combined - an rsync/gvfs-fuse incompatibility specific to
|
||||
# that share). `cp -r` confirmed to work there, so use it everywhere.
|
||||
mkdir -p "$2"
|
||||
cp -r "$1/." "$2/"
|
||||
}
|
||||
|
||||
ACCOUNT="$(whoami)"
|
||||
EMAIL="${1:-${ACCOUNT}@psi.ch}"
|
||||
|
||||
# --- 0. pick a storage backend ----------------------------------------------
|
||||
if [[ -d "${HOME}/network_drives/home" ]]; then
|
||||
BACKEND="network_drives"
|
||||
ROOT_DIR="${HOME}/network_drives/home/gopass"
|
||||
NEEDS_SCRATCH=1
|
||||
else
|
||||
BACKEND="sf_bernina"
|
||||
ROOT_DIR="${PERSONAL_ROOT}/${ACCOUNT}"
|
||||
NEEDS_SCRATCH=0
|
||||
log "~/network_drives/home not reachable on this host - falling back to ${ROOT_DIR}"
|
||||
fi
|
||||
GOPASS_HOME_DIR="${ROOT_DIR}/gopass-home"
|
||||
GNUPG_DIR="${ROOT_DIR}/gnupg"
|
||||
SSH_DIR="${ROOT_DIR}/ssh"
|
||||
STORE_DIR="${ROOT_DIR}/gopass-${STORE_ALIAS}"
|
||||
ENV_FILE="${ROOT_DIR}/env.sh"
|
||||
|
||||
SCRATCH="$(mktemp -d)"
|
||||
cleanup() { rm -rf "${SCRATCH}"; }
|
||||
trap cleanup EXIT
|
||||
|
||||
echo "Setting up host-independent gopass access for ${ACCOUNT} (${EMAIL})"
|
||||
echo "Everything will live under: ${PERSONAL_DIR}"
|
||||
echo "Backend: ${BACKEND} (everything will live under ${ROOT_DIR})"
|
||||
echo
|
||||
|
||||
# --- 1. personal directory on the shared NFS mount --------------------------
|
||||
log "Step 1/5: personal directory"
|
||||
"${BIN_DIR}/create_personal_dir.sh" "${ACCOUNT}"
|
||||
# --- 1. target directory -----------------------------------------------------
|
||||
log "Step 1/5: target directory (${ROOT_DIR})"
|
||||
if [[ "${BACKEND}" == "sf_bernina" ]]; then
|
||||
"${BIN_DIR}/create_personal_dir.sh" "${ACCOUNT}"
|
||||
fi
|
||||
mkdir -p "${GOPASS_HOME_DIR}"
|
||||
mkdir -m 700 -p "${GNUPG_DIR}"
|
||||
mkdir -m 700 -p "${SSH_DIR}"
|
||||
|
||||
# --- 2. GPG key: migrate existing if present, else offer to generate --------
|
||||
# --- 2. GPG key: migrate existing if present, else generate passphrase-less -
|
||||
log "Step 2/5: GPG key for ${EMAIL}"
|
||||
if GNUPGHOME="${GNUPG_DIR}" gpg --list-secret-keys "${EMAIL}" >/dev/null 2>&1; then
|
||||
log " already present under ${GNUPG_DIR}"
|
||||
elif gpg --list-secret-keys "${EMAIL}" >/dev/null 2>&1; then
|
||||
log " found an existing key for ${EMAIL} in this shell's current \$HOME/.gnupg"
|
||||
if ask " Copy it to ${GNUPG_DIR} (keeps the SAME key/recipient — no re-authorization needed)?"; then
|
||||
cp -r "${HOME}/.gnupg/." "${GNUPG_DIR}/"
|
||||
find "${GNUPG_DIR}" -type d -exec chmod 700 {} \;
|
||||
find "${GNUPG_DIR}" -type f -exec chmod 600 {} \;
|
||||
copy_tree "${HOME}/.gnupg" "${GNUPG_DIR}"
|
||||
log " copied"
|
||||
else
|
||||
warn " without this, decrypt calls will fail until a key exists under ${GNUPG_DIR}"
|
||||
fi
|
||||
else
|
||||
warn " no GPG key found anywhere for ${EMAIL}"
|
||||
if ask " Generate a new passphrase-less one now (needed for unattended use — see gopass.md §11 for the security trade-off)?"; then
|
||||
GNUPGHOME="${GNUPG_DIR}" gpg --batch --gen-key <<EOF
|
||||
if ask " Generate a new passphrase-less one now (needed for unattended use — see gopass.md for the security trade-off)?"; then
|
||||
KEYGEN_HOME="${GNUPG_DIR}"
|
||||
[[ "${NEEDS_SCRATCH}" -eq 1 ]] && KEYGEN_HOME="${SCRATCH}/gnupg" && mkdir -m 700 -p "${KEYGEN_HOME}"
|
||||
GNUPGHOME="${KEYGEN_HOME}" gpg --batch --gen-key <<EOF
|
||||
%no-protection
|
||||
Key-Type: RSA
|
||||
Key-Length: 4096
|
||||
@@ -95,10 +154,11 @@ Name-Email: ${EMAIL}
|
||||
Expire-Date: 1y
|
||||
%commit
|
||||
EOF
|
||||
PUBKEY_FILE="${PERSONAL_DIR}/${ACCOUNT}-gopass-pubkey.asc"
|
||||
[[ "${NEEDS_SCRATCH}" -eq 1 ]] && copy_tree "${KEYGEN_HOME}" "${GNUPG_DIR}"
|
||||
PUBKEY_FILE="${ROOT_DIR}/${ACCOUNT}-gopass-pubkey.asc"
|
||||
GNUPGHOME="${GNUPG_DIR}" gpg --armor --export "${EMAIL}" > "${PUBKEY_FILE}"
|
||||
log " generated, exported public key to ${PUBKEY_FILE}"
|
||||
log " send it to the store admin — they run 'gopass recipients add --store ${STORE_ALIAS} ${EMAIL}' (gopass.md §6)"
|
||||
log " send it to the store admin — they run 'gopass recipients add --store ${STORE_ALIAS} ${EMAIL}' (gopass.md)"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -111,7 +171,6 @@ elif [[ -f "${HOME}/.ssh/id_ed25519" ]]; then
|
||||
if ask " Copy it to ${SSH_DIR} (keeps the SAME key — no re-registering the deploy key needed)?"; then
|
||||
cp "${HOME}/.ssh/id_ed25519" "${SSH_DIR}/id_ed25519"
|
||||
[[ -f "${HOME}/.ssh/id_ed25519.pub" ]] && cp "${HOME}/.ssh/id_ed25519.pub" "${SSH_DIR}/id_ed25519.pub"
|
||||
chmod 600 "${SSH_DIR}/id_ed25519"
|
||||
log " copied"
|
||||
else
|
||||
warn " without this, git/gitea access will fail until a key exists under ${SSH_DIR}"
|
||||
@@ -119,12 +178,19 @@ elif [[ -f "${HOME}/.ssh/id_ed25519" ]]; then
|
||||
else
|
||||
warn " no SSH key found anywhere"
|
||||
if ask " Generate a new passphrase-less deploy key now?"; then
|
||||
ssh-keygen -t ed25519 -f "${SSH_DIR}/id_ed25519" -N "" -C "${ACCOUNT} deploy key"
|
||||
log " generated. Register the PUBLIC half as a Deploy Key on the repo (gopass.md §11):"
|
||||
if [[ "${NEEDS_SCRATCH}" -eq 1 ]]; then
|
||||
# ssh-keygen chmods the private key file after writing it -
|
||||
# fails directly on network_drives like git/gpg do.
|
||||
ssh-keygen -t ed25519 -f "${SCRATCH}/id_ed25519" -N "" -C "${ACCOUNT} deploy key"
|
||||
cp "${SCRATCH}/id_ed25519" "${SCRATCH}/id_ed25519.pub" "${SSH_DIR}/"
|
||||
else
|
||||
ssh-keygen -t ed25519 -f "${SSH_DIR}/id_ed25519" -N "" -C "${ACCOUNT} deploy key"
|
||||
fi
|
||||
log " generated. Register the PUBLIC half as a READ-ONLY Deploy Key on the repo (gopass.md):"
|
||||
echo
|
||||
cat "${SSH_DIR}/id_ed25519.pub"
|
||||
echo
|
||||
log " https://gitea.psi.ch/Bernina/bernina-secrets/settings/keys -> Add Deploy Key"
|
||||
log " https://gitea.psi.ch/Bernina/bernina-secrets/settings/keys -> Add Deploy Key (leave 'Allow write access' UNCHECKED)"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -132,22 +198,30 @@ fi
|
||||
log "Step 4/5: writing ${ENV_FILE}"
|
||||
cat > "${ENV_FILE}" <<EOF
|
||||
# Source this from ~/.bashrc (LOCAL to each host - not shared) to make
|
||||
# ${ACCOUNT}'s gopass access work the same everywhere, regardless of
|
||||
# whether \$HOME is shared. Generated by gopass_bernina_service_setup.sh -
|
||||
# see gopass.md §11 for what each of these does and why.
|
||||
# ${ACCOUNT}'s gopass access work the same everywhere this account runs,
|
||||
# regardless of whether \$HOME is shared. Generated by
|
||||
# gopass_bernina_service_setup.sh - see gopass.md for what each of these
|
||||
# does and why.
|
||||
export GOPASS_HOMEDIR="${GOPASS_HOME_DIR}"
|
||||
export GNUPGHOME="${GNUPG_DIR}"
|
||||
export GIT_SSH_COMMAND="ssh -i ${SSH_DIR}/id_ed25519 -o IdentitiesOnly=yes"
|
||||
EOF
|
||||
log " done"
|
||||
|
||||
# --- 5. clone/mount the store using the new env ------------------------------
|
||||
log "Step 5/5: cloning '${STORE_ALIAS}' to ${STORE_DIR}"
|
||||
# --- 5. root store + clone/mount the store using the new env -----------------
|
||||
log "Step 5/5: '${STORE_ALIAS}' store at ${STORE_DIR}"
|
||||
export GOPASS_HOMEDIR="${GOPASS_HOME_DIR}" GNUPGHOME="${GNUPG_DIR}"
|
||||
export GIT_SSH_COMMAND="ssh -i ${SSH_DIR}/id_ed25519 -o IdentitiesOnly=yes"
|
||||
|
||||
if ! gopass mounts >/dev/null 2>&1; then
|
||||
gopass init "${EMAIL}"
|
||||
if [[ "${NEEDS_SCRATCH}" -eq 1 ]]; then
|
||||
GNUPGHOME="${GNUPG_DIR}" GOPASS_HOMEDIR="${SCRATCH}/gopass-home" gopass init "${EMAIL}"
|
||||
(cd "${SCRATCH}/gopass-home/.local/share/gopass/stores/root" && git config core.fileMode false)
|
||||
copy_tree "${SCRATCH}/gopass-home" "${GOPASS_HOME_DIR}"
|
||||
sed -i "s|${SCRATCH}/gopass-home|${GOPASS_HOME_DIR}|g" "${GOPASS_HOME_DIR}/.config/gopass/config"
|
||||
else
|
||||
gopass init "${EMAIL}"
|
||||
fi
|
||||
fi
|
||||
|
||||
if gopass mounts 2>/dev/null | grep -q "${STORE_ALIAS} ("; then
|
||||
@@ -155,21 +229,32 @@ if gopass mounts 2>/dev/null | grep -q "${STORE_ALIAS} ("; then
|
||||
elif [[ -d "${STORE_DIR}/.git" ]]; then
|
||||
gopass mounts add "${STORE_ALIAS}" "${STORE_DIR}"
|
||||
elif [[ -d "${STORE_DIR}" ]]; then
|
||||
warn " ${STORE_DIR} exists but has no .git — see gopass.md §12 Troubleshooting"
|
||||
warn " ${STORE_DIR} exists but has no .git — see gopass.md Troubleshooting"
|
||||
exit 1
|
||||
else
|
||||
# Plain `git clone`, deliberately NOT `gopass clone` - confirmed by
|
||||
# hand that `gopass clone` can hang indefinitely (Go-level deadlock at
|
||||
# its post-clone "Gathering information for the git repository"
|
||||
# bookkeeping, unrelated to repo size or network) - see gopass.md.
|
||||
CLONE_DEST="${STORE_DIR}"
|
||||
[[ "${NEEDS_SCRATCH}" -eq 1 ]] && CLONE_DEST="${SCRATCH}/${STORE_ALIAS}"
|
||||
CLONE_ERR="$(mktemp)"
|
||||
if ! gopass clone --path "${STORE_DIR}" "${GITEA_REMOTE}" "${STORE_ALIAS}" 2>"${CLONE_ERR}"; then
|
||||
if ! git clone "${GITEA_REMOTE}" "${CLONE_DEST}" 2>"${CLONE_ERR}"; then
|
||||
cat "${CLONE_ERR}" >&2
|
||||
rm -f "${CLONE_ERR}"
|
||||
warn " clone failed — check the deploy key is registered and has repo access (gopass.md §11)"
|
||||
warn " clone failed — check the deploy key is registered and has repo access (gopass.md)"
|
||||
exit 1
|
||||
fi
|
||||
rm -f "${CLONE_ERR}"
|
||||
(cd "${CLONE_DEST}" && git config core.fileMode false)
|
||||
if [[ "${NEEDS_SCRATCH}" -eq 1 ]]; then
|
||||
copy_tree "${CLONE_DEST}" "${STORE_DIR}"
|
||||
fi
|
||||
gopass mounts add "${STORE_ALIAS}" "${STORE_DIR}"
|
||||
fi
|
||||
|
||||
if ! gopass mounts 2>/dev/null | grep -q "${STORE_ALIAS} ("; then
|
||||
warn " mount verification failed — see gopass.md §12 Troubleshooting"
|
||||
warn " mount verification failed — see gopass.md Troubleshooting"
|
||||
exit 1
|
||||
fi
|
||||
log " mounted and verified OK"
|
||||
@@ -178,7 +263,7 @@ echo
|
||||
echo "======================================================================"
|
||||
echo "Done for THIS shell. To make it stick on every host this account runs"
|
||||
echo "on, add ONE line to that host's LOCAL ~/.bashrc (not shared — do this"
|
||||
echo "once per host):"
|
||||
echo "once per host, and only where ${BACKEND} is reachable for this account):"
|
||||
echo
|
||||
echo " echo 'source ${ENV_FILE}' >> ~/.bashrc"
|
||||
echo
|
||||
|
||||
+236
-49
@@ -2,9 +2,27 @@
|
||||
#
|
||||
# gopass_bernina_setup.sh — one-shot onboarding for a *human* PSI account
|
||||
# joining the shared "bernina" gopass secrets store. See gopass.md (same
|
||||
# directory) for the full picture; this automates §5 (Onboarding additional
|
||||
# users) using the §10.2 NFS-shared store location, which is recommended
|
||||
# for anyone working from Bernina beamline hosts.
|
||||
# directory) for the full picture; this automates the "Onboarding someone
|
||||
# new" section.
|
||||
#
|
||||
# Storage backend: prefers ~/network_drives (SMB, Partially Shared Home) -
|
||||
# the account's GPG keyring, gopass config, and the store clone all end up
|
||||
# there, so they're already the same on every host without any separate
|
||||
# provisioning step. Falls back to /sf/bernina/config/personal/<you> (NFS4)
|
||||
# if network_drives isn't reachable on this host - see gopass.md
|
||||
# "Store location" for the trade-off.
|
||||
#
|
||||
# THE WORKAROUND THIS SCRIPT USES for network_drives (confirmed by hand -
|
||||
# see gopass.md): network_drives (SMB) rejects the chmod calls that
|
||||
# `git init`/`git clone`/`git config`/`gpg --gen-key` make internally, so
|
||||
# those must never run directly there ("Operation not supported"). An
|
||||
# ALREADY-initialized repo or keyring, copied on with `cp -r` (rsync also
|
||||
# works on some shares but confirmed to fail outright on at least one -
|
||||
# see copy_tree() below), keeps working normally afterward:
|
||||
# add/commit/push/pull, encrypt/decrypt, and `gopass insert`/`show`/`sync`
|
||||
# all confirmed. So: do the actual init/clone/gen-key/config in a local
|
||||
# scratch directory first, then copy the result onto network_drives and
|
||||
# use it from there.
|
||||
#
|
||||
# What this script CANNOT do for you — gitea has no API for these, and
|
||||
# they're each an action only another human/account can take:
|
||||
@@ -14,7 +32,7 @@
|
||||
# Bernina/bernina-secrets repo
|
||||
# 3. Getting your GPG key authorized as a recipient - an existing
|
||||
# recipient (currently: lemke_h) has to run `gopass recipients add`
|
||||
# for you (see gopass.md §6) after you send them your exported
|
||||
# for you (see gopass.md) after you send them your exported
|
||||
# public key
|
||||
# The script detects each of these and tells you exactly what's missing
|
||||
# and what to do, then stops - re-run it after fixing that one thing.
|
||||
@@ -23,7 +41,11 @@
|
||||
# Defaults to <your-username>@psi.ch.
|
||||
#
|
||||
# Safe to re-run: every step checks current state first and skips work
|
||||
# that's already done.
|
||||
# that's already done. If you already have a working GPG key/store in an
|
||||
# old location (e.g. $HOME/.gnupg, or a previous /sf/bernina-only setup),
|
||||
# this OFFERS to copy it into the new location rather than generating a
|
||||
# fresh one - keeps the same recipient registration, no re-authorization
|
||||
# needed.
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
@@ -39,36 +61,84 @@ ask() {
|
||||
read -r -p "$1 [y/N] " reply
|
||||
[[ "$reply" =~ ^[Yy]$ ]]
|
||||
}
|
||||
copy_tree() {
|
||||
# `rsync -rlt` was the first choice here (no -p/-a - permission
|
||||
# preservation doesn't work on network_drives) and works fine on some
|
||||
# network_drives/SMB shares, but confirmed to fail on at least one
|
||||
# (gac-bernina's: exit 3, "Errors selecting input/output files, dirs",
|
||||
# with -v producing zero output even with stderr combined - looks
|
||||
# like an rsync/gvfs-fuse incompatibility specific to that share).
|
||||
# `cp -r` confirmed to work where that rsync failed, so use it
|
||||
# everywhere instead for consistency.
|
||||
mkdir -p "$2"
|
||||
cp -r "$1/." "$2/"
|
||||
}
|
||||
|
||||
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||
cat <<'USAGE'
|
||||
gopass_bernina_setup.sh [your-psi-email]
|
||||
|
||||
For: any HUMAN PSI account that needs to read/write the shared "bernina"
|
||||
gopass secrets store - this is the SAME script and procedure whether
|
||||
you're the admin (the one who also runs `gopass recipients add` for
|
||||
others) or a normal group user. There is no separate "admin mode".
|
||||
|
||||
NOT for the gac-bernina service account - see
|
||||
gopass_bernina_service_setup.sh for that (different: passphrase-less key,
|
||||
deploy key instead of a personal SSH key, non-interactive).
|
||||
|
||||
Defaults to <your-username>@psi.ch. Safe to re-run.
|
||||
USAGE
|
||||
exit 0
|
||||
fi
|
||||
|
||||
USERNAME="$(whoami)"
|
||||
EMAIL="${1:-${USERNAME}@psi.ch}"
|
||||
PERSONAL_DIR="${PERSONAL_ROOT}/${USERNAME}"
|
||||
STORE_PATH="${PERSONAL_DIR}/gopass-${STORE_ALIAS}"
|
||||
|
||||
# --- 0. pick a storage backend ----------------------------------------------
|
||||
if [[ -d "${HOME}/network_drives/home" ]]; then
|
||||
BACKEND="network_drives"
|
||||
ROOT_DIR="${HOME}/network_drives/home/gopass"
|
||||
NEEDS_SCRATCH=1
|
||||
else
|
||||
BACKEND="sf_bernina"
|
||||
ROOT_DIR="${PERSONAL_ROOT}/${USERNAME}"
|
||||
NEEDS_SCRATCH=0
|
||||
log "~/network_drives/home not reachable on this host - falling back to ${ROOT_DIR}"
|
||||
fi
|
||||
GNUPG_DIR="${ROOT_DIR}/gnupg"
|
||||
GOPASS_HOME_DIR="${ROOT_DIR}/gopass-home"
|
||||
STORE_DIR="${ROOT_DIR}/gopass-${STORE_ALIAS}"
|
||||
ENV_FILE="${ROOT_DIR}/env.sh"
|
||||
|
||||
SCRATCH="$(mktemp -d)"
|
||||
cleanup() { rm -rf "${SCRATCH}"; }
|
||||
trap cleanup EXIT
|
||||
|
||||
echo "Setting up gopass access to the '${STORE_ALIAS}' store for ${EMAIL}"
|
||||
echo "(local store path will be: ${STORE_PATH})"
|
||||
echo "Backend: ${BACKEND} (everything will live under ${ROOT_DIR})"
|
||||
echo
|
||||
|
||||
# --- 1. personal directory on the shared NFS mount -------------------------
|
||||
log "Step 1/6: personal directory (${PERSONAL_DIR})"
|
||||
if [[ -d "${PERSONAL_DIR}" ]]; then
|
||||
log " already exists"
|
||||
else
|
||||
# --- 1. target directory -----------------------------------------------------
|
||||
log "Step 1/6: target directory (${ROOT_DIR})"
|
||||
if [[ "${BACKEND}" == "sf_bernina" ]]; then
|
||||
"${BIN_DIR}/create_personal_dir.sh"
|
||||
fi
|
||||
mkdir -p "${GOPASS_HOME_DIR}"
|
||||
mkdir -m 700 -p "${GNUPG_DIR}"
|
||||
|
||||
# --- 2. SSH key + gitea reachability ---------------------------------------
|
||||
# --- 2. SSH key + gitea reachability -----------------------------------------
|
||||
log "Step 2/6: SSH key for gitea.psi.ch"
|
||||
if [[ ! -f ~/.ssh/id_ed25519 && ! -f ~/.ssh/id_rsa ]]; then
|
||||
if ask " No SSH key found. Generate one now (ssh-keygen -t ed25519)?"; then
|
||||
ssh-keygen -t ed25519 -C "${EMAIL}"
|
||||
else
|
||||
warn " Can't continue without an SSH key - see gopass.md §3.2"
|
||||
warn " Can't continue without an SSH key - see gopass.md"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if ssh -T -o BatchMode=yes -o ConnectTimeout=8 git@gitea.psi.ch 2>&1 | grep -qi "successfully authenticated"; then
|
||||
if ssh -T -o BatchMode=yes -o ConnectTimeout=8 git@gitea.psi.ch </dev/null 2>&1 | grep -qi "successfully authenticated"; then
|
||||
log " SSH to gitea.psi.ch: OK"
|
||||
else
|
||||
warn " SSH to gitea.psi.ch did not authenticate."
|
||||
@@ -78,44 +148,127 @@ else
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- 3. GPG key --------------------------------------------------------------
|
||||
log "Step 3/6: GPG key for ${EMAIL}"
|
||||
if gpg --list-secret-keys "${EMAIL}" >/dev/null 2>&1; then
|
||||
log " usable GPG key already exists for ${EMAIL}"
|
||||
# --- 3. GPG key: already in place / migrate from an old location / generate -
|
||||
# NOTE: we detect "is there a usable key" by presence, not by matching
|
||||
# ${EMAIL} - your real PSI email often isn't <username>@psi.ch (e.g.
|
||||
# lemke_h's is henrik.lemke@psi.ch), so a literal-match check would miss
|
||||
# an existing key and offer to generate a redundant, unauthorized one.
|
||||
# Once a key is found/generated, EMAIL is corrected to that key's own
|
||||
# uid email for every step from here on (gopass init, the final
|
||||
# instructions, etc).
|
||||
log "Step 3/6: GPG key"
|
||||
|
||||
key_email_in() {
|
||||
GNUPGHOME="$1" gpg --list-secret-keys --with-colons 2>/dev/null \
|
||||
| awk -F: '/^uid/{print $10; exit}' | sed -n 's/.*<\(.*\)>.*/\1/p'
|
||||
}
|
||||
has_secret_key_in() {
|
||||
GNUPGHOME="$1" gpg --list-secret-keys --with-colons 2>/dev/null | grep -q '^sec'
|
||||
}
|
||||
|
||||
if has_secret_key_in "${GNUPG_DIR}"; then
|
||||
EMAIL="$(key_email_in "${GNUPG_DIR}")"
|
||||
log " already present under ${GNUPG_DIR} (${EMAIL})"
|
||||
else
|
||||
if ask " No GPG key found for ${EMAIL}. Generate one now (gpg --full-generate-key)?"; then
|
||||
gpg --full-generate-key
|
||||
OLD_GNUPGHOME=""
|
||||
for candidate in "${GNUPGHOME:-}" "${HOME}/.gnupg" "${PERSONAL_ROOT}/${USERNAME}/gnupg"; do
|
||||
[[ -z "${candidate}" || "${candidate}" == "${GNUPG_DIR}" ]] && continue
|
||||
if has_secret_key_in "${candidate}"; then
|
||||
OLD_GNUPGHOME="${candidate}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ -n "${OLD_GNUPGHOME}" ]]; then
|
||||
FOUND_EMAIL="$(key_email_in "${OLD_GNUPGHOME}")"
|
||||
log " found an existing key (${FOUND_EMAIL}) under ${OLD_GNUPGHOME}"
|
||||
if ask " Copy it to ${GNUPG_DIR} (keeps the SAME key/recipient - no re-authorization needed)?"; then
|
||||
copy_tree "${OLD_GNUPGHOME}" "${GNUPG_DIR}"
|
||||
EMAIL="${FOUND_EMAIL}"
|
||||
log " copied - using ${EMAIL} as your identity from here on"
|
||||
else
|
||||
warn " without this, decrypt calls will fail until a key exists under ${GNUPG_DIR}"
|
||||
fi
|
||||
else
|
||||
warn " Can't continue without a GPG key - see gopass.md §3.3"
|
||||
exit 1
|
||||
warn " no GPG key found anywhere (checked ${GNUPG_DIR}, \$GNUPGHOME, ~/.gnupg, ${PERSONAL_ROOT}/${USERNAME}/gnupg)"
|
||||
if ask " Generate one now (gpg --full-generate-key - use your REAL psi.ch email, set a real passphrase)?"; then
|
||||
if [[ "${NEEDS_SCRATCH}" -eq 1 ]]; then
|
||||
# gpg --gen-key does chmods internally that fail directly on
|
||||
# network_drives - generate locally, then copy the result.
|
||||
mkdir -m 700 -p "${SCRATCH}/gnupg"
|
||||
GNUPGHOME="${SCRATCH}/gnupg" gpg --full-generate-key
|
||||
copy_tree "${SCRATCH}/gnupg" "${GNUPG_DIR}"
|
||||
else
|
||||
GNUPGHOME="${GNUPG_DIR}" gpg --full-generate-key
|
||||
# safety net in case this gpg version generated into the
|
||||
# default homedir despite the GNUPGHOME override above
|
||||
if ! has_secret_key_in "${GNUPG_DIR}" && has_secret_key_in "${HOME}/.gnupg"; then
|
||||
copy_tree "${HOME}/.gnupg" "${GNUPG_DIR}"
|
||||
fi
|
||||
fi
|
||||
if has_secret_key_in "${GNUPG_DIR}"; then
|
||||
EMAIL="$(key_email_in "${GNUPG_DIR}")"
|
||||
log " generated - using ${EMAIL} as your identity from here on"
|
||||
fi
|
||||
else
|
||||
warn " Can't continue without a GPG key"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- 4. root store -----------------------------------------------------------
|
||||
if ! has_secret_key_in "${GNUPG_DIR}"; then
|
||||
warn " no usable key under ${GNUPG_DIR} - can't continue. Re-run and"
|
||||
warn " agree to the copy/generate step above, or place a key there by hand first."
|
||||
exit 1
|
||||
fi
|
||||
log " using GPG identity: ${EMAIL}"
|
||||
export GNUPGHOME="${GNUPG_DIR}"
|
||||
|
||||
# --- 4. root store -------------------------------------------------------------
|
||||
log "Step 4/6: gopass root store"
|
||||
export GOPASS_HOMEDIR="${GOPASS_HOME_DIR}"
|
||||
if gopass mounts >/dev/null 2>&1; then
|
||||
log " already initialized"
|
||||
elif [[ "${NEEDS_SCRATCH}" -eq 1 ]]; then
|
||||
GNUPGHOME="${GNUPG_DIR}" GOPASS_HOMEDIR="${SCRATCH}/gopass-home" gopass init "${EMAIL}"
|
||||
(cd "${SCRATCH}/gopass-home/.local/share/gopass/stores/root" && git config core.fileMode false)
|
||||
copy_tree "${SCRATCH}/gopass-home" "${GOPASS_HOME_DIR}"
|
||||
sed -i "s|${SCRATCH}/gopass-home|${GOPASS_HOME_DIR}|g" "${GOPASS_HOME_DIR}/.config/gopass/config"
|
||||
else
|
||||
gopass init "${EMAIL}"
|
||||
fi
|
||||
|
||||
# --- 5. clone/mount the bernina store ---------------------------------------
|
||||
log "Step 5/6: cloning '${STORE_ALIAS}' to ${STORE_PATH}"
|
||||
# --- 5. clone/mount the bernina store ----------------------------------------
|
||||
log "Step 5/6: '${STORE_ALIAS}' store at ${STORE_DIR}"
|
||||
if gopass mounts 2>/dev/null | grep -q "${STORE_ALIAS} ("; then
|
||||
log " already mounted"
|
||||
elif [[ -d "${STORE_PATH}/.git" ]]; then
|
||||
elif [[ -d "${STORE_DIR}/.git" ]]; then
|
||||
log " directory already looks cloned, mounting it"
|
||||
gopass mounts add "${STORE_ALIAS}" "${STORE_PATH}"
|
||||
elif [[ -d "${STORE_PATH}" ]]; then
|
||||
warn " ${STORE_PATH} exists but has no .git - a previous attempt likely"
|
||||
warn " failed partway. See gopass.md §12 Troubleshooting before removing"
|
||||
gopass mounts add "${STORE_ALIAS}" "${STORE_DIR}"
|
||||
elif [[ -d "${STORE_DIR}" ]]; then
|
||||
warn " ${STORE_DIR} exists but has no .git - a previous attempt likely"
|
||||
warn " failed partway. See gopass.md Troubleshooting before removing"
|
||||
warn " it by hand (confirm it really lacks .git first)."
|
||||
exit 1
|
||||
else
|
||||
# Plain `git clone` + `gopass mounts add`, deliberately NOT `gopass
|
||||
# clone`: confirmed by hand that `gopass clone` can hang indefinitely
|
||||
# (Go-level deadlock, wchan futex_do_wait, zero subprocesses/network
|
||||
# activity - not a slow/big-repo thing, reproduced even against the
|
||||
# tiny real bernina-secrets repo) at its post-clone "Gathering
|
||||
# information for the git repository" bookkeeping step. A plain git
|
||||
# clone plus `gopass mounts add` (the same call already used just
|
||||
# above when a store directory is pre-existing) reaches the same end
|
||||
# state without going through that codepath - confirmed instant
|
||||
# against the real repo.
|
||||
CLONE_DEST="${STORE_DIR}"
|
||||
if [[ "${NEEDS_SCRATCH}" -eq 1 ]]; then
|
||||
CLONE_DEST="${SCRATCH}/${STORE_ALIAS}"
|
||||
fi
|
||||
CLONE_ERR="$(mktemp)"
|
||||
if ! gopass clone --path "${STORE_PATH}" "${GITEA_REMOTE}" "${STORE_ALIAS}" 2>"${CLONE_ERR}"; then
|
||||
if ! git clone "${GITEA_REMOTE}" "${CLONE_DEST}" 2>"${CLONE_ERR}"; then
|
||||
cat "${CLONE_ERR}" >&2
|
||||
if grep -qi "not authorized\|access rights\|could not read from remote" "${CLONE_ERR}"; then
|
||||
if grep -qi "not authorized\|access rights\|could not read from remote\|permission denied" "${CLONE_ERR}"; then
|
||||
warn " Repo access not granted yet. Ask the admin (lemke_h) to add you"
|
||||
warn " as a Collaborator (Write access) on Bernina/bernina-secrets:"
|
||||
warn " https://gitea.psi.ch/Bernina/bernina-secrets/settings/collaboration"
|
||||
@@ -125,35 +278,69 @@ else
|
||||
exit 1
|
||||
fi
|
||||
rm -f "${CLONE_ERR}"
|
||||
(cd "${CLONE_DEST}" && git config core.fileMode false)
|
||||
if [[ "${NEEDS_SCRATCH}" -eq 1 ]]; then
|
||||
copy_tree "${CLONE_DEST}" "${STORE_DIR}"
|
||||
fi
|
||||
gopass mounts add "${STORE_ALIAS}" "${STORE_DIR}"
|
||||
fi
|
||||
|
||||
# verify (gopass.md §5.3 - don't skip this, a broken mount fails confusingly later)
|
||||
# verify (don't skip this, a broken mount fails confusingly later)
|
||||
if ! gopass mounts 2>/dev/null | grep -q "${STORE_ALIAS} ("; then
|
||||
warn " mount verification failed - see gopass.md §5.3/§12 Troubleshooting"
|
||||
warn " mount verification failed - see gopass.md Troubleshooting"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -d "${STORE_PATH}/.git" ]]; then
|
||||
warn " ${STORE_PATH} has no .git - clone did not fully succeed, see gopass.md §5.3"
|
||||
if [[ ! -d "${STORE_DIR}/.git" ]]; then
|
||||
warn " ${STORE_DIR} has no .git - clone did not fully succeed"
|
||||
exit 1
|
||||
fi
|
||||
log " mounted and verified OK"
|
||||
|
||||
# --- 6. export pubkey, tell them what's left --------------------------------
|
||||
log "Step 6/6: exporting your GPG public key"
|
||||
PUBKEY_FILE="${HOME}/${USERNAME}-gopass-pubkey.asc"
|
||||
# --- 6. env.sh + export pubkey + tell them what's left -----------------------
|
||||
log "Step 6/6: writing ${ENV_FILE} and exporting your GPG public key"
|
||||
cat > "${ENV_FILE}" <<EOF
|
||||
# Source this from ~/.bashrc (LOCAL to each host) to make your gopass
|
||||
# access to '${STORE_ALIAS}' work the same on every host where ${ROOT_DIR}
|
||||
# is reachable. Generated by gopass_bernina_setup.sh - see gopass.md.
|
||||
export GNUPGHOME="${GNUPG_DIR}"
|
||||
export GOPASS_HOMEDIR="${GOPASS_HOME_DIR}"
|
||||
EOF
|
||||
|
||||
PUBKEY_FILE="${ROOT_DIR}/${USERNAME}-gopass-pubkey.asc"
|
||||
gpg --armor --export "${EMAIL}" > "${PUBKEY_FILE}"
|
||||
log " wrote ${PUBKEY_FILE}"
|
||||
|
||||
# is this identity already an authorized recipient? (true for anyone
|
||||
# re-running this script after migrating an already-authorized key to a
|
||||
# new location - e.g. the admin's own re-run - not just for brand-new
|
||||
# people)
|
||||
ALREADY_RECIPIENT=0
|
||||
if gopass recipients 2>/dev/null | grep -qi "<${EMAIL}>"; then
|
||||
ALREADY_RECIPIENT=1
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "======================================================================"
|
||||
echo "Almost done. One step only an existing recipient can do for you:"
|
||||
echo "Add ONE line to this host's LOCAL ~/.bashrc (not shared - do this once"
|
||||
echo "per host, or replace any older GNUPGHOME/GOPASS_HOMEDIR exports there):"
|
||||
echo
|
||||
echo " Send ${PUBKEY_FILE} to the store admin (lemke_h) - internal email,"
|
||||
echo " Mattermost/Slack DM, or in person. They will run:"
|
||||
echo " echo 'source ${ENV_FILE}' >> ~/.bashrc"
|
||||
echo
|
||||
echo " gpg --import ${USERNAME}-gopass-pubkey.asc"
|
||||
echo " gopass recipients add --store ${STORE_ALIAS} ${EMAIL}"
|
||||
echo
|
||||
echo "Until then, 'gopass show ${STORE_ALIAS}/...' will fail with a"
|
||||
echo "'not a recipient' error - that's expected, not a bug (see gopass.md §1)."
|
||||
if [[ "${ALREADY_RECIPIENT}" -eq 1 ]]; then
|
||||
echo "You're already an authorized recipient of '${STORE_ALIAS}' (${EMAIL}) -"
|
||||
echo "nothing else to do. Open a fresh shell (or source ~/.bashrc above),"
|
||||
echo "then 'gopass show ${STORE_ALIAS}/...' should work (it'll ask for your"
|
||||
echo "GPG passphrase once, then cache it)."
|
||||
else
|
||||
echo "One step only an existing recipient can do for you:"
|
||||
echo
|
||||
echo " Send ${PUBKEY_FILE} to the store admin (lemke_h) - internal email,"
|
||||
echo " Mattermost/Slack DM, or in person. They will run:"
|
||||
echo
|
||||
echo " gpg --import ${USERNAME}-gopass-pubkey.asc"
|
||||
echo " gopass recipients add --store ${STORE_ALIAS} ${EMAIL}"
|
||||
echo
|
||||
echo "Until then, 'gopass show ${STORE_ALIAS}/...' will fail with a"
|
||||
echo "'not a recipient' error - that's expected, not a bug (see gopass.md)."
|
||||
fi
|
||||
echo "======================================================================"
|
||||
@@ -23,15 +23,37 @@ if [ -L "$EXT_DIR" ] && [ ! -e "$EXT_DIR" ]; then
|
||||
rm -f "$EXT_DIR"
|
||||
fi
|
||||
|
||||
# Clear anything GNOME Shell's own extensions.gnome.org auto-update check
|
||||
# may have already staged here before allow-extension-installation (set by
|
||||
# the main startup script) had a chance to stop it -- a stale staged build
|
||||
# would otherwise still be sitting here ready to silently swap in on some
|
||||
# future shell restart, superseding the pinned version reinstalled below.
|
||||
rm -rf "${HOME}/.local/share/gnome-shell/extension-updates/${UUID}"
|
||||
|
||||
src_version=$(sed -n 's/.*"version": *\([0-9]*\).*/\1/p' "$SRC/metadata.json")
|
||||
dest_version=""
|
||||
[ -f "$DEST/metadata.json" ] && dest_version=$(sed -n 's/.*"version": *\([0-9]*\).*/\1/p' "$DEST/metadata.json")
|
||||
|
||||
reinstalled=false
|
||||
if [ ! -d "$DEST" ] || [ "$src_version" != "$dest_version" ]; then
|
||||
mkdir -p "$(dirname "$DEST")"
|
||||
rsync -a --delete "$SRC/" "$DEST/"
|
||||
glib-compile-schemas "$DEST/schemas" 2>/dev/null
|
||||
echo "install_gtile: installed gTile version $src_version to $DEST"
|
||||
reinstalled=true
|
||||
fi
|
||||
|
||||
# A plain `enable` is a no-op if the extension is already enabled -- the
|
||||
# normal case every login -- so it does NOT make gnome-shell pick up a
|
||||
# version this script just rsynced onto disk above (extensions aren't
|
||||
# hot-reloaded on file change). Force a disable/enable cycle whenever we
|
||||
# actually changed what's on disk, so the running session matches disk
|
||||
# immediately instead of only from the next full login onward (confirmed:
|
||||
# this exact gap is what left saresb-cons-05 running a stale in-memory
|
||||
# version even after the rsync above had already put the correct one back
|
||||
# on disk).
|
||||
if $reinstalled && gnome-extensions list --enabled 2>/dev/null | grep -qx "$UUID"; then
|
||||
gnome-extensions disable "$UUID" 2>/dev/null
|
||||
fi
|
||||
|
||||
# enable the extension (needs a live gnome-shell D-Bus session)
|
||||
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
source /sf/bernina/bin/bpy-env
|
||||
python -c "from eco import ioc_finder; ioc_finder()"
|
||||
|
||||
Executable
+331
@@ -0,0 +1,331 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate the "Scripts in this directory" section of cheatsheet.md from
|
||||
the actual contents of /sf/bernina/bin -- a full index of every script
|
||||
here, grouped by topic, as a set of "Command | Description" tables.
|
||||
|
||||
Descriptions are hand-curated below (COMMANDS dict), not auto-derived --
|
||||
most of these scripts have no usable header comment (many are a single
|
||||
command line), so a heuristic would mostly just echo the command back.
|
||||
Any file present in the directory but missing from COMMANDS lands in an
|
||||
"Uncategorized" bucket at the end instead of being silently dropped, so
|
||||
new scripts get noticed next time this runs rather than staying invisible
|
||||
forever. Anything in COMMANDS that no longer exists on disk is silently
|
||||
skipped (self-heals when scripts are removed).
|
||||
|
||||
These tables are headed "Command | Description" -- cheatsheet_to_shortcuts_json
|
||||
recognizes that header (alongside "Shortcut") so this section also shows up
|
||||
in the Super+F1 overlay, not just the full document.
|
||||
|
||||
To keep one script out of this listing entirely, put a comment containing
|
||||
HIDE_MARKER (see below) anywhere in its first HEADER_SCAN_LINES lines --
|
||||
no need to touch this generator. Takes priority over everything else,
|
||||
including an entry already in COMMANDS.
|
||||
|
||||
Usage: list_bin_commands [bin-dir] [cheatsheet.md path]
|
||||
Rewrites the content between the BEGIN/END markers in-place; everything
|
||||
else in the file is left untouched.
|
||||
"""
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
HIDE_MARKER = "cheatsheet: hide"
|
||||
HEADER_SCAN_LINES = 20
|
||||
|
||||
|
||||
def is_hidden(path):
|
||||
try:
|
||||
with path.open(encoding="utf-8", errors="replace") as f:
|
||||
for _ in range(HEADER_SCAN_LINES):
|
||||
line = f.readline()
|
||||
if not line:
|
||||
break
|
||||
if HIDE_MARKER in line:
|
||||
return True
|
||||
except OSError:
|
||||
pass
|
||||
return False
|
||||
|
||||
BEGIN_MARKER = "<!-- BEGIN SCRIPT INDEX (generated by list_bin_commands) -->"
|
||||
END_MARKER = "<!-- END SCRIPT INDEX -->"
|
||||
|
||||
# Files that aren't really commands: docs, backups, empty placeholders,
|
||||
# data files consumed by another script, the cheat sheet doc itself, and
|
||||
# this generator's own sibling infra (already covered in its own section
|
||||
# above in cheatsheet.md).
|
||||
EXCLUDE = {
|
||||
"cheatsheet.md", "anaconda_env.bak", "bpy-select_bak",
|
||||
"jungfrau_console_nox~", "me", "mosaic01.vlm",
|
||||
"bs_pvs_SLAAR21-CVME-LAS6991", "create_personal_dir.md", "gopass.md",
|
||||
"migrate_to_network_drive.md", "list_bin_commands",
|
||||
}
|
||||
|
||||
# (category, description) per command. Category order below is the
|
||||
# display order; commands are sorted alphabetically within each.
|
||||
COMMANDS = {
|
||||
# --- Cheat sheet & help ---
|
||||
"cheatsheet": ("Cheat sheet & help", "Show this cheat sheet in a terminal (glow + less, full detail)"),
|
||||
"cheatsheet_overlay": ("Cheat sheet & help", "Show this cheat sheet as a floating overlay panel (bound to Super+F1)"),
|
||||
"cheatsheet_to_shortcuts_json": ("Cheat sheet & help", "Regenerate the overlay's JSON from this file's headings/tables"),
|
||||
"check_kerberos_renewal": ("Cheat sheet & help", "Diagnose why the Kerberos ticket renewer might be renewing/prompting too often"),
|
||||
"useful_commands": ("Cheat sheet & help", "Show the older, plain-text useful-commands reference"),
|
||||
|
||||
# --- EPICS / eco environment ---
|
||||
"eco": ("EPICS / eco environment", "Start eco, the beamline's Python control environment"),
|
||||
"eco_bams_bpms": ("EPICS / eco environment", "Start eco configured for BAM/BPM work"),
|
||||
"eco-dev": ("EPICS / eco environment", "Start eco from the development checkout (symlink into the eco repo)"),
|
||||
"eco_historic": ("EPICS / eco environment", "Start eco against historic/legacy config"),
|
||||
"eco-in": ("EPICS / eco environment", "Start eco inside a chosen environment (arg, default bpy-default)"),
|
||||
"eco_mm_env": ("EPICS / eco environment", "Start eco in a chosen micromamba environment"),
|
||||
"eco_new": ("EPICS / eco environment", "Start eco using bpy-default.sh directly"),
|
||||
"eco_noenv": ("EPICS / eco environment", "Start eco's startup.py directly, no environment activation step"),
|
||||
"eco-notlazy": ("EPICS / eco environment", "Start eco with lazy-loading disabled (default env bernina37)"),
|
||||
"eco_old": ("EPICS / eco environment", "Start eco against the older sfb312 micromamba environment"),
|
||||
"eco_roman": ("EPICS / eco environment", "Start eco configured for the bernina_robot setup"),
|
||||
"eco-status-server": ("EPICS / eco environment", "Start/stop/check the eco status server (namespace mode)"),
|
||||
"eco-status-server-install-user-service": ("EPICS / eco environment", "Install the eco status server as a systemd --user service"),
|
||||
"eco-test": ("EPICS / eco environment", "Start eco against the sfb312 test micromamba environment"),
|
||||
"ecotest": ("EPICS / eco environment", "Start eco in the jungfrau_client conda environment"),
|
||||
|
||||
# --- Python, conda & pixi environments ---
|
||||
"anaconda_bernina_env": ("Python, conda & pixi environments", "Conda init hook for the bernina anaconda env (source this)"),
|
||||
"anaconda_env": ("Python, conda & pixi environments", "Activate a named anaconda environment"),
|
||||
"anaconda_manual": ("Python, conda & pixi environments", "Show the anaconda environment manual (rendered markdown)"),
|
||||
"anaconda_testenv": ("Python, conda & pixi environments", "Activate the anaconda test environment"),
|
||||
"bipython": ("Python, conda & pixi environments", "ipython from the shared bernina_default anaconda env"),
|
||||
"bjupyter": ("Python, conda & pixi environments", "jupyter from the shared bernina_default anaconda env"),
|
||||
"bpy": ("Python, conda & pixi environments", "Activate a named bpy (pixi) environment"),
|
||||
"bpy-default.sh": ("Python, conda & pixi environments", "Set PATH for the default shared bpy312 pixi environment (source this)"),
|
||||
"bpy-dev.sh": ("Python, conda & pixi environments", "Set PATH for the bpy312-dev pixi environment (source this)"),
|
||||
"bpy-env": ("Python, conda & pixi environments", "Activate the Bernina pixi Python env -- local per-host mirror if in sync, else shared (source this)"),
|
||||
"bpy-select": ("Python, conda & pixi environments", "Interactively pick/switch the active bpy pixi environment"),
|
||||
"bpython": ("Python, conda & pixi environments", "python from the shared bernina_default anaconda env"),
|
||||
"micromamba": ("Python, conda & pixi environments", "micromamba package manager"),
|
||||
"mm_bernina": ("Python, conda & pixi environments", "Micromamba shell-init hook for bernina (source this)"),
|
||||
"mm_bernina_env": ("Python, conda & pixi environments", "Activate a named micromamba environment"),
|
||||
"pixi": ("Python, conda & pixi environments", "pixi package manager"),
|
||||
"pixi_local_sync.sh": ("Python, conda & pixi environments", "Keep the local per-host pixi env mirror in sync (runs in background at login)"),
|
||||
"pixi_rebuild_local.sh": ("Python, conda & pixi environments", "Rebuild the bernina pixi env in an isolated local sandbox, for testing"),
|
||||
|
||||
# --- Jupyter & notebooks ---
|
||||
"bernina-jupyter": ("Jupyter & notebooks", "Start JupyterLab in the bernina-scripts config dir (bpy env)"),
|
||||
"ipylab": ("Jupyter & notebooks", "Start an ipylab session in a chosen anaconda environment"),
|
||||
"jupyterlab-on-ra": ("Jupyter & notebooks", "Open JupyterLab on a remote-access host, for a given user"),
|
||||
"jupyter-notebook-on-ra": ("Jupyter & notebooks", "Open a Jupyter Notebook on a remote-access host, for a given user"),
|
||||
|
||||
# --- Browsers ---
|
||||
"chrome": ("Browsers", "Open Chrome with a per-host profile"),
|
||||
"chrome_as": ("Browsers", "Open another user's Chrome profile from your own desktop"),
|
||||
"chromeAs": ("Browsers", "Open another user's Chrome profile from your own desktop (older variant)"),
|
||||
"firefox": ("Browsers", "Open Firefox with a per-host profile"),
|
||||
"firefox_as": ("Browsers", "Open another user's default Firefox profile from your own desktop"),
|
||||
"firefox-create-hostprofile": ("Browsers", "Create a fresh per-host Firefox profile"),
|
||||
"firefox_default": ("Browsers", "Default Firefox profile launcher (symlink to firefox_shared_profile)"),
|
||||
"firefox-hostprofile": ("Browsers", "Open Firefox with the per-host profile (alt entrypoint)"),
|
||||
"firefox_shared_profile": ("Browsers", "Open Firefox using a shared/synced profile"),
|
||||
"google-chrome": ("Browsers", "Open Chrome with a per-host profile (alt entrypoint)"),
|
||||
|
||||
# --- Displays & control panels ---
|
||||
"alarms_caqtdm": ("Displays & control panels", "Open the EPICS alarms caQtDM panel"),
|
||||
"Aramis": ("Displays & control panels", "Open the Aramis overview display (mydm/medm)"),
|
||||
"aramis-home": ("Displays & control panels", "Open the Aramis photonics overview caQtDM panel"),
|
||||
"bernina-home": ("Displays & control panels", "Open the esbMaster caQtDM panel"),
|
||||
"cams_sf": ("Displays & control panels", "Open the SwissFEL cameras caQtDM panel"),
|
||||
"diffractometer-config": ("Displays & control panels", "Open the diffractometer motor IOC configuration tool"),
|
||||
"motion-home": ("Displays & control panels", "Open the ZamofingMaster motion caQtDM panel"),
|
||||
"screen_panel": ("Displays & control panels", "Open the SF controls screen panel"),
|
||||
"screenpanel": ("Displays & control panels", "Open the screen panel (uses screenpanel-sf if available)"),
|
||||
"sf_archiver_panel": ("Displays & control panels", "Open the SwissFEL archiver viewer panel"),
|
||||
"strip_chart": ("Displays & control panels", "Open the PShell strip chart viewer"),
|
||||
"swissfelStatus": ("Displays & control panels", "Open the SwissFEL OP overview caQtDM panel"),
|
||||
"thc_angles": ("Displays & control panels", "Open the THC angles bokeh app in Firefox"),
|
||||
"thc_stripchart": ("Displays & control panels", "Open the THC temperature strip chart"),
|
||||
"timetool_online_plot": ("Displays & control panels", "Open the timetool online plot (voila notebook)"),
|
||||
|
||||
# --- Cameras ---
|
||||
"bernina-cam-mobile1": ("Cameras", "View the mobile1 camera feed (VLC)"),
|
||||
"bernina-cam-n": ("Cameras", "View the north camera feed (VLC)"),
|
||||
"bernina-cam-s": ("Cameras", "View the south camera feed (VLC)"),
|
||||
"bernina-cams-fix": ("Cameras", "Launch the fixed multi-camera mosaic view (VLC)"),
|
||||
"bernina-cam-w": ("Cameras", "View the west camera feed (VLC)"),
|
||||
"bernina-cam-wideangle1": ("Cameras", "View wide-angle camera 1 feed (VLC)"),
|
||||
"bernina-cam-wideangle2": ("Cameras", "View wide-angle camera 2 feed (VLC)"),
|
||||
"bernina-cam-wideangle3": ("Cameras", "View wide-angle camera 3 feed (VLC)"),
|
||||
"bernina-cam-wideangle4": ("Cameras", "View wide-angle camera 4 feed (VLC)"),
|
||||
"monitoring_cameras": ("Cameras", "Open the monitoring cameras panel (pshell-based)"),
|
||||
"record_camera": ("Cameras", "Record a camera feed to file"),
|
||||
|
||||
# --- FEL & laser control ---
|
||||
"feloff": ("FEL & laser control", "Stop/block the FEL beam and announce it"),
|
||||
"felon": ("FEL & laser control", "Open/enable the FEL beam and announce it"),
|
||||
"laseroff": ("FEL & laser control", "Block the laser shutter and announce it"),
|
||||
"laseron": ("FEL & laser control", "Open the laser shutter and announce it"),
|
||||
|
||||
# --- pshell & robot control ---
|
||||
"pshell_be": ("pshell & robot control", "Start the PShell Workbench for the main Bernina beamline environment"),
|
||||
"pshell_correlation": ("pshell & robot control", "Open the correlation panel"),
|
||||
"pshell_default": ("pshell & robot control", "Default pshell entrypoint (symlink to pshell_be)"),
|
||||
"robot_server": ("pshell & robot control", "Start the PShell Workbench for the Bernina robot -- needs a new binary, /opt/gfa/pshell is gone (see chat history: /sf/controls/bin/pshell -wb is the likely replacement, blocked on a dataFormat=null config issue)"),
|
||||
|
||||
# --- Desktop & window management ---
|
||||
"code": ("Desktop & window management", "Launch VS Code"),
|
||||
"darkscreens": ("Desktop & window management", "Blank/turn off the screens (xset dpms)"),
|
||||
"flameshot": ("Desktop & window management", "Take a screenshot"),
|
||||
"gnome-terminal_as": ("Desktop & window management", "Open a GNOME Terminal as another user"),
|
||||
"launcher": ("Desktop & window management", "Main Bernina application launcher"),
|
||||
"launcher_dev": ("Desktop & window management", "Application launcher (dev/legacy python 3.5 environment)"),
|
||||
"launcher_lca": ("Desktop & window management", "Open the launcher on the machine/LCA network, for a given user"),
|
||||
"launcher_legacy": ("Desktop & window management", "Legacy application launcher (python 3.5 environment)"),
|
||||
"rdpWater": ("Desktop & window management", "RDP into the WaterLab Windows machine"),
|
||||
"remoteDesktop": ("Desktop & window management", "RDP into an arbitrary PSI Windows host"),
|
||||
"sf-launcher": ("Desktop & window management", "Open the launcher on the sf network, for a given user"),
|
||||
"termAs": ("Desktop & window management", "Open a terminal as another user"),
|
||||
"terminalAs": ("Desktop & window management", "Open a terminal as another user (alt entrypoint)"),
|
||||
"window_arrange.py": ("Desktop & window management", "Arrange/tile windows helper script"),
|
||||
"winterm": ("Desktop & window management", "Open a Windows-side terminal session"),
|
||||
|
||||
# --- Applications & viewers ---
|
||||
"hdfview": ("Applications & viewers", "Open HDFView (HDF5 file viewer)"),
|
||||
"jungfrau_console": ("Applications & viewers", "Open the Jungfrau detector console"),
|
||||
"ncdu": ("Applications & viewers", "Disk usage analyzer"),
|
||||
"paraview": ("Applications & viewers", "Open ParaView"),
|
||||
"pycharm": ("Applications & viewers", "Open PyCharm Community"),
|
||||
"pylote": ("Applications & viewers", "Open Pylote"),
|
||||
"vesta": ("Applications & viewers", "Open VESTA (crystal structure viewer)"),
|
||||
|
||||
# --- Login session & keybinding setup ---
|
||||
"create-links-in-home": ("Login session & keybinding setup", "Force-apply PSI's Partially Shared Home symlinks now"),
|
||||
"gogh": ("Login session & keybinding setup", "Gogh terminal color-scheme installer (legacy -- upstream download is broken, see install_terminal_profiles)"),
|
||||
"install_gtile": ("Login session & keybinding setup", "Install/update and enable the gTile gnome-shell extension (runs at login)"),
|
||||
"install_terminal_profiles": ("Login session & keybinding setup", "Install Solarized/Spacedust GNOME Terminal profiles, set Spacedust default (runs at login)"),
|
||||
"kerberos_ticket_renew.sh": ("Login session & keybinding setup", "Keep the Kerberos ticket cache alive for the login session (runs in background)"),
|
||||
"migrate_to_network_drive.sh": ("Login session & keybinding setup", "Move a local dotfile onto PSI's Partially Shared Home and register it for sync"),
|
||||
"set_gnome_keyboard_shortcut": ("Login session & keybinding setup", "Register one GNOME custom keyboard shortcut (used by startup_bernina_terminal)"),
|
||||
"startup_bernina_terminal": ("Login session & keybinding setup", "Full per-login setup for a Bernina console -- runs automatically at login"),
|
||||
"sync_personal_config.sh": ("Login session & keybinding setup", "Seed .ipython/.condarc onto PSI's Partially Shared Home for cross-host sync"),
|
||||
|
||||
# --- gopass secrets & git ---
|
||||
"git-push": ("gopass secrets & git", "git push, forcing the origin remote to SSH first"),
|
||||
"gopass": ("gopass secrets & git", "Bernina gopass secrets store CLI"),
|
||||
"gopass_bernina_service_setup.sh": ("gopass secrets & git", "One-shot setup: give a service account (gac-bernina) access to the gopass store"),
|
||||
"gopass_bernina_setup.sh": ("gopass secrets & git", "One-shot onboarding for a human PSI account joining the gopass store"),
|
||||
"gopass.py": ("gopass secrets & git", "Minimal Python interface to the Bernina gopass store"),
|
||||
|
||||
# --- Personal dirs & data ---
|
||||
"create_data_symlinks": ("Personal dirs & data", "(Re)create the ~/bernina_data convenience symlink"),
|
||||
"create_personal_dir.sh": ("Personal dirs & data", "Create/verify a personal working directory under /sf/bernina/config/personal/ (your username)"),
|
||||
"name2pgroups": ("Personal dirs & data", "Look up p-group(s) for a given user name"),
|
||||
"pgroup2name": ("Personal dirs & data", "Look up the user name for a given p-group"),
|
||||
"quotas-bernina": ("Personal dirs & data", "Show Bernina's storage quota usage"),
|
||||
|
||||
# --- Beamline status & monitoring ---
|
||||
"chiller_flowrate": ("Beamline status & monitoring", "Query the JF16 chiller flowrate over telnet"),
|
||||
"feedbacks_bernina": ("Beamline status & monitoring", "Open the undulator/feedbacks control tool, for a given user"),
|
||||
"get_bernina_PVs_alive": ("Beamline status & monitoring", "Check whether a set of Bernina PVs are alive"),
|
||||
"go2daq": ("Beamline status & monitoring", "SSH to the SwissFEL DAQ host (sf-daq-3)"),
|
||||
"ioc-control-bernina": ("Beamline status & monitoring", "Open the IOC process monitor (PBMonitor)"),
|
||||
"ioc-finder": ("Beamline status & monitoring", "Find/locate an EPICS IOC"),
|
||||
"is_control": ("Beamline status & monitoring", "Check whether a given hostname is a primary controls terminal"),
|
||||
"phonebook": ("Beamline status & monitoring", "Show the Bernina phone list"),
|
||||
"remaining-slurm-job-time": ("Beamline status & monitoring", "Show remaining time for a SLURM job"),
|
||||
"restart_service": ("Beamline status & monitoring", "Restart a systemd service as the dbe account (sudo)"),
|
||||
"run-and-position.py": ("Beamline status & monitoring", "Run-and-position helper script"),
|
||||
"saythis": ("Beamline status & monitoring", "Speak given text aloud"),
|
||||
"set_bs_pvs": ("Beamline status & monitoring", "Configure the SLAAR21 beam-synchronous PVs"),
|
||||
"slurm_resources": ("Beamline status & monitoring", "Show SLURM partition resource usage"),
|
||||
"undulator-pointing": ("Beamline status & monitoring", "Open the undulator pointing control tool, for a given user"),
|
||||
|
||||
# --- Desktop entries ---
|
||||
"bernifox.desktop": ("Desktop entries (.desktop files)", "Bernina-branded Firefox launcher (per-host profile), set as default browser"),
|
||||
"Bernina.desktop": ("Desktop entries (.desktop files)", "Bernina launcher desktop icon (runs launcher)"),
|
||||
"kerberos-ticket-renew.desktop": ("Desktop entries (.desktop files)", "Autostart entry for the Kerberos ticket renewer"),
|
||||
}
|
||||
|
||||
CATEGORY_ORDER = [
|
||||
"Cheat sheet & help",
|
||||
"EPICS / eco environment",
|
||||
"Python, conda & pixi environments",
|
||||
"Jupyter & notebooks",
|
||||
"Browsers",
|
||||
"Displays & control panels",
|
||||
"Cameras",
|
||||
"FEL & laser control",
|
||||
"pshell & robot control",
|
||||
"Desktop & window management",
|
||||
"Applications & viewers",
|
||||
"Login session & keybinding setup",
|
||||
"gopass secrets & git",
|
||||
"Personal dirs & data",
|
||||
"Beamline status & monitoring",
|
||||
"Desktop entries (.desktop files)",
|
||||
"Uncategorized (new, needs review)",
|
||||
]
|
||||
|
||||
|
||||
def build_section(bin_dir):
|
||||
present = {p.name for p in bin_dir.iterdir() if p.is_file() and not p.name.startswith(".")}
|
||||
by_category = {cat: [] for cat in CATEGORY_ORDER}
|
||||
|
||||
for name in sorted(present):
|
||||
if name in EXCLUDE:
|
||||
continue
|
||||
if is_hidden(bin_dir / name):
|
||||
continue
|
||||
if name in COMMANDS:
|
||||
cat, desc = COMMANDS[name]
|
||||
else:
|
||||
cat, desc = "Uncategorized (new, needs review)", "(no description yet -- add this to list_bin_commands)"
|
||||
by_category[cat].append((name, desc))
|
||||
|
||||
lines = ["## Scripts in this directory", ""]
|
||||
lines.append(
|
||||
"Every script in `/sf/bernina/bin`, grouped by topic. Generated by "
|
||||
"`list_bin_commands` from the actual directory contents -- re-run it "
|
||||
"after adding/removing scripts to keep this in sync."
|
||||
)
|
||||
lines.append("")
|
||||
for cat in CATEGORY_ORDER:
|
||||
entries = by_category[cat]
|
||||
if not entries:
|
||||
continue
|
||||
lines.append(f"### {cat}")
|
||||
lines.append("")
|
||||
lines.append("| Command | Description |")
|
||||
lines.append("|---|---|")
|
||||
for name, desc in entries:
|
||||
lines.append(f"| `{name}` | {desc} |")
|
||||
lines.append("")
|
||||
return "\n".join(lines).rstrip() + "\n"
|
||||
|
||||
|
||||
def main():
|
||||
bin_dir = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("/sf/bernina/bin")
|
||||
md_path = Path(sys.argv[2]) if len(sys.argv) > 2 else bin_dir / "cheatsheet.md"
|
||||
|
||||
section = build_section(bin_dir)
|
||||
text = md_path.read_text(encoding="utf-8")
|
||||
|
||||
if BEGIN_MARKER in text and END_MARKER in text:
|
||||
before = text.split(BEGIN_MARKER)[0]
|
||||
after = text.split(END_MARKER)[1]
|
||||
new_text = f"{before}{BEGIN_MARKER}\n\n{section}\n{END_MARKER}{after}"
|
||||
else:
|
||||
sep = "\n" if text.endswith("\n") else "\n\n"
|
||||
new_text = f"{text}{sep}\n{BEGIN_MARKER}\n\n{section}\n{END_MARKER}\n"
|
||||
|
||||
md_path.write_text(new_text, encoding="utf-8")
|
||||
|
||||
present = {p.name for p in bin_dir.iterdir() if p.is_file() and not p.name.startswith(".")}
|
||||
missing = sorted(
|
||||
n for n in present
|
||||
if n not in COMMANDS and n not in EXCLUDE and not is_hidden(bin_dir / n)
|
||||
)
|
||||
if missing:
|
||||
print("Uncategorized (added to the doc under 'needs review', but worth "
|
||||
"adding real descriptions to list_bin_commands):", file=sys.stderr)
|
||||
for n in missing:
|
||||
print(f" {n}", file=sys.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+115
-312
@@ -1,338 +1,159 @@
|
||||
# Syncing dotfiles/config across hosts with PSI's Partially Shared Home
|
||||
|
||||
How `~/network_drives`, `linux_home_links.yaml`, and the two helper
|
||||
scripts in your home directory (`sync_personal_config.sh`,
|
||||
`migrate_to_network_drive.sh`) fit together to keep personal config
|
||||
(`.vimrc`, `.condarc`, SSH keys, ...) the same across every PSI Linux
|
||||
host you log into.
|
||||
How `~/network_drives`, `linux_home_links.yaml`, and two helper scripts
|
||||
(`sync_personal_config.sh`, `migrate_to_network_drive.sh`) keep personal
|
||||
config (`.vimrc`, `.condarc`, SSH keys, ...) the same across every PSI
|
||||
Linux host you log into. This is PSI's official
|
||||
[Partially Shared Home](https://linux.psi.ch/documentation/user-guide/personal_drive/)
|
||||
feature.
|
||||
|
||||
This is PSI's official "Partially Shared Home" feature
|
||||
(<https://linux.psi.ch/documentation/user-guide/personal_drive/>); this
|
||||
doc explains the mechanics and the gotchas found while wiring up a shared
|
||||
gopass secrets store (see the companion
|
||||
See also: the
|
||||
**[gopass + gitea guide](https://claude.ai/code/artifact/ee455a99-fc83-44d8-952d-e85f4bcf4cbb)**
|
||||
for why that one secret store ended up **not** using this mechanism), and
|
||||
while tracking down why SSH key-based login kept silently falling back to
|
||||
a password on some hosts (§7).
|
||||
for why the gopass secrets store deliberately does *not* use this
|
||||
mechanism (§3 below).
|
||||
|
||||
---
|
||||
|
||||
## 1. The three pieces
|
||||
## The pieces
|
||||
|
||||
```
|
||||
~/network_drives/home/ same network location on EVERY host
|
||||
├── linux_config/ (SMB share, auto-mounted at login)
|
||||
│ ├── ssh/ <- real files live here
|
||||
│ ├── vimrc
|
||||
│ ├── condarc
|
||||
│ └── ...
|
||||
└── linux_home_links.yaml <- the manifest: what goes where
|
||||
│ ├── ssh/, vimrc, condarc, ...
|
||||
└── linux_home_links.yaml <- manifest: what goes where
|
||||
|
||||
~/ (LOCAL home, different content per host, NOT shared)
|
||||
├── .ssh/ <- real local directory (see §7 - NOT
|
||||
│ ├── config -> ... a whole-directory symlink anymore)
|
||||
│ ├── known_hosts -> ...
|
||||
│ ├── id_ed25519(.pub) -> ...
|
||||
│ ├── id_rsa(.pub) -> ...
|
||||
│ └── authorized_keys <- real local file, copied in, NOT linked
|
||||
├── .vimrc -> network_drives/home/linux_config/vimrc (symlink)
|
||||
├── .bashrc (real local file,
|
||||
│ refreshed by copy)
|
||||
~/ (LOCAL home, different per host, NOT shared)
|
||||
├── .vimrc -> network_drives/home/linux_config/vimrc (symlink)
|
||||
├── .bashrc (real local file, refreshed by copy)
|
||||
└── ...
|
||||
|
||||
create-automatic-links-in-home.service (systemd --user, runs at login/
|
||||
ssh/`become`) reads linux_home_links.yaml and makes the local side match it.
|
||||
create-automatic-links-in-home.service (systemd --user) reads the
|
||||
manifest and makes the local side match it, at login/ssh/`become`.
|
||||
```
|
||||
|
||||
1. **`~/network_drives`** is your personal PSI network drive (the "U:"
|
||||
drive on Windows), auto-mounted via SMB at the same path on every
|
||||
Linux client. Nothing needs to be copied between hosts for anything
|
||||
that lives here — it's already the same storage everywhere.
|
||||
2. **Your local `$HOME`** is per-host. Files here do **not** follow you
|
||||
between machines unless something explicitly puts them there.
|
||||
3. **`linux_home_links.yaml`** (at `~/network_drives/home/linux_home_links.yaml`)
|
||||
is the manifest bridging the two: it says which local-home paths should
|
||||
be a symlink into `network_drives`, or should be refreshed by copying
|
||||
from it.
|
||||
4. **`create-automatic-links-in-home.service`** (a systemd `--user`
|
||||
service backed by `/opt/pli/bin/create-automatic-links-in-home`, a PSI-
|
||||
provided Python script) reads that manifest and creates the symlinks /
|
||||
performs the copies. It runs automatically on login, `ssh`, and
|
||||
`become`; you can also force it:
|
||||
```bash
|
||||
systemctl --user restart create-automatic-links-in-home.service
|
||||
```
|
||||
**Caveat (see §7):** "runs on ssh" means it runs as part of PAM
|
||||
*session* setup for that login — which happens only *after* sshd has
|
||||
already decided whether pubkey authentication succeeded. For anything
|
||||
sshd itself needs to read during authentication (`authorized_keys`,
|
||||
above all), this service is structurally too late to help.
|
||||
- **`~/network_drives`**: your personal PSI network drive (SMB), same
|
||||
path on every Linux client — nothing to sync for what lives here.
|
||||
- **Local `$HOME`**: per-host. Nothing follows you between machines
|
||||
unless something explicitly puts it there.
|
||||
- **`linux_home_links.yaml`**: the manifest bridging the two.
|
||||
- **The service** (`/opt/pli/bin/create-automatic-links-in-home`) applies
|
||||
it. Force it manually: `systemctl --user restart create-automatic-links-in-home.service`.
|
||||
**Caveat:** it runs via PAM *session* setup, which happens *after*
|
||||
`sshd` has already decided on pubkey auth — so it's structurally too
|
||||
late for anything `sshd` itself needs during authentication, notably
|
||||
`authorized_keys` (see the `.ssh` gotcha below).
|
||||
|
||||
## 2. `link:` vs `copy:` vs `download:`
|
||||
|
||||
`linux_home_links.yaml` has up to three top-level sections:
|
||||
## `link:` / `copy:` / `download:`
|
||||
|
||||
```yaml
|
||||
link:
|
||||
'.ssh':
|
||||
to: 'network_drives/home/linux_config/ssh'
|
||||
|
||||
'.vimrc':
|
||||
to: 'network_drives/home/linux_config/vimrc'
|
||||
copy:
|
||||
'.bashrc':
|
||||
from: 'network_drives/home/linux_config/bashrc'
|
||||
|
||||
download:
|
||||
'.config/tool.config':
|
||||
from: 'https://token@gitea.psi.ch/you/linux_config/tool.config'
|
||||
```
|
||||
|
||||
- **`link:`** — the local path becomes an actual symlink pointing into
|
||||
`network_drives`. The real file lives only on the network drive; the
|
||||
local path is just a pointer. Use this for anything you might edit
|
||||
locally (SSH keys, `.vimrc`, `.gitconfig`) — edits go straight to the
|
||||
shared copy.
|
||||
- **`copy:`** — the local path is overwritten with a fresh copy from
|
||||
`network_drives` **every time the service runs** (login/ssh/become, or
|
||||
a manual restart). This is one-directional: network → local. **Edit the
|
||||
network_drives copy, not the local file** — local edits get silently
|
||||
clobbered on the next apply. This exists for files read before the
|
||||
network mount is ready (e.g. `.bashrc` at shell startup, or
|
||||
`authorized_keys` at SSH auth time — see §7).
|
||||
- **`download:`** — like `copy:`, but the source is an HTTP(S) URL
|
||||
instead of a network-drive path (e.g. pulling a token-authenticated
|
||||
config from a private git host).
|
||||
- **`link:`** — local path becomes a symlink into `network_drives`. Edits
|
||||
go straight to the shared copy.
|
||||
- **`copy:`** — local path is overwritten from `network_drives` **every
|
||||
time the service runs** (one-directional). Edit the network-drive
|
||||
source, not the local file, or your edit gets clobbered on next apply.
|
||||
Used for anything needed before the mount is ready (`.bashrc` at shell
|
||||
startup, `authorized_keys` at SSH auth time).
|
||||
- **`download:`** — like `copy:`, from an HTTP(S) URL instead.
|
||||
|
||||
## 3. The nesting-depth gotcha (read this before adding anything nested)
|
||||
## Nesting-depth bug
|
||||
|
||||
This is a real, confirmed bug/limitation in the linking tool, not a
|
||||
hypothetical. The tool takes the `to:` value and creates the symlink with
|
||||
that exact string as the target — with **no adjustment for how deep the
|
||||
local path is**:
|
||||
The tool creates each symlink using the literal `to:` string, with **no
|
||||
adjustment for how deep the local path is** — a relative target resolves
|
||||
relative to the symlink's *own* directory, not `$HOME`. So it only works
|
||||
out of the box for paths directly under `$HOME` (one segment). A nested
|
||||
path (e.g. `.local/share/gnome-shell/extensions`, 3 segments) needs that
|
||||
many `../` prepended to `to:`:
|
||||
|
||||
```python
|
||||
# from /opt/pli/bin/create-automatic-links-in-home
|
||||
target = Path(link_config["to"])
|
||||
link_path.symlink_to(target)
|
||||
```
|
||||
|
||||
A relative symlink target is resolved relative to the symlink's *own*
|
||||
directory, not `$HOME`. So this works correctly only when the local path
|
||||
is directly under `$HOME` (one path segment) — every currently-working
|
||||
example in this account's config (`.gitconfig`, `.vimrc`, `.condarc`,
|
||||
`.scilog_psi`) is exactly that.
|
||||
|
||||
**Proof it breaks for nested paths:** this account's own
|
||||
`linux_home_links.yaml` has
|
||||
```yaml
|
||||
'.local/share/gnome-shell/extensions':
|
||||
to: 'network_drives/home/gnome-ext'
|
||||
```
|
||||
which is 3 segments deep (`.local/share/gnome-shell/` is the symlink's
|
||||
parent), so the tool creates `.local/share/gnome-shell/extensions ->
|
||||
network_drives/home/gnome-ext` — which resolves to
|
||||
`~/.local/share/gnome-shell/network_drives/home/gnome-ext`, a path that
|
||||
doesn't exist. **This symlink is dangling right now.** Confirmed with:
|
||||
```bash
|
||||
$ ls ~/.local/share/gnome-shell/extensions
|
||||
ls: cannot access '...': No such file or directory
|
||||
```
|
||||
|
||||
**The fix** for a nested path is to prefix `to:` with one `../` per path
|
||||
segment below `$HOME`. For the example above (3 segments:
|
||||
`.local`, `share`, `gnome-shell`), that's:
|
||||
```yaml
|
||||
'.local/share/gnome-shell/extensions':
|
||||
to: '../../../network_drives/home/gnome-ext'
|
||||
```
|
||||
The `.ssh/*` entries in §7 are a live example of this at depth 1 (one
|
||||
`../`). `migrate_to_network_drive.sh` (§5) computes this automatically —
|
||||
it's the reason to prefer the script over hand-editing the yaml for
|
||||
anything not directly under `$HOME`.
|
||||
|
||||
## 4. Filesystem limitation: no git repositories on `network_drives`
|
||||
`migrate_to_network_drive.sh` computes this automatically — prefer it
|
||||
over hand-editing the yaml for anything not directly under `$HOME`.
|
||||
|
||||
Confirmed by hand while setting up a team gopass store (see the gopass
|
||||
guide's "Limitations & workarounds" section for the full story):
|
||||
`network_drives` is an SMB mount (via `gvfs`) that does **not** support
|
||||
the atomic rename / `mkstemp`-then-rename writes git needs for its object
|
||||
store, and doesn't support setting file permissions either:
|
||||
## No git repos on `network_drives` (SMB) — use `/sf/bernina` (NFS) instead
|
||||
|
||||
`network_drives` is an SMB/`gvfs` mount that doesn't support the atomic
|
||||
rename/`mkstemp` writes git needs, or plain permission-preservation:
|
||||
|
||||
```
|
||||
rsync: [receiver] mkstemp "...git/objects/.../abc123.XXXXXX" failed: Operation not supported (95)
|
||||
cp: preserving permissions for '...': Operation not supported
|
||||
```
|
||||
|
||||
Plain files copy there fine (that's what `link:`/`copy:` are for) — it's
|
||||
specifically git's internals, and permission-preservation, that fail.
|
||||
**Don't put a live git working tree here**, directly or via a `link:`
|
||||
symlink pointing at one. That includes: the gopass store, any other git
|
||||
clone, or generally anything that needs real POSIX file-locking/atomic-
|
||||
write semantics (databases, etc.). Those should stay in local home and
|
||||
sync via their own network protocol (git+a remote, in gopass's case —
|
||||
see the [gopass + gitea guide](https://claude.ai/code/artifact/ee455a99-fc83-44d8-952d-e85f4bcf4cbb)).
|
||||
Plain files are fine there (that's what `link:`/`copy:` are for) — just
|
||||
never `git init`/`clone`/`config` (or `gpg --gen-key`) directly against a
|
||||
path under `network_drives`; those all fail the same way, trying to
|
||||
`chmod` an internal lock file. `/sf/bernina` (NFS4) doesn't have this
|
||||
problem — `git init`/`add`/`commit` work cleanly on it. Rule of thumb:
|
||||
check the mount type (`df -T <path>`) — `nfs`/`nfs4` is generally fine,
|
||||
`cifs`/`smb`/`gvfs` is not.
|
||||
|
||||
**Not every network mount has this problem, though.** `~/network_drives`
|
||||
specifically is SMB; `/sf/bernina` (Bernina beamline group storage,
|
||||
`lx-bl-fs.psi.ch:/export/bernina`, NFS4) is a different, real POSIX
|
||||
filesystem, and `git init`/`add`/`commit` and the `mkstemp`+rename pattern
|
||||
both work on it cleanly — tested the same way as the failure above. The
|
||||
gopass guide (§10.2) uses exactly this: one shared clone at
|
||||
`/sf/bernina/code/<you>/gopass-bernina`, visible identically on every
|
||||
Bernina host with no `linux_home_links.yaml` entry needed at all, since
|
||||
`/sf/bernina` is already mounted at the same path everywhere. The
|
||||
takeaway isn't "no network filesystem can hold a git repo" — it's "check
|
||||
what kind of mount it actually is" (`df -T <path>` — `nfs`/`nfs4` is
|
||||
generally fine, `cifs`/`smb`/a `gvfs` FUSE mount usually is not).
|
||||
That said, an *already-initialized* repo (or GPG keyring), copied on
|
||||
afterward with `rsync -rlt` (no `-p`/`-a`), keeps working normally from
|
||||
`network_drives` — `add`/`commit`/`push`/`pull` and `gpg` encrypt/decrypt
|
||||
all confirmed. `gopass.md` ("Store location") uses exactly this
|
||||
init-locally-then-copy workaround as the *preferred* location for a
|
||||
human's gopass setup, precisely because `network_drives` is already the
|
||||
same mount on every host. Do this by hand only if you understand the
|
||||
gotcha above; otherwise use the gopass scripts, which handle it for you.
|
||||
|
||||
## 5. The two helper scripts
|
||||
## The scripts
|
||||
|
||||
### `~/sync_personal_config.sh` (pre-existing)
|
||||
**`~/sync_personal_config.sh`** (pre-existing) — seeds two hardcoded
|
||||
items (`.ipython`, `.condarc`). Never edits an existing
|
||||
`linux_home_links.yaml`, only prints the snippet to add by hand.
|
||||
|
||||
Seeds two hardcoded items (`.ipython`, `.condarc`) onto the network
|
||||
drive. Deliberately conservative: if `linux_home_links.yaml` already
|
||||
exists, it **never edits it** — it only prints the snippet you'd need to
|
||||
add, to avoid clobbering your comments or other entries. Good for a quick
|
||||
look at how the seeding step works; limited to those two items.
|
||||
|
||||
### `/sf/bernina/bin/migrate_to_network_drive.sh` (general-purpose)
|
||||
|
||||
Handles any local file or directory, for either `link:` or `copy:`, and
|
||||
optionally pulls the source from another host first:
|
||||
**`/sf/bernina/bin/migrate_to_network_drive.sh`** (general-purpose) —
|
||||
handles any file/directory, `link:` or `copy:`:
|
||||
|
||||
```bash
|
||||
migrate_to_network_drive.sh <path-relative-to-HOME> [link|copy] [dest-name] [from-host]
|
||||
# e.g.
|
||||
migrate_to_network_drive.sh .npmrc
|
||||
migrate_to_network_drive.sh .config/htop copy htop-config
|
||||
migrate_to_network_drive.sh .npmrc link "" saresb-cons-07
|
||||
migrate_to_network_drive.sh .npmrc link "" saresb-cons-07 # pull from another host first
|
||||
```
|
||||
|
||||
What it does, in order:
|
||||
1. **Optionally fetches the source via `scp` from another host first**
|
||||
(§5.1 below) — either because you asked explicitly (`[from-host]`), or
|
||||
because it offers to when the local path doesn't exist on this host at
|
||||
all.
|
||||
2. **Refuses outright** if the (local or freshly-fetched) source contains
|
||||
a `.git` directory (§4).
|
||||
3. **Skips the seed-copy step entirely if local and network are already
|
||||
identical** (added while wiring up §7's `.ssh/*` split — re-running
|
||||
this to register one more already-symlinked file, or a file whose
|
||||
content already matches, just registers the yaml entry without
|
||||
prompting to overwrite anything).
|
||||
4. Otherwise, if something already exists at the destination on
|
||||
`network_drives`, shows a diff and asks before overwriting anything.
|
||||
5. Copies the content over (without trying to preserve permissions —
|
||||
see §4's second `Operation not supported` case).
|
||||
6. For `link:` mode: after you confirm (skipped if `$HOME` path is
|
||||
already a symlink resolving to the right place), removes the local
|
||||
copy and replaces it with a symlink — with the `../` depth computed
|
||||
correctly for wherever the path sits (§3), then verifies the symlink
|
||||
actually resolves before touching the yaml.
|
||||
7. If `linux_home_links.yaml` already has a `link:`/`copy:` section,
|
||||
inserts the new entry into it directly (after showing a diff and a
|
||||
timestamped backup path, and asking for confirmation) rather than just
|
||||
printing a snippet.
|
||||
8. Offers to apply immediately via
|
||||
`systemctl --user restart create-automatic-links-in-home.service`.
|
||||
It refuses anything containing a `.git` directory, diffs before
|
||||
overwriting anything already on the network drive, computes the correct
|
||||
`../` depth, and inserts the yaml entry itself (with a backup and a
|
||||
confirmation prompt) rather than just printing a snippet. `[from-host]`
|
||||
lets it `scp` the source from another host first — explicitly, or
|
||||
offered automatically when the local path doesn't exist yet (defaults to
|
||||
`saresb-vcons-01`).
|
||||
|
||||
**Note on `dest-name` for a file that already lives inside a shared
|
||||
subdirectory** (e.g. adding another file under the existing
|
||||
`linux_config/ssh/` alongside `config`/`known_hosts`/the key files from
|
||||
§7): the script's *default* `dest-name` flattens `/` to `-`
|
||||
(`.ssh/foo` → `ssh-foo`), which does **not** match paths already nested
|
||||
under `linux_config/ssh/`. Pass the real relative path explicitly, e.g.:
|
||||
For a file that belongs inside an existing shared subdirectory (e.g.
|
||||
another key under `linux_config/ssh/`), pass the real relative path as
|
||||
`dest-name` explicitly — the script's automatic flattening (`.ssh/foo` →
|
||||
`ssh-foo`) won't match a path that should nest under `ssh/`:
|
||||
```bash
|
||||
migrate_to_network_drive.sh .ssh/some_new_key link ssh/some_new_key
|
||||
```
|
||||
|
||||
Tested end-to-end: a flat `copy:` entry, a nested `link:` entry (verified
|
||||
the computed `../` prefix actually resolves, unlike the pre-existing
|
||||
gnome-ext one), the git-repo refusal, the scp fetch (§5.1), and the
|
||||
already-in-sync skip added for §7.
|
||||
## The `.ssh` gotcha: a whole-directory `link:` breaks SSH key login
|
||||
|
||||
#### 5.1 Fetching from another host
|
||||
**Symptom:** your key is genuinely in the shared `authorized_keys`, but a
|
||||
fresh `ssh` to some host still falls back to a password — inconsistently.
|
||||
|
||||
Consolidating several hosts' independently-drifted copies of the same
|
||||
dotfile onto one canonical, network-synced version is exactly what
|
||||
`sync_personal_config.sh`'s own header comment describes doing by hand
|
||||
("Run this on host_old ... Run on host_new to pick up"). The script
|
||||
automates the "pull from host_old" half:
|
||||
**Cause:** if `.ssh` is linked as a single whole-directory entry, it
|
||||
resolves through the same GVFS/SMB mount as `network_drives` generally —
|
||||
which only exists while a graphical session for your uid is already
|
||||
active on that host. `sshd` reads `authorized_keys` during authentication
|
||||
itself, *before* the linking service has run (see the caveat above), so
|
||||
a fresh connection only succeeds if some earlier session happened to
|
||||
leave the mount warm.
|
||||
|
||||
- **`[from-host]` given explicitly** (4th argument): always fetches a
|
||||
fresh copy via `scp host:<path>` (same relative-to-`$HOME` path on the
|
||||
remote side) and, if a local copy also exists, shows a diff and asks
|
||||
before replacing it.
|
||||
- **`[from-host]` omitted, local path missing:** offers the fetch
|
||||
interactively, defaulting the hostname to `saresb-vcons-01` (just hit
|
||||
enter to accept, or type a different host).
|
||||
|
||||
Either way, PSI accounts are the same username on every host, so a plain
|
||||
`host:path` works — no `user@` needed. The fetch requires that host to be
|
||||
SSH-reachable from wherever you're running the script; `saresb-vcons-01`
|
||||
didn't answer from this particular session's network path when tested
|
||||
(DNS resolved fine — `129.129.243.41` — the connection itself timed out),
|
||||
so this was validated against a loopback self-fetch instead (same code
|
||||
path, `host:path` resolved via SSH to the local machine's own hostname).
|
||||
Worth a quick manual check the first time you rely on the default host
|
||||
from wherever you're actually running this.
|
||||
|
||||
## 6. Troubleshooting
|
||||
|
||||
- **A `link:` entry resolves to "No such file or directory":** almost
|
||||
always the nesting-depth bug (§3). Check how many `../` the `to:`
|
||||
value has versus how deep the local path is.
|
||||
- **Changes to a `copy:` file disappear:** you edited the local copy
|
||||
instead of the network-drive source. Edit
|
||||
`~/network_drives/home/linux_config/<name>` instead, then re-apply.
|
||||
- **Nothing updates after editing `linux_home_links.yaml`:** the service
|
||||
only runs at login/ssh/become; force it with
|
||||
`systemctl --user restart create-automatic-links-in-home.service`.
|
||||
- **`~/network_drives/home` missing entirely:** run `setup-network-drives`
|
||||
(both helper scripts do this automatically first).
|
||||
- **Trying to sync a git repo (gopass or otherwise) this way:** don't —
|
||||
see §4 and the [gopass + gitea guide](https://claude.ai/code/artifact/ee455a99-fc83-44d8-952d-e85f4bcf4cbb).
|
||||
- **SSH key-based login silently falls back to a password on some
|
||||
hosts:** see §7 — almost certainly a `link:`-ed `authorized_keys`.
|
||||
|
||||
## 7. `.ssh` as a whole-directory `link:` breaks SSH key login (confirmed)
|
||||
|
||||
**Symptom:** `ssh-copy-id`/manually-appended keys are genuinely present in
|
||||
the shared `authorized_keys`, yet a fresh `ssh` to some host still prompts
|
||||
for a password instead of using the key — inconsistently, seemingly at
|
||||
random.
|
||||
|
||||
**Root cause, confirmed on `saresb-cons-05` (2026-08-27):** the stock
|
||||
setup has one whole-directory entry,
|
||||
```yaml
|
||||
link:
|
||||
'.ssh':
|
||||
to: 'network_drives/home/linux_config/ssh'
|
||||
```
|
||||
`~/.ssh` is therefore a symlink resolving through a `gvfsd-fuse`/
|
||||
`gvfsd-smb` mount under `/run/user/<uid>/gvfs/...` — a mount that only
|
||||
exists while some **graphical desktop session** for that uid is active on
|
||||
that specific host (confirmed via `findmnt`/`mount`; it's the exact same
|
||||
mechanism behind `~/network_drives` generally).
|
||||
|
||||
`sshd` reads `~/.ssh/authorized_keys` as part of the authentication
|
||||
handshake itself — **before** any PAM session setup for the incoming
|
||||
connection runs. `create-automatic-links-in-home.service` is only started
|
||||
via `pam_systemd`, i.e. as part of that same PAM *session* setup (`WantedBy=
|
||||
default.target` inside the user's `systemd --user` instance) — confirmed
|
||||
via `systemctl --user cat`/`status`. So the service is structurally
|
||||
**always too late** to make a `link:`-ed `.ssh` resolve for pubkey auth on
|
||||
a genuinely fresh connection; it only ever appeared to work because some
|
||||
earlier session for the same uid had left the GVFS mount warm already.
|
||||
This is the identical root-cause class the `.bashrc` `copy:` entry already
|
||||
exists to work around ("linking does not work as the shell is started
|
||||
before the mounts are ready") — it just hadn't been extended to
|
||||
`authorized_keys`, likely because that failure is silent (falls back to
|
||||
password) rather than a visibly broken shell.
|
||||
|
||||
**Fix applied and verified end-to-end:** split the single whole-directory
|
||||
`link:` entry into per-file entries, with `authorized_keys` specifically
|
||||
made a `copy:` (real local file, refreshed from the network master, never
|
||||
a symlink):
|
||||
**Fix:** split `.ssh` into per-file entries, with `authorized_keys`
|
||||
specifically as `copy:` (a real local file, never a symlink):
|
||||
|
||||
```yaml
|
||||
link:
|
||||
@@ -344,47 +165,29 @@ link:
|
||||
to: '../network_drives/home/linux_config/ssh/id_ed25519'
|
||||
'.ssh/id_ed25519.pub':
|
||||
to: '../network_drives/home/linux_config/ssh/id_ed25519.pub'
|
||||
'.ssh/id_rsa':
|
||||
to: '../network_drives/home/linux_config/ssh/id_rsa'
|
||||
'.ssh/id_rsa.pub':
|
||||
to: '../network_drives/home/linux_config/ssh/id_rsa.pub'
|
||||
|
||||
copy:
|
||||
'.ssh/authorized_keys':
|
||||
from: 'network_drives/home/linux_config/ssh/authorized_keys'
|
||||
```
|
||||
|
||||
Each `link:` entry needs exactly one `../` (§3's rule: one segment below
|
||||
`$HOME`, since the symlink now sits at `~/.ssh/<file>` rather than
|
||||
`~/.ssh` itself).
|
||||
Each `link:` entry needs exactly one `../` (one segment below `$HOME` now
|
||||
that the symlink is `~/.ssh/<file>`, not `~/.ssh` itself). Verified fix on
|
||||
a real host: pubkey auth failed outright before the split, succeeded
|
||||
immediately after, and survived a service restart with `authorized_keys`
|
||||
re-copied identically. Apply the same split on any other host that hits
|
||||
this, or use `migrate_to_network_drive.sh` per file (remember the
|
||||
explicit `dest-name`, e.g. `ssh/config`).
|
||||
|
||||
**Verified, in this order, on `saresb-cons-05`:**
|
||||
1. Fresh non-multiplexed connection *before* the fix:
|
||||
`debug1: Offering public key ...` followed by outright rejection — no
|
||||
`Server accepts key` line, straight to `Permission denied
|
||||
(publickey,gssapi-keyex,gssapi-with-mic,password)` — even though the
|
||||
key was already correctly present in `authorized_keys` (confirmed
|
||||
present, correct owner, correct `700`/`600` permissions the whole
|
||||
time — this was never a permissions/`StrictModes` problem).
|
||||
2. Applied the split above, both on disk and in `linux_home_links.yaml`.
|
||||
3. Fresh non-multiplexed connection *after* the fix:
|
||||
`Authenticated to saresb-cons-05 (via proxy) using "publickey".`
|
||||
4. Ran `systemctl --user restart create-automatic-links-in-home.service`
|
||||
(simulating the service actually firing on a future login) — the
|
||||
per-file symlinks were left untouched, and `authorized_keys` was
|
||||
re-copied from the network master with **identical** content
|
||||
(`md5sum` matched before/after).
|
||||
5. Re-ran the fresh non-multiplexed connection test once more —
|
||||
`Authenticated to saresb-cons-05 (via proxy) using "publickey".` again.
|
||||
## Troubleshooting
|
||||
|
||||
**Old backup:** the original whole-directory symlink is preserved at
|
||||
`~/.ssh.netlink` (not referenced by anything, safe to remove once you've
|
||||
confirmed things stay stable for a while).
|
||||
|
||||
**If you hit this on another host:** either hand-edit that host's
|
||||
`linux_home_links.yaml` the same way, or use
|
||||
`migrate_to_network_drive.sh` (§5) once per file — it already computes
|
||||
the right `../` depth and registers the yaml entry; just remember to pass
|
||||
an explicit `dest-name` (e.g. `ssh/config`) since the files already live
|
||||
under `linux_config/ssh/` and the script's automatic flattening wouldn't
|
||||
match that path by default.
|
||||
- **A `link:` entry resolves to "No such file or directory"**: the
|
||||
nesting-depth bug — check the `../` count against the path's depth.
|
||||
- **Changes to a `copy:` file disappear**: you edited the local copy
|
||||
instead of `~/network_drives/home/linux_config/<name>`.
|
||||
- **Nothing updates after editing the yaml**: force
|
||||
`systemctl --user restart create-automatic-links-in-home.service`.
|
||||
- **`~/network_drives/home` missing**: run `setup-network-drives`.
|
||||
- **Trying to sync a git repo this way**: don't — see above.
|
||||
- **SSH key login silently falls back to a password on some hosts**: the
|
||||
`.ssh` gotcha above.
|
||||
@@ -9,6 +9,12 @@ gsettings set org.gnome.settings-daemon.plugins.media-keys email []
|
||||
gsettings set org.gnome.settings-daemon.plugins.media-keys home []
|
||||
gsettings set org.gnome.settings-daemon.plugins.media-keys screenshot []
|
||||
gsettings set org.gnome.settings-daemon.plugins.media-keys help []
|
||||
# Note: org.gnome.mutter has its own copy of this key that stays at its
|
||||
# default (false) and looks unmanaged/inconsistent -- it isn't. The
|
||||
# shell.overrides schema documents itself as overriding the mutter key
|
||||
# "when running GNOME Shell" (confirmed via `gsettings describe`), which is
|
||||
# always the case here, so this is the authoritative key and the mutter one
|
||||
# is inert. No need to set both.
|
||||
gsettings set org.gnome.shell.overrides workspaces-only-on-primary true
|
||||
|
||||
# use Caps Lock as a compose key, so accented/special characters (and
|
||||
@@ -142,15 +148,30 @@ if gnome-extensions list --enabled 2>/dev/null | grep -qx "Shortcuts@kyle.aims.a
|
||||
echo "Disabled leftover Shortcuts@kyle.aims.ac.za gnome-shell extension (superseded by cheatsheet_overlay)"
|
||||
fi
|
||||
rm -rf "$HOME/.local/share/gnome-shell/extensions/Shortcuts@kyle.aims.ac.za"
|
||||
# The extension's own settings subtree is a separate relocatable schema and
|
||||
# survives disabling it or deleting its directory -- reset it too so it
|
||||
# doesn't linger as orphaned dconf state (found still present on hosts that
|
||||
# ran the extension long after it was disabled above).
|
||||
dconf reset -f /org/gnome/shell/extensions/shortcuts/
|
||||
|
||||
#custom bernina keyboard shortcuts
|
||||
gsettings reset org.gnome.settings-daemon.plugins.media-keys custom-keybindings
|
||||
# Full subtree wipe, not just the list: set_gnome_keyboard_shortcut always
|
||||
# appends to the next free customN slot and never reuses/cleans up old
|
||||
# ones, so a `gsettings reset ... custom-keybindings` here only clears which
|
||||
# paths are active -- the individual customN/{name,command,binding} nodes
|
||||
# below are a relocatable schema and persist forever once written. Any
|
||||
# future change to the number/order of the set_gnome_keyboard_shortcut
|
||||
# calls below would otherwise orphan whatever slots existed under the old
|
||||
# call count (found on saresb-cons-05: 11 stray custom18-custom28 nodes
|
||||
# from a since-removed shortcut).
|
||||
dconf reset -f /org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/
|
||||
if [ "$(hostname)" = "saresb-cons-03.psi.ch" ] || [ "$(hostname)" = "saresb-cons-04.psi.ch" ] || [ "$(hostname)" = "saresb-cons-05.psi.ch" ] || [ "$(hostname)" = "saresb-cons-08.psi.ch" ] || [ "$(hostname)" = "saresb-cons-09.psi.ch" ] || [ "$(hostname)" = "saresb-cons-10.psi.ch" ]; then
|
||||
echo "This is a primary controls terminal"
|
||||
set_gnome_keyboard_shortcut 'stop_fel' 'feloff' 'Calculator'
|
||||
set_gnome_keyboard_shortcut 'open_fel' 'felon' '<Super>Calculator'
|
||||
set_gnome_keyboard_shortcut 'stop_laser' 'laseroff' 'Mail'
|
||||
set_gnome_keyboard_shortcut 'open_laser' 'laseron' '<Super>Mail'
|
||||
echo "done"
|
||||
else
|
||||
echo "no primary controls terminal"
|
||||
fi
|
||||
@@ -170,6 +191,16 @@ set_gnome_keyboard_shortcut 'dark screens' 'xset dpms force off' '<Ctrl><Alt>d'
|
||||
|
||||
#install/update and enable the gtile extension, then set its keyboard
|
||||
#shortcuts for easier use of 4K screens
|
||||
# GNOME Shell's own extensions.gnome.org auto-update check doesn't know or
|
||||
# care that gTile's UUID is pinned/managed here by install_gtile plus the
|
||||
# shared /sf/bernina/applications directory -- it silently fetched and
|
||||
# swapped in a newer upstream build once already (confirmed: v52 replacing
|
||||
# our pinned v44, with a different settings/keybinding schema, breaking
|
||||
# every gTile shortcut on saresb-cons-05). allow-extension-installation
|
||||
# gates that whole background fetch/stage/swap mechanism; there's no
|
||||
# reason for it to be on when every extension this desktop uses is already
|
||||
# managed by hand.
|
||||
gsettings set org.gnome.shell allow-extension-installation false
|
||||
install_gtile
|
||||
dconf write /org/gnome/shell/extensions/gtile/resize21 "'3x3 0:2 0:2, 0:1 0:2, 0:2 1:2, 0:1 1:2'"
|
||||
dconf write /org/gnome/shell/extensions/gtile/resize22 "'3x3 1:2 1:2, 1:1 1:2, 0:2 2:2, 0:1 2:2'"
|
||||
@@ -180,6 +211,13 @@ dconf write /org/gnome/shell/extensions/gtile/resize26 "'3x3 2:1 2:1, 2:0 2:2, 1
|
||||
dconf write /org/gnome/shell/extensions/gtile/resize27 "'3x3 0:0 0:0, 0:0 0:1, 0:0 1:0, 0:0 1:1'"
|
||||
dconf write /org/gnome/shell/extensions/gtile/resize28 "'3x3 1:0 1:0, 1:0 1:1, 0:0 2:0, 0:0 2:1'"
|
||||
dconf write /org/gnome/shell/extensions/gtile/resize29 "'3x3 2:0 2:0, 2:0 2:1, 1:0 2:0, 1:0 2:1'"
|
||||
# preset-resize-21 (numpad-1's keybinding for the resize21 preset above)
|
||||
# had a stray override missing its Shift modifier on saresb-cons-04 (bound
|
||||
# to plain <Super>KP_1 instead of gTile's own default <Super><Shift>KP_1;
|
||||
# every other preset-resize-2N sibling was untouched and still works fine)
|
||||
# -- reset it back to gTile's own default rather than hardcoding a value
|
||||
# here, so this self-heals regardless of what that default actually is.
|
||||
gsettings reset org.gnome.shell.extensions.gtile preset-resize-21
|
||||
|
||||
# gnome terminal color profiles: install Solarized Dark/Light and Spacedust
|
||||
# (plus a "1-Spacedust" copy that sorts to the top of the profile menu) if
|
||||
|
||||
Reference in new issue
Block a user