Skip to content

feat: drop the subscription licence and the file that gated it - #1005

Open
blaipr wants to merge 49 commits into
ctrliq:mainfrom
blaipr:feat/drop-subscription
Open

blaipr wants to merge 49 commits into
ctrliq:mainfrom
blaipr:feat/drop-subscription

Conversation

@blaipr

@blaipr blaipr commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Stacked on #997 and #998, so the diff below carries their commits until those land. This change is the last four commits.

get_licenser chose between two answers by whether /var/lib/ascender/.tower_version existed: present meant the subscription product, an entitlement manifest to parse and a subscription to keep in date. Nothing in this ecosystem has ever written that file, so the only way to reach that branch was to carry it over from the Tower an install was migrated from, and Ascender has no subscription to check. It returns the open licence, always.

This follows #948, which stopped the remote calls to Red Hat. What was left was the local machinery and the pages in front of it.

The server

  • The Licenser class: entitlement certificate parsing, signature verification, expiry arithmetic. licensing.py is 51 lines where it was 288, and the cryptography, zipfile and date parsing imports go with it.
  • validate_entitlement_manifest, which only that class used.
  • POST /api/v2/config/, which took a manifest. The method stays and answers 400 saying there is no licence to install, rather than disappearing and leaving a client with a bare 405.
  • The EULA on the config endpoint, rendered only when the licence was not open, so never.
  • Two branches that asked which product this is and now cannot vary: server_product_name, and the inventory source that chose redhat.satellite over theforeman.foreman. Every install already took the second.

The UI

Both pages were already unreachable, which is the point @cigamit made on the forum. The settings page redirects to /settings when the licence type is open, which it now always is. The usage route is deleted from the route config unless SUBSCRIPTION_USAGE_MODEL is unique_managed_hosts, and the default is empty.

  • The Subscription settings screen, its detail view and its manifest upload wizard, which posted to the endpoint above.
  • The Subscription Usage screen and its chart, and the SubscriptionUsage API model.
  • The settings list entry, the settings route and its breadcrumbs.
  • The app level gate. useAuthorizedPath answered false when the licence had no valid key, which sent every route to /subscription_management and the wizard. It returns true now, and the branch that rendered the wizard is gone. The hook itself stays because AppContainer reads it for sidebar visibility.

Host metrics is deliberately untouched. That is the other half of the thread, and opening those up for everyone is a decision rather than a removal.

The enforcement

Two later commits, because removing the licence left more dead code behind it than the pages did.

check_license and check_org_host_limit in access.py both opened by returning early when the licence type was open, so neither has done anything on an install that was not carried over from Tower. They are gone, and with them the validate_license parameter that seventeen can_start and can_add signatures carried for callers wanting to skip the check.

That parameter had outlived its purpose in a way worth naming. check_related called can_access(type, 'start', resource, None) where the None was meant as the data argument can_change takes, and instead landed on validate_license. A positional argument meaning "no data" was silently reading as "no licence check".

inventory_import held its own copies, including a remote_tower_license_compare that only the test suite ever called, and the try/except around its atomic block existed only to turn a licence PermissionDenied into a flag on the inventory update. license_error and org_host_limit_error stay on the model and in the serializers, since they are API surface, and are simply never set now.

The rest were call sites asking the licenser a question with one answer: the scheduler refusing to spawn a job when the probe raised, the bulk host create counting free instances after it had already enforced the organisation limit, the webhook status context choosing between ansible/awx and ansible/tower, and the User-Agent naming the licence type. The last two keep the exact string they produced, so nothing on the wire moves. PENDO_TRACKING_STATE stays read-only, which is what the open licence already made it.

LICENSE is unregistered, which takes with it the read-only case in the settings serializer, the key it was skipped under on update, the exclusion on destroy and its place in the set deciding when the authentication backends default is recomputed. DELETE /api/v2/config/ used to write it back as an empty dict and now answers the way POST does.

The Red Hat analytics upload

The other half of Insights. Nineteen /api/v2/analytics/ routes proxied to console.redhat.com and authenticated with the subscription credentials, so with those gone every one answers an error. The routes go, the gather_analytics task that shipped the payload goes, and the collectors that only existed to fill it go with them. What stays is the part that was never about Red Hat: config, counts, instance_info, job_counts and job_instance_counts still feed /api/v2/metrics, and config() keeps everything except the twenty licence fields it carried.

Nine settings went with it, INSIGHTS_TRACKING_STATE and the four AUTOMATION_ANALYTICS_* among them. Conf migration 0014 deletes the rows for all ten removed keys, so a Red Hat username and password do not sit in an upgraded database with no screen left to clear them from.

What does not move

The API shape. get_licenser().validate() still returns license_type: open with a valid key, which is what the config view reads and what the UI reads out of license_info.

Follow-ups in the other repositories

Both call routes this removes: ascender-collection#309 drops the license and subscriptions modules and the license role, and ascender-kit#74 drops the config properties reading fields the endpoint no longer returns.

Checked

The Python suite passes whole: 4,192 passing, 6 skipped, across functional, unit, api, conf and sso. ruff check, ruff format --check and yamllint clean, and makemigrations --check reports no missing migrations. Migration 0014 was run against a database with all ten keys planted: it deletes exactly those ten and leaves the rest.

UI eslint, prettier, tsc and the catalogue check are clean, and both UI suites pass: 181 general files and 368 screen files. api.generated.ts and the nine message catalogues are regenerated, the latter dropping 62 strings that belonged to the removed screens.

ctrliq#984 renamed the eleven settings the database registers. Seventeen more were
left, because they are not registered: thirteen are defined in defaults.py and
overridden in a settings file, and four are read from the environment.

The thirteen take the same route as ctrliq#984: renamed at the definition and every
use, with the old name added to the _FORMER_NAMES table so a conf.d file that
writes AWX_NOTIFICATION_REQUEST_TIMEOUT still lands on the current name.

The four from the environment could not, and this is the part worth reading.
That table runs after the settings modules have loaded, and AWX_SETTINGS_FILE
and AWX_SETTINGS_DIR decide which settings file loads at all, so an alias
applied afterwards would be too late to matter. They read both names directly
instead, through one helper, in the same shape as the ASCENDER_ variables
ascender-kit took in its ctrliq#72.

Nothing has to move to the new names. AWX_SETTINGS_FILE is what the Dockerfile
passes to collectstatic, AWX_LOGGING_MODE is what the three installers write
into their compose environments, and AWX_WEB_PROCESS is what supervisor sets.
Those keep working for as long as they exist rather than for a release.

A test asserts that no settings module reads one of the four with a bare
os.environ.get, since that would answer to one name only, which is the failure
the helper exists to prevent. Both directions are checked against a real
production settings load: the old environment name still selects the settings
file, and a file written with the old setting name still carries over.
The package on disk is ascender/ now, and awx/ is the alias. Same machinery as
one module object.

    >>> import ascender.main.models, awx.main.models
    >>> awx.main.models is ascender.main.models
    True

The old name cannot be dropped, and not only for a release. The
awx.credential_plugins entry points live in other people's packages and cannot
be rewritten from here. The dispatcher is the one that cannot wait for anything
to move: task names travel as strings and a worker imports the path it was
sent, so a message queued before an upgrade and read after it names
awx.main.tasks.system.delete_inventory and has to resolve.

What moved: 2,035 import statements, the module paths in INSTALLED_APPS, the
app configs, the settings module, the middleware and filter class paths, the
periodic task paths, the mock targets in the tests, and the packaging, the
Makefile, the Dockerfile, tox, CI and the documentation paths.

What deliberately did not move, because each is a published name rather than an
import path:

- The logger names. Every one of the 155 is identical to main, checked by
  extracting them from both trees and comparing. The five loggers that derived
  their name from __name__ are now named explicitly, or they would have slipped
  out from under the LOGGING keys that configure them.
- LOGGER_BLOCKLIST, a tuple of those logger names. Moving it stopped awx.conf
  records being blocked, so the level filter read LOG_AGGREGATOR_LEVEL for each
  one and preloaded the settings cache from the database: the suite went from
  seventeen seconds to fourteen minutes.
- CREDENTIAL_PLUGIN_GROUPS, where awx.credential_plugins is the group a third
  party declares itself under. Moving it left both entries naming the same
  group, so nothing outside this repository would have loaded at all.
- The distribution name in pyproject, still awx, so dist/awx.tar.gz and every
  installer that names it are untouched.
- The venv at /var/lib/awx/venv, the image layout, and the container names.

Django app labels are unaffected: a label is the last component of the app
config's name, so awx.main and ascender.main are both `main`, and no migration
or content type changes.
The alias already exists. The Dockerfile creates /var/lib/ascender as a symlink
onto /var/lib/awx in both image stages, and the platform's own settings have
read and written through it for a while: STATIC_ROOT, the projects root and the
logging spool all name it already. What lagged behind was the build tooling,
which still spelled the old path.

Moved: the version file and the collected UI static in the Makefile, the working
directory of every supervisor program, the config watcher's hash file, and the
directory tree the support bundle collects.

Left on /var/lib/awx deliberately:

- VENV_BASE and everything under venv/awx. The virtualenv is built by
  make requirements_awx, which the Dockerfile runs before it creates the
  symlink, and virtualenv_awx does mkdir $(VENV_BASE) rather than mkdir -p, so
  pointing it at the alias breaks the image build.
- The compose volume mounts, and everything in the Dockerfile that creates,
  copies or chowns the directory or declares the VOLUME. Those own the real
  path, and a mount landing on /var/lib/ascender would replace the symlink with
  a directory.
- HOME and the passwd entries written at startup, which match ENV HOME.
The previous commit moved the callers onto /var/lib/ascender while the data
still sat in /var/lib/awx. That leaves the retirement of the old name as a move
of everything on disk. Pointed the other way round it is one deleted symlink,
so the image now builds the directory as /var/lib/ascender and links
/var/lib/awx onto it.

Nothing has to move for this to be safe, in either direction:

- A deployment still mounting a volume at /var/lib/awx keeps working. runc
  resolves the link inside the container and lands the mount on the directory it
  names, verified here with both a bind mount and a named volume. Even if a
  runtime did create a directory over the link instead, the operator's configmap
  names /var/lib/awx for PROJECTS_ROOT, STATIC_ROOT and JOBOUTPUT_ROOT, so it
  would read the mount either way.
- VENV_BASE can move with it. virtualenv_awx runs before the link exists, but
  its mkdir has always failed harmlessly on a missing parent and python -m venv
  creates the tree with makedirs. That is as true of /var/lib/awx today as it is
  of /var/lib/ascender now, checked by running the sequence in the base image.

The CI step that asserts both names reach the same inode is unchanged and still
passes, since it tests the link rather than its direction.
The two composite actions were still named for AWX, and so were a handful of
things around them:

- .github/actions/awx_devel_image is now ascender_devel_image, and
  run_awx_devel is run_ascender_devel. All five uses: references and both
  dependabot directories entries move with them, since a composite action is
  only picked up per directory.
- The devel image matrix names ascender_devel, which is what it publishes.
- The admin password step calls ascender-manage rather than awx-manage. The
  alias is in the image either way, so CI may as well exercise the name we
  ship.
- Two comments pointed at awx/settings/defaults.py, which moved.

What deliberately stays: the tools_awx_1 container name, the awx-uvicorn
supervisor program, and the awx_image and awx_version variables the build
playbooks take. None of those are CI's own names, they are the compose service,
the supervisor config and the ansible roles, and renaming them from here would
leave the Makefile and the docs pointing at something that no longer exists.
Three names the package rename left behind, none of which is a published
identifier: nothing outside imports them and nothing refers to them as a string.

- AWXBaseEmailBackend, the base every notification backend subclasses. Only
  ever imported as a class, never named as a settings path, so there is no
  EMAIL_BACKEND string anywhere to keep working.
- get_awx_http_client_headers, which builds the User-Agent the notification
  backends and analytics send.
- get_awx_version, used by the API version response, the analytics collectors
  and the UI context processor.

The rsyslog template moves with them, from template(name="awx") to
template(name="ascender"). That one is safe because it is declared and
referenced inside the config this file generates, so the name never leaves.

The queue file name does not move, and now says why in a comment. It is the
spool file on disk, so renaming it would start a new queue at upgrade and
strand whatever was still spooled in the old one, losing those records without
logging anything to say so.
AWXLogin becomes AscenderLogin, with its props interface and its test export.
The component is internal to the Login screen and nothing outside the two files
refers to it.

Two neighbouring names were checked and deliberately left, each now saying so in
place, because both are strings a deployment can be holding:

- The awx-* class names the UI renders. CUSTOM_THEME is an administrator's own
  CSS file, applied to the live UI, and those class names are the only stable
  hooks a theme has to target. Renaming the 177 of them would stop every theme
  already uploaded from applying, with no error and nothing in the logs.
- AWXModelBackend. AUTHENTICATION_BACKENDS names it by dotted path rather than
  importing it, and the path is offered as a choice in the authentication
  settings, so a deployment that has set it has the old string stored.

Both want a deprecation rather than a rename, which is a decision rather than a
mechanical change.
The compose service, its container and its hostname were awx_1 and
tools_awx_1, which is the name a developer types all day and the one every doc
page uses. They are ascender_1 and tools_ascender_1 now, along with the init
container, and everything that addresses them follows: the Makefile, CI, the
six docs pages, the smoke test, the haproxy backends and the l18n script.

Two of those had to move together with the hostname or the environment would
come up broken rather than merely misnamed:

- The receptor node id, its firewall rule and its tcp-peer address, which are
  the hostname by another name. The hop node's peer address as well.
- The peer registration in bootstrap_development.sh, which writes the names
  itself in a shell loop rather than reading them from the compose file.

The volume names stay: tools_awx_db_15 and var_lib_awx hold the development
database and the projects directory, and renaming them hands every developer an
empty environment instead of a renamed one.

Worth knowing on upgrade: the hostname is what provision_instance registers, so
an existing environment has an instance called awx_1 that the new container will
not answer to. A fresh environment is unaffected, and an existing one wants
make docker-compose-container-group-clean or a new instance registration.
The tree a developer edits was mounted at /awx_devel, which is the path in the
Dockerfile, the compose file, both supervisor configs, the launch scripts, the
Makefile, CI and the docs. It is /ascender_devel now.

The old path stays as an alias, because the mount comes from outside the image
and this repository does not own every mount that names it: the operator mounts
/awx_devel in its kube-dev deployments, and anyone can have a compose override
of their own. The dev image creates /ascender_devel and links /awx_devel onto
it, so a deployment still naming the old path has its mount resolved onto the
real directory. Verified both directions with a real mount.

The alias is dev only. Nothing mounts a source tree into the production image,
so there is no empty directory shipped there.

The provenance comments in requirements.txt move with it. pip-compile writes
the path it ran under, so this is what requirements/updater.sh produces on its
next run rather than an edit that will drift.
What the directory rename left saying AWX, none of it reaching outside the
process:

- The private settings attributes and cache keys, _awx_conf_settings,
  _awx_conf_version, _awx_conf_memoizedcache, _awx_conf_preload_expires and
  _awx_conf_init_readonly. Two of those are written to the shared settings
  cache, so an upgrade leaves the old keys behind, but they carry a TTL and the
  only effect of missing them is one preload.
- The make target awx-link, and the six scripts that call it.
- The placeholder setting names the conf tests register, AWX_SOME_SETTING and
  its relatives, which exist only inside those tests.
- Ninety one source paths quoted in comments and docstrings, which named
  awx/api/serializers.py and its neighbours after the files had moved.

The paths were rewritten only where they name this tree. A path inside a
github.com/ansible/awx URL is upstream's, ~/.awx is the directory an old
install left behind, and venv/awx is the virtualenv's own name, which
ascender-install addresses by absolute path from another repository.
The comments, docstrings and documentation that describe what this code does
still called it AWX. They say Ascender now, along with one user facing string:
the label on the default subscription model, which the settings API hands to
the UI.

The pass was deliberately narrow, because most of what is left is not prose:

- A line naming upstream keeps the upstream name. docs/rbac.md distinguishes
  the original AWX role system from the django-ansible-base one, SEARCH.md is
  about the AngularJS UI that predates the fork, and the data migration note
  names AWX 18.0, which is a version that exists.
- kind: AWX in the install guide stays, because that is the resource the
  operator serves. Writing Ascender there documents something that does not
  exist yet.
- The toleration value in the clustering guide stays. It is a Kubernetes value
  matched against a node taint, not a sentence.

Two things had to move with the prose or they would have broken quietly. The
markdown anchors follow their headings, so the in page links were rewritten and
every one of them checked to resolve. And the subscription label lives in
conf.py, so the generated API types were regenerated from it rather than edited,
which is what the drift check compares.
The production configs had already dropped it for most of them, so nginx,
uvicorn, dispatcher, callback-receiver, wsrelay and ws-heartbeat were bare there
while the development config still spelled every one awx-. The four that kept
the prefix on both sides, rsyslogd, rsyslog-configurer, cache-clear and
autoreload, lose it too, and the development names now match the production ones
rather than being a second set.

Nothing outside this repository names a program. The operator runs supervisorctl
status and counts what is not RUNNING, never naming one, so the config and the
code that restarts by name ship in the same image and move together. Both places
that name one were moved with it: the rsyslog restart after a logging settings
change, and the 4xx recovery script that matches on processname. All four groups
are still tower-processes, so the group qualified name that code builds
resolves in every config.

The autoreload script is renamed on disk to match its program, along with the
make target and the three supervisor templates that invoke it.

/var/run/awx-receptor keeps its name, unlike the program that writes into it.
ascender-install mounts a volume there and passes it as RECEPTORCTL_SOCKET from
its own compose file, so the directory is not ours alone to rename.
The directory holding the receptor control socket keeps the same shape as the
data directory and the development tree: the real path takes the Ascender name
and the old one becomes an alias onto it, so retiring it later is a deleted
symlink rather than a coordinated move.

The alias is needed rather than tidy. ascender-install mounts a named volume at
/var/run/awx-receptor and passes that path as RECEPTORCTL_SOCKET from its own
compose file, so the old name has to keep resolving until that repository moves.
Checked with a named volume mounted at the old path: the socket written through
it appears at the new path, and the symlink survives the mount, so receptorctl
on the old name and the platform on the new one are looking at one file.
Every message in the eight Django catalogues and the UI ones carries a #: line
naming the file and line it was extracted from, and all 17,573 of them still
said awx/. They are not translations and nothing reads them at runtime, but they
are what a translator follows to find the string in context, and they now point
at files that are not there.

Rewritten rather than regenerated, and only on the #: lines. The files moved
without their contents changing, so the line numbers still hold and this is
exactly what makemessages writes on its next run. Regenerating instead would
have reflowed entries and churned the msgids along with them, which is a much
larger diff for the same result.

Checked with msgfmt. The Django catalogues all parse clean. The UI ones report
duplicate definitions, which they did before this change as well: lingui
tolerates them where msgfmt does not.
The only translatable strings left naming AWX were the description at the API
root, which the browsable API prints at the top of /api/, and the provisioning
callback help text on a job template. Both name the product to the person
reading them.

Changing a msgid costs the translations of that string until the catalogues are
regenerated, which is why this is two strings and not a sweep. Nothing asserts
either one, and the documentation already called it the Ascender REST API, so
the API now agrees with its own reference manual.
A Prometheus metric name is a contract. A dashboard, a recording rule and an
alert all key on it, so renaming one breaks them with nothing to say why: the
panel goes empty and the alert stops firing. The thirty one metrics at
/api/v2/metrics are ascender_ now, and every one of them is served a second time
under its former awx_ name, so nothing that scrapes this has to change on the
day of the upgrade. The old names can go once the dashboards have moved.

The duplication is done on the exposition text, matching the metric name only
where it can appear: the start of a HELP line, a TYPE line, or a sample. A label
value that happens to contain the prefix is left alone, which a test covers.

The internal keys move without a second name, because both ends of each are in
this repository and nothing outside reads them: the subsystem metrics hash in
Valkey, the dispatcher and callback receiver statistics keys, and the broadcast
websocket gauges that run_wsrelay reads back for its status output. An upgrade
leaves the old keys behind to expire, and the only effect is one window of
subsystem metrics while the new ones fill.

test_metrics now asserts what it should: that every family appears under both
names, and that the two carry the same value rather than being two readings.
All 1,029 of them, across the stylesheets that define them and the components
that carry them.

I had left these alone on the assumption that a custom theme would target them,
and that was worth checking rather than assuming. None of the four shipped
themes references a single one: they scope to html[data-theme="..."] and style
PatternFly classes and element ids, which is the shape the CUSTOM_THEME help
text asks an administrator to follow. The comment that said otherwise is
corrected in place.

Checked that the rename kept every class paired with its definition: one class
defined and never referenced, thirteen referenced and never defined, and both
counts are exactly what they were before the rename. Nothing outside
ascender/ui/src referred to one, so there is nothing left dangling.

Two things the pattern caught that are not class names, both put back:
awx-manage in the generated types header, where the source header file moved
with it so the drift check still compares equal, and the execution environment
image in the UI fixtures, which had become quay.io/ansible/ascender-ee. That
registry path does not exist; the fixtures now name the image the product
actually ships, ghcr.io/ctrliq/ascender-ee.

The theme whose id is awx keeps it. That id names a look rather than the
product, and it is stored per user in their browser, so renaming it would reset
the theme of anyone who had chosen it.
A job's private data goes in /tmp/awx_42_xiwm, and that prefix is swept by the
cleanup task and read by ascender-kit. It is ascender_ now, and both of those
had to move with it rather than after it.

The cleanup sweeps both prefixes, one pass each. A single glob cannot spell
them: they share only their first letter, and a pattern loose enough to match
both matches a great deal else in /tmp, which is not a thing to be casual about
in a routine whose job is deleting directories. So the pattern is built from
whichever prefix the pass is for, and the callers run it once per prefix, both
locally and on each execution node. A folder left by a release that used the old
name is still rubbish, and would otherwise sit there forever.

The exclusion list carries both names for every active job regardless of which
pass is running, so a job that is still going is never swept by the pass looking
for the other prefix.

ascender-kit asserts the prefix when it reads a job's artifacts, and needs the
matching change. Its own comment already points back at this constant.
The last AWX-branded name a user's own browser holds. The two places that fall
back to a literal when the setting is missing move with it, and so does the
documentation that quotes the cookie in a Set-Cookie example.

This one is visible on upgrade and should be in the release notes: a browser
holding awx_sessionid presents a name the server no longer reads, so everyone is
logged out once. It is a login rather than a loss, and only on the upgrade that
carries this change, but nobody should meet it by surprise.

A deployment that set SESSION_COOKIE_NAME in its own settings file keeps its
value, and a client has never needed to assume either name: the API returns
X-API-Session-Cookie-Name on a successful login for exactly this reason, which
the settings comment now says out loud.
The recorded job event payloads the UI tests compare against still carried
/tmp/awx_<id>_ paths, which is a shape the product no longer produces, and so
did the receptor cleanup CLI test and one docstring.

Fixtures are evidence of what the server sends, so leaving them on the old name
would have them quietly describing a release that no longer exists.
…ames

AWXProfiler and AWXProfileBase, which the callback worker and the timing
middleware instantiate, and the awx_host variable the keycloak, LDAP, tacacs and
vault plumbing playbooks pass to the collection lookups. All internal: nothing
outside this repository imports the classes or sets the variable.

The dispatcher test's fixture hostnames move with them, and the metrics guide
stops using awx_host as the example server name.

test_fields.py keeps awx_secret and AWX_SECRET. They look like brand references
and are not: that test feeds names to the credential injector schema validation
and pairs each with the answer it should get, so renaming them changes what is
being tested rather than what it is called. Four of its cases failed on the
first attempt, which is what makes the point.
awx_image, awx_image_tag, awx_version, awx_docker_version and awx_official are
the variables the build playbook and the compose sources role take, set by the
Makefile and by the release and devel image workflows. All of them are this
repository's own, read only by tools/ansible and tools/docker-compose, so they
move together with nothing outside to keep in step.

The job extra vars of the same name are a different thing and stay: awx_version
and awx_license_type are passed into the project update playbook, not read by
the build.

Two image defaults were wrong and are fixed here rather than left. The release
build defaulted to ansible/awx, which is upstream's image rather than the one
this repository produces, and it is ctrliq/ascender now. The compose default had
become ghcr.io/ansible/ascender_devel, which is a registry path that does not
exist: my earlier rename of the /awx_devel mount point matched inside the image
name. It is ghcr.io/ctrliq/ascender_devel, which is what CI builds and pushes.
awx.example.org, awxhost and https://awx across the OAuth2, credential plugin,
credential type, notification parameter and tips pages. They are placeholders a
reader substitutes, so they should read as this product rather than the one it
came from.
Sixty five files under common/images, configure-awx-*, the splunk and loggly
logging examples, the gitlab webhook status shot, the released image
verification shot and the hop node topology diagram, along with every page that
embeds one.

Counted the image references before and after rather than trusting the sed: 546
both times, nought unresolvable both times. The first pass left one broken,
instances_awx_task_pods_hopnode, because the file rename and the reference
rewrite used different patterns, which is exactly what counting catches.
virtualenv_awx, requirements_awx and requirements_awx_dev, the four awx-kube
build targets, the buildx builder names they create, and the kube_devel
variable. All of them are this repository's own entry points, called by the
Dockerfile, CI and the docs, which move with them.

MANAGEMENT_COMMAND now defaults to ascender-manage. The image has always
installed both names, so the Makefile may as well drive the one we ship; a
caller overriding it keeps whatever they set.

One stale path in a comment: the locale directories were described as living
under awx/locale.

Checked by asking make to resolve each renamed target rather than by reading:
all eight, plus the two the Dockerfile calls.
Three places told the outside world it was AWX: the User-Agent on every
notification and analytics request, the X-API-Product-Name response header by
way of server_product_name, and the product_name in the open licence. The
browsable API's error page title as well.

The names are constants now rather than a literal repeated at each site, because
one of those sites was not printing the name but comparing against it:
inventory.py decided whether to use the downstream collection namespace with
server_product_name() != 'AWX'. Changing what the function returns without that
line would not have failed, it would have quietly taken the other branch on
every inventory update. It now asks the question it means, against
OPEN_PRODUCT_NAME.

About.tsx keeps its indexOf('AWX') check, deliberately. BRAND_NAME already ships
as "Ascender Automation" so that branch is already dead in practice, and
changing it would alter what the About box prints rather than what anything is
called. That is product copy and wants deciding rather than renaming.
About.tsx chose its wording with brandName.indexOf('AWX'), which was true when
the brand was AWX and has been false ever since BRAND_NAME became "Ascender
Automation". So the branch meant for the subscription product was the one
running, and the box has been printing "Ascender Automation Controller 25.6.3"
on an install that is not a Controller.

It checks for Ascender now, which puts each product back on its own branch:

  Ascender Automation                  -> Ascender Automation 25.6.3
  Red Hat Ansible Automation Platform  -> Red Hat Ansible Automation Platform Controller 25.6.3

The brand fixtures across the UI tests move to the name the product actually
ships rather than staying on AWX, and the one assertion paired with a fixture,
the login logo's alt text, moves with its fixture so the test still checks that
the two agree.
Every caller in this repository now runs ascender-manage: the bootstrap script,
the Makefile, the supervisor programs, the launch scripts, the migration wait,
the smoke test, the minikube bootstrap and the support bundle. The wrapper in
tools/docker-compose is renamed to match.

What deliberately does not move is the alias itself. Both console scripts stay
declared, both /usr/bin symlinks are still created in the production image and
both /usr/local/bin ones in the development image, and the builder still calls
the old name for collectstatic. The installers and the operator exec awx-manage
by absolute path, so it has to keep working until they move; this changes who
calls which name, not which names exist.

Also here: the SQL profiling cache key, which is set and read in this repository
alone, the remaining instance hostnames in the dispatcher fixtures, and prose in
the receptor cleanup docstring and the named URL script.

test_dispatch keeps its awx.main.tasks.system.delete_inventory assertions. That
test exists to prove a task name queued before the rename still resolves after
it, so the old prefix is the thing under test.
tools/scripts/awx-python is the wrapper that runs the platform's virtualenv
python, installed on PATH and used as the shebang by the management command
wrapper, the rsyslog recovery script and firehose. It is ascender-python now,
and the three shebangs move with it.

Both names are installed in both images. The rename is safe inside this
repository because every shebang moved with the file, but a shebang is the kind
of line someone copies into a script of their own, so the old name is a symlink
onto the new one rather than gone.

Rendered both Dockerfiles to check each name is actually installed rather than
reading the template: the first attempt renamed the source path and left the
destination, which would have left every one of those shebangs pointing at a
file that is not there.
The package on disk has been ascender since the rename, but the distribution
pip installs was still called awx, which is the name in the metadata, in the
egg-info directory, in the sdist filename and in the entry point lookup.

Built it and installed it rather than assuming: the sdist comes out as
ascender-25.6.3.tar.gz, the metadata reads ascender, and both console scripts
are still declared and still land on PATH, which is what keeps awx-manage
working.

Two things keep the old name so that nothing outside has to move first:

- The version lookup asks for ascender and then for awx. An environment
  installed by an earlier release carries the old metadata, and without the
  second attempt it would fall through to reading the version out of git, which
  an installed tree does not have. All three paths are checked, including which
  name is asked for and in what order.
- make sdist still leaves a dist/awx.tar.gz symlink beside dist/ascender.tar.gz.
  ascender-install pip installs that exact path from its own repository, so the
  old one stays until that moves.
The Ascender credential type sets the same five values under three names, and
the Ascender ones were last, under the CONTROLLER_ block and the TOWER_ block it
inherited.

Nothing changes at runtime: all three are set either way. What changes is which
name reads as the one that matters and which blocks are the ones to delete. The
CONTROLLER_ and TOWER_ groups go when nothing reads them any more, and having
them below rather than above means removing them leaves the file as it should
end up rather than needing a reshuffle at the same time.

This is the shape everywhere else already: the task name prefixes, the
credential plugin entry point groups, the job folder prefixes, the metric
prefixes and the settings environment lookup all put Ascender first with the old
name behind it, and the settings alias table gives the Ascender name precedence
when both are written down.
The group every platform process belongs to was tower-processes, which is the
name the code builds a qualified process name against, the name CI restarts the
web process through, and the name the config watcher matches on.

Self-contained. The operator runs supervisorctl status and counts what is not
RUNNING without naming a group, so the four configs that declare it and the
three places that qualify against it all ship in the same image and move
together. The recorded job environment fixtures carry SUPERVISOR_GROUP_NAME, so
they move with it too.
/etc/tower holds the settings files and /var/log/tower the logs, and they are
/etc/ascender and /var/log/ascender now, with the Tower names as aliases onto
them. Same shape as the data directory, the development tree and the receptor
socket: the real directory takes the Ascender name so retiring the old one is a
deleted symlink.

The aliases are needed rather than tidy. ascender-install names /etc/tower
thirty seven times and the operator thirty eight, mounting individual files at
/etc/tower/SECRET_KEY, /etc/tower/settings.py and /etc/tower/conf.d/*.py. A file
mounted through a symlinked parent is a different case from the directory mounts
the other aliases rely on, so it was checked rather than assumed: both a
SECRET_KEY and a conf.d file mounted at the old paths land in /etc/ascender and
the symlink survives the mount.

production.py keeps naming /etc/tower in its two defaults. Those lines are
rewritten by ctrliq#998 and resolve through the alias in the meantime, so they move
when that lands rather than conflicting with it now.

One thing worth naming, because I did it: the sweep that moved the tooling paths
also rewrote the two ln -s lines that create the aliases, leaving symlinks
pointing at themselves. Every symlink the image creates is now checked for that.
The classmethod that registers the managed credential types is
setup_managed_defaults now. The brand in the middle of it had already stopped
matching the field it sets: managed_by_tower became managed some releases ago.

Thirteen migrations call it, which is what made this worth checking rather than
assuming. A migration that has already run will not run again, but a fresh
install runs all of them, so a missed call site is a install that dies at
migrate time rather than a test that goes red. Ran every migration against an
empty database: all applied, and the thirty one managed credential types those
migrations exist to create are there afterwards.

Renamed rather than aliased for that reason too. An alias would have hidden a
missed call site instead of surfacing it, and the run above is what proves there
are none.

The migration tables in ascender-install and ascender-pro-install's
awx_migrate_ascender READMEs name the old method in prose. They describe what
each historical migration does, so they want the same edit when those move.
…file

Three more, none of them a name anything outside resolves:

- TowerSAMLIdentityProvider, which the SAML backend instantiates directly rather
  than naming by path, so nothing has the old string stored.
- The tower_warnings log handler, which ten loggers route through. Checked that
  every handler a logger names still exists after the rename, and that Django
  configures the logging dictionary without complaint.
- The test method names in the secret key regeneration tests.

The handler's file moves with it, tower.log to ascender.log, and the RBAC
migration log alongside it. That one is visible: an administrator tailing
/var/log/tower/tower.log, or a log shipper globbing for it, finds nothing after
the upgrade. The directory still resolves through its alias but the file inside
it has a new name, so this belongs in the release notes with the session cookie.

TowerSettings is deliberately untouched. It only appears in a frozen conf
migration, resolved through apps.get_model against historical state, and that
model has not existed for many releases.
The setting identifying a cluster to an external log aggregator is
LOG_AGGREGATOR_ASCENDER_UUID now.

It is a registered setting, so the value lives in the database under its key
rather than only in a file, and renaming it in the code alone would leave
whatever an administrator set behind under a name nothing reads. This follows
the path TOWER_URL_BASE took: a conf migration moving the row with
_rename_setting, and an entry in the former names table so a deployment that
spells the old name in /etc/ascender/conf.d keeps being understood.

Checked by planting a value under the old key, running the migration, and
reading it back under the new one. A full migration from an empty database also
runs clean, which is the thing a fresh install does.

The UI fixtures, the generated API types and the logging guide move with it, and
the generated settings types are still in step, which its own test asserts.
A custom credential type's injectors are a Jinja template the administrator
writes, and a file injector references the file it wrote as {{tower.filename}}.
That template is stored in the database and cannot be rewritten from here, so
the namespace could not simply be renamed: every credential type written before
this would stop resolving, and a job would fail at injection time with an
undefined variable rather than anything naming the cause.

Both names are bound to the same object instead, at both the render site and the
validator, from one tuple in constants. {{ascender.filename}} is what the
documentation now shows, {{tower.filename}} keeps working, and a field may not
be called either or it would shadow the namespace.

A parametrized test renders a file injector under each name and reads the file
back, which is the behaviour this exists for and nothing covered before.

The reserved-name error message names the field it rejected rather than assuming
which of the two it was.
The scheduler's singleton was named for Tower, and a model name is a table name,
so this needed saying in a migration as well as in the class. Without the
RenameModel the table stays main_towerschedulestate while Django starts looking
for main_ascenderschedulestate, and the scheduler falls over on its next run.

Checked the way a rename should be: makemigrations --check reports no drift, so
the migration accounts for the whole change; then a full migrate against an
empty database applies it, the table in postgres is main_ascenderschedulestate,
and get_solo returns the singleton from it.

The squashed 0004 migration keeps the old name, because that is what the model
was called at that point in the history it describes.
api-lint runs ruff format --check, and ascender is longer than the names it
replaced, so five lines that fitted before no longer do. Formatted with the
version CI pins, ruff 0.16.6, rather than whatever is to hand: the API root
response dict, a setting registration in the conf tests, the execution node
generator in the cleanup task, a cleanup kwargs assertion, and a blank line in
the licensing module.

No behaviour in it, only line breaks. ruff check and yamllint -s . both pass.
The inventory script endpoint takes ?towervars=1 to add the platform's own
variables to each host. That is a published query parameter: it appears in
someone's script or bookmark and cannot be rewritten from here, so the view
reads the Ascender spelling first and falls back to the old one.

Everything behind the query string is internal and renamed outright: the keyword
argument through get_script_data, the inventory import comments and the callers
in the job tasks.

A parametrized functional test asks the endpoint under each spelling and checks
the variables come back, which is the compatibility this exists for.
tower_broadcast_all and tower_settings_change are the postgres LISTEN/NOTIFY
channels the dispatcher and the cache clearer subscribe to. They are
ascender_broadcast_all and ascender_settings_change now, named once in constants
rather than spelled at each site.

A NOTIFY reaches whoever is listening at that moment and is otherwise dropped,
which is what shapes this. Both listeners subscribe to the old name as well as
the new one, so during a rolling upgrade a node still on the old release
publishes to tower_broadcast_all and is still heard. Publishers write to the
Ascender name only: the reverse direction, a new node broadcasting to an old
one, cannot be covered from here, and the traffic is settings invalidation and
receptor config, so the worst of it is a node holding a stale setting until its
cache expires.

The old names are the ones to delete once no release in the field publishes to
them, which is why they are constants beside the current pair rather than
literals.
Two things CI caught.

prettier reflowed fifteen files, all of it line wrapping: ascender- class names
are longer than the awx- ones they replaced, so JSX that fitted on one line no
longer does. Run at the version package.json pins.

And AppContainer's about modal test still expected "<  AWX 222  >" in the speech
bubble. It is the second test covering that box, which I missed when fixing the
first: its own fixture already says the brand is Ascender Automation, so the
assertion was checking against a name nothing in the test provides.
…version

The analytics collectors and the metrics payload reported the release under
tower_version. That is a field name in data leaving the product, so a consumer
keys on it: both names are sent now, Ascender first, and the old one can go when
nothing reads it.

/var/lib/ascender/.tower_version keeps its name, and now says why in place. Its
presence is what tells get_licenser this is the subscription product rather than
the open one, and nothing in this ecosystem writes it: an install that has the
file got it from the Tower it was migrated from. Renaming the check would move
such an install onto the open licence without a word, which is the opposite of
what a rebrand should do.

Worth noting since it was not deliberate: the build variable sweep a few commits
back also renamed the file make version_file writes, from .awx_version to
.ascender_version. Nothing reads that one, in this repository or any of the
others, so it is write-only either way, and the new name is the right one.
…nto feat/drop-subscription

# Conflicts:
#	ascender/main/notifications/pagerduty_backend.py
#	ascender/main/notifications/twilio_backend.py
#	ascender/main/tests/unit/settings/test_environment_names.py
#	ascender/main/tests/unit/settings/test_logging_mode.py
#	ascender/settings/environment.py
get_licenser chose between two answers by whether /var/lib/ascender/.tower_version
existed: present meant the subscription product, an entitlement manifest to
parse and a subscription to keep in date. Nothing in this ecosystem has ever
written that file, so the only way to reach that branch was to carry it over
from the Tower an install was migrated from, and Ascender has no subscription to
check. It returns the open licence, always.

What goes with it:

- The Licenser class, its entitlement certificate parsing, its signature
  verification and its expiry arithmetic. licensing.py is 76 lines where it was
  288, and the cryptography, zipfile and date parsing imports go with it.
- validate_entitlement_manifest, which only that class used.
- POST to /api/v2/config/, which took a manifest. The method stays and answers
  400 saying there is no licence to install, rather than disappearing and
  leaving a client with a bare 405.
- The EULA on the config endpoint, which was rendered only when the licence was
  not open, so never.
- Two branches that asked which product this is and now cannot vary:
  server_product_name, and the inventory source that chose redhat.satellite over
  theforeman.foreman. Every install already took the second one.

The API shape does not move. get_licenser().validate() is still what the config
view, the access checks, the bulk serializer and the host metrics read, and it
still returns license_type open with a valid key.

test_tasks patched Licenser.validate to keep the project update off the network;
it patches OpenLicense.validate now, which is the class that method is on.
…nd them

The previous commit took away the manifest endpoint and made the licence always
open. That left the UI for it standing: a Subscription settings page whose
wizard posted a manifest to an endpoint that now answers 400, and a Subscription
Usage graph reading a usage model nothing sets.

Both were already unreachable, which is the point cigamit made on the forum
thread. The settings page redirects to /settings when the licence type is open,
which it now always is. The usage route and the host metrics route are both
deleted from the route config unless SUBSCRIPTION_USAGE_MODEL is
unique_managed_hosts, and the default is empty. So this removes pages nobody
could open, rather than taking anything away.

Gone: the Subscription settings screen and its detail and edit wizard, the
Subscription Usage screen and its chart, the SubscriptionUsage API model, the
settings list entry, the settings route and its breadcrumbs.

The app level gate goes with them. useAuthorizedPath answered false when the
licence had no valid key, which sent every route to /subscription_management and
the wizard. The open licence is always valid and there is no wizard to send
anyone to, so it returns true, and the branch that rendered the wizard is gone.
The hook stays rather than being deleted because AppContainer reads it for
sidebar visibility.

Host metrics is deliberately untouched. cigamit said those are useful and might
be opened up for everyone, which is a decision rather than a removal.

Checked: type check, eslint and prettier clean, and the UI suite passes whole,
549 files and 3,050 tests.
The nineteen /api/v2/analytics/ routes proxied to console.redhat.com and
authenticated with the subscription credentials, which PR 1005 has already
taken out. Without them every one of those endpoints answers an error, so
they go, along with the gather_analytics task that shipped the payload and
the collectors that only existed to fill it.

What stays is the part that was never about Red Hat: the config, counts,
instance_info, job_counts and job_instance_counts collectors still feed
/api/v2/metrics, and config() keeps everything except the twenty licence
fields it used to carry.

The nine settings that configured the upload are unregistered, and conf
migration 0014 deletes their rows, so a Red Hat username and password do
not sit in an upgraded database with no screen left to clear them from.

test_secret_key_regeneration exercised encryption through REDHAT_PASSWORD;
it now uses LOG_AGGREGATOR_PASSWORD, which is the other encrypted setting
and tests the same path.
blaipr added a commit to blaipr/ascender-collection that referenced this pull request Sep 15, 2026
…ence role

The license module posted a Red Hat subscription manifest to /api/v2/config/
and attached a pool through config/attach. The platform now answers that POST
with an error, and config/attach has never had a route in ctrliq/ascender at
all, so the pool_id path was broken before this and the manifest path is broken
as of ctrliq/ascender#1005.

subscriptions read what a Red Hat or Satellite account was entitled to through
config/subscriptions, another route the platform no longer serves, and the
license role existed only to call the two of them.

get_stats read awx_license_instance_total and awx_license_instance_free, the
two gauges that counted entitlements, and registered a settings request only to
print a LICENSE setting. All three are gone from the platform, so the playbook
stops asking for them.
…skipped

check_license and check_org_host_limit in access.py both opened by returning
early when the licence type was open, which it always is, so neither has done
anything on an install that was not carried over from Tower. They are gone, and
with them the validate_license parameter that seventeen can_start and can_add
signatures carried for callers that wanted to skip the check.

That parameter had outlived its purpose in a way worth naming: check_related
called can_access(type, 'start', resource, None) where the None was meant as
the data argument that can_change takes, and instead landed on
validate_license. A positional argument meaning "no data" was silently reading
as "no licence check".

inventory_import held its own copies, including a remote_tower_license_compare
that nothing but the test suite ever called, and the try/except around the
atomic block existed only to turn a licence PermissionDenied into a flag on the
inventory update. license_error and org_host_limit_error stay on the model and
in the serializers, since they are API surface, and are simply never set now.

The rest are call sites that asked the licenser a question with one answer: the
scheduler refusing to spawn a job when the probe raised, the bulk host create
counting free instances after it had already enforced the organisation limit,
the webhook status context choosing between ansible/awx and ansible/tower, and
the User-Agent that named the licence type. The last two keep the string they
produced, WEBHOOK_STATUS_CONTEXT and a literal open, so nothing on the wire
moves. PENDO_TRACKING_STATE stays read-only, which is what the open licence
already made it.

The LICENSE setting is unregistered and conf migration 0014 deletes its row,
which takes with it the read-only special case in the settings serializer, the
key it was skipped under on update, the exclusion on destroy, and its place in
the set that decides when the authentication backends default is recomputed.
DELETE /api/v2/config/ used to write it back as an empty dict; it now answers
the way POST does.

The sosreport plugin stops running ascender-manage check_license, the two
sections of docs/tasks.md describing tasks that no longer exist go with it, and
the Miscellaneous System settings screen drops the code editor it rendered
AUTOMATION_ANALYTICS_LAST_ENTRIES in.

get_licenser().validate() is unchanged, and the config endpoint still serves
license_info, which is what the UI reads.
cigamit pushed a commit to ctrliq/ascender-kit that referenced this pull request Sep 15, 2026
Config carried six properties reading fields the config endpoint no longer
returns: valid_key, instance_count, trial, features, and a license_type that
is only ever open. is_aws_license read metadata a Tower AMI put there. None of
them can answer anything but a default now, which is worse than not being
there, because a caller reading is_valid_license gets False rather than an
error telling it the question no longer applies.

The subscriptions page posted to config/subscriptions/ and ConfigAttach posted
to config/attach/. Neither route exists in ctrliq/ascender: config/subscriptions
went with ctrliq/ascender#1005 and config/attach was never served at all.

page.py sniffed response bodies for eleven licence phrases to decide between
LicenseInvalid, LicenseExceeded and the ordinary Forbidden or BadRequest. The
platform does not emit any of those phrases any more, so every response takes
the else branch, and the two exception classes go with the sniffing.
cigamit pushed a commit to ctrliq/ascender-collection that referenced this pull request Sep 15, 2026
…ence role (#309)

* feat: remove the licence module, the subscriptions module and the licence role

The license module posted a Red Hat subscription manifest to /api/v2/config/
and attached a pool through config/attach. The platform now answers that POST
with an error, and config/attach has never had a route in ctrliq/ascender at
all, so the pool_id path was broken before this and the manifest path is broken
as of ctrliq/ascender#1005.

subscriptions read what a Red Hat or Satellite account was entitled to through
config/subscriptions, another route the platform no longer serves, and the
license role existed only to call the two of them.

get_stats read awx_license_instance_total and awx_license_instance_free, the
two gauges that counted entitlements, and registered a settings request only to
print a LICENSE setting. All three are gone from the platform, so the playbook
stops asking for them.

* ci: name the test database credentials rather than inheriting them

The unit job started a PostgreSQL service as awx/awxpass and relied on
settings_for_test in the ascender checkout falling back to the same names when
no AWX_TEST_DATABASE_* was set. Those fallbacks moved to ascender/ascenderpass
with the package rename, so the job has been failing on main with a password
authentication error that reads as a broken test rather than a renamed default.

Naming them in the step decouples this workflow from what the platform happens
to default to.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant