TeamcenterKnowledge

Skills

Graphstudio Hyperv Keycloak

Skill graphstudio-hyperv-keycloak. Build a Rocky Linux Hyper-V VM via unattended kickstart (no Windows admin elevation, no RHEL subscription) and install Siemens Rapidminer Graph Studio (rebrand of Altair Graph Studio, née Cambridge Semantics Anzo - internal code/paths still say "anzo" everywhere) with SSO wired to a shared Keycloak realm. Covers the OEMDRV-ISO-via-Docker recipe (COM/IMAPI2FS from PowerShell is not worth fighting), three separate kickstart traps that each look like a different failure, the install4j console-mode driver pattern with tail-anchored prompt matching, and the multi-stage Keycloak SSO debugging chain: a discoveryURI:null red herring, a redirect-uri port+slash mismatch, a "Trust All" checkbox that only covers one of two TLS code paths, and Graph Studio's own SEPARATE certificate trust store (distinct from the JVM cacerts) that is the actual fix for the second path. Also covers Altair Units Licensing machine-slot exhaustion and its Auth Token fix, deploying a Graph Lakehouse (AnzoGraph) backend via Docker instead of a second VM (including the gRPC ports a graphmart deploy needs that the web UI alone doesn't expose), and a Hyper-V Default Switch IP change that silently breaks the browser URL, the Keycloak redirect URI, and the VM's own hosts-file entry all at once. Also covers what a locked-out sysadmin account actually requires (a full reinstall - no offline reset exists anywhere in the product) and the install4j memory-prompt trap that validates against live Hyper-V dynamic memory rather than the stated minimum. Also covers three independently-reproducible backend bugs blocking every no-code path to get real data into a graphmart (HttpSource auto-browse NPE, a missing OSGi commons-net bundle that makes FTP File Connections unusable, and a shared file-mapping-preview crash on any source type), a working vsftpd staging-server recipe including the pam_shells//etc/shells FTP-login trap, the sticky per-graphmart File Staging Area binding, and why the one working ingestion path (a hand-written SPARQL/GDI CONSTRUCT query) is virtualization-only, not onboarding, in this build. Use for any Hyper-V unattended Linux install, any Graph Studio/Anzo deployment, any Graph Lakehouse/AnzoGraph connection, a locked-out admin account, connecting Graph Studio to an external REST/HTTP data source, or any "OIDC login redirects but throws an internal SSL error" investigation.

Provenance: EXERCISED end to end 2026-08-19/24 on GraphStudio-Dev (Rocky Linux 9.8, Hyper-V, host MARTINI-GAMING), Graph Studio 6.3.2 (graph_studio_server_linux_amd64_6_3_2_r202607081222.sh), against the shared xcelerator Keycloak realm (xc-sso-keycloak container, see tc-sso-keycloak skill for that realm's own history). A real user (ed) logged in through the full chain with a role assigned. Do not promote any line marked otherwise.

0. The one-paragraph version

"Rapidminer Graph Studio" is Siemens' current name for a product that shipped as Cambridge Semantics Anzo, then Altair Graph Studio - internal package names, ontology URIs, install paths and error strings still say anzo throughout, and the generic install docs sometimes describe an /opt/Anzo layout that this exact build does not use (it defaults to /opt/graphstudio). It is a Linux-only server product (RHEL/CentOS/Rocky 7.9-9.3, no Windows build), so on a Windows host without a RHEL subscription the path is: Rocky Linux (free, binary-compatible, no subscription) in a Hyper-V VM, unattended-installed via kickstart, then the vendor's install4j shell installer, then Keycloak OIDC SSO wired to a shared realm. Every stage had at least one silent-failure trap; none of them were fixed by guessing twice.

1. Building the VM: Rocky Linux via Hyper-V, zero elevation needed for the VM itself

EXERCISED. New-VM/New-VHD/Set-VMProcessor/Enable-VMTPM etc. all succeeded in a non-elevated PowerShell session on a host where the user is in the Hyper-V Administrators group - no UAC prompt, no sudo-equivalent needed. Gen2 VM, MicrosoftUEFICertificateAuthority Secure Boot template (Rocky/RHEL ship a Microsoft-signed shim, boots fine under default Secure Boot). Dev-tier sizing per Siemens' own docs: 8 vCPU / 32GB RAM / 200GB dynamic disk is the documented minimum for a Graph Studio dev environment, not a made-up number.

What genuinely does need elevation, and is a dead end without it: Mount-VHD, Initialize-Disk, Format-Volume - the whole Windows disk-management stack, not just Hyper-V. Mount-VHD fails with 0x80070522 ("A required privilege is not held by the client") even though New-VM/New-VHD (creating the file) succeed fine. Don't spend time trying to build an OEMDRV delivery mechanism around a VHDX for this reason - see section 2.

2. Delivering the kickstart file: build the OEMDRV ISO in a throwaway Docker container, not via Windows COM

EXERCISED, after two dead ends. Anaconda auto-detects a kickstart with zero boot-arg editing if it's named ks.cfg at the root of a device labeled OEMDRV - this is standard, documented Anaconda/dracut behavior (confirmed against current Red Hat docs, not assumed from memory), and works from a second virtual DVD drive on the VM, not just physical media.

Dead end 1: PowerShell's IMAPI2FS.MsftFileSystemImage COM object. The classic "New-IsoFile"-style scripts you'll find online rely on marshaling an IStream through PowerShell's dynamic COM dispatch, and in practice this throws (does not contain a method named 'Stat', then 'LoadFromStream') on modern PowerShell without a working Add-Type-compiled unsafe-C# shim (which itself is unreliable under PowerShell 7's Roslyn-based Add-Type, since it doesn't cleanly forward /unsafe the way the old CodeDom/csc.exe path did). Burned two full attempts before abandoning it.

Dead end 2: a FAT32-labeled VHDX, blocked by section 1's elevation wall (Mount-VHD needs real admin).

What actually worked: if Docker Desktop is already on the host, build the ISO inside a throwaway alpine container with xorriso (apk add xorriso), which is a real, well-tested ISO tool with zero Windows-specific footguns:

MSYS_NO_PATHCONV=1 docker run --rm -v "/d/path/to/workdir:/work" alpine sh -c \
  "apk add --no-cache xorriso >/dev/null 2>&1 && \
   xorriso -as mkisofs -o /work/iso/OEMDRV.iso -V OEMDRV -J -R /work/oemdrv_src && \
   echo ISO_BUILD_OK"

MSYS_NO_PATHCONV=1 is not optional under git-bash/MSYS. Without it, git-bash silently rewrites the container-side path after the colon in -v host:/container too, turning /work into something like C:/Program Files/Git/work - the container then reports /work: No such file or directory even though the host-side path is completely correct. Scope the env var to just the docker invocation line, not a global export (a global export breaks unrelated commands like docker build -f).

Once the OEMDRV.iso is attached and the VM is running, it must be detached before it can be rebuilt. The Windows host holds a lock on the file while any DVD drive references it; rm/ Remove-Item on it while attached fails or hangs. Stop-VM -TurnOff -Force first, then Set-VMDvdDrive -Path $null on both drives, then rebuild.

3. Kickstart traps: three different errors, three different fixes, none guessable from memory

EXERCISED, in order, across three boot attempts on RHEL9/Rocky9's current Anaconda (34.25.7.14-1.el9.rocky.0.6):

  1. %packages --nobase is a hard parse error, not a warning. --nobase was removed from modern kickstart syntax; the installer terminates immediately with unrecognized arguments: --nobase before touching the disk. Fix: bare %packages with @^minimal-environment.
  2. pwpolicy needs --nostrict, not --notstrict (the latter doesn't exist and produces policy name required for pwpolicy, a confusingly-unrelated-sounding error that actually means the argument parser choked on an unrecognized flag). More importantly: pwpolicy is deprecated and unnecessary for a pure kickstart flow - password-strength policy only applies to passwords typed interactively in the GUI/TUI; a pre-hashed user --password=... --iscrypted value in the kickstart is never validated against it. Simplest fix: delete the whole %anaconda block rather than trying to get its syntax right.
  3. A package missing from the install media (e.g. wget on the minimal ISO, which only ships curl) stops the automated install with an interactive yes/no prompt - "Problems in request: missing packages: wget. Would you like to ignore this and continue with installation?"
    • and OEMDRV auto-detection does not auto-answer this. It only supplies the kickstart file itself; a package-resolution conflict still blocks on a real keypress at the console. Either drop wget from %packages (use curl, already present) or be ready to answer the console once.

Validate the kickstart with the real parser before spending 10+ minutes on another boot cycle. ksvalidator (from PyPI pykickstart) can be run in a throwaway container with zero host pollution and catches all three of the above before they cost a boot:

MSYS_NO_PATHCONV=1 docker run --rm -v "/d/path/to/workdir:/work" python:3.12-slim sh -c \
  "pip install --quiet pykickstart >/dev/null 2>&1 && ksvalidator -v RHEL9 /work/ks.cfg"

4. Verifying an unattended install actually progressed, and catching the reboot

EXERCISED, and this is where guessing from Get-VM/Get-VHD alone goes wrong.

  • A dynamic VHDX's reported size can be stale while the VM holds it open. Get-Item/Get-VHD against the file while the VM is running returned the untouched 4MB header size long after a real, successful package install had happened (confirmed independently by the free-space drop on the host's D: drive, and later by Get-ChildItem on the same file after Stop-VM, which showed the true ~2.8GB). Stop the VM before trusting a host-side file-size check - or use an independent signal (host free-disk-space delta) instead of re-querying the same locked file twice and treating agreement as confirmation.
  • The Hyper-V host-level event log is more trustworthy than VM State/Uptime for detecting a guest-initiated reboot. A kickstart's reboot command doesn't produce a Stop/Start cycle Hyper-V reports the way Stop-VM/Start-VM do; instead Microsoft-Windows-Hyper-V-Worker-Admin logs event ID 18514, "was reset by the guest operating system", immediately followed by a fresh 18601 "successfully booted an operating system". Poll for 18514 rather than inferring a reboot happened from Uptime resetting, which is noisier to interpret.
  • The VM's firmware boot order still points at the install ISO after a kickstart reboot. Unless you detach both DVD drives before the post-install reboot fires, the VM re-enters the installer instead of booting the freshly-installed disk. Catch the 18514 event, Stop-VM -TurnOff -Force, Set-VMDvdDrive -Path $null on both drives, Start-VM.
  • No screen-capture tool ships with stock Hyper-V PowerShell. When indirect signals disagree (or before spending another 10-minute boot cycle on a guess), the fastest real diagnostic is asking a human to glance at Hyper-V Manager -> Connect and read the actual console text - this caught two of the three kickstart bugs in section 3 in under a minute each, versus many minutes of blind log-spelunking per bug. Note mmc.exe (Hyper-V Manager) requires a UAC-elevated launch; a computer-use/automation tool cannot click through that dialog (Windows UIPI blocks cross-integrity-level input) - a human has to approve it.

5. Installing the product itself: install4j console mode, not blind stdin piping

EXERCISED. The vendor installer is an install4j-generated shell script. Its --help/-h output is authoritative for available flags - check it live rather than assuming install4j's typical -q -varfile convention applies to this specific build:

-varfile [file]  Use a response file
-c               Run in console mode
-q               Run in unattended mode
-dir [directory] In unattended mode, set the installation directory
-overwrite / -nofilefailures / -splash [title] / -alerts / -Dname=value / -h

Fully unattended -q -varfile needs the response-file's internal variable names, which aren't guessable and weren't reverse-engineered here; -c console mode driven by a scripted interactive session was the path actually used and is fully reliable once done correctly.

The failure mode that cost a redo: matching a prompt pattern against the WHOLE accumulated scrollback instead of just its tail. A regex like r"1.*[Aa]gree" intended to detect the EULA's final "type 1 to accept" prompt instead false-matched the license body's own text ("1. LICENSE GRANT: ... License Agreement") many screens earlier, causing 1 to be sent mid-scroll instead of at the real prompt. Match only against the last few non-empty lines of newly-arrived output, not the full buffer:

def tail_text(acc, n_lines=4):
    lines = [l for l in acc.splitlines() if l.strip() != ""]
    return "\n".join(lines[-n_lines:])
# then re.search(pattern, tail_text(acc), re.IGNORECASE) - never against the raw acc

Drive it over SSH with paramiko.invoke_shell() (a real interactive channel with a pty), not a one-shot exec_command - the installer's own prompts need to be read incrementally and answered in turn. Read with an idle-timeout loop (keep reading while new bytes keep arriving; stop once output goes quiet for N seconds) rather than a fixed sleep, since package extraction and the EULA pager take wildly different amounts of time per screen.

The install directory needs to exist and be owned by the service user before running the installer, if not running as root. This build's default install path is /opt/graphstudio (not the /opt/Anzo the generic vendor docs describe for other Anzo-lineage builds) - /opt is root-owned by default, so a non-root service user hits "You have no write permissions for the directory /opt/graphstudio" and the installer loops re-asking for a directory. Fix: sudo mkdir -p /opt/graphstudio && sudo chown <serviceuser>:<serviceuser> /opt/graphstudio before launching. If a scripted driver doesn't detect this specific error text and abort, it will silently feed all its remaining scripted answers into the repeating "invalid directory" retry loop - add an explicit abort guard matching "no write permissions|is invalid|Please choose another" right after the install-dir prompt, not just after the final prompt.

firewall-cmd inside a kickstart %post block silently no-ops - %post runs inside Anaconda's chroot before the target system's firewalld daemon is ever started, so every firewall-cmd --permanent --add-port=... call logs FirewallD is not running and does nothing, even though the kickstart step itself reports no error. Apply firewall rules live over SSH after first boot instead, then firewall-cmd --reload.

Starting the service: install4j-generated server launchers accept {start|stop|run|run-redirect|status|restart|force-reload} - check the real script name via ls/--help rather than assuming startup.sh/start.sh (this build's is literally graphstudioServer, matching the product's own binary/package naming, not the generic docs' Anzo-branded examples).

6. Keycloak SSO: the discoveryURI:null red herring, and the real two-trust-store bug

EXERCISED, and this is the section worth reading in full before touching a Graph Studio OIDC provider - most of the debugging time here was spent chasing a symptom, not a cause.

6.1 Two completely separate Keycloak integrations exist - know which one you need

Graph Studio has two distinct Keycloak-related configuration surfaces, easy to conflate:

  1. SSO login provider (Admin -> User Management -> SSO Config -> Keycloak OIDC Provider). This is purely for authenticating a browser session - client id/secret, realm, base URI, callback.
  2. External User Management Provider (Admin -> Servers -> Advanced Configuration -> "Altair Graph Studio Keycloak User Management Bundle"). This is for directory sync - pulling user/group lists from Keycloak's own admin API, needs a different Keycloak client with realm-management service-account permissions, and is a prerequisite only if you want Admin -> Users -> "Add Existing Directory Users" to list anything.

If the SSO provider's "Use username directly" checkbox is enabled, #2 is not required at all - a successful OIDC login auto-provisions a plain internal Graph Studio user record, no directory sync needed. Don't build out the heavier external-user-management-provider integration (which needs its own client, its own realm-management roles, and is a much bigger lift) unless you actually need group-based auto-role-assignment (section 6.5) or centrally-managed user lifecycle.

The pre-existing default config for surface #2 on a fresh install (OSGi PID com.cambridgesemantics.anzo.usermanagement.keycloak.KeycloakUserManagementImpl) points at the Graph Studio host's own hostname and a nonexistent anzo realm - it logs a real startup error (Error starting keycloak user management / KeycloakBuilder.build() failure) that is completely unrelated to the SSO login provider and safe to ignore if you're only using surface #1.

6.2 discoveryURI: null in the error log is a symptom, not a diagnosis - don't chase it directly

A broken SSO provider produces this exact error on every login-page render, regardless of which of several different underlying causes is actually in play:

o.o.s.s.s.anzo.AnzoWrappedClient - RedirectionActionBuilder was null for client:#KeycloakOidcClient#
... discoveryURI: null ...
java.lang.IllegalArgumentException: redirectAction was not valid:Optional.empty

This exact log line was produced by three unrelated root causes across three restarts: misconfigured %anaconda-adjacent product config (n/a here, but the pattern generalizes) - in this case specifically, first a benign "just needs a restart" false lead (restart did nothing), then a TLS trust failure in the metadata-fetch path (section 6.4), and even after that was fixed, the identical log line persisted for the next real bug (the redirect_uri mismatch, section 6.3) before finally clearing. A restart is cheap - try it once, but do not treat "the log line looks the same after a restart" as proof a fix didn't work, and don't treat "the log line is gone" as proof a fix did work without also confirming the actual browser flow reaches Keycloak's own login page.

Before touching TLS or client config: re-open the saved provider in the admin UI and confirm every field actually persisted (client ID, realm, base URI). This ruled out an RDF-predicate-mismatch theory (the ontology has both a generic SSOProvider#realm/realmName and a Keycloak-specific SSOProvider#keycloakRealm predicate, and it was plausible the form wrote to the wrong one) in one screenshot, saving a much longer binary-journal-inspection detour. The values do save correctly; if a fix isn't working, look at code paths downstream of storage, not the storage layer itself.

6.3 The registered redirect_uri needs to match the ADMIN port, and tolerate a stray double slash

Keycloak's own error page (Invalid parameter: redirect_uri) reveals the ground truth callback URL by decoding the actual redirect_uri= query parameter Graph Studio built - do this instead of assuming the callback matches whatever port the generic docs' example uses. In this build, the callback fired on port 8946 (the Admin application's SSL port), not 8443 (the main app's SSL port, which wasn't listening at the time of this build - it activates after the license-key wizard) - because all testing then necessarily happened against the Admin app.

Do not read that as a standing licence blocker. It describes one moment during the install, and it hardened into a fabricated constraint: the companion handoff wrote it up as "blocked on a real Altair/Siemens license key (Chris's task)", a later session repeated it three times without checking and recommended parking Graph Studio on the strength of it. Corrected 2026-08-20 by Chris directly: Graph Studio is fully licensed and functional; the VM is powered off by choice. This is the "an unchecked caveat is a fabricated blocker" rule with a worked example: a sentence that was true about a moment, restated as a fact about the environment, believed because it was specific. Check it against the live thing before repeating it. Additionally the built URL contained a literal double slash (https://<host>:8946//anzo_authenticate?...), likely from [CONTEXT_PATH] in the Callback URL field resolving to an empty/root value. Register the Keycloak client's redirect URIs as a wildcard (https://<host>:8946/*) rather than chasing the exact string - it absorbs both the port surprise (if you register both :8443/* and :8946/* up front) and the double-slash quirk in one move.

6.4 "Trust All" only covers ONE of at least two outbound TLS calls in the OIDC flow

The SSO provider's "Trust All" checkbox ("Trust all certificates from Oidc source") is undocumented beyond a one-line description in the vendor docs ("controls whether all responses from the identity provider are trusted") - in practice, enabling it fixed the discovery/metadata fetch (org.openanzo...AnzoSecurityClients$SslDefaultResourceRetriever, used to build the authorization-request URL and render the login-page link) but had zero effect on a later, separate SSL failure in org.pac4j.oidc.credentials.authenticator.OidcAuthenticator.validate()

  • the server-to-server token-exchange call made after Keycloak redirects back with an authorization code. Same underlying private-CA-not-trusted problem, two different HTTP client code paths inside the same product, only one of them gated by that checkbox. Applies without a server restart (config re-read live); a restart is not needed to test whether checking it helped.

Diagnosing which TLS layer is actually broken, in order of cost:

  1. openssl s_client -connect <host>:<port> -servername <host> from the VM - cheap, shows the real chain and issuer (verify error:num=20:unable to get local issuer certificate = private/ self-signed CA, expected for an internal realm).
  2. The exact same JRE the running server uses, via keytool -printcert -sslserver <host>:<port> -trustcacerts -keystore <jre>/lib/security/cacerts -storepass changeit - proves whether the system JVM trust store (which you can patch directly, see below) is the blocker.
  3. A two-line bare HttpsURLConnection test compiled and run with that same JRE, zero special flags - proves default JVM trust end to end, closer to what the app's own default-config HTTP calls do than keytool is.
  4. If (2) and (3) both succeed cleanly, the failure is NOT in the JVM's system trust store at all - stop patching cacerts and look for an application-level trust store instead (6.5).

Patching the system JVM cacerts directly is real and sometimes necessary, but do it knowing what it does and does not cover:

JRE=$(dirname $(dirname $(readlink -f $(which java))))   # or find the exact i4j_jres path in use
sudo $JRE/bin/keytool -importcert -trustcacerts -noprompt \
  -alias <name> -file <ca.crt> -keystore $JRE/lib/security/cacerts -storepass changeit

6.5 The actual fix for the token-exchange TLS failure: Graph Studio's OWN certificate trust store

This is the section worth reading before spending an hour on cacerts. Graph Studio ships a separate, application-level trust store, entirely independent of the JVM's cacerts - patching cacerts (however correctly, however well-verified per 6.4) has no effect on the OidcAuthenticator.validate() token-exchange call, because that code path reads from Graph Studio's own store instead. The vendor docs name this explicitly (Admin -> Server Administration -> Managing Certificates -> Adding a Certificate to the Trust Store):

In the Administration application, expand the Servers menu and click Server Certificates. ... click the Trusted Certificates tab ... click Upload Certificate, browse to the certificate file ... Once the file is uploaded, restart Graph Studio to apply the change.

Upload the same root CA (not just the leaf/server cert - the CA that signed it) that fixed step 4 of section 6.4's diagnostic ladder, then restart. This is the fix that actually cleared OidcAuthenticator.validate()'s SSLHandshakeException: (certificate_unknown) No trusted certificate found after both the JVM-level cacerts patch AND the "Trust All" checkbox had already been applied and neither one touched it.

General lesson, same shape as tc-sso-keycloak section 3/20 (three/four independent trust stores for a single TC login): a TLS trust fix verified against one HTTP client (the JVM default, curl, Node's bundled CA list, whatever) says nothing about a DIFFERENT client library used by a different code path in the same application. Always ask "which trust store does THIS specific failing call actually read from" before re-testing the same fix a third time.

6.6 New SSO users get "Access Denied" until a Role is assigned - and there's a real auto-assign mechanism

A successful login (Keycloak auth + token exchange both clean) can still end in Graph Studio's own Internal Server Error / Access Denied - this is Graph Studio's own authorization layer, expected behavior for a freshly-provisioned user, not a bug. Checking Licensed on the user is necessary for login at all but is not sufficient for actual page access.

Auto-assigning a baseline role on first login is a real, documented mechanism - it's not a separate toggle, it's editing the built-in role directly: "If a user account is created in Graph Studio but no roles are assigned, that user has the permissions of the Authenticated User role... By default, authenticated users cannot access the Graph Studio application but can access the Hi-Res Analytics application." Add whatever baseline permission set you want every new SSO user to have (Data Analyst/Data Scientist/etc.) directly onto the Authenticated User role (Admin -> User Management -> Roles -> Authenticated User), and it applies to every future new user automatically with zero per-user step. The alternative - syncing Keycloak groups and assigning a group to a role - is real (roles.htm: "groups (or users) are added to roles") but requires the full external-user-management-provider directory sync from section 6.1, a materially bigger lift for the same outcome if a single flat baseline role is all that's needed.

7. Altair Units Licensing: restarts burn a fresh "Authorized Machine" slot, and the real fix is a machine-scoped Auth Token, not a bare ANYHOST code

EXERCISED, 2026-08-20, on graphstudio-dev - this is the same class of trap as Panopticon's Altair Units licensing problem on a sibling VM the same day (independent confirmation it's a product-wide pattern, not a one-off).

The trap: choosing Altair Units Licensing -> Managed Licensing -> "Log into Altair One" with a username/password causes Graph Studio to register the current machine's MAC address(es) as a new row in Altair One's own "Authorized Machines" list on every validation, not just the first. A normal dev loop (config tweaks, JVM heap edits, licensing experiments) restarts the service repeatedly, and each restart mints another row bound to the same MACs. Altair One's service appears to hard-fail once too many registrations pile up for one account in a short window - first as SOAP timeouts (End of file or no input, SSL_connect() failed in tcp_connect()), eventually as a flat [Altair SSO]: The credentials provided were invalid. This presents as a permanently stuck OSGi bootstrap (all web ports 404, boot log going completely silent ~3-3.5 minutes in) that looks identical to an external Altair outage on first inspection - it isn't; it's self-inflicted by the restart loop.

The fix is not a bare/unscoped ANYHOST auth code. A hand-generated ANYHOST-type token from the portal was tried first and failed with the same "credentials provided were invalid" error, because Graph Studio's licensing UI submits the machine's MAC alongside the code, and the code needs to correspond to an already-registered row for that exact machine - it isn't machine-agnostic the way the name suggests.

Real working recipe:

  1. Let Graph Studio create its own Authorized Machines row the normal way - the first-ever Altair One username+password login does this automatically.
  2. On the Altair One portal, open Authorized Machines, find the row matching this machine (by hostname/MAC - not necessarily the row from the login that triggered it), check its box, click Generate Auth Token. This mints a token scoped to that machine's already-registered MACs, distinct from an unscoped ANYHOST code.
  3. In Graph Studio: System Administration -> Servers -> Licensing -> Altair Units Licensing -> Managed Licensing -> Use Auth Code -> paste the token -> Save.
  4. Restart (graphstudioServer restart) and confirm a clean All Currently Registered Services started line with no repeated Altair/SOAP errors.

Confirmed on graphstudio-dev: Authorized Machines row anzo/graphstudio-dev (both NIC MACs), token suffix 50sj..._001, generated 2026-08-20 22:07 UTC. Once switched to Auth Code mode, a plain graphstudioServer restart does not mint a new Authorized Machines row - confirmed by two clean restarts afterward adding zero new portal entries. That's what actually stops the slot-exhaustion cycle; the username/password mode is the thing to avoid for anything but the very first login.

A plausible-looking detour that turned out to be unnecessary, worth naming so it isn't repeated: the real OSGi ConfigAdmin property names behind this screen - com.cambridgesemantics.anzo.licensemanager.isAuthCodeAvailable and ...altairAuthCode - are genuine (found by string-scanning com.cambridgesemantics.anzo.licensemanager_*.jar's class constants, not guessed) and match what the Admin UI itself writes on Save. It's possible to hand- patch them directly in the serialized org.eclipse.equinox.internal.cm.ConfigurationDictionary files under Server/data/osgi/configAdmin/data<pid>.pid.N - stop the service first, use the exact same JRE the server runs (ps aux | grep AnzoLauncher to find its path) plus the real org.eclipse.equinox.cm_*.jar on the classpath for a faithful deserialize/mutate/reserialize via jshell, and check each property's actual runtime class before writing (this store mixes real java.lang.Booleans with plain "true"/"false" Strings for what look like identical boolean flags - writing a real Boolean where the app expects a String silently produces a value that fails an .equals("true") check while looking correct in every other respect). But this was a dead end here: the token available at the time was scoped to the wrong machine (a different Windows host, not this VM), so the edit had no effect once the Admin UI's own next Save rotated the PID's generation counter past it anyway. The GUI path with a correctly machine-scoped token is what actually fixed it. Keep the property names on file for reading back what's currently persisted during diagnosis; do the writing through the Admin UI, not the binary store.

8. Connecting Graph Studio to a real Graph Lakehouse: Docker beats a second VM, and the ports that actually matter

EXERCISED, 2026-08-21. Deploying a graphmart (needed even for a bare ad-hoc query - see 7's sibling finding that Dry Run itself is gated on the graphmart being deployed) requires a real Altair Graph Lakehouse (AnzoGraph) backend registered in Admin -> Connections. There is no embedded/local fallback - confirmed from Altair's own docs: "The Graph Lakehouse is the massively parallel processing engine that powers every knowledge graph deployment."

Don't build a second Hyper-V VM for this - a Docker container is the documented, supported path and is dramatically cheaper. Altair's own docs (Plan & Deploy -> Container Image Deployments) cover Docker/Podman/Rancher as a first-class single-server deployment target, right alongside the Enterprise-Linux-9-VM path this skill uses for Graph Studio itself. If Docker Desktop is already on the host (it was here, already running other unrelated containers), this is minutes instead of the hour-plus a fresh kickstart VM build costs:

docker pull cambridgesemantics/anzograph:latest
docker run -d -p 18080:8080 -p 18443:8443 -p 15600:5600 -p 15700:5700 `
  -v "D:\GraphLakehouse\shared-files:/opt/shared-files" `
  --name=anzograph --memory=10g cambridgesemantics/anzograph:latest

Default login admin / Passw0rd1. RAM: Altair states 8 GB minimum / 16 GB recommended for the container path (lower than the 16 GB minimum quoted for a bare-VM AnzoGraph install) - 10 GB was plenty here, confirmed by a real AnzoGraph database started log line and clean HTTP 200.

The trap that actually cost the most time here: the web UI ports are not the only ports Graph Studio needs. docker run with only -p 18080:8080 -p 18443:8443 (the Jetty web console) looks complete - the console itself works fine, license info loads, everything responds. But Graph Studio's own "Create Altair Graph Lakehouse" connection form in Admin needs two more ports it gets from a separate, easy-to-miss "Advanced" tab, not the main form: a Management Port (gRPC), default 5600, and a SPARQL Port (gRPC), default 5700. Registering the connection with only the container's web ports published produces a misleading error - Connection Failed: Error authenticating with Altair Graph Lakehouse Server: grpc://<host>:5700 - that reads exactly like a wrong password, not like "nothing is listening on that port because it was never published from the container." Confirmed via docker port anzograph: with only the web ports mapped, 5600 and 5700 simply aren't there. Fix: publish them too (-p 15600:5600 -p 15700:5700 here, remapped off their defaults for the same reason the web ports were - avoid clashing with anything else already bound to 5600/5700 on a host running many other services) and use the remapped numbers in the connection form's Advanced tab, exactly the same pattern as the web HTTP/HTTPS ports.

The connection form also wants a separate "Query User" / "Query Password", and its default value is a real but effectively unusable account. The AnzoGraph image ships a genuine query entry in /opt/anzograph/config/passwd (confirmed: admin lives in mgrpasswd, query lives in passwd

  • two different files for two different auth planes), but its password is not documented anywhere public (checked Altair's docs, Docker Hub's own page, and the container's own credential-seeding scripts, which only populate ui_query_user/grpc_query_user from files under /k8s-activation-properties/ - a directory that doesn't exist at all in a plain docker run deployment, so those variables stay empty and the account's password is whatever shipped baked into the image). azgpasswd genuinely validates the current password before allowing a change (tested: a wrong guess is rejected outright, not silently accepted), so there is no cheap reset without already knowing it. Practical workaround that worked: just reuse admin/Passw0rd1 in the Query User/Password fields too. admin has at least as much privilege as a restricted query account would need, and the form only validates that the credentials authenticate successfully, not that they belong to a distinct identity.

Changing the AnzoGraph admin UI-login password via azgpasswd writes the file but has NO EFFECT on the actual web console login, even after a full container restart - a real, controlled negative result, not a guess: azgpasswd /opt/anzograph/config/mgrpasswd -u admin -p admin -o Passw0rd1 returned exit 0 with no error, the file changed, the container was fully restarted (AnzoGraph database started again in the logs), and admin/admin still failed with 401 while admin/Passw0rd1 kept working throughout. The Jetty web console's pac4j-based login evidently reads from a different store than the file the product's own native CLI tool writes to. Don't chase this further without a decompile - the GUI's own Roles/password-management React component exists in the shipped JS bundle (found by string-scanning assets/index-*.js for validatePasswordsMatch/renderLogin) but isn't routed anywhere in this build - every nav path was checked (/, /manage, /query, /settings and every one of their sub-tabs) and none exposed it. This is the "AnzoGraph DB For Docker Free 8GB License" tier specifically (confirmed via /manage's own "License Status" line) - the feature may simply not be licensed on this tier.

9. A Hyper-V "Default Switch" IP change breaks three unrelated things at once, and none of the errors say so

EXERCISED, 2026-08-21 - this is the single most expensive class of failure hit in this whole skill's history, because each symptom looks like an unrelated new bug.

The fact: Hyper-V's Default Switch (used for graphstudio-dev's primary, DHCP-assigned NIC, eth0) can silently renumber its own host-side gateway IP with no warning and no log a normal session would see. Observed directly three times in one sitting: 172.27.144.1 -> 172.19.176.1 -> 172.21.160.1. The guest's own DHCP client handled its side fine every time (eth0 picked up a fresh lease in the new range automatically) - the guest was never actually broken. Every failure that followed was stale configuration pointing at the old IP, in three unrelated places that all had to be found independently because none of the error messages mention networking at all:

  1. The browser URL (https://172.27.152.170:8946/) - just stops loading. Obvious once you think to check, but nothing tells you the IP moved.
  2. The Keycloak client's redirect URIs (https://172.27.152.170:8946/* etc, registered per section 6.3) - produces "invalid redirect uri" on the Keycloak side, which reads like a configuration mistake in the client registration, not a stale IP.
  3. The VM's own /etc/hosts entry for sso.xcelerator.local (added in section 6, originally pointed at the same Default Switch gateway IP because that's what the host resolves to from inside the VM) - produces java.net.SocketTimeoutException: Connect timed out deep in Graph Studio's own server-side OIDC token-exchange call, which reads like a server-side Java bug or a firewall problem, not like a one-line hosts-file fix from section 6 having gone stale.

All three had the same root cause and the same fix shape (re-point at the current IP), but nothing about any single error message says "check the other two as well" - each was found by directly testing the assumption (curl to the Keycloak realm URL from inside the VM; grepping /etc/hosts) rather than reasoning from the symptom.

Re-patching the /etc/hosts entry to the current Default Switch gateway (the first fix tried) is not durable - it broke again within the same session, a third time, proving the point rather than just theorizing it. The actual durable fix, applied and confirmed on the third occurrence: graphstudio-dev already has a second NIC (eth1, added in section "connecting to Teamcenter" work, on the separate TM_HYPERV_NAT switch) with a static IP, 192.168.222.101, that has not moved once across this entire skill's history. The host itself also has a stable IP on that same switch, 192.168.222.1 - and since Keycloak runs as a Docker container on the host (same as the Graph Lakehouse container in section 8), it's reachable there too, confirmed with a direct curl. Point /etc/hosts's sso.xcelerator.local entry at 192.168.222.1, not at whatever Default Switch's gateway currently is - this stopped being a moving target once made to depend on the stable switch instead. Do the same for every one of the three stale pointers above (browser bookmark, Keycloak redirect URIs, and - for the Graph Lakehouse work in section 8 - the Docker container's host address): move them all onto the stable subnet instead of Default Switch's DHCP range. Prefer the static NAT switch's addresses over Default Switch's for anything durable - browser bookmarks, OAuth redirect URIs, hosts-file entries, container connection strings - and reserve Default Switch/DHCP for whatever genuinely needs outbound internet only.

A same-symptom trap worth flagging separately: a stale CSRF token on Graph Studio's local (non-SSO) admin login form, producing Invalid CSRF Token '...' was found on the request parameter

  • hit immediately after a failed Keycloak SSO attempt in the same browser tab. Not network-related at all; a hard refresh or a fresh incognito window cleared it. Worth ruling out before assuming a fresh networking problem when an error shows up right after switching login paths in one tab.

10. Locked out of sysadmin: no offline reset exists, and a full reinstall is the real fix

EXERCISED, 2026-08-24 - after a working sysadmin login was lost (exact cause unclear - the password was set once during the original scripted install session and never verified or recorded again afterward), this section covers what was actually checked before concluding a reinstall was necessary, and the reinstall itself.

What was actually checked before giving up on recovery - worth listing so a future session doesn't re-walk the same dead ends: the shipped install docs (confirms the username is fixed as sysadmin, cannot be changed - "Do not change the System User ID. It must be sysadmin"); the install4j installation.log and response.varfile (no password ever echoed to either, by design); every firstbootFiles/*.properties file (none seed a sysadmin credential); every bundled plugin jar searched by class name for resetpassword/resetadmin/passwordreset/createadmin (zero hits - genuinely absent, not just undocumented); the gs CLI and the lakehouse CLI (gs help lists no user/password/security subcommand at all; lakehouse is a client for the separate Graph Lakehouse product's gRPC ports, not Graph Studio's own login); and the OSGi SSH management console on port 8022 (org.openanzo.SSH bundle, confirmed via ConfigAdmin PID - genuinely a Felix Gogo shell, but authenticates against the same locked credential store, not a separate bypass). The actual credential store is data/journal/anzo.jnl, a ~600MB proprietary RDF journal - hand-editing it was considered and rejected as too risky compared to the small, well-understood ConfigAdmin PID files from section 7's licensing fix. A single Hyper-V checkpoint existed (Automatic Checkpoint, dated to first VM boot) but predated the Graph Studio install entirely, so restoring it would have meant redoing everything anyway - no shortcut there either.

The reinstall itself worked, and corrected a wrong assumption from section 5: the System Administrator username/password is not an install4j console-mode prompt at all in this build - console mode only asks for component selection, install directory, symlinks, and memory size. sysadmin's password is set through a post-install first-boot web wizard (http://<host>:8945/ per the installer's own final message), a completely different mechanism than assumed when writing section 5. This matters because it means the credential is never at risk of being silently mistyped by a scripted driver - a real human sets it, in a real form, and can confirm it works immediately. Once logged in, Admin → Server Settings → Administrator → EDIT is the genuine, documented way to change that password later - matching an Altair release note found during this investigation about a fixed "internal user password change" flow. It only works authenticated, though, so it is not a rescue path for the locked-out case this section opens with.

A new install-time trap, distinct from anything in section 5: the console installer's "Maximum Memory in MB" prompt validates against currently-available RAM live, not the stated minimum. Hit directly: the installer printed "The minimum amount currently supported is 1024 MB. 834 MB are available" - a self-contradictory pair of numbers, since 834 < 1024 - and then rejected every value above 834 with Value cannot be greater than 834, including the deliberately-chosen 16000. This happens because the VM's Hyper-V dynamic memory allocation shrinks toward its floor while mostly idle (exactly the mechanism documented in section "connecting to Teamcenter" for GraphStudio-Dev's dynamic memory config), and the installer reads whatever Hyper-V has granted at that instant, not the VM's real ceiling. Raising MemoryMinimum to force more headroom requires the VM to be off first (Set-VMMemory refuses to set a minimum above the current startup value on a running VM) - not worth doing mid-install. The actual fix, and it is the same fix section 5 already documents: accept whatever value the installer's live ceiling allows (send it back its own stated number, e.g. from a Value cannot be greater than (\d+) match, minus a small margin), let the install complete, then edit graphstudioServer.vmoptions directly afterward (-Xmx700m -> -Xmx16000m) and restart. A scripted driver that blindly resends a rejected value on this specific prompt will loop forever - parse the actual ceiling out of the rejection message instead of resending the same guess.

The Altair Units Licensing SOAP timeout from section 7 reappeared on the very first login attempt of the fresh install, confirmed via the identical log signature (AltairUnitLicenseServiceImpl - Error occured getting Altair unit licensing auth code: SOAP 1.1 fault ... "message transfer interrupted or timed out") landing within seconds of choosing "email login" (the Option 2/username-password path) on the post-install wizard's licensing step. This is strong evidence the machine-slot mechanism from section 7 is tied to the install, not just to individual login attempts - a fresh install re-triggers it the same way a restart under the old Option 2 config did. The already-issued Auth Token from section 7 was still valid and reusable across this fresh install - entering it directly on the wizard's license screen cleared the timeout immediately, without needing to generate a new one on the Altair One portal. Auth Tokens appear to be scoped to the machine's registered MACs, not to a specific installation, so keep the token from section 7 on hand rather than assuming a reinstall needs a fresh one.

The post-install wizard's "User Management" step offers a real choice - Internal / External Keycloak / External SCIM - presented as if selecting one is exclusive. It is not: "Use Internal User Management" here only governs how the bootstrap sysadmin account authenticates, and Keycloak SSO for other users (section 6) is still added afterward as a completely separate step (Admin -> User Management -> SSO Config), exactly as it was in the original install. Don't pick "Use External Keycloak" here just because SSO is the end goal - that's for a different, later screen.

Driving this reinstall with a scripted paramiko session hit two failure shapes worth naming generally, beyond the memory-prompt trap above: first, idle_secs-based prompt detection (read until N seconds of silence, then assume a prompt is waiting) produced a false stop during Unpacking JRE ..., which is slow and bursty rather than genuinely idle - fixed by checking whether the last non-empty line actually looks like a prompt (ends in :, ?, ], >, or contains "Enter") before treating silence as "waiting for input," and retrying with a longer wait otherwise. Second, the same EULA-pager trap from section 5 recurred in a new shape: the 31-page EULA's body text never falsely matched the real "I accept the agreement / Yes [1], No [2]" prompt this time (the earlier tail-anchoring fix held), but the initial pattern written to detect it - requiring a literal ? in the tail - didn't match either, because this build's real accept prompt has no question mark at all. Write the acceptance-prompt pattern against the exact captured text of the real prompt, not a guessed shape of what an accept prompt "should" look like - a pattern that's too strict fails closed (safe, but requires a human to notice and fix it) rather than too loose failing open (matches body text early, sends the wrong answer, corrupts the flow) - failing closed is the right default when the two options are the actual tradeoff on offer.

11. Getting real data INTO a graphmart via GDI/HTTP source: three separate vendor bugs, and the one thing that actually worked

EXERCISED, 2026-08-24, testing a "zero-code REST connector" from Graph Studio to the Teamcenter Whisperer gateway (http://whisperer.xcelerator.local:8760/data/rows/Item?array=1&limit=500, no auth, User-Agent header required). Bottom line up front: no GUI/no-code path in this build (6.3.2 DEV, banner "2026.1.2") got real data into a graphmart's persisted graph. Every no-code path hit a genuine backend defect, not a configuration mistake. The one thing that did demonstrably pull correctly field-mapped data was a hand-written SPARQL/GDI query - but see the caveat at the end of this section before trusting that it reads live on every run; that specific claim was proposed as testable and never actually differentially proven before this section was written.

11.1 The Database-Connection catalog: create the HTTP source here, works fine on its own

Admin -> Connections -> Database Connections -> New Connection -> HTTP Data Source. Just a URL, optional user/password, optional "Enable SSO" checkbox - all left blank/unchecked for Whisperer, since its /data layer needs no auth. This object alone is inert (holds only the base call); it does nothing until something tries to browse or query through it, which is where all three bugs below live.

11.2 Bug 1: any attempt to auto-browse an HttpSource's schema throws a null-locator NPE, unconditionally

Reproduced identically five separate ways: the "Add Source Data" dialog auto-fires this the INSTANT an HttpSource connection is selected, before you touch anything else; the SOURCE row's menu -> Regenerate; the same row's menu -> Edit (opens the HTTP Endpoints mapping screen, which immediately throws the same error on load); the graphmart's file-staging connection detail view when it happens to browse an HttpSource-backed path; and a fresh "New Connection" -> select existing connection flow started from scratch after deleting the prior broken source entry. Every path produces the identical browser dialog:

Error: HttpSource does not support browsing
    at https://<host>:8946/sdl/assets/main-CTVj1wuQ.js:3547:55

and, when actually invoked via the "Edit" flow specifically:

Error: Cannot invoke "com.cambridgesemantics.anzo.datatoolkit.DataLocator.source()"
because "locator" is null

Read this as: the "zero-code HTTP source" auto-schema-detection feature is unimplemented for HttpSource in this build, not merely finicky. The UI offers "Add Source Data" -> pick a connection -> auto-detect fields, but the underlying call it makes explicitly documents (in its own error text) that HttpSource doesn't support the browse operation it's about to attempt anyway - a straightforward null-check-missing bug, not a misconfiguration on the source side. Deleting and recreating the source, toggling it, disabling instead of deleting, and reordering the click sequence all reproduce the identical crash. The connection-picker dialog's up-arrow/refresh/search toolbar icons are NOT a manual-sample-data workaround, despite looking like one - the up-arrow stays permanently disabled even with the connection actively selected (it's a breadcrumb "go up one folder level" control for hierarchical sources, inert for something that was never browsable to begin with).

11.3 Bug 2: FTP File Connection is completely unusable - a real, missing OSGi dependency, confirmed from the server's own log

Chasing a workaround for 11.2 (Admin -> Connections -> File Store - a separate catalog from Database Connections, used specifically for a graphmart's file staging area, gated by an "Is Graphmart File Staging Area" checkbox that can only be set at file-store creation time, not edited in afterward) led to standing up a real vsftpd server (recipe in 11.5, since it's a reusable artifact even though this bug blocks its actual use here). Creating the FTP File Connection itself worked - note: its Test Connection button throws a red herring, "Could not read from sftp://host/path because it is not a file" - that's a cosmetic bug in the test button trying to read a directory as a file, not a real auth/reachability failure; the connection still saves and the underlying protocol genuinely works (verified independently with paramiko, both login and a real file write/read-back). The REAL failure is server-side, once Graph Studio actually tries to use the saved FTP connection to list a graphmart's staging folder:

Caused by: java.lang.NoClassDefFoundError: org/apache/commons/net/ftp/parser/FTPFileEntryParserFactory
	at org.apache.commons.vfs2.provider.ftp.FtpFileSystemConfigBuilder.<clinit>(FtpFileSystemConfigBuilder.java:47)
Caused by: java.lang.ClassNotFoundException: org.apache.commons.net.ftp.parser.FTPFileEntryParserFactory
cannot be found by org.apache.commons.vfs2_2.10.0.csi

(full trace in /opt/graphstudio/Server/logs/graphstudio_execution_error.log, operation UtilityServiceVFS#getFiles). This is genuine OSGi bundle isolation: the commons-vfs2 bundle's FTP provider needs commons-net to implement the actual FTP protocol, and whatever commons-net bundle should supply org.apache.commons.net.ftp.parser.FTPFileEntryParserFactory either isn't deployed or isn't exporting that package. This makes the FTP File Connection type entirely non-functional in this build for any purpose, independent of anything on our side - the infrastructure in 11.5 is real and correctly built, it's just blocked by a missing jar inside Graph Studio's own runtime.

11.4 Bug 3: the file field-mapping/schema-preview UI throws the SAME "not a file" crash on a file that genuinely exists - this is the real, broader defect

After 11.3, switching the graphmart's File Staging Area to a plain Local File Connection (Base Folder on the VM's own disk, e.g. /home/anzo/graphstudio-uploads) sidestepped the FTP bug entirely - and Graph Studio's own built-in browser upload ("Staging Area", reached via a Source Layer's + -> Upload files, no manual VM path-wrangling needed) genuinely wrote real bytes to disk: confirmed via SSH, the graphmart auto-creates a per-graphmart subdirectory under the staging base folder (named after the graphmart's own URI, e.g. http___cambridgesemantics.com_Graphmart_700f592985ad45d188fca59cab92cbda), and the uploaded file landed there byte-for-byte identical to the source (111544 bytes, confirmed both sides).

But clicking into that file's own row to see its field mapping throws the identical error as 11.2:

Error: Could not read from "file:///home/anzo/graphstudio-uploads/<graphmart-subdir>/whisperer_item_sample.json"
because it is not a file.

on a path that demonstrably exists and is a real, correctly-sized file at that exact moment (confirmed via ls -la on the VM in the same breath as the browser error). This is the important correction from an earlier, narrower theory: the bug is not specific to HTTP sources or to browsing an unreachable connection - it's in whatever shared schema/field-mapping-preview code path handles ANY source type in this build, HTTP or file, live or already-uploaded. That's why a real, present, correctly-written file still produces zero ontology classes after a full Deploy -> Current Configuration (Process Layers) cycle: nothing ever told the graphmart how to map the JSON's fields into RDF, because the UI that would let you define or auto-detect that mapping crashes before showing it, on every source type tested.

11.5 Working vsftpd recipe (blocked by 11.3 here, kept because it's a real reusable artifact)

Dedicated, unprivileged account, chrooted, allow-listed as the only FTP user, passive mode with a fixed port range (mandatory - the VM sits behind Hyper-V's Default Switch/TM_HYPERV_NAT NAT, so active-mode FTP's server-initiated data connection can't traverse it):

sudo useradd -m -d /srv/graphstudio-ftp -s /sbin/nologin gsftp
sudo mkdir -p /srv/graphstudio-ftp/uploads && sudo chown gsftp:gsftp /srv/graphstudio-ftp/uploads
sudo chown root:root /srv/graphstudio-ftp && sudo chmod 755 /srv/graphstudio-ftp   # NOT writable by gsftp - see below
sudo dnf install -y vsftpd

The pam_shells.so trap: a dedicated FTP-only account with shell /sbin/nologin fails every login with a flat 530 Login incorrect even with the exact right password, because /etc/pam.d/vsftpd includes auth required pam_shells.so, which requires the account's shell to be listed in /etc/shells - and /sbin/nologin normally isn't. Fix: echo /sbin/nologin | sudo tee -a /etc/shells. This does NOT grant the account real shell access (that's still independently blocked by the nologin shell itself); it only satisfies PAM's validity check for the FTP auth path.

The chroot-writability trap: vsftpd refuses (or requires allow_writeable_chroot=YES, a real hardening downgrade) if the chroot root itself is writable by the FTP user. Keep the chroot root root:root mode 755, and make only a subdirectory (uploads/) owned by the FTP account - avoids needing the writeable-chroot escape hatch entirely.

Key /etc/vsftpd/vsftpd.conf additions:

listen=YES
listen_ipv6=NO
chroot_local_user=YES
user_config_dir=/etc/vsftpd/user_conf     # holds a per-user local_root override
userlist_deny=NO                          # + /etc/vsftpd/user_list containing only "gsftp"
pasv_enable=YES
pasv_min_port=21100
pasv_max_port=21110
pasv_address=192.168.222.101              # the VM's own stable TM_HYPERV_NAT IP, see section 9

Open both the control port and the full passive range in firewalld (firewall-cmd --permanent --add-port=21/tcp --add-port=21100-21110/tcp).

Verify with a real client that actually implements the protocol, not curl - curl's bundled libssh2 (irrelevant here, that's SFTP) is fine, but for testing reachability from inside a Docker container, curl ftp://user:pass@host/path worked cleanly and is sufficient; what's NOT sufficient is a bare port-open check. The decisive test done here: docker exec anzograph curl -v ftp://gsftp:...@192.168.222.101/uploads/ from inside the AnzoGraph container itself, not just from the Windows host - confirming both sides the vendor docs require ("the location must be accessible by both the Altair Graph Studio server, as well as the Altair Graph Lakehouse Server") can genuinely reach it, including negotiating EPSV/passive mode across the NAT.

11.6 A graphmart's File Staging Area binding is sticky and easy to fight by accident

The first File Store connection a graphmart's upload flow resolves to (even implicitly, on the first "Add Source Data" attempt) gets bound as that graphmart's Graphmart File Staging Area, a setting on the graphmart's own top-level SETTINGS tab (not the layer's) - visible there as a plain dropdown, editable at any time despite the confusing rule about File Store connections themselves only being flaggable as staging-capable at their own creation time. Creating a new, different File Store connection later does NOT automatically switch the graphmart to use it - it keeps resolving through whatever was picked first (surfaced here as errors referencing ftp://... long after an FTP connection had been abandoned for a Local one). If uploads keep referencing a connection you didn't intend, check the graphmart's own SETTINGS tab for this field before assuming anything else is wrong or restarting from a clean graphmart.

11.7 What did work: a hand-written SPARQL/GDI query - but "live" was asserted, not proven

Native GUI paths all failing (11.2-11.4), a manually-authored SPARQL query against the GDI's SERVICE-clause HttpSource mechanism (docs: 2025.help.altair.com/2025.0/graphstudio/userdoc/gdi-http-source.htm) was tried instead, as a "New Query" catalog object under the Data Layer (ADD -> New Query). Two real findings on the query object itself:

  • This "Query" catalog object's parser rejects a bare INSERT/WITH ... INSERT SPARQL Update entirely - confirmed by the parser's own error listing its accepted top-level keywords (prefix / select / construct / ask / describe / explain / with, with with requiring an IRI literal, not the ${targetGraph} template macro used in the vendor docs' own INSERT-shaped examples). Those doc examples appear to target the generic Anzo SPARQL-endpoint/API execution context, not this specific in-UI query editor.
  • A CONSTRUCT query against the same SERVICE <http://cambridgesemantics.com/services/DataToolkit> / s:HttpSource pattern does run and return real, correctly field-mapped data - confirmed with actual Whisperer field values appearing in the result grid (tcw:owningUser "infodba (infodba)", tcw:itemId "Drawing-Micrometer-A2-Size-template", etc., matching a direct curl pull of the same endpoint). Its "More" menu offers only Copy Query to Clipboard / Copy CURL Command to Clipboard / View Query Explanation - no materialize/save/persist action of any kind.
  • Running this query does NOT write anything into the graphmart's persisted graph. After multiple REFRESH LAYER cycles and a full Deploy -> Current Configuration (Process Layers), the layer's Ontology Model still showed "No classes to show" (with "hide elements with no instance data" on) - zero instance data, consistent across every check. The vendor docs for the GDI explicitly name two different usage modes - "ingest data into Graph Studio" (onboarding) vs. "create a virtual graph that accesses the source only when it is needed without ingesting the data" (virtualization) - and everything observed here (a CONSTRUCT-only query object, no materialize action, zero graph-side effect after running it) is consistent with this specific in-UI "New Query" object being virtualization-only, not an onboarding/ingest mechanism, in this build.

What was NOT verified, flagged explicitly per a direct challenge on this point during the session: whether the query's Run action genuinely re-executes the live HTTP call against Whisperer on every invocation, versus returning some cached/stale response. The evidence for liveness so far is only that the returned data matches what a fresh curl independently pulled at roughly the same time - which is consistent with a live call, but doesn't rule out a very recent cache. A real differential test was designed but never run: temporarily edit the query's s:url to a limit value below Whisperer's real row count (e.g. limit=2, which the gateway is deliberately built to refuse with an error JSON rather than truncate - confirmed separately in this session, {"error":"result_too_large", ...}) and re-run; a genuinely live call should return zero triples (the error JSON has none of the bound field names), while a cached/stale response would still show the original 500 rows. Do not cite the "live HTTP read" claim as confirmed without running this test (or an equivalent one) first - as written, it's a single-sample correlation, not a proven mechanism.

11.8 Net result

Real Teamcenter data (via Whisperer) was proven reachable and correctly field-mappable from Graph Studio's SPARQL/GDI layer, live in a query-result grid. It was not proven to land in, or be readable from, a graphmart's actual persisted/deployed graph by any method tried - three separate, independently-reproducible backend defects (11.2, 11.3, 11.4) blocked every no-code ingestion path, and the one working query mechanism appears architecturally scoped to on-demand virtualization, not ingestion, in this build. Worth filing with Altair/Siemens support as three distinct defects rather than one; each has a clean, minimal repro path documented above.

Related skills

tc-sso-keycloak (the parallel Teamcenter integration against the same class of shared-realm problem - three/four independent trust stores, issuer-hostname strictness, wildcard vs exact redirect URIs, "verify by reading the config back" discipline - almost every general lesson there applies here too), diagnose-silent-failure.


Generated from skills/graphstudio-hyperv-keycloak/SKILL.md in the tc-automation-skills library, which is the canonical copy and also serves as the agent skill set for Teamcenter work.