Skip to content

feat(remote-desktop): browser-based noVNC desktops for Slurm clusters - #6141

Open
scott-nag wants to merge 2 commits into
GoogleCloudPlatform:developfrom
nagconsulting:remote-desktop-modules
Open

feat(remote-desktop): browser-based noVNC desktops for Slurm clusters#6141
scott-nag wants to merge 2 commits into
GoogleCloudPlatform:developfrom
nagconsulting:remote-desktop-modules

Conversation

@scott-nag

@scott-nag scott-nag commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

What this adds

Browser-based Linux desktops for Cluster Toolkit clusters: two public modules and the shared runtime they are built on.

Module Purpose
community/modules/remote-desktop/novnc-runtime Emits startup-script runners that layer the desktop stack onto an existing VM — typically a Slurm login node, so desktop sessions keep the scheduler tooling. Creates no VM resources.
community/modules/remote-desktop/novnc-desktop The same runtime plus a dedicated VM and firewall rule, for standalone visualisation nodes.
community/modules/internal/desktop-broker The shared foundation both wrap. Not intended for direct use in blueprints.

Note on this PR replacing its own history

The earlier revision bundled the Guacamole front end, the OFE integration and an nginx gateway alongside this runtime. It was too large to review sensibly, so it has been split.

Secrets

Both modules take the shared secret either as a literal or as a Secret Manager secret ID fetched on the instance at boot. The _id form keeps the value out of Terraform state and out of the startup script staged in Cloud Storage, and is what the example and the integration test use.

Testing

  • 57 unit tests for the broker (community/modules/internal/desktop-broker/files/tests/).
    Mutation-tested to confirm they are load-bearing: removing -rfbport -1,
    accepting any secret, making session records world-readable, changing the
    security type, and breaking the OS Login index were each caught.
  • A daily integration test (tools/cloud-build/daily-tests/.../novnc-desktop.*).

Packaging

TurboVNC, TigerVNC, VirtualGL and noVNC install from pinned upstream releases verified by SHA-256. No third-party apt/yum repositories are added and no GPG keys are imported. The only existing repository file this PR modifies is examples/README.md, to index the new example.

Note for reviewer

The daily test will require a hpc-test-desktop-proxy-secret proxy secret added to test project's Secret Manager. One can be created with openssl rand -base64 48 | gcloud secrets create ghpc-desktop-proxy-secret.

@scott-nag
scott-nag requested a review from a team as a code owner August 13, 2026 10:01
@github-actions github-actions Bot added the external PR from external contributor label Aug 13, 2026
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces comprehensive remote desktop capabilities to the Cluster Toolkit and OFE. By providing a modular architecture for VNC-based desktop sessions, it allows users to access interactive Linux environments directly from their browsers. The implementation includes robust identity verification, secure proxying, and integration with existing Slurm cluster management workflows, while ensuring that cluster teardown processes respect active desktop sessions.

Highlights

  • Remote Desktop Modules: Introduced three new modules (vnc-runtime, novnc-runtime, novnc-desktop) to provide browser-based Linux desktop support, enabling both login-node and standalone visualization host deployments.
  • OFE Integration: Integrated cluster-scoped desktop support into OFE, including UI controls for launch/status, Slurm-backed lifecycle management, and cluster destroy safety checks.
  • Identity and Auth: Implemented a secure identity assertion mechanism using the IAM Credentials API, allowing the frontend to vouch for users without distributing sensitive credentials to the desktop brokers.
  • GPU Acceleration: Added support for hardware-accelerated visualization desktops using TurboVNC and VirtualGL, with automatic fallback to software rendering if no GPU is detected.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@scott-nag scott-nag added release-new-modules Added to release notes under the "New Modules" heading. and removed external PR from external contributor labels Aug 13, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds browser-based remote desktop support for Slurm clusters using noVNC, introducing new Terraform modules, blueprints, and frontend integration (OFE) to manage per-user desktop sessions. The review feedback identifies several critical and high-severity issues, including a missing grp import in the daemon script, potential permission issues during recursive directory creation, and performance bottlenecks from synchronous network connections and sequential API calls in Django views. Additionally, the feedback recommends addressing potential runtime errors (such as AttributeError and TypeError on null or non-string values), avoiding blocking synchronous disk I/O on the main event loop, and pinning the Terraform version in versions.tf to comply with repository style guidelines.

"""Submit a long-lived desktop allocation job."""
outfile = job_dir / "desktop.out"
errfile = job_dir / "desktop.err"
group_name = grp.getgrgid(gid).gr_name

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The grp module is used here to look up the group name from the GID, but it is not imported in this file. This will result in a NameError at runtime when attempting to submit a desktop job. Please import grp at the top of the file or inline.

Suggested change
group_name = grp.getgrgid(gid).gr_name
import grp
group_name = grp.getgrgid(gid).gr_name

@scott-nag scott-nag Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a false positive - grp is imported on line 19 already.

Comment on lines +266 to +271
with socket.create_connection(
(service["host"], service["port"]),
timeout=2,
):
return None
except OSError:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Performing a synchronous socket.create_connection check inside the Django view block is a major performance bottleneck. Because this is called during the rendering of the Cluster Detail page, any network latency, firewall block, or offline node will block the entire page load for up to 2 seconds per desktop target (potentially 4+ seconds if both login and viz desktops are enabled). Consider moving this connectivity check to an asynchronous AJAX endpoint called by the frontend, or caching the status with a short TTL.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair assessment but the premise is wrong as this wouldn't slow down/block the cluster detail page since these aren't used on the cluster detail page directly.

Timeout could certainly be reduced from 2 seconds but if there's a transient networking issue or packet loss of sorts you'd get an error. Therefore 2 seconds is a sensible timeout window in my opinion.

Comment on lines +1160 to +1162
job_dir = Path(homedir) / "desktop" / str(config["cluster_id"])
job_dir.mkdir(parents=True, exist_ok=True)
os.chown(job_dir, uid, gid)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When creating job_dir using mkdir(parents=True), if the intermediate desktop directory does not exist, it will be created by root (since the daemon runs as root) and will not be chowned to the user. Depending on the system umask, this can prevent the user from accessing their own desktop directory. It is safer to create and chown both directories sequentially.

    desktop_dir = Path(homedir) / "desktop"
    desktop_dir.mkdir(exist_ok=True)
    os.chown(desktop_dir, uid, gid)
    job_dir = desktop_dir / str(config["cluster_id"])
    job_dir.mkdir(exist_ok=True)
    os.chown(job_dir, uid, gid)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed now

Comment on lines +266 to +272
def _desktop_mount_options(self, mount_options, fs_type):
if fs_type != "nfs":
return mount_options
options = [option.strip() for option in mount_options.split(",") if option.strip()]
if "_netdev" not in options:
options.append("_netdev")
return ",".join(options)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If mount_options is None (which can happen if the field is null/empty in the database), calling mount_options.split(",") will raise an AttributeError. Adding a guard for None or empty values will make this method more robust.

Suggested change
def _desktop_mount_options(self, mount_options, fs_type):
if fs_type != "nfs":
return mount_options
options = [option.strip() for option in mount_options.split(",") if option.strip()]
if "_netdev" not in options:
options.append("_netdev")
return ",".join(options)
def _desktop_mount_options(self, mount_options, fs_type):
if fs_type != "nfs":
return mount_options
if not mount_options:
return "_netdev"
options = [option.strip() for option in mount_options.split(",") if option.strip()]
if "_netdev" not in options:
options.append("_netdev")
return ",".join(options)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed now


zones = get_region_zone_info(cloud_provider, credentials).get(region, [])
supported = []
for zone in zones:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Querying get_machine_types sequentially in a loop for each zone can introduce significant latency during web requests (e.g., us-central1 has 6 zones, leading to 6 sequential API calls). Consider caching the results of get_machine_types or utilizing GCP's aggregated list API to retrieve machine types across all zones in a single call.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Runs at blueprint generation time. get_machine_types already has LRU caching too enabled. False positive.

Comment on lines +322 to +324
elif not machine_type:
pass
else:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If zone is None (e.g., if the user has not selected a zone yet on a new cluster form), calling validate_gcp_reservation will trigger an API call with a null zone, which will fail. It is safer to skip validation if zone is not yet available.

Suggested change
elif not machine_type:
pass
else:
elif not machine_type or not zone:
pass
else:

# limitations under the License.

terraform {
required_version = ">= 1.12.2"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Pin the Terraform version to a specific value (using '=') in modules to ensure compatibility with the version specifically supported by the repository or toolkit, rather than using a minimum version constraint.

  required_version = "= 1.5.0"
References
  1. Pin the Terraform version to a specific value (using '=') in modules to ensure compatibility with the version specifically supported by the repository or toolkit.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Repo standard is >= 1.12.2 (80 modules using it).

This suggestion goes against convention and is a lower version than the toolkit itself requires...

That being said the modules are inconsistent in that I've got vnc-runtime set to >= 1.0 so I will bring this in-line to match the rest of the repo.

while True:
await asyncio.sleep(300)
now = int(time.time())
for session in self._list_sessions():

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Calling _list_sessions() directly on the main event loop blocks the loop with synchronous disk I/O (glob and read_text operations). This can degrade the performance of the broker under load or on slow filesystems. It should be run in the executor using _run_blocking.

Suggested change
for session in self._list_sessions():
sessions = await self._run_blocking(self._list_sessions)
for session in sessions:

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if "SSH_AUTH_SOCK" in new_env:
del new_env["SSH_AUTH_SOCK"]
new_env.update(extra_env)
terraform_env = {key: os.fspath(value) for key, value in extra_env.items()}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using os.fspath(value) will raise a TypeError if any value in extra_env is not a string, bytes, or path-like object (for example, if an integer port or boolean flag is passed). Since environment variables must ultimately be strings, using str(value) is safer and more robust.

Suggested change
terraform_env = {key: os.fspath(value) for key, value in extra_env.items()}
terraform_env = {key: str(value) for key, value in extra_env.items()}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@github-actions github-actions Bot added the external PR from external contributor label Aug 13, 2026
@scott-nag

Copy link
Copy Markdown
Collaborator Author

Addressed the issues raised by Gemini now.

Note: see my comment replies to Gemini for explanation as to why certain parts weren't changed.

@arpit974 arpit974 self-assigned this Aug 18, 2026
@arpit974
arpit974 self-requested a review August 18, 2026 06:49
@scott-nag

Copy link
Copy Markdown
Collaborator Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces browser-based Linux remote desktop blueprints and modules (novnc-desktop, novnc-runtime, and vnc-runtime) integrated with the On-Demand Frontend (OFE) to support interactive access. The code review identified critical security vulnerabilities regarding symlink attacks when executing os.chown as root in ghpcfe_c2daemon.py and desktop-broker.py. Additionally, feedback was provided to resolve a double-expansion bug in bootstrap-remote-desktop-gateway.sh, prevent thread-blocking synchronous socket connections in views/desktop.py, and handle potential MultipleObjectsReturned exceptions in adapters.py.

Comment on lines +1160 to +1167
# Create and chown each level rather than using parents=True: the daemon runs as root
# so an intermediate directory created that way stays owned by root inside users home
desktop_dir = Path(homedir) / "desktop"
desktop_dir.mkdir(exist_ok=True)
os.chown(desktop_dir, uid, gid)
job_dir = desktop_dir / str(config["cluster_id"])
job_dir.mkdir(exist_ok=True)
os.chown(job_dir, uid, gid)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-critical critical

Operating on user-controlled directories as root is vulnerable to symlink attacks. If a malicious user pre-creates desktop or the cluster directory as a symlink pointing to a system directory (e.g., /etc), os.chown will follow the symlink and grant the user ownership of that system directory, leading to privilege escalation. Pass follow_symlinks=False to os.chown to prevent following symlinks.

Suggested change
# Create and chown each level rather than using parents=True: the daemon runs as root
# so an intermediate directory created that way stays owned by root inside users home
desktop_dir = Path(homedir) / "desktop"
desktop_dir.mkdir(exist_ok=True)
os.chown(desktop_dir, uid, gid)
job_dir = desktop_dir / str(config["cluster_id"])
job_dir.mkdir(exist_ok=True)
os.chown(job_dir, uid, gid)
# Create and chown each level rather than using parents=True: the daemon runs as root
# so an intermediate directory created that way stays owned by root inside users home
desktop_dir = Path(homedir) / "desktop"
desktop_dir.mkdir(exist_ok=True)
os.chown(desktop_dir, uid, gid, follow_symlinks=False)
job_dir = desktop_dir / str(config["cluster_id"])
job_dir.mkdir(exist_ok=True)
os.chown(job_dir, uid, gid, follow_symlinks=False)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the established pattern used by c2daemon which has been part of OFE for many years.

Nevertheless it is worth updating and has now been updated per the suggested fix.

Comment on lines +623 to +636
def _ensure_home_directory(self, home_dir, uid, gid):
home_path = Path(home_dir)
try:
home_created = not home_path.exists()
home_path.mkdir(parents=True, exist_ok=True)
if home_created:
os.chown(home_path, uid, gid)
os.chmod(home_path, 0o700)
vnc_dir = home_path / ".vnc"
vnc_created = not vnc_dir.exists()
vnc_dir.mkdir(parents=True, exist_ok=True)
if vnc_created:
os.chown(vnc_dir, uid, gid)
os.chmod(vnc_dir, 0o700)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-critical critical

Creating directories and files inside a user-controlled home directory as root is highly vulnerable to symlink attacks. If a user pre-creates ~/.vnc or ~/.vnc/xstartup as symlinks to critical system files, the broker will overwrite them or change their ownership, leading to privilege escalation. Ensure we check if the paths are symlinks before operating on them, and pass follow_symlinks=False to os.chown.

Suggested change
def _ensure_home_directory(self, home_dir, uid, gid):
home_path = Path(home_dir)
try:
home_created = not home_path.exists()
home_path.mkdir(parents=True, exist_ok=True)
if home_created:
os.chown(home_path, uid, gid)
os.chmod(home_path, 0o700)
vnc_dir = home_path / ".vnc"
vnc_created = not vnc_dir.exists()
vnc_dir.mkdir(parents=True, exist_ok=True)
if vnc_created:
os.chown(vnc_dir, uid, gid)
os.chmod(vnc_dir, 0o700)
def _ensure_home_directory(self, home_dir, uid, gid):
home_path = Path(home_dir)
try:
if home_path.is_symlink():
raise BrokerError(403, "Home directory cannot be a symlink.")
home_created = not home_path.exists()
home_path.mkdir(parents=True, exist_ok=True)
if home_created:
os.chown(home_path, uid, gid, follow_symlinks=False)
os.chmod(home_path, 0o700)
vnc_dir = home_path / ".vnc"
if vnc_dir.is_symlink():
raise BrokerError(403, ".vnc directory cannot be a symlink.")
vnc_created = not vnc_dir.exists()
vnc_dir.mkdir(parents=True, exist_ok=True)
if vnc_created:
os.chown(vnc_dir, uid, gid, follow_symlinks=False)
os.chmod(vnc_dir, 0o700)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the established pattern used by c2daemon which has been part of OFE for many years.

Nevertheless it is worth updating and has now been updated per the suggested fix.

Comment on lines +488 to +497
cat > "${nginx_conf_path}" <<EOF_NGINX
map \$http_upgrade \$connection_upgrade {
default upgrade;
'' close;
}

${redirect_server_block}

${ssl_server_block}
EOF_NGINX

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using an unquoted heredoc (<<EOF_NGINX) to write the final configuration file causes the shell to perform a second pass of parameter expansion on the contents of ${redirect_server_block} and ${ssl_server_block}. This prematurely evaluates Nginx variables (like $host and $request_uri) to empty strings, breaking the configuration. Writing the static map block with a quoted heredoc and appending the pre-expanded blocks using printf avoids this double-expansion issue.

Suggested change
cat > "${nginx_conf_path}" <<EOF_NGINX
map \$http_upgrade \$connection_upgrade {
default upgrade;
'' close;
}
${redirect_server_block}
${ssl_server_block}
EOF_NGINX
cat > "${nginx_conf_path}" <<'EOF_NGINX'
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
EOF_NGINX
printf "%s\n\n%s\n" "${redirect_server_block}" "${ssl_server_block}" >> "${nginx_conf_path}"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive on this one. Shell expansion is a single pass. Result of the variable substitution is never re-scanned.

Comment on lines +265 to +270
try:
with socket.create_connection(
(service["host"], service["port"]),
timeout=2,
):
return None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Performing a synchronous socket.create_connection with a 2-second timeout inside the Django request-response cycle can block the request thread and exhaust the worker pool if the desktop host is unreachable. Reduce the timeout to a safer value (e.g., 0.5 seconds) since local VPC connections are extremely fast, or consider caching the status.

Suggested change
try:
with socket.create_connection(
(service["host"], service["port"]),
timeout=2,
):
return None
try:
with socket.create_connection(
(service["host"], service["port"]),
timeout=0.5,
):
return None

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a re-framing of the comment Gemini raised previously. 2s is a safe timeout. A single dropped SYN guarantees a false "not listening" because Linux's initial TCP retransmit is ~1s. Cutting the timeout trades a real correctness bug for a marginal latency win.

Comment on lines +47 to +50
try:
existing_user = user_model.objects.get(email__iexact=email)
except user_model.DoesNotExist:
return

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If multiple users exist with the same email address, user_model.objects.get will raise a MultipleObjectsReturned exception, causing a 500 Internal Server Error. Catch MultipleObjectsReturned along with DoesNotExist to handle this gracefully.

Suggested change
try:
existing_user = user_model.objects.get(email__iexact=email)
except user_model.DoesNotExist:
return
try:
existing_user = user_model.objects.get(email__iexact=email)
except (user_model.DoesNotExist, user_model.MultipleObjectsReturned):
return

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid. Updated and fixed.

@AdarshK15 AdarshK15 self-assigned this Aug 18, 2026
@AdarshK15
AdarshK15 self-requested a review August 18, 2026 14:33
@scott-nag

Copy link
Copy Markdown
Collaborator Author

Addressed Gemini's suggestions now and improved displayed messaging in OFE when stopping remote desktop running on cluster partition too.

@scott-nag

Copy link
Copy Markdown
Collaborator Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces comprehensive support for browser-based remote desktop sessions, including both login-node and visualization-partition desktops. It adds the necessary infrastructure modules, a desktop broker service, and integration with the OFE frontend, including secure identity assertion signing. I have reviewed the changes and identified a few critical and high-priority issues, including a missing import in the C2 daemon, a potential bootstrap failure due to package installation order, and a security consideration regarding social account linking. Please address these issues and ensure the new example is indexed in the examples README as per the style guide.

Comment on lines +25 to +31
import socket
import subprocess
import sys
import tempfile
import time
import concurrent.futures
from datetime import datetime

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The grp module is used on line 1033 (group_name = grp.getgrgid(gid).gr_name) but it is not imported at the top of the file. This will cause a NameError at runtime when attempting to submit a desktop job. Please add import grp to the imports.

Suggested change
import socket
import subprocess
import sys
import tempfile
import time
import concurrent.futures
from datetime import datetime
import grp
import socket
import subprocess
import sys
import tempfile
import time
import concurrent.futures
from datetime import datetime

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

false positive (discussed above previously)

Comment on lines +275 to +280
try:
with socket.create_connection(
(service["host"], service["port"]),
timeout=2,
):
return None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Performing synchronous network I/O (socket.create_connection) on the main request thread during a page load is a performance and reliability anti-pattern. If the desktop host is down, slow, or firewalled, this call will block the Django worker thread for up to 2 seconds. Under high load or multiple concurrent requests, this can easily exhaust the worker pool and cause a Denial of Service (DoS) for the entire front-end.

Consider caching the connectivity status (e.g., using Django's cache framework for 15 seconds) to avoid blocking the page render on every load.

Suggested change
try:
with socket.create_connection(
(service["host"], service["port"]),
timeout=2,
):
return None
cache_key = f"desktop_conn_{cluster.id}_{target}"
is_ready = cache.get(cache_key)
if is_ready is None:
try:
with socket.create_connection(
(service["host"], service["port"]),
timeout=2,
):
is_ready = True
except OSError:
is_ready = False
cache.set(cache_key, is_ready, 15)
if is_ready:
return None

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously discussed the justification for the 2 second timeout (above)

Comment on lines +121 to +125
access_token="$(
metadata_get \
'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token' |
python3 -c 'import json, sys; print(json.load(sys.stdin)["access_token"])'
)"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The script uses python3 to parse the metadata access token here, but install_packages (which installs python3) is not called until line 205. If the base image does not have python3 pre-installed, the script will fail with a command-not-found error. Please ensure install_packages is called before retrieving the access token.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not necessary. Example blueprint only.

Comment on lines +38 to +42
def pre_social_login(self, request, sociallogin):
if sociallogin.is_existing:
return

email = (sociallogin.user.email or "").strip().lower()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

Automatically linking any social account to an existing local user based solely on the email address can be a security risk if unverified or non-Google social providers are enabled in the future. An attacker could register an account with a victim's email on a provider that does not enforce email verification, and then log in to the victim's OFE account.

Since the log message specifically mentions Google, please explicitly restrict this automatic linking to the 'google' provider.

Suggested change
def pre_social_login(self, request, sociallogin):
if sociallogin.is_existing:
return
email = (sociallogin.user.email or "").strip().lower()
def pre_social_login(self, request, sociallogin):
if sociallogin.is_existing:
return
if sociallogin.account.provider != "google":
return
email = (sociallogin.user.email or "").strip().lower()

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration isn't possible so not relevant.

Comment on lines +1 to +5
# Remote Desktop Examples

These blueprints deploy browser-based Linux desktops alongside a Slurm cluster,
using the [novnc-runtime](../../modules/remote-desktop/novnc-runtime/README.md)
and [novnc-desktop](../../modules/remote-desktop/novnc-desktop/README.md)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

According to the Repository Style Guide, if new examples are added, they must be added to the index in examples/README.md. Please ensure this new remote desktop example is indexed there.

References
  1. If new examples (core or community) are added, ensure they are added to the index in examples/README.md. (link)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is.

@scott-nag

Copy link
Copy Markdown
Collaborator Author

I believe this PR is in a good position now in terms of addressing Gemini's concerns. Please feel free to get in touch with me if there's any questions or suggestions from a human being too 😄

@scott-nag

Copy link
Copy Markdown
Collaborator Author

I'm going to split this up into multiple PRs next

@aslam-quad

Copy link
Copy Markdown
Contributor

Please NOTE : We recently upgraded our repository's Go version to 1.26 (#6173). We strongly recommend rebasing your open branches onto the latest develop branch to avoid or resolve any PR test failures.

Thank you!

@scott-nag

Copy link
Copy Markdown
Collaborator Author

Please NOTE : We recently upgraded our repository's Go version to 1.26 [#6173]. We strongly recommend rebasing your open branches onto the latest develop branch to avoid or resolve any PR test failures.

Thank you!

Thanks for letting me know - will test things out and get it rebased 👍

…ters

Adds two public modules and the shared runtime they are built on:

- novnc-runtime: emits startup-script runners that install the desktop
  stack onto an existing VM, so a Slurm login node can host desktops and
  keep its scheduler tooling.
- novnc-desktop: the same runtime plus a dedicated VM and firewall rule
  for standalone visualisation nodes.
- internal/desktop-broker: the shared foundation both wrap. A small
  Python service that maps an authenticated request to a POSIX account
  via OS Login, starts a per-user Xvnc session, and relays RFB over a
  websocket to noVNC in the browser.

Sessions are isolated per user: each Xvnc listens on a 0700 unix socket
with no TCP listener at all, so one user cannot reach another's display
even with a local shell on the host.

The broker takes identity from request headers and requires a shared
secret, so it must sit behind an authenticating proxy. This change
reaches desktops over an IAP tunnel with a local nginx header injector,
included as an example config. A load-balancer/IAP module that removes
the local proxy is left to follow-up work.

Both modules accept the shared secret either as a literal or as a
Secret Manager secret ID fetched on the instance at boot, which keeps
the value out of Terraform state.

Includes 57 unit tests for the broker and a daily integration test.
@scott-nag
scott-nag force-pushed the remote-desktop-modules branch from 5de12c4 to dc10d9e Compare August 24, 2026 12:03
@scott-nag scott-nag changed the title Remote desktop modules feat(remote-desktop): browser-based noVNC desktops for Slurm clusters Aug 24, 2026
@scott-nag

Copy link
Copy Markdown
Collaborator Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a browser-based Linux desktop solution for Slurm clusters using noVNC. It adds a shared internal Python-based desktop session broker (desktop-broker) that handles user authentication via OS Login, session lifecycle, and VNC backends (TigerVNC and TurboVNC with optional VirtualGL GPU acceleration). It also introduces two new community modules, novnc-runtime and novnc-desktop, along with a validation blueprint (hpc-slurm-remote-desktop.yaml), local proxy configuration, comprehensive pytest suites, and daily integration tests. The new example has been correctly indexed in examples/README.md. No review comments were provided for this pull request.

Comment thread examples/README.md
* [cae-slurm.yaml](#cae-slurmyaml-) ![core-badge]
* [hpc-build-slurm-image.yaml](#hpc-build-slurm-imageyaml--) ![community-badge] ![experimental-badge]
* [hpc-slurm-ubuntu2204.yaml](#hpc-slurm-ubuntu2204yaml-) ![community-badge]
* [hpc-slurm-remote-desktop.yaml](#hpc-slurm-remote-desktopyaml--) ![community-badge] ![experimental-badge]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add entries for new modules added to the file modules/README.md.


```bash
./gcluster create community/examples/remote-desktop/BLUEPRINT.yaml -w
./gcluster deploy BLUEPRINT

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: the BLUEPRINT placeholder for the deploy command is incorrect. once you run ./gcluster create <blueprint path>, we need to run ./gcluster deploy <deployment_name>

deployment_name is defined in the blueprint.


## Teardown Instructions

Replace `BLUEPRINT` with the `deployment_name` used in the blueprint vars block.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: same here, it is confusing to use BLUEPRINT as placeholder, we need to use deployment_name.

settings:
machine_type: n2-standard-4
enable_public_ips: false
service_account_email: $(desktop-service-account.service_account_email)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: instead of referencing other module, please add the module name to use clause.

# openssl rand -base64 48 | gcloud secrets create ghpc-test-desktop-proxy-secret \
# --replication-policy automatic --data-file=-
---
test_name: "novnc-{{ os }}"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for adding the Test, will get back to you after validating it.

services:
- compute.googleapis.com
- secretmanager.googleapis.com
- storage.googleapis.com

@AdarshK15 AdarshK15 Sep 3, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README notes that novnc_proxy_secret and novnc_proxy_secret_id are mutually exclusive and exactly one is required.

To fail fast and prevent users from hitting strange Terraform errors later in the deployment process, we can enforce this immediately during the ./gcluster create phase using the toolkit's metadata validation.

Could you add validations block like this to the metadata.yaml for both novnc-desktop and novnc-runtime modules?

validations:
  - validator: exclusive
    inputs:
      vars: [novnc_proxy_secret, novnc_proxy_secret_id]
    error_message: "You must provide exactly one of 'novnc_proxy_secret' or 'novnc_proxy_secret_id'."

spec:
requirements:
services:
- secretmanager.googleapis.com

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, please add metadata validator where ever applicable to fail fast.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

external PR from external contributor release-new-modules Added to release notes under the "New Modules" heading.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants