Master (Client) Setup#
This guide covers setting up a master computer as a login client β a machine that authenticates students against the login server (via SSSD) and mounts their home directory over NFS, so any student can sit at any master. It is one of three companion guides:
Login Server Setup β the login server itself
Master (Client) Setup β master computers that authenticate against the login server
Master Setup - Standalone β a single master with a local account, no login server
Complete login-server.md first β the login server must be running before a client can authenticate against it.
Network Planning#
Each master needs two independent network connections:
Purpose |
Carries |
Interface |
|---|---|---|
Lab network ( |
LDAP authentication, NFS home directories, internet |
Ethernet or USB WiFi dongle ( |
Robot link |
ROS 2 traffic to the TurtleBot3 |
Onboard WiFi ( |
The onboard adapter wlo1 is always dedicated to the robot. Only the lab-network side changes between the two supported topologies:
Topology A β Ethernet available (preferred)#
ββββββββββββββββββββββββββββ
β master01 β
β β
wired ββ€ eth βββΊ ECE387 network β LDAP, NFS, internet
β wlo1 βββΊ robotX AP β ROS 2 to the robot
ββββββββββββββββββββββββββββ
Use this wherever a wired drop exists. Ethernet is faster and more reliable than the dongle, and it frees a USB port. No USB WiFi dongle is needed.
Topology B β No Ethernet#
ββββββββββββββββββββββββββββ
β master01 β
β β
β wlan1 ββΊ ECE387 network β LDAP, NFS, internet (USB dongle)
β wlo1 ββΊ robotX AP β ROS 2 to the robot
ββββββββββββββββββββββββββββ
The USB dongle is renamed to a fixed name wlan1 so one config works across all 14 masters regardless of which dongle is plugged in.
Addresses#
Host |
Hostname |
IP |
|---|---|---|
WiFi router / DHCP / gateway |
β |
|
Login Server |
|
|
Master 01β14 |
|
DHCP (dynamic) |
Robot access point |
|
|
Masters are identified by hostname, not IP, since their IP can change after a reboot or DHCP lease renewal. Ubuntu Desktop ships with
avahi-daemon(mDNS) enabled, so each master is reachable at<hostname>.localfrom any other machine onECE387. This guide uses the domainece387.localfor LDAP.
The robot AP hands out its own DHCP lease and default route. Left alone,
wlo1will hijack the masterβs default route and break access to the login server and the internet. Section 1.8 pins the robot connection so this cannot happen β do not skip it.
1. Master Computer Setup (NUC 9 Γ 14)#
Do this on each of the 14 master computers. Most steps can be scripted and run via Ansible β see Ansible Automation.
Recommended order: get one master fully working by hand, verify it end to end, then push the rest with Ansible.
1.1 Fresh Install Ubuntu 24.04 Desktop#
Boot from the Ubuntu 24.04 Desktop ISO.
During install:
Hostname:
master01,master02, β¦master14Local admin account:
ece387adminConnect to
ECE387(or plug in Ethernet) during setup β DHCP by default, which is what we want. No static IP on masters.
After install, confirm connectivity:
# Identify the interfaces present on this machine.
# Expect: lo, an ethernet device (eno1 / enp*), and wlo1 (onboard WiFi).
# A USB dongle appears as wlx<mac> until renamed in 1.7.
ip addr
# Confirm a DHCP lease and that the login server is reachable
ping -c 3 10.99.1.50
# mDNS β lets other machines find this master as masterNN.local
systemctl status avahi-daemon
If
avahi-daemonisnβt running:sudo apt install -y avahi-daemon && sudo systemctl enable --now avahi-daemon.
Do you actually need mDNS?#
Nothing students do depends on it. Both connections they use are addressed numerically:
Path |
Uses |
|---|---|
Master β login server (LDAP, NFS) |
No β hardcoded |
Master β robot |
No β |
Student login and workflow |
No |
Login server β masters (Ansible, Section 4; the |
Yes |
So mDNS exists purely so you can reach the masters by name from the login server, without caring what address DHCP handed out this week.
Keep it unless you have a reason not to. avahi-daemon ships enabled on Ubuntu Desktop, uses roughly 3β5 MB of RAM, and is idle otherwise β not a meaningful load on a NUC 9. Check on a running machine with:
systemctl status avahi-daemon | grep Memory
If you prefer to disable it, you need another way to address 14 machines for Ansible. The robust option is DHCP reservations on the ECE387 router β pin each masterβs MAC to a fixed address, then use plain IPs everywhere. That is more reliable than mDNS (no multicast, no name resolution to fail) at the cost of some time in the routerβs admin page.
To disable, on each master:
sudo systemctl disable --now avahi-daemon
On the login server:
sudo systemctl disable --now avahi-daemon
sudo apt remove -y libnss-mdns
Then replace the ansible_host=masterNN.local entries in the Section 4 inventory with the reserved IP addresses, and use IPs in the Section 5 loop.
Disabling mDNS without setting up DHCP reservations first will leave you walking to each NUC with a keyboard whenever a config changes. Do the reservations first, confirm they hold across a reboot, then disable.
Note the exact Ethernet and WiFi interface names from ip addr β you need them in 1.7. The onboard WiFi is wlo1 on the NUC 9; confirm rather than assume.
1.2 Install ROS 2 Jazzy and Course Packages#
# Prerequisites for adding a new apt repository
sudo apt install -y software-properties-common curl
# ROS 2 package signing key
sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key \
-o /usr/share/keyrings/ros-archive-keyring.gpg
# ROS 2 apt repository for Ubuntu Noble (24.04)
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] \
http://packages.ros.org/ros2/ubuntu $(. /etc/os-release && echo $UBUNTU_CODENAME) main" | \
sudo tee /etc/apt/sources.list.d/ros2.list
sudo apt update
# ROS 2 Jazzy Desktop (rviz2, rqt, etc.) plus TurtleBot3 packages
sudo apt install -y ros-jazzy-desktop ros-jazzy-turtlebot3* ros-dev-tools
Course-specific packages:
# Simulation β Gazebo Sim (Harmonic) via ros_gz.
# NOTE: Gazebo Classic (the old `gazebo` / ros-*-gazebo-* packages) is end-of-life
# and is NOT available for Jazzy. There is no /usr/share/gazebo/setup.sh to source.
sudo apt install -y ros-jazzy-ros-gz
# Robot hardware and transforms
sudo apt install -y ros-jazzy-dynamixel-sdk ros-jazzy-tf-transformations
# Vision
sudo apt install -y ros-jazzy-usb-cam ros-jazzy-image-proc \
ros-jazzy-v4l2-camera ros-jazzy-cv-bridge \
ros-jazzy-camera-calibration \
ros-jazzy-apriltag ros-jazzy-apriltag-ros libapriltag-dev
# Teleop
sudo apt install -y ros-jazzy-joy ros-jazzy-teleop-twist-joy jstest-gtk
# Utilities
sudo apt install -y tree terminator python3-pip obs-studio qtwayland5
This list must stay in step with MasterSetupJazzy.md, which covers the standalone master. Students run identical labs on both, so a package present on one and missing on the other produces a lab that works at one bench and not another.
Python packages are installed system-wide, not in a per-student virtual environment. Ubuntu 24.04 blocks pip from writing to the system Python (PEP 668), so --break-system-packages is required:
sudo pip install --break-system-packages "pydantic<2"
sudo pip install --break-system-packages imutils
sudo pip install --break-system-packages pupil-apriltags
dlib is installed from a prebuilt wheel, not from source. PyPI ships dlib only as C++ source, so a plain pip install dlib compiles it β 30β60 minutes per machine, or over ten hours across fourteen masters. Build the wheel once and install the binary everywhere else in seconds. See Building a dlib Wheel in the standalone master guide for the build procedure and the reasoning.
# Fetch the prebuilt wheel from the login server and install it
scp ece387admin@ece387server:/srv/ece387/wheels/dlib-*.whl ~/
sudo pip install --break-system-packages ~/dlib-*.whl
The wheel is architecture-specific. A master needs the
linux_x86_64build; the robotsβlinux_aarch64wheel will not install here. Keep them in separate directories on the server so they cannot be confused.
This is an instructor task, not a student one. These packages live on the masterβs local disk, not in the studentβs NFS home directory, so they do not follow a student to another bench. Every master must have an identical set or a lab will work at one bench and fail at the next β which is exactly the failure mode that is hardest to diagnose during class. Students have pip in their sudo whitelist for the occasional one-off, but anything a lab depends on belongs in this list and in the Ansible playbook.
Package names occasionally differ between distros. If any line fails, check availability with
apt-cache search <name>before assuming the mirror is broken.
The shell environment is not configured here. .bashrc for student accounts comes from /etc/ece387/bashrc_template on the login server and is distributed by the push procedure in login-server.md Β§1.7. Do not add ROS environment lines to student .bashrc files on the master β home directories are on NFS, so a local edit would be overwritten by the next push and would silently diverge from the other 13 machines.
1.2b Install Visual Studio Code#
Install from the .deb, not the snap. Snap confinement interferes with serial device access and with picking up the ROS environment from the studentβs shell.
wget -O /tmp/code.deb 'https://code.visualstudio.com/sha/download?build=stable&os=linux-deb-x64'
sudo apt install -y /tmp/code.deb
rm /tmp/code.deb
The package registers Microsoftβs apt repository and key itself, so updates arrive through sudo apt upgrade. Do not add the repository manually first β configuring both leaves a duplicate source and floods every apt update with configured multiple times warnings.
Remove a pre-existing snap install if there is one:
snap list | grep code && sudo snap remove code
Verify:
code --version
apt-cache policy code # expect a single packages.microsoft.com origin
If two origins appear, a hand-made source file is left over:
sudo rm -f /etc/apt/sources.list.d/vscode.list # keep vscode.sources
sudo apt update
Raise the file watcher limit#
Required on every master. VS Code watches every file in an open folder, and a built master_ws has tens of thousands across build/ and install/ β well past the default inotify ceiling. Students hit βfile watcher limit reachedβ the first time they open their workspace.
echo "fs.inotify.max_user_watches=524288" | sudo tee /etc/sysctl.d/60-inotify.conf
sudo sysctl -p /etc/sysctl.d/60-inotify.conf
Seed workspace settings for students#
Because home directories are on NFS, VS Codeβs per-user configuration follows students between benches β extensions and settings install once and are available everywhere. Two consequences worth planning for:
The first extension install is slow. Something like Pylance writes thousands of small files over NFS. Have students install extensions early in the term, not at the start of a lab.
Extensions are unavailable during a server outage. They live in
~/.vscode/extensionson the server, like everything else in the home directory.
Seed sensible defaults so students are not each discovering the build-directory problem themselves. Add to /etc/skel on the login server, so new accounts pick it up:
# On ece387server
sudo mkdir -p /etc/skel/.config/Code/User
sudo tee /etc/skel/.config/Code/User/settings.json > /dev/null << 'EOF'
{
"files.watcherExclude": {
"**/build/**": true,
"**/install/**": true,
"**/log/**": true
},
"search.exclude": {
"**/build/**": true,
"**/install/**": true,
"**/log/**": true
}
}
EOF
search.exclude matters as much as files.watcherExclude: without it a project-wide search returns thousands of matches from compiled artifacts and --symlink-install symlinks.
/etc/skelis copied only when an account is created, so existing students will not receive this. To apply it to the current 62, copy the file into each home directory with a loop modeled on the push in login-server.md β and be aware it overwrites any VS Code settings a student has already chosen.
1.3 Configure SSSD for LDAP Authentication#
SSSD is the bridge between the master and the login serverβs LDAP database. When a student types their username and password, SSSD verifies it against LDAP.
sudo apt install -y sssd sssd-ldap libpam-sss libnss-sss oddjob oddjob-mkhomedir
sudo nano /etc/sssd/sssd.conf
[sssd]
# nss = name lookups ("id username", "getent passwd")
# pam = login authentication
services = nss, pam
domains = ece387.local
config_file_version = 2
[domain/ece387.local]
id_provider = ldap
auth_provider = ldap
ldap_uri = ldap://10.99.1.50
ldap_search_base = dc=ece387,dc=local
ldap_user_search_base = ou=students,dc=ece387,dc=local
ldap_group_search_base = ou=groups,dc=ece387,dc=local
ldap_default_bind_dn = cn=admin,dc=ece387,dc=local
ldap_default_authtok_type = password
ldap_default_authtok = LdapAdmin387!
# No TLS on the isolated lab network
ldap_id_use_start_tls = false
# Cache credentials so students can log in when the server is unreachable.
# Applies only to accounts that have logged into THIS machine before.
cache_credentials = true
# Cached credentials never expire (0 = no limit). Without this, offline logins
# stop working after a number of days.
offline_credentials_expiration = 0
# Pre-load all accounts so getent and tab-completion work
enumerate = true
# SSSD refuses to start if this file is readable by others
sudo chmod 600 /etc/sssd/sssd.conf
sudo systemctl enable --now sssd
# Create a home directory on first login if one does not exist
sudo pam-auth-update --enable mkhomedir
# SSSD may cache "offline" responses during the first seconds after boot,
# before the LDAP connection is established. Clear the cache for fresh lookups.
sudo systemctl stop sssd
sudo rm -rf /var/lib/sss/db/*
sudo systemctl start sssd
sleep 5
# Expected: uid=20000(a27-m0) gid=10000(ece387students) groups=10000(ece387students)
id a27-m0
1.4 Mount NFS Home Directories#
autofs mounts a network directory on first access and unmounts it after a period of inactivity β more efficient than a static /etc/fstab mount.
sudo apt install -y autofs nfs-common
sudo nano /etc/auto.master.d/students.autofs
# Anything accessed under /home/students uses the rules in /etc/auto.students.
# --timeout=600 unmounts after 10 minutes of inactivity.
/home/students /etc/auto.students --timeout=600
sudo nano /etc/auto.students
# The * wildcard matches any username; & substitutes the matched name.
# Accessing /home/students/a27-m0 mounts 10.99.1.50:/home/students/a27-m0
#
# soft = return an error if the server is unreachable, rather than hanging
# timeo=30 = 3.0 seconds per attempt (units are 0.1s)
# retrans=2 = retry twice, so failure surfaces after roughly 6 seconds
* -fstype=nfs,soft,timeo=30,retrans=2 10.99.1.50:/home/students/&
sudo systemctl enable --now autofs
sudo systemctl restart autofs
# Test: the home directory should mount on access
sudo su - a27-m0
pwd # /home/students/a27-m0
ls -la # .bashrc, .profile, etc.
exit
softtrades a hang for an error. With ahardmount (the NFS default) an unreachable server freezes the terminal indefinitely. Withsoft, I/O fails after ~6 seconds instead β the student seesInput/output errorand keeps a usable session, but a write interrupted mid-operation can leave a truncated file. See Section 2.
1.5 Create the Local Rescue Account#
A local account β not LDAP, not NFS β on every master. It is the fallback when the login server or the lab network is down, and it is how you debug a hung NFS mount without your own shell living on the mount that is hung.
sudo adduser ece387rescue
# Hardware access for robot and camera work
sudo usermod -aG dialout,video,plugdev ece387rescue
Give it a home on local disk (the default /home/ece387rescue already is β it is outside /home/students, so autofs never touches it) and a minimal ROS environment:
sudo tee -a /home/ece387rescue/.bashrc > /dev/null << 'EOF'
# ECE 387 rescue environment
source /opt/ros/jazzy/setup.bash
export TURTLEBOT3_MODEL=burger
export LDS_MODEL=LDS-02
export ROS_DOMAIN_ID=99
alias ssh_robot='ssh pi@192.168.50.1'
EOF
Post the password where students can find it during a lab β it is a shared convenience account, not a security boundary. It has no access to student home directories, which is the point.
Use a different password than
ece387admin. Students will know the rescue password; they should not be able to become the machineβs administrator with it.
Verify it works with the network unplugged:
# Physically disconnect Ethernet / disable WiFi, then:
su - ece387rescue
echo $HOME # /home/ece387rescue
ls -la # local files present
exit
1.6 Grant Students Restricted sudo Access#
This section is the single definition of the student sudo whitelist. Other guides reference it rather than restating it.
Two layers work together, and they protect different things:
root_squashon the NFS server rewrites root from any master to an unprivileged anonymous user, sosudo cat /home/students/a27-t02/fileis refused by the server itselfThe sudoers whitelist limits what can be run with sudo at all
Server side β root_squash#
sudo nano /etc/exports
/home/students 10.99.1.0/24(rw,sync,no_subtree_check,root_squash)
sudo exportfs -rav
sudo exportfs -v # confirm root_squash appears in the options
This must be
root_squash, notno_root_squash.no_root_squashgives every masterβs root full write access to every studentβs home directory. It does not affect the instructor push loop in login-server.md β that runs locally on the server, not over NFS.
What
root_squashdoes and does not cover. It rewrites UID 0 only. A student who reaches root on a master can stillsu - a27-t02and read that accountβs files, because those requests carry UID 21002 and there is nothing to squash. This is inherent toAUTH_SYS, NFSβs default authentication, where the client asserts its own UID and the server takes its word. Closing it entirely requires Kerberos (sec=krb5), which is a substantial addition. In practice, student file privacy here rests on the honor code, withroot_squashand the whitelist removing the easy paths.
Master side β the whitelist#
The guiding principle: never whitelist a general-purpose binary. Package managers, service managers, and network tools all execute code or open editors as root by design, so permitting any of them is equivalent to granting a root shell. Most things students actually need are solved by group membership or udev rules instead, with no privilege involved at all.
sudo nano /etc/sudoers.d/ece387-students
# ECE387 student sudo permissions
# The % prefix applies the rule to a group rather than a user.
# NOPASSWD avoids a password prompt β practical in a lab setting.
# Package management β students install their own libraries during labs.
# See "What this grants" below: this is equivalent to full root.
Cmnd_Alias ECE387_PKG = /usr/bin/apt, /usr/bin/apt-get, /usr/bin/dpkg, \
/usr/bin/pip, /usr/bin/pip3
# Power β Lab 1 teaches sudo shutdown and sudo reboot.
# (polkit already permits this for a local console user; kept because the
# lab teaches the command.)
Cmnd_Alias ECE387_PWR = /usr/sbin/shutdown, /usr/sbin/reboot
%ece387students ALL=(ALL) NOPASSWD: ECE387_PKG, ECE387_PWR
# sudoers files must be mode 440 β sudo silently ignores files with wrong permissions
sudo chmod 440 /etc/sudoers.d/ece387-students
# Verify syntax before logging out. A malformed sudoers file can lock out sudo entirely.
sudo visudo -c -f /etc/sudoers.d/ece387-students
# Confirm what a student can actually run
sudo -l -U a27-m0
What ECE387_PKG grants#
Package management is on the list deliberately: resolving their own dependencies is part of what students are learning. Be clear about what it costs, because it shapes what the rest of this setup can promise.
Package installation means running someone elseβs code with privilege β that is what installing software is. So ECE387_PKG is functionally equivalent to unrestricted root:
Command |
Path to root |
|---|---|
|
Runs configuration hooks as root; |
|
|
|
Executes the packageβs |
Two consequences to plan around:
Host-level restrictions become deterrents, not controls. Immutable files, hosts-file blocking, and browser policy files can all be undone by a student who escalates to root. Where such restrictions are used, audit logging stops being optional β it is what converts an unenforceable rule into a recorded action.
root_squashstill holds for the casual case but not the determined one. See the note above: a student with root cansu - a27-t02, and those requests carry a legitimate UID.
This is a reasonable trade in an honor-code environment. It is just worth making knowingly.
Prefer pip install --user over sudo pip#
For Python specifically there is a better option that needs no sudo at all:
pip install --user --break-system-packages imutils # any package name
This writes to ~/.local/lib/python3.12/site-packages, which is in the studentβs NFS home β so the package follows them to every bench, needs no privilege, and cannot break the machine for the next student. --break-system-packages is still required (Ubuntu 24.04 marks the environment as externally managed even for --user), but nothing runs as root.
Teach this as the default and reserve sudo pip for the rare case where a package must be system-wide. sudo pip on a shared master installs for everyone and is wiped by the next re-image; --user is per-student and portable.
What is still left out, and why#
Command |
Why it is excluded |
|---|---|
|
|
|
Redundant β it resolves dependencies from a student-written |
|
|
|
Neither needs sudo: |
Solve hardware access with groups and udev, not sudo#
Serial and camera access is a permissions problem, not a privilege problem:
# Device permissions set at plug-in time β no sudo needed by anyone
sudo tee /etc/udev/rules.d/99-ece387.rules > /dev/null << 'EOF'
# OpenCR / Dynamixel controller
SUBSYSTEM=="tty", ATTRS{idVendor}=="0483", MODE="0666"
# USB cameras
KERNEL=="video[0-9]*", MODE="0666"
EOF
sudo udevadm control --reload
sudo udevadm trigger
dmesg is restricted to root on Ubuntu, which students need when debugging USB. Open it rather than whitelisting it:
echo "kernel.dmesg_restrict=0" | sudo tee /etc/sysctl.d/61-dmesg.conf
sudo sysctl -p /etc/sysctl.d/61-dmesg.conf
If a lab genuinely needs root#
Write a wrapper that does exactly one thing, rather than whitelisting a general binary. For example,
sudo tee /usr/local/sbin/ece387-flash-opencr > /dev/null << 'EOF'
#!/bin/bash
set -e
exec /opt/ece387/opencr_update/update.sh /dev/ttyACM0 burger.opencr
EOF
sudo chmod 755 /usr/local/sbin/ece387-flash-opencr
sudo chattr +i /usr/local/sbin/ece387-flash-opencr
Then add only that path:
Cmnd_Alias ECE387_TOOLS = /usr/local/sbin/ece387-flash-opencr
%ece387students ALL=(ALL) NOPASSWD: ECE387_PWR, ECE387_TOOLS
Root-owned, immutable, no student-controlled arguments, no shell. That is a permission; sudo systemctl is a root shell.
Discover what students actually need#
Rather than guessing, log real usage for a week of labs:
echo 'Defaults log_input, log_output' | sudo tee /etc/sudoers.d/00-logging
sudo chmod 440 /etc/sudoers.d/00-logging
sudo journalctl -t sudo | grep COMMAND
Build the whitelist from that evidence. Anything appearing repeatedly probably belongs in the machine image rather than the whitelist.
1.7 Configure the Lab Network Connection#
Follow Option A if the bench has a wired drop, Option B if it does not.
Option A β Ethernet (preferred)#
Ubuntu Desktopβs NetworkManager brings up a wired interface with DHCP automatically, so there is usually nothing to configure. Confirm:
# Substitute your ethernet interface name from 1.1
ip addr show eno1
nmcli connection show --active
ping -c 3 10.99.1.1 # router
ping -c 3 10.99.1.50 # login server
Give the wired connection a better route metric than any WiFi, so it is preferred whenever the cable is plugged in:
# Find the connection name (usually "Wired connection 1")
nmcli connection show
nmcli connection modify "Wired connection 1" ipv4.route-metric 50
nmcli connection up "Wired connection 1"
No USB dongle is required in this topology. Skip to 1.8.
Option B β USB WiFi Dongle#
a. Rename the dongle to a fixed wlan1. Each masterβs dongle has a different MAC, so match on bus and type instead β the same config then works on every machine:
sudo nano /etc/systemd/network/10-usb-wifi.link
[Match]
Type=wlan
Property=ID_BUS=usb
[Link]
Name=wlan1
sudo udevadm control --reload
sudo udevadm trigger --subsystem-match=net --action=add
# Unplug and replug the dongle (or reboot) so it re-enumerates under the new name
ip addr show wlan1 # confirm; the old wlx<mac> name should be gone
This
.linkfile matches any USB WiFi adapter. It has no effect onwlo1, which is a PCIe device, so the onboard adapter keeps its name and stays available for the robot.
b. Configure the lab networks on wlan1. Ubuntu Desktop uses NetworkManager, and netplan hands control to it. ls /etc/netplan/ typically shows:
File |
What it is |
Edit it? |
|---|---|---|
|
Ships by default; tells netplan to let NetworkManager handle everything |
No |
|
Auto-generated per connection profile; rewritten whenever a profile changes |
No β hand edits are overwritten |
|
Written by the installer at provisioning |
Yes |
sudo nano /etc/netplan/50-cloud-init.yaml
network:
version: 2
renderer: NetworkManager
wifis:
wlan1:
dhcp4: true
access-points:
"ECE387":
password: "ece387only"
networkmanager:
passthrough:
connection.autoconnect-priority: "20"
"AF_ACADEMY_GUEST":
networkmanager:
passthrough:
connection.autoconnect-priority: "10"
"ECE":
password: "dfec3141"
networkmanager:
passthrough:
connection.autoconnect-priority: "5"
Higher autoconnect-priority wins when several networks are in range: ECE387 (lab) first, then AF_ACADEMY_GUEST (internet), then ECE (backup internet).
c. Fix permissions β these files hold WiFi passwords in plaintext, and netplan warns if they are world-readable:
sudo chmod 600 /etc/netplan/*.yaml
d. Apply and verify:
sudo netplan apply
ip addr show wlan1
nmcli connection show --active
ping -c 3 10.99.1.50
e. Stop cloud-init from reverting the file on next boot:
sudo nano /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg
network: {config: disabled}
f. Reconnecting manually. NetworkManager does not preempt an active connection when a higher-priority network reappears. If wlan1 is on ECE and ECE387 comes back into range:
nmcli connection up "ECE387"
1.8 The Robot Link on wlo1#
The onboard adapter wlo1 connects to the robotβs access point robotX, where X is the robot ID. The robot is at 192.168.50.1.
Students connect through the GUI, using the AP password provided in lab β there is nothing in /etc/netplan for the robot, and no pre-created profile. The connection is deliberately manual: benches are shared and robots move, so an auto-connecting profile would put students on whichever robot happened to be in range, including their neighborβs.
Students select the network from the top-right network menu (or Settings β Network β Wi-Fi), pick robotX, and enter the password. NetworkManager saves the profile, so subsequent sessions at that bench are one click.
The one setting that must be changed#
The robotβs access point runs its own DHCP server and advertises itself as a default gateway. If NetworkManager accepts that, the masterβs default route points at the robot and LDAP, NFS, and internet access all stop working β while the robot itself responds normally, so it presents as a server outage rather than a routing problem.
After connecting to robotX for the first time:
Settings β Network β robotX (gear icon) β IPv4 tab β check βUse this connection only for resources on its networkβ
Do the same on the IPv6 tab, then click Apply and reconnect.
That checkbox tells NetworkManager to route only the robotβs own subnet over wlo1 and to ignore its gateway and DNS offers. It is stored in the saved profile, so it only has to be set once per robot per master.
The robot can fix this for every master at once. DHCP option 3 is what carries the gateway offer, and it is set on the robotβs
dnsmasq, not here. Addingdhcp-option=3anddhcp-option=6to/etc/dnsmasq.confon each robot stops the offer at the source β see the dnsmasq step in RobotSetupJazzy.md. With that in place the checkbox below is belt-and-braces rather than load-bearing. Set it anyway on any master that may meet a robot flashed from an older image.
The equivalent from a terminal, if you are configuring a machine yourself:
nmcli connection modify robotX \
ipv4.never-default yes \
ipv6.never-default yes \
ipv4.ignore-auto-dns yes
nmcli connection up robotX
How much this matters depends on your topology#
Topology |
Risk |
|---|---|
A β Ethernet |
Lower. NetworkManager assigns wired connections a much better route metric (~100) than WiFi (~600), so Ethernet usually keeps the default route regardless. DNS can still be affected. |
B β Two WiFi links |
High. |
Set the checkbox in both cases. It costs one click and removes the uncertainty.
Verify#
Run this after connecting to a robot. All three must pass:
# The robot is reachable
ping -c 3 192.168.50.1
# The lab network still works β this is the one that fails if the route was hijacked
ping -c 3 10.99.1.50
# The default route must NOT be via 192.168.50.1
ip route | grep default
The last command should show the default via 10.99.1.1 on the Ethernet or wlan1 interface. A default route via 192.168.50.1 means the checkbox was not applied.
Then SSH in:
ssh pi@192.168.50.1
# Or use the alias from the course .bashrc
ssh_robot
ROS 2 discovery across two interfaces. DDS multicasts on every active interface, so a master can see nodes on both the robot subnet and the lab subnet. Distinct
ROS_DOMAIN_IDvalues per bench keep benches isolated from each other; if node discovery behaves strangely, check that first.
2. Resilience β Server Down or Network Unreliable#
Two subsystems fail differently. Authentication degrades gracefully; home directories do not.
Authentication β survives an outage#
cache_credentials = true means SSSD stores a credential hash locally after each successful login. With offline_credentials_expiration = 0 it never expires.
The limit is that the cache is per-machine. A student who has used master07 before can log into master07 with the server down. A student who has never used master07 cannot β there is no cached hash to check.
Home directories β fail, and do not resync#
NFS has no offline mode. There is no local replica on the master; every read and write is a network round-trip. When the server is unreachable there is nothing local to fall back to.
What actually happens at login:
autofs attempts the mount, gets no response, gives up after ~6 seconds
/home/students/<uid>does not existpam_mkhomedirtries to create it from/etc/skelβ and fails, because/home/studentsis an autofs-managed mount point wheremkdiris not permitted
That third step failing is fortunate. If /home/students were an ordinary directory, pam_mkhomedir would succeed and the student would get a local home directory shadowing their NFS one β they would work in it all period, and when the server returned, autofs could not mount over the now-occupied path. Fourteen masters would each hold a divergent local copy. autofs prevents this, but by side effect rather than design.
The result: $HOME points at a path that does not exist. Text console and SSH sessions start but land in / with no .bashrc, so no ROS environment, no aliases, no ccbuild. Graphical login typically fails outright, since GNOME needs a writable $HOME for dconf and D-Bus β expect a bounce back to the GDM login screen.
Verify this behavior on your hardware before you need it. On the server:
sudo systemctl stop nfs-kernel-server. Then try both a graphical and an SSH login on a master, and note which works. This determines whether students can reach the rescue account from the login screen or needCtrl+Alt+F3.
Mid-session failure#
If the server dies while students are working, soft mounts return EIO after ~6 seconds rather than hanging:
colcon buildfails partway, possibly leaving a corruptbuild/orinstall/treeEditor saves fail; unsaved work is lost
An interrupted write can truncate a file without an obvious error
Tell students that Input/output error means stop and wait, not retry.
When the server returns#
Nothing syncs back. There is no local copy and no queued writes β the writes never landed anywhere. Students see exactly the state the server had when it went down; anything attempted during the outage is gone.
autofs recovers on next access. A stale mount occasionally needs a nudge:
sudo systemctl restart autofs
Recovery procedure for students#
Post this by the benches.
1. Log into the rescue account (Section 1.5) β local, needs no server:
username: ece387rescue
password: <posted in lab>
If the graphical login refuses, switch to a text console with Ctrl+Alt+F3 and log in there.
2. Clone the workspace from GitHub to local disk:
mkdir -p /tmp/rescue && cd /tmp/rescue
git clone https://github.com/<username>/<repo>.git master_ws
cd master_ws && colcon build --symlink-install
source install/setup.bash
AF_ACADEMY_GUEST provides internet independently of the lab network, so GitHub stays reachable when the login server does not.
3. Push before logging out. /tmp is cleared on reboot and the rescue account is shared:
git add -A && git commit -m "work from <date>" && git push
Git is the actual safety net. SSSD caching and
softmounts limit the damage; only a pushed commit preserves the work. Make an end-of-lab commit and push a graded habit early in the term, not an emergency procedure students read for the first time during an outage.
Checking status during an outage#
# From a master
ping -c 3 10.99.1.50
showmount -e 10.99.1.50 # lists exports; errors or hangs if NFS is down
mount | grep students # what is actually mounted
systemctl status autofs sssd
# Is SSSD in offline mode?
sudo sssctl domain-status ece387.local
3. Student Workflow#
When a student sits down at a master:
Log in with the LDAP username (e.g.
a27-m0) and password. SSSD authenticates against the login server; the home directory mounts automatically over NFS..bashrc,.ssh/, and the workspace are already there β same files at every bench, no re-setup. This is what the login server is for.Connect to the robot from the network menu in the top-right corner: select
robotXand enter the AP password. First time on a given master, also tick βUse this connection only for resources on its networkβ under Settings β Network β robotX β IPv4 and IPv6 (see Section 1.8) β without it, the lab network drops.# Robot reachable ping -c 2 192.168.50.1 # Lab network still working ping -c 2 10.99.1.50 # SSH into the robot ssh_robot # alias for: ssh pi@192.168.50.1
Set the benchβs ROS domain so benches do not see each otherβs traffic:
export ROS_DOMAIN_ID=7 # use your robot's ID
Build and run:
ccbuild # builds ~/master_ws and sources it
git pushat the end of every session. The GitHub SSH key lives in~/.ssh/and follows the student to any master.
4. Ansible Automation (Recommended for 14 Machines)#
Rather than repeating Section 1 on each master by hand, Ansible runs the same steps on all 14 from the login server.
# On the login server
sudo apt install -y ansible
# Ubuntu Server does not ship mDNS resolution; install it so *.local resolves
sudo apt install -y avahi-daemon libnss-mdns
sudo systemctl enable --now avahi-daemon
# Should resolve to master01's current DHCP address
ping -c 1 master01.local
# Inventory. Masters use DHCP, so address them by mDNS hostname rather than IP β
# this keeps working after a reboot changes the lease.
cat > ~/Documents/ece387/masters-inventory.ini << 'EOF'
[masters]
master01 ansible_host=master01.local
master02 ansible_host=master02.local
master03 ansible_host=master03.local
master04 ansible_host=master04.local
master05 ansible_host=master05.local
master06 ansible_host=master06.local
master07 ansible_host=master07.local
master08 ansible_host=master08.local
master09 ansible_host=master09.local
master10 ansible_host=master10.local
master11 ansible_host=master11.local
master12 ansible_host=master12.local
master13 ansible_host=master13.local
master14 ansible_host=master14.local
[masters:vars]
ansible_user=ece387admin
ansible_become=yes
EOF
# Write setup-masters.yml covering sections 1.2-1.8, then:
ansible-playbook -i ~/Documents/ece387/masters-inventory.ini setup-masters.yml
Get one master fully working first. Verify login, NFS mount, robot connectivity, and the rescue account by hand before pushing to the other 13 β a mistake applied simultaneously to 14 machines is much harder to unwind than one applied to a single machine you were watching.
Be careful with network steps over SSH. Applying a netplan or NetworkManager change to the interface carrying your SSH session will drop the connection mid-play. Run 1.7 and 1.8 at the console, or accept that Ansible will report a failure it cannot recover from.
5. Maintenance#
Check who is logged in across all masters#
for i in $(seq -w 1 14); do
echo "=== master$i ==="
ssh ece387admin@master$i.local who 2>/dev/null
done
Verify a masterβs full configuration#
# Run on the master; every line should succeed
id a27-m0 # LDAP lookup works
ls /home/students/a27-m0 > /dev/null # NFS mount works
id ece387rescue # rescue account exists
ip route | grep default # default route is NOT 192.168.50.1
sudo -l -U a27-m0 # sudo whitelist is in effect
Update the shell environment#
Student .bashrc and .inputrc live on the login server, not here. See login-server.md Β§1.7. Nothing needs to be done on the masters.
Troubleshooting#
Problem |
Command |
Fix |
|---|---|---|
Home dir not mounting |
|
Check NFS exports on server, autofs config on master, server reachable |
SSSD not resolving users |
|
|
NFS mount fails |
|
Check server firewall; verify |
Login slow (~30s) |
|
Check LDAP URI is reachable: |
Canβt reach a master by hostname |
|
Check |
Lab network dies when connecting to robot |
|
Default route was taken by the robot AP β apply |
Robot unreachable after |
|
Confirm the profile is bound to |
Dongle not named |
|
Re-run |
Graphical login bounces to GDM |
|
Usually a missing |
|
|
NFS server unreachable; |
|
|
Check |
ROS 2 nodes not discovered |
|
Confirm matching |
For server-side symptoms (student account issues, LDAP connection refused), see the troubleshooting table in login-server.md.