Hi all,
While reading through the Ascender code I kept wanting one page that explains what Ascender is actually made of, so here it is: what runs on a node, what lives in which package, and which repository owns the rest. It is aimed at anyone new to the codebase who wants to land on the right file without grepping the whole thing. Corrections welcome, especially from anyone who knows the operator side better than I do.
Ascender nodes
A node is one of four types:
- control: runs the web and task trees and hands work out. No playbook ever runs here.
- execution: runs the playbooks in the execution environment. No Django at all, only receptor.
- hybrid: both of the above on one node, which is the model default and what a single node install is.
- hop: neither. It passes receptor traffic on, to reach nodes that are not directly routable.
What runs on a control node
Three supervisor trees, and the templates in tools/ansible/roles/dockerfile/templates/ say what is in each.
The web tree:
- nginx: TLS, the built UI, the static files, and routing to the two application servers behind it.
- uwsgi: the REST API, serving
awx.wsgi.
- daphne: websockets only, serving
awx.asgi.
- ws-heartbeat: announces that this node's websocket endpoint is alive.
- awx-cache-clear: listens on
tower_settings_change and drops the cached settings when one changes.
The task tree:
- dispatcher: runs the tasks. Every job, project update and system task starts in a worker here.
- callback-receiver: takes job events off the queue and bulk inserts them, at a volume nothing else here sees.
- wsrelay: forwards websocket messages between nodes, so a job on one node reaches a browser on another.
The rsyslog tree:
- rsyslogd: delivery to external log aggregators.
- rsyslog-configurer: rewrites the rsyslog config when a logging setting changes in the database.
Beside the three trees:
- receptor: carries work out to execution nodes, and the only way the control plane reaches them.
- PostgreSQL: the database, and through
LISTEN and NOTIFY the message bus the dispatcher runs on.
- Valkey: the channel layer behind the websockets, and the queue the callback receiver reads.
Path to ASGI is about collapsing the first two of the web tree into one.
The backend
Everything outside awx/ui is one Django project, reached through awx-manage and served as awx.wsgi and awx.asgi.
Built on, from requirements/requirements.in, apart from the three git installs in requirements/requirements_git.txt:
- Django: the ORM, the migrations, the request stack, and the settings everything else hangs off.
- Django REST framework: the serializers, the generic views and the browsable API the REST layer sits on.
- drf-spectacular: the OpenAPI 3 schema for the API, which replaced coreapi.
- channels and channels-valkey: the websocket side, speaking ASGI, with Valkey as the channel layer.
- django-polymorphic: what lets one query span the
UnifiedJob subclasses.
- django-oauth-toolkit: OAuth2 applications and the API tokens issued against them.
- social-auth-app-django: the Django side of the Google, GitHub, Azure AD and OIDC logins.
- social-auth-core: the backends themselves, the ones listed in
AUTHENTICATION_BACKENDS.
- django-auth-ldap: the LDAP backends, six of them, each configured on its own.
- django-radius: the RADIUS backend.
- django-guid: the correlation id, as an app and a middleware, threading one request through the logs.
- django-cors-headers: CORS.
- django-solo: the
SingletonModel that awx/main/models/ha.py uses for one-row state.
- django-split-settings:
include and optional, which is how production and development compose.
- django-pgware: PostgreSQL advisory locks, wrapped in
awx/main/utils/pglock.py.
- django-valkey: the
ValkeyCache that awx.main.cache.AWXValkeyCache extends.
- psycopg: the PostgreSQL driver, version 3.
- valkey: the client under both the cache and the queue the callback receiver reads.
- cryptography: what encrypts credentials at rest, and the TLS pieces around them.
- pyjwt: the token the
awx.dab.jwt_consumer app validates.
- python-ldap: what django-auth-ldap speaks to the directory with.
- tacacs-plus: the TACACS+ backend.
- python3-saml: the SAML implementation behind
awx.sso.backends.SAMLAuth, installed from git.
- certifi: ansible's system-certifi fork, installed from git beside ansible-runner.
- ansible-runner: the library that actually launches a playbook and streams its events back.
- receptorctl: the client side of the receptor mesh, for work that runs on another node.
- kubernetes: container groups, in
awx/main/scheduler/kubernetes.py.
- GitPython: project updates against a git repository.
- jinja2: templating, for custom notification messages among other things.
- jsonschema: validating the structured fields in
awx/main/fields.py.
- pyyaml: YAML wherever variables are read or written.
- requests: the HTTP client the credential plugins and most notification backends use.
- aiohttp: the websocket client
awx/main/wsrelay.py opens to the other nodes.
- psutil: the process and memory numbers the worker pool autoscales on.
- prometheus-client: the metrics endpoint.
- asciichartpy: the terminal plot the
graph_jobs command draws.
- boto3 and botocore: AWS, for the Secrets Manager plugin and the EC2 inventory source.
- azure-identity, azure-keyvault-secrets and msal: Azure Key Vault and Azure logins.
- pygithub: the GitHub App credential plugin.
- python-dsv-sdk and python-tss-sdk: the two Delinea secret servers.
- slack-sdk: the Slack notification backend, the one chat backend with a real SDK.
- uWSGI and daphne: the two application servers from the first section.
- pytest and pytest-django: the test suites, one
tests/ per Django app rather than one tree.
- pytest-asyncio, pytest-cov, pytest-mock, pytest-timeout, pytest-xdist: the plugins it runs with.
- ruff: the linter and formatter, run over the staged Python files by
pre-commit.sh.
- tox: the environments for the linters, the two type check runs, and the docs build.
- yamllint: the YAML checks, configured by
.yamllint at the root.
- django-debug-toolbar and django-test-migrations: development only, not in a production image.
Laid out as:
awx/api: the REST layer. Serializers, views, routes, permissions, filters, the browsable API and metrics.
awx/main: the application itself, broken down below.
awx/conf: the settings framework, which makes a setting database backed and editable in the UI.
awx/sso: the authentication integrations, LDAP and SAML and the OAuth2 social backends.
awx/dab: a vendored copy of django-ansible-base, with its own licence file and a VENDORED.md.
awx/settings: Django settings proper. defaults.py is where a new setting gets its default.
Inside awx/main
One Django app, holding the domain, the work, and the machinery that connects them:
models/: the domain. Jobs, inventories, projects, credentials, schedules, notifications, instances, RBAC.
tasks/: the work. The job runner, the receptor interface, fact caching, host metrics, system tasks.
dispatch/: the task bus. The pg_notify connection, the worker pool, the reaper and the periodic scheduler.
scheduler/: decides what runs next and on which instance, against capacity.
access.py: RBAC enforcement, one access class per model. The API layer does not decide permissions.
analytics/: the collectors behind insights and subscription usage.
notifications/: one backend per notification type.
credential_plugins/: lookups into external secret stores.
management/commands/: the awx-manage surface, including every service listed above.
consumers.py, routing.py, wsrelay.py: the websocket side.
A missing field is usually a serializer in awx/api, a permission surprise an access class in awx/main/access.py.
Pinned but not imported
From requirements/requirements.in: nothing under awx/ imports these, pinned for a CVE floor or to be present at all.
- twisted[tls] and pyopenssl: daphne's, both pinned directly beside it.
- sqlparse: Django's own dependency, pinned here too.
- Markdown: nothing under
awx/ imports it, but DRF reaches for it when formatting the API help.
Build toolchain
From requirements/requirements.in too. Used to build the image and compile the requirements, never at runtime:
- pip: installs everything, pinned exactly rather than floored, for CVE-2026-13346.
- setuptools: builds the sdist, pinned exactly for CVE-2026-59890.
- setuptools-scm: supplies the built version from
git describe, and xmlsec needs it to build.
- setuptools-rust: cryptography's build dependency, for the parts of it written in Rust.
- wheel: the built distribution format, floored for CVE-2026-24049.
- Cython: compiles PyYAML, which is why the old
<3 cap on it could go.
- pkgconfig: xmlsec's build dependency, and what an offline build needs to find its libraries.
Development extras
From requirements/requirements_dev.txt, none of it in a production image:
- ipython, jupyter, notebook: the interactive shells and the notebooks in
docs/.
- debugpy, remote-pdb, sdb: attaching a debugger to a running process.
- gprof2dot: turning a profile into a call graph.
- mockldap: a fake directory for the LDAP tests, installed from a tarball.
- unittest2, atomicwrites, logutils, backports.tempfile: older test helpers still pinned.
The frontend
awx/ui is the web application: static files served by nginx, talking to the same REST API as everything else.
Built on, from awx/ui/package.json:
- React: the whole UI is one single page application, with no server rendered templates behind it.
- PatternFly: the component library and the design system the screens are assembled from.
- react-router: the routing behind
routeConfig.js, and the most imported library here after React.
- CodeMirror 6: the editor behind the variables and YAML fields, as
@codemirror/* packages.
- lingui: the translation layer, with the catalogues extracted from the source.
- d3: the charts.
- @dagrejs/dagre: the graph layout behind the workflow visualiser.
- @tanstack/react-query: the cache every list read and every OPTIONS request goes through.
- @tanstack/react-virtual: virtualised rendering, so a long list stays responsive.
- luxon: dates and time zones.
- rrule: the recurrence rules behind schedules.
- js-yaml: parsing and dumping the variable fields.
- ansi-to-html and has-ansi: turning the playbook's colour codes into markup.
- html-entities: decoding entities in that same output.
- dompurify: sanitising anything that is rendered as HTML.
- react-error-boundary: catching a render error without taking the whole page down.
- vite and vitest: the build, and the unit tests that sit beside the code they cover.
- Playwright: the browser tests, in their own package under
e2e/.
Laid out as:
src/screens: one directory per screen, with src/routeConfig.js mapping the URLs onto them.
src/components: the shared widgets that every screen is built from.
src/api: the REST client, one model per endpoint, so no screen builds a URL by hand.
src/contexts, src/hooks, src/util: session and config state, the request hooks, the helpers.
src/locales: the message catalogues, one directory per language.
src/themes, customTheme.js: the Ascender look laid over the PatternFly base.
How a job runs
Every part above appears in one sequence, which is the shortest way to see how they fit:
- The API creates it. A launch request writes a job row and marks it pending. Nothing has run yet.
- The task manager picks a node.
scheduler/task_manager.py checks dependencies and capacity first.
- The dispatcher runs it. A pg_notify hands it to that node, and a worker starts
RunJob in tasks/jobs.py.
- receptor carries it out.
tasks/receptor.py submits the work, and ansible-runner runs the playbook in the EE.
- Events come back through Valkey.
tasks/callback.py pushes each event onto a list as the playbook runs.
- The callback receiver writes them down. It pops them, bulk inserts, and puts each one on the channel layer.
- The browser gets them. wsrelay carries it to the node that browser is connected to, and daphne delivers it.
Outside this repository
The rest of Ascender, one repository for each piece, all under ctrliq:
For what Ascender adds or replaces relative to AWX, rather than what it is made of: From AWX to Ascender.
Product documentation is at docs.ascender-automation.org, and docs/ covers clustering, credentials and the mesh.
To run it rather than read it, make docker-compose brings the stack up in containers, per CONTRIBUTING.md.
Anything above that is wrong, say so and I will fix it.