Skip to content

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

Merged
cigamit merged 12 commits into
ctrliq:mainfrom
blaipr:feat/drop-subscription
Sep 15, 2026
Merged

cigamit merged 12 commits into
ctrliq:mainfrom
blaipr:feat/drop-subscription

Conversation

@blaipr

@blaipr blaipr commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

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. It returns the open licence, always.

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 licence

  • The Licenser class, its certificate parsing and expiry arithmetic: licensing.py is 51 lines where it was 288.
  • POST and DELETE /api/v2/config/ answer 400 saying there is no licence, rather than vanishing into a bare 405.
  • The EULA, server_product_name, and the inventory source that chose redhat.satellite over theforeman.foreman.

The pages, which were already unreachable

The settings page redirects to /settings when the licence type is open, and the usage route is deleted unless SUBSCRIPTION_USAGE_MODEL is unique_managed_hosts, which defaults to empty. That is the point @cigamit made on the forum.

Gone: the Subscription settings screen and its manifest wizard, the Subscription Usage screen and its API model, the settings entry, route and breadcrumbs, and the gate that sent every route to the wizard when no licence was valid.

The enforcement

check_license and check_org_host_limit both opened by returning early on an open licence, so neither has done anything on a normal install. They are gone, and with them the validate_license parameter on seventeen can_start and can_add signatures.

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. An argument meaning "no data" was reading as "no licence check".

inventory_import held its own copies, including a remote_tower_license_compare only the tests called. license_error and org_host_limit_error stay on the model, since they are API surface, and are simply never set now.

The rest were questions 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, and the User-Agent naming the licence type. The last two keep the exact string they produced, so nothing on the wire moves.

The Red Hat analytics upload

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, the gather_analytics task and the collectors that only filled that payload go with them. config, counts, instance_info, job_counts and job_instance_counts still feed /api/v2/metrics.

Ten settings are unregistered, LICENSE, INSIGHTS_TRACKING_STATE and the four AUTOMATION_ANALYTICS_* among them, 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.

The config endpoint

license_info answered with four constants: valid_key and subscription_name meant nothing, license_type was always open, and product_name was a second brand string. eula was already an empty string nothing read. Both are gone from /api/v2/config/, and nothing replaces them.

product_name needed no new home. The brand already has a canonical source, BRAND_NAME in default.strings.json, which is the file a white-labelled install edits and which useBrandName() feeds to the About modal, the login page, the settings list and six other places. Only AppContainer read the licence blob instead, so its masthead labelled the logo Ascender while everything else said Ascender Automation. It uses the hook now.

With nothing left calling it, licensing.py is a constant and server_product_name(), which is all the X-API-Product-Name header ever needed. Licenser, OpenLicense, get_licenser and MAX_INSTANCES are gone.

Every remaining license_type comparison in the UI was dead, so three go with it:

  • Max Hosts on the two organisation screens. Both blocks were wrapped in license_type !== 'open', so neither has rendered for as long as this fork has existed, and removing them changes nothing a user can see. max_hosts stays on the model, in the serializer, in the form's payload so an edit round-trips it rather than clearing it, and enforced on bulk host create. Whether it should return as a visible control is a separate question.
  • The Pendo field in UI settings, disabled under an open licence, which the backend already refuses to change.
  • issuePendoIdentity wrote three values into license_info that nothing read, and fed Pendo planLevel, planPrice, creationDate and trial from top-level fields that only ever existed inside license_info, so they have been sending undefined since AWX.

AppContainer used !!config.license_info to decide the config had loaded, which made the app wait on a licence to render. It reads version now.

Host metrics is deliberately untouched: opening that up for everyone is a decision rather than a removal.

Follow-ups

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

Full Python suite green, ruff check, ruff format --check and yamllint clean, no missing migrations. Migration 0014 run against a database with every removed key planted deletes exactly those and leaves the rest. UI eslint, prettier, tsc, the catalogue check and both UI suites pass.

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.
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.
@cigamit cigamit self-assigned this Sep 15, 2026
@cigamit cigamit added Needs triage When a Issue needs to be researched or a PR has an issue that needs fixing before merging Refactor labels Sep 15, 2026
@blaipr
blaipr force-pushed the feat/drop-subscription branch from 52ea7ad to 2a077e8 Compare September 15, 2026 19:26
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.
…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.
It answered with four constants. valid_key and subscription_name meant
nothing, license_type was always open, and product_name was a second brand
string. eula was already an empty string nothing read. Both are gone from
/api/v2/config/, and nothing replaces them.

product_name needed no new home. The brand already has a canonical source,
BRAND_NAME in default.strings.json, which is the file a white-labelled install
edits and which useBrandName() feeds to the About modal, the login page, the
settings list and six other places. Only AppContainer read the licence blob
instead, so its masthead labelled the logo Ascender while everything else said
Ascender Automation. It uses the hook now, and takes its readiness from
version rather than from a licence being present.

Every remaining license_type comparison was dead. The two Max Hosts blocks on
the organisation screens were wrapped in != 'open', so neither has rendered
for as long as this fork has existed; max_hosts stays on the model, in the
serializer, and in the form's payload so an edit round-trips it rather than
clearing it, and stays enforced on bulk host create. The Pendo field was
disabled under an open licence, which the backend already refuses to change.
issuePendoIdentity wrote three values into license_info that nothing read, and
fed Pendo four fields that only ever existed inside license_info, so they have
been sending undefined since AWX.

With nothing left calling it, licensing.py is a constant and
server_product_name(), which is all the X-API-Product-Name header needed.

The browsable API's own page for this endpoint documented license_info, the
eula, a POST that installs a licence and a DELETE that removes one. It now
describes what the endpoint actually returns.
asgi.py and wsgi.py opened /var/lib/ascender/.tower_version in production and
refused to start if it was present and did not match the running version. That
is the same file get_licenser used to pick the subscription product, and it is
written by nothing in the Ascender repositories, so on a normal install the
check was a FileNotFoundError and on an install carried over from Tower it
could refuse to boot, citing a setup playbook that does not exist here.
Verified by planting a stale file: the application starts now.

make version_file wrote /var/lib/ascender/.ascender_version on every dev
container start and on make refresh. Nothing in this repository or in any of
the other ten reads it, and with the .tower_version readers gone the whole
mechanism was write-only, so the target and its two callers go too.

The module docstrings said WSGI and ASGI config for AWX project, which the
rename missed. The copyright headers stay: they are attribution for inherited
code rather than branding.
Four places still named one:

- RADIUSBackend and SAMLAuth carried "Custom backend to verify license status".
  Neither has ever verified a licence; each answers only when its own settings
  are configured, which is what the docstrings say now.
- The AUTHENTICATION_BACKENDS help text, which is shown in Settings, said the
  list follows "license features". There are none. It follows whether each
  backend has the settings it requires.
- The inventory import logged every PermissionDenied as a "License error". The
  licence checks that raised it are gone, so the label was wrong for whatever
  is left.
- docs/insights.md described the Insights credential type, the Insights project
  type and the proxied Insights API views, all removed in ctrliq#931, ctrliq#932 and ctrliq#933.
  Nothing it documents exists and nothing links to it.
The host filter lookup offered "Insights system ID", keyed on
insights_system_id. That field is not on the Host model, not in any serializer
and has no column in the database: it went with the Insights removal in ctrliq#931,
ctrliq#932 and ctrliq#933, and the filter entry did not. Choosing it sent the API a field
it rejects.

Broken on main today rather than by this branch, and included here because it
is the same Insights debris the rest of this pull request clears.
Four msgids leave with the screens that carried them: Max Hosts and its two
help texts from the organisation screens, and Insights system ID from the host
filter. No id changed, so no translation is orphaned.
The Insights credential type, the Insights project type and the Insights
integration went in ctrliq#931, ctrliq#932 and ctrliq#933. The user guide still described all
three as things to select, with screenshots of forms that no longer exist:
scm_type choices are '', git, svn and archive, and there is no Insights
credential type to pick.

Gone with them: the two screenshots nothing referenced any more, and the
notification parameter list, which named hg and insights as scm_type choices
and omitted archive, so it was wrong three ways rather than one.

awx-manage.rst documented gather_analytics --ship, a command this pull request
removes. The section keeps its other half, host_metric, which stays, and is
retitled for what it now covers.
MiscSystemDetail special-cased AUTOMATION_ANALYTICS_LAST_ENTRIES, rendering it
in a CodeDetail because the value was a JSON blob. That setting is unregistered
now, so the branch could never be taken and CodeDetail was its only use here.
@blaipr
blaipr force-pushed the feat/drop-subscription branch from 4d69b57 to 065dbf1 Compare September 15, 2026 21:32
The AUTHENTICATION_BACKENDS help text is part of the schema, so changing it
drifted api.generated.ts. ui-api-types-drift caught it, which is what that job
is for.

typed.py still declared LICENSE: dict after the setting was unregistered, so it
named a key that resolves nowhere.
@cigamit
cigamit merged commit e6905df into ctrliq:main Sep 15, 2026
14 checks passed
@blaipr
blaipr deleted the feat/drop-subscription branch September 15, 2026 22:09
blaipr added a commit to blaipr/ascender that referenced this pull request Sep 16, 2026
license_capacity and license_consumed were the compliance layer: capacity was
the entitlement and consumed the running total against it. ctrliq#1005 already left
capacity always zero, since its only source was the licence's instance_count,
and consumed measures a headroom that no longer exists.

Both columns go, with migration 0217. What stays is the part that was never
about a subscription: hosts_added and hosts_deleted, the per-month record of
which hosts an install first automated and which it deleted, and HostMetric
itself with its automation counts.

That removes the running total from the summary task, so _update_summary just
records the two monthly figures and _get_license_consumed_before goes with it.
The three help texts that named a licence now describe what the columns hold.

Verified by rolling the migration back on a database holding
license_consumed=42 and license_capacity=99, then forward again: the columns
drop and the row keeps its hosts_added and hosts_deleted.
blaipr added a commit to blaipr/ascender that referenced this pull request Sep 16, 2026
BulkHostCreateSerializer subtracted existing names belonging to organization 1
whatever organization the request was for, sitting next to an org_active_count
that scopes correctly. On any other organization a host that already existed
counted as new, so the limit could refuse a request that was inside it. The
test covers a non-1 organization and fails without the fix.

Raised by Copilot on this pull request, along with two others: the comment
above the imports in inventory_import named a DRF error class the file no
longer imports, and the organization host limit page described Max Hosts as a
control to set, when ctrliq#1005 removed that field from the web interface and left
max_hosts writable only through the API.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Needs triage When a Issue needs to be researched or a PR has an issue that needs fixing before merging Refactor

Development

Successfully merging this pull request may close these issues.

2 participants