tl;dr โ IBM WebSphere has a clean configuration API (ConfigService) buried under a broken string-based wrapper (AdminConfig). I built an object-oriented Jython layer that hooks into ConfigService directly via JMX โ easing configuration and ensuring type correctness through metadata introspection โ plus a persistent daemon that eliminates JVM boot overhead, and 55 idempotent scripts that integrate with Ansible’s change detection. github.com/vjt/ansible-wsadmin
In 2021, I spent six months automating the IFAD WebSphere infrastructure with Ansible. The stack was IBM WebSphere Application Server (WAS), WebSphere Portal Server (WPS), and Business Automation Workflow (BAW) โ a clustered deployment with a Deployment Manager, multiple nodes, federated LDAP, SIB messaging, the works.
The standard approach to automating WAS is to write Jython scripts using AdminConfig, AdminTask, and AdminApp โ the four global scripting objects that IBM provides inside wsadmin. I tried that. It lasted about a day before I started looking at what’s underneath.
What I found changed how I approached the entire project. It also produced a library full of ideas I never had a chance to describe properly โ until now, with a little help from Claude.
What’s underneathยถ
IBM WebSphere has a clean, well-designed configuration API. It’s called ConfigService, it’s a JMX MBean, and it does everything you’d expect from a proper Java API: it takes typed objects, returns typed objects, and has a consistent interface for every configuration type in the system.
ConfigService.resolve() returns ObjectName[]. ConfigService.getAttributes() returns an AttributeList โ a list of Attribute(name, value) pairs with proper Java types. ConfigService.setAttributes() takes an AttributeList. ConfigService.createConfigData() takes a parent ConfigDataId, a type name, and an AttributeList of initial attributes, and returns the new object’s ObjectName. There’s getAttributesMetaInfo() that returns the full metadata for any configuration type โ attribute names, types, constraints, whether an attribute is a reference to another object, whether it’s a collection.
It’s an honest API. You ask for a thing, you get a thing. You give it a thing, it does the thing.
Then IBM covered it with AdminConfig.
The AdminConfig problemยถ
AdminConfig is the scripting wrapper that IBM ships as the “official” way to interact with WebSphere configuration from Jython. It takes the typed objects that ConfigService returns and flattens them all to strings. Every method returns a string. Every method that returns multiple objects returns a single string with newlines between them. You split on \n, except sometimes the line separator is \r\n, and sometimes values contain spaces, and sometimes values contain square brackets which collide with the [attr value] syntax that AdminConfig.modify() uses.
Here’s what AdminConfig.show() returns for a data source:
[name "MyDataSource"] [jndiName "jdbc/MyDS"] [description "The data source"]
[authDataAlias ""] [datasourceHelperClassname "com.ibm.websphere.rsadapter.Oracle11gDataStoreHelper"]
That’s not data. That’s a string that looks like data. You need a parser to extract anything from it. And every time IBM adds an attribute with a bracket in the value, the parser breaks.
Compare what ConfigService gives you for the same object: a javax.management.AttributeList โ a proper Java collection of name-value pairs with typed values. You iterate it. You read attributes. You write attributes. No parsing.
The invoke variant of AdminControl has the same problem: it converts everything to and from strings. But there’s AdminControl.invoke_jmx(), which works with actual Java objects. The _jmx variant is the one that can actually call ConfigService methods โ because ConfigService takes AttributeList and ConfigDataId parameters, which can’t be represented as strings.
So the escape hatch exists. IBM just doesn’t point you at it.
How I got thereยถ
The commit history tells the story better than I can.
I started where everyone starts: parsing AdminConfig output with regex. March 10: strengthen the parser on corner cases…. March 20: fix parsing of single-item lists and anonymous objects. You can hear the frustration in the commit messages โ each fix uncovered a new corner case where AdminConfig’s string format was ambiguous or self-contradictory.
By March 25 I was already looking at the JMX layer: abstract jmx invoke and app info parsing. I used IntelliJ IDEA to decompile the WAS bytecode and traced how AdminConfig’s own code worked โ turns out it was calling ConfigService internally and then converting the results to strings. All that parsing I was fighting was reconstructing what ConfigService had already returned as proper Java objects.
March 27, 4 AM: wip: remove regex-based parsing, move down one layer. The pivot. The old parser gets demoted to configparser.py (still used for the few corners where AdminTask is unavoidable), and activeconfig is born: move legacy wrapper in configparser, implement new object-oriented activeconfig wrapper.
48 hours later: extend activeconfig to a true object oriented ORM for configservice. March 31: finalise object oriented traversal. The string parser had been taking weeks of incremental fixes. The ConfigService-based replacement took a weekend.
The daemon showed up even earlier โ March 14: EXPERIMENTAL - add wsadmin server to speed things up dramatically. The next day: use the server for all tasks yippee.
What I builtยถ
I built a Jython library that hooks directly into ConfigService via AdminControl.invoke_jmx(), and on top of that, an Active Record-style ORM that makes WebSphere configuration objects behave like regular Python objects.
The core is activeconfig โ five files that give you .find(), .create(), .update(), .delete() on any WebSphere configuration type:
# Find a cluster
cluster = activeconfig.find('ServerCluster=PortalCluster')
# Read an attribute โ it's a Python attribute, not a string parse
print(cluster.name)
# Update attributes โ type conversion happens automatically
server.find('JavaVirtualMachine').update({'initialHeapSize': 2048, 'maximumHeapSize': 4096})
# Save and sync to all nodes
activeconfig.save()
Under the hood, find() calls ConfigService.resolve() via the JMX bridge to get the object’s ObjectName. It reads _Websphere_Config_Data_Type from the ObjectName to determine the type. It calls ConfigService.getAttributesMetaInfo() through the metadata introspection layer to learn what attributes exist and what their Java types are. It fetches all attributes via ConfigService.getAttributes() and converts them to Python types using the metadata. The result is a ConfigObject instance with attribute access.
When you call .update(), it converts your Python values back to Java types (again, driven by metadata), builds an AttributeList, and calls ConfigService.setAttributes(). References to other config objects? Automatically resolved to their ConfigDataId. Collections? Converted to java.util.ArrayList. Enums? Validated against the allowed values from the metadata. You never touch a string.
The dynamic model loading in __init__.py scans for specialized model files โ Security.py, Classloader.py, CacheInstance.py โ and auto-registers them. When you activeconfig.find('Security'), you get a Security instance with methods like jaas_alias() and ssl_config_for(), not a generic ConfigObject.
The daemonยถ
Every wsadmin script invocation boots a fresh JVM. That’s 2-3 seconds of startup before a single line of your script runs. When you’re applying 30 configuration changes from Ansible, that’s a minute and a half of staring at JVM boot messages.
server.py solves this. It’s a SocketServer.TCPServer that boots the JVM once, imports the entire activeconfig library, and then listens for script execution requests on a TCP port. The protocol is trivial: send script_name arg1 arg2\n, get the output back, connection closes.
The shell dispatcher (wsadmin-script.sh) checks if the daemon is running with nc -z. If it is, it routes through the daemon. If not, it falls back to a direct wsadmin invocation. The caller doesn’t know or care which path was taken.
Before each script execution, the daemon calls AdminConfig.reset() to clear pending session state. After execution, reset() again. Each script runs in a clean session. On unrecoverable errors, the daemon exits and systemd restarts it automatically.
The daemon has no authentication โ it’s meant to run on the DMGR host on an airgapped management VLAN. Ansible connects via SSH and runs wsadmin-script.sh as a regular command. The daemon is a transparent localhost optimization that Ansible isn’t even aware of.
The Ansible integrationยถ
Each of the 55 management scripts follows one rule: print output only when something changes. This maps directly to Ansible’s idempotency model:
- name: Ensure TLS 1.2 HIGH is configured
command: wsadmin-script.sh set-qop sslProtocol:TLSv1.2 securityLevel:HIGH
register: tls_setup
changed_when: tls_setup.stdout | length > 0
- name: Install CA certificate
command: wsadmin-script.sh add-cert signer my-ca /opt/certs/ca.crt CellDefaultTrustStore
register: cert_install
changed_when: cert_install.stdout | length > 0
- name: Set JVM heap
command: wsadmin-script.sh set-jvm WebSphere_Portal initialHeapSize:2048 maximumHeapSize:4096
register: jvm_heap
changed_when: jvm_heap.stdout | length > 0
First run: the script finds settings that differ, changes them, prints what changed โ Ansible reports “changed”. Second run: settings already match, no output โ Ansible reports “ok”. No custom Ansible modules, no check mode logic, no state files. The idempotency lives in the scripts themselves, where it belongs.
The scripts cover everything I need: JDBC providers and data sources, SSL certificates with fingerprint comparison to avoid reimporting, SIB messaging, JVM tuning, application deployment, shared libraries, admin role mapping, BPM configuration, and more.
The repoยถ
github.com/vjt/ansible-wsadmin โ MIT licensed.
The README has full documentation: which IBM APIs are called and where, the ORM architecture, the daemon protocol, all 55 scripts with links, Ansible examples, and Mermaid architecture diagrams.
The wsadmin problem: a historyยถ
The rest of this post is context. If you work with WebSphere โ or if you just enjoy watching an enterprise vendor make life unnecessarily difficult โ this is for you.
The timelineยถ
2002 โ WAS 5.0: The rewrite. IBM rewrites WebSphere from a common codebase. The database-backed configuration repository is replaced with XML files, managed by a Deployment Manager that replicates to nodes. wsadmin replaces the old WSCP tool. The JMX-based admin framework ships with two layers: ConfigService (the MBean โ typed Java objects, consistent interface) and AdminConfig/AdminControl/AdminApp (scripting wrappers โ everything is strings). The scripting layer targets JACL (Tcl-in-Java), which explains the string obsession: Tcl is a string-oriented language. But then they add Jython support in WAS 5.1 (2004), and the string API makes even less sense.
2006 โ WAS 6.1: AdminTask arrives. A fourth scripting object, designed to provide “task-oriented” convenience commands. Instead of making things simpler, it adds another layer of indirection with its own argument syntax: AdminTask.createSIBJMSQueue('...(cells/...)', '[-name Queue -jndiName jms/Queue -busName Bus]'). That’s a string containing key-value pairs inside brackets inside a string. IBM’s documentation for AdminTask is hundreds of pages of incantations like configuring session management. The admin console gains “Command Assistance” โ showing the wsadmin equivalent of console actions โ which only highlights how convoluted the commands are.
2008 โ WAS 7.0: Script Libraries. IBM ships pre-built Jython functions organized by category. A step in the right direction, but still built on the string-parsing AdminConfig layer underneath. A taste of the naming: IBM’s application update scripts include addSingleModuleFileToAnAppWithUpdateCommand, updateApplicationWithUpdateIgnoreOldOption, and addUpdateSingleModuleFileToAnAppWithUpdateCommand. My equivalent is app-deploy.py โ 80 lines, handles both install and update, compares versions, provides sensible defaults.
The community responseยถ
The telling part isn’t that the community built wrappers. It’s who built wrappers.
wsadminlib.py โ used internally at IBM for years, eventually moved to GitHub โ was created by IBM product developers to provide over 500 methods with intuitive names and parameters to replace complex AdminConfig commands. Let that sink in: IBM’s own engineers built a 500-method wrapper because the API they shipped needed replacing. The WASUG 2011 presentation by Rohit Kelapure walks through method after method designed to “hide syntax” โ the diplomatic way of saying the syntax shouldn’t be seen by humans.
WDR (WebSphere Deployment Robot) takes a different approach: it wraps AdminConfig output in Python objects so you can do jvm.initialHeapSize = 64 instead of AdminConfig.modify(jvm, [['initialHeapSize', '64']]). Their pitch is making wsadmin scripts “more Pythonic and readable” โ replacing AdminConfig.listConfigObjects('Node').splitlines() with a direct iterable. That this needs to be a selling point says everything about the baseline experience.
myarch.com documents the API with a candor that IBM’s official docs never achieve. Their description of AdminTask’s data structures as “truly baffling” and AdminApp.install()’s “ten thousand options” captures the community’s collective frustration. They built their own automation framework with a declarative DSL on top.
For Ansible specifically, there are amimof/ansible-websphere, ebasso/ansible-ibm-websphere, and BertRaeymaekers/ansible-was โ all shelling out to wsadmin scripts built on the AdminConfig/AdminTask layer.
The patternยถ
Every one of these projects โ wsadminlib, WDR, myarch, the Ansible modules โ works within the AdminConfig wrapper layer. They make AdminConfig’s output bearable by parsing the strings into data structures, by wrapping the calls in friendlier functions, by providing higher-level abstractions that hide the square brackets.
None of them go below the wrapper.
The approach I take with ansible-wsadmin is different: use AdminControl.invoke_jmx() to call ConfigService directly. Get back proper Java objects. Let ConfigService’s own metadata drive the type conversion. Build the ORM on top of the real API, not on top of the wrapper.
AdminConfig still has a role โ save(), getCurrentSession(), and reset() are session management primitives with no ConfigService equivalent. But for reading and writing configuration, it’s bypassed entirely. No strings. No parsing. No square brackets.
Further readingยถ
- IBM: ConfigService MBean documentation โ the official reference, which somehow manages to document AdminConfig without mentioning that ConfigService exists underneath
- IBM: ObjectName, Attribute, and AttributeList classes โ the typed Java objects that AdminConfig goes out of its way to hide from you
- Alvin Abad: Administering WebSphere Using JMX โ one of the few articles that shows how to bypass wsadmin entirely with direct JMX
- wsadminlib blog โ IBM engineers documenting their own escape from AdminConfig
- WDR documentation โ “Pythonic” wrapper over AdminConfig
- myarch.com: Getting Started with wsadmin โ the most honest introduction to the API you’ll find
- myarch.com: AdminControl vs AdminConfig โ understanding the architectural split