Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 41 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,23 +108,55 @@ GNOME already holds** -- it mints a password only when Remote Login has none, so
re-running it cannot lock out whoever is already connecting. To rotate
deliberately, delete `/etc/hyperi/rdp-credentials` and re-run.

### Which user gets the user-level settings
### Which users get the user-level settings

Most of what this installs is system-wide, but some of it is per-user: shell
config, `~/.cargo`, `~/.local/bin`, dconf/GNOME settings, the Arcane and
local-services stacks. Those follow **`hyperi_target_user`**.
local-services stacks.

It defaults to whoever is running the install, which is the right answer on a
laptop. It is the wrong answer on a fleet machine reached over SSH as a service
account, because that account's home is not the desktop:
`./install.sh` applies those for **every account a person actually works in**.
Three kinds are skipped:

- `root`
- system accounts -- uid below 1000, the systemd range above 60000, and
anything shelled to `nologin`/`false`/`sync`
- **the cloud image's own account** -- `ubuntu` on an Ubuntu cloud image,
`cloud-user` on a Red Hat one. It exists to provision the machine, not to
work in, so it gets the system-wide setup and none of the dotfiles

That last one is read from `system_info.default_user.name` in
`/etc/cloud/cloud.cfg`, which is where cloud-init declares it, rather than
matching on the name.

Name any set yourself when that is not what you want -- including the cloud
account:

```bash
./install.sh --users hyperi,ubuntu
```

**If no account qualifies, nothing is applied and the run stops** with the
reason. That happens on a machine holding only `root`, system accounts and the
image's own -- a fresh cloud image, typically. It does not guess: writing
dotfiles into `root` or the provisioning account is the outcome the criteria
exist to prevent, so name the account you meant with `--users`.

Each user gets their own pass. The system-wide work is idempotent, so it
happens once in effect and later passes no-op; one user's settings never
overwrite another's.

Driving Ansible directly instead of `install.sh`? It takes one user per run, so
loop it:

```bash
# Fleet machine: connect as the service account, install for the desktop user
ansible-playbook ... -e hyperi_target_user=hyperi
for u in ubuntu hyperi; do
ansible-playbook ... -e hyperi_target_user="$u"
done
```

Get this wrong and the run still reports success -- the settings simply land in
the service account's home where nobody sees them.
That matters most on a fleet machine reached over SSH as a service account,
because that account's home is not the desktop. Get it wrong and the run still
reports success -- the settings simply land where nobody sees them.

## What Gets Installed

Expand Down
47 changes: 44 additions & 3 deletions docs/install-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -365,13 +365,54 @@ is the most expensive mistake available here.
`hyperi_target_user` names them; `actual_user` and `user_home` derive from it,
and every user-scoped task keys off those.

They are the same person on a laptop, which is why the default (the invoking
user) is right there and wrong on a fleet machine:
They are the same person on a laptop, which is why the default is right there
and wrong on a fleet machine.

### One run, one user -- and how the loop works

**The playbook applies user-level settings for exactly ONE user per run.**
There is no loop inside the roles, and that is deliberate: roughly 350
user-scoped references across 49 files means a per-task loop or a
`user-scope` tag would eventually miss one silently, which is the failure mode
this whole area already has a history of.

`install.sh` loops instead, once per user:

| Selection | Behaviour |
|---|---|
| default | every real login account, discovered from `/etc/passwd` |
| `--users a,b` | exactly those, in that order |

Discovery skips three kinds of account: `root`; system accounts (uid below 1000,
the systemd range above 60000, shells of `nologin`/`false`/`sync`); and **the
cloud image's own account**, which exists to provision the machine rather than
to work in.

That last one is read from `system_info.default_user.name` in
`/etc/cloud/cloud.cfg` rather than matched by name, because it is `ubuntu` on an
Ubuntu image and `cloud-user` on a Red Hat one. An override dropped in
`cloud.cfg.d` is not consulted -- use `--users` on a machine that does that.
`--users` is also how you deliberately include the cloud account.

**No qualifying account means nothing is applied and the run stops**, naming the
reason. A fresh cloud image holding only `root`, system accounts and its own is
exactly that case. There is no fallback to the invoking user on purpose --
falling back would write dotfiles into `root` or the provisioning account, which
is what the criteria exist to prevent.

Driving Ansible directly (Packer, hyperi-infra) means writing the loop
yourself:

```bash
ansible-playbook ... -e hyperi_target_user=hyperi
for u in ubuntu hyperi; do
ansible-playbook ... -e hyperi_target_user="$u"
done
```

The system-wide tasks are idempotent, so the second and later passes no-op on
everything except that user's own files, and no user's settings overwrite
another's.

**Getting it wrong reports success.** A task that writes to a home directory as
the connecting user writes to the WRONG home and exits 0; the tools land where
nobody looks. `ansible.builtin.file` is worse -- it cannot read across into the
Expand Down
89 changes: 78 additions & 11 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@ EXAMPLES:
Install the RDP server (GNOME Remote Login) for inbound access:
./install.sh --tags rdp-server

Apply the user-level settings for named users instead of the detected ones:
./install.sh --users hyperi,ubuntu

Dry-run to see what would change:
./install.sh --check

Expand All @@ -130,6 +133,11 @@ NOTES:
random password, shown once, and never overwrites credentials already set
- Use --tags-exclude to skip specific tags within a chosen group
- Use --list-apps to see every per-app sub-tag for granular installs
- User-level settings (shell config, ~/.cargo, ~/.local, dconf, the container
stacks) are applied for every account a person works in. Skipped by default:
root, the system ranges, and the cloud image's own account (ubuntu,
cloud-user). Name any set yourself with --users, that account included.
System-wide work happens once either way.
EOF
exit 0
}
Expand Down Expand Up @@ -263,12 +271,37 @@ append_extra_var() {
fi
}

# cloud-init DECLARES the account it created, and the name differs per image
# family (ubuntu, cloud-user, ec2-user), so read it rather than guessing. An
# override dropped in cloud.cfg.d is not consulted -- name the users with
# --users on a machine that does that.
cloud_image_user() {
[[ -r /etc/cloud/cloud.cfg ]] || return 0
awk '/^[[:space:]]*default_user:/ { in_block = 1; next }
in_block && /^[[:space:]]*name:/ {
sub(/^[[:space:]]*name:[[:space:]]*/, ""); print; exit
}' /etc/cloud/cloud.cfg
}

# Every account a person actually works in: root, the system ranges and the
# image's own provisioning account are all out. uid 60000 is the ceiling because
# systemd allocates its own users above it.
discover_target_users() {
local cloud_user
cloud_user="$(cloud_image_user)"
getent passwd | awk -F: -v cloud="$cloud_user" '
$3 >= 1000 && $3 < 60000 &&
$1 != "root" && $1 != cloud &&
$7 !~ /(nologin|\/false|\/sync)$/ { print $1 }' | sort | tr '\n' ' '
}

# Parse arguments
ANSIBLE_CHECK=""
ANSIBLE_TAGS=""
ANSIBLE_SKIP_TAGS=""
ANSIBLE_EXTRA_VARS=""
GIT_BRANCH="main"
TARGET_USERS=""

while [[ $# -gt 0 ]]; do
case $1 in
Expand Down Expand Up @@ -371,6 +404,10 @@ while [[ $# -gt 0 ]]; do
append_extra_var "hyperi_pinned=true"
shift
;;
--users)
TARGET_USERS="$(printf '%s' "$2" | tr ',' ' ')"
shift 2
;;
--list-apps)
list_apps
;;
Expand Down Expand Up @@ -573,20 +610,50 @@ print_info "Command: $ANSIBLE_BIN playbooks/main.yml -i inventories/localhost/in

cd ansible || exit 1

# Settle who gets the user-level settings -- shell config, ~/.cargo, ~/.local,
# dconf, the container stacks. macOS never separates the desktop user from the
# installing user, so there is nothing to discover there.
if [[ -z "$TARGET_USERS" ]]; then
if [[ "$OS_FAMILY" == "macos" ]]; then
TARGET_USERS="$(id -un)"
else
TARGET_USERS="$(discover_target_users)"
fi
fi

# No qualifying account means no user-level settings, full stop. Falling back to
# whoever invoked this would write dotfiles into root or the image's own
# provisioning account, which is the outcome the criteria exist to prevent.
if [[ -z "${TARGET_USERS// /}" ]]; then
print_error "No account qualifies for the user-level settings"
print_info "Skipped: root, system accounts, and the cloud image's own account"
print_info "Name one explicitly if that is not what you want: --users <name>"
exit 1
fi

print_info "User-level settings will be applied for: $TARGET_USERS"

# One pass per user rather than a loop inside the roles: the system-wide tasks
# are idempotent and no-op on later passes, and nothing user-scoped can be
# silently missed the way a forgotten loop or tag would miss it.
# The EXIT trap set above removes the temp venv on any outcome. Run the
# playbook inside the `if` condition so set -e does not abort before we can
# report a friendly failure (and the trap still fires on exit).
# shellcheck disable=SC2086
if ! "$ANSIBLE_BIN" \
playbooks/main.yml \
-i inventories/localhost/inventory.yml \
$ANSIBLE_CHECK \
$ANSIBLE_TAGS \
$ANSIBLE_SKIP_TAGS_ARG \
$ANSIBLE_EXTRA_VARS; then
print_error "Ansible playbook failed"
exit 1
fi
for target_user in $TARGET_USERS; do
print_info "Applying for $target_user ..."
# shellcheck disable=SC2086
if ! "$ANSIBLE_BIN" \
playbooks/main.yml \
-i inventories/localhost/inventory.yml \
$ANSIBLE_CHECK \
$ANSIBLE_TAGS \
$ANSIBLE_SKIP_TAGS_ARG \
$ANSIBLE_EXTRA_VARS \
-e "hyperi_target_user=$target_user"; then
print_error "Ansible playbook failed for $target_user"
exit 1
fi
done

print_success "Hyperi Developer Environment installation complete!"
print_info ""
Expand Down
Loading