-
Notifications
You must be signed in to change notification settings - Fork 23
AAP-68113 [PENDING] Candle pin v2 #363
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
cshiels-ie
wants to merge
22
commits into
ansible:devel
Choose a base branch
from
cshiels-ie:candle-pin-v2
base: devel
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 12 commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
b0f959c
Add cryptography dependency and implement mTLS support in PackageCRC
cshiels-ie 3b5c983
Merge branch 'devel' into CRC-Changes
cshiels-ie cee5334
Merge branch 'devel' into CRC-Changes
cshiels-ie 6de5370
Refactor PackageCRC shipping configuration method and add comprehensi…
cshiels-ie 360d145
Enhance billing configuration and error handling in PackageCRC
cshiels-ie 6536acb
Refactor cleanup logic in PackageCRC to improve code readability
cshiels-ie d4f1f07
Add Candlepin client and lifecycle management for certificate operations
cshiels-ie 9fb7955
Refactor PackageCRC shipping method to utilize CandlepinClient for ce…
cshiels-ie 860dd61
Enhance Candlepin lifecycle management and validation logic
cshiels-ie 51599e8
Update proxy URL in CandlepinClient tests to use HTTPS
cshiels-ie 5422145
Add consumer registration functionality to CandlepinClient
cshiels-ie dc81e78
Refactor proxy handling in CandlepinClient initialization
cshiels-ie c96eeed
Update coverage exclusions in sonar-project.properties
cshiels-ie 70afc53
Refactor renewal days handling in lifecycle management
cshiels-ie 24e4774
Refactor proxy handling in CandlepinClient initialization
cshiels-ie 0bf77ea
Enhance CandlepinClient and validation logic for TLS verification
cshiels-ie def7e32
Add Candlepin management command and enhance validation logic
cshiels-ie f092a55
Refactor registration handling in Candlepin management command
cshiels-ie e6b8fb0
Merge branch 'devel' into candle-pin-v2
cshiels-ie 0681498
Merge branch 'devel' into candle-pin-v2
cshiels-ie 56bf837
Merge branch 'devel' into candle-pin-v2
cshiels-ie 58c971d
Merge branch 'devel' into candle-pin-v2
cshiels-ie File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,220 @@ | ||
| import os | ||
| import tempfile | ||
| import uuid as _uuid_mod | ||
|
|
||
| from datetime import datetime, timezone | ||
|
|
||
| import requests | ||
|
|
||
| from metrics_utility.logger import logger | ||
|
|
||
|
|
||
| class CandlepinClient: | ||
| """ | ||
| Minimal Candlepin REST client for certificate lifecycle operations. | ||
|
|
||
| All API calls authenticate with the consumer identity certificate (mTLS), | ||
| matching the pattern used by subscription-manager after initial registration. | ||
|
|
||
| Candlepin's own CA is not in the system trust store, so TLS server verification | ||
| defaults to False unless a CA path is provided via ``candlepin_ca``. | ||
| """ | ||
|
|
||
| DEFAULT_CANDLEPIN_URL = 'https://subscription.rhsm.redhat.com/subscription' | ||
|
|
||
| def __init__(self, base_url=None, candlepin_ca=None, proxy=None): | ||
| self.base_url = (base_url or self.DEFAULT_CANDLEPIN_URL).rstrip('/') | ||
| self.verify = candlepin_ca if candlepin_ca else False | ||
| if proxy: | ||
| # Normalise: strip any existing scheme, then prefix per-protocol. | ||
| host = proxy.split('://', 1)[-1] | ||
| self.proxies = {'https': f'https://{host}', 'http': f'http://{host}'} | ||
|
cursor[bot] marked this conversation as resolved.
Outdated
|
||
| else: | ||
| self.proxies = {} | ||
|
cshiels-ie marked this conversation as resolved.
|
||
|
|
||
| # ------------------------------------------------------------------ | ||
| # Public API | ||
| # ------------------------------------------------------------------ | ||
|
|
||
| def register_consumer(self, username, password, org, install_uuid=None): | ||
| """POST /consumers?owner={org} — register a new AAP consumer with basic auth. | ||
|
|
||
| Uses the customer's Red Hat subscription credentials (SUBSCRIPTIONS_USERNAME / | ||
| SUBSCRIPTIONS_PASSWORD from AWX conf_setting) to register this controller | ||
| instance as a Candlepin consumer and obtain an identity certificate for mTLS. | ||
|
|
||
| Args: | ||
| username: Red Hat subscription username (from SUBSCRIPTIONS_USERNAME). | ||
| password: Red Hat subscription password (from SUBSCRIPTIONS_PASSWORD). | ||
| org: Candlepin owner/org key (from LICENSE.account_number). | ||
| install_uuid: AWX INSTALL_UUID used as the consumer's aap.instance_uuid | ||
| fact; falls back to a random UUID if not provided. | ||
|
|
||
| Returns: | ||
| Tuple ``(cert_pem, key_pem, consumer_uuid)``. | ||
|
|
||
| Raises: | ||
| RuntimeError on any network or API failure. | ||
| """ | ||
| url = f'{self.base_url}/consumers' | ||
| instance_uuid = install_uuid or str(_uuid_mod.uuid4()) | ||
| payload = { | ||
| 'name': f'aap-{instance_uuid[:8]}', | ||
| 'type': {'label': 'aap'}, | ||
| 'facts': { | ||
| 'system.certificate_version': '3.3', | ||
| 'system.name': 'aap-controller', | ||
| 'aap.instance_uuid': instance_uuid, | ||
| }, | ||
| } | ||
| try: | ||
| resp = requests.post( | ||
| url, | ||
| params={'owner': org}, | ||
| auth=(username, password), | ||
| json=payload, | ||
| headers={'Content-Type': 'application/json'}, | ||
| verify=self.verify, | ||
| proxies=self.proxies, | ||
| timeout=120, | ||
| ) | ||
| except Exception as e: | ||
| raise RuntimeError(f'Candlepin register_consumer network error: {e}') from e | ||
|
|
||
| if not resp.ok: | ||
| raise RuntimeError(f'Candlepin register_consumer failed with status {resp.status_code}: {resp.text}') | ||
|
|
||
| try: | ||
| body = resp.json() | ||
| consumer_uuid = body.get('uuid') | ||
| id_cert = body.get('idCert', {}) | ||
| cert_pem = id_cert.get('cert') | ||
| key_pem = id_cert.get('key') | ||
| except Exception as e: | ||
| raise RuntimeError(f'Candlepin register_consumer: could not parse response JSON: {e}') from e | ||
|
|
||
| if not consumer_uuid or not cert_pem or not key_pem: | ||
| raise RuntimeError('Candlepin register_consumer: response missing uuid, idCert.cert or idCert.key') | ||
|
|
||
| logger.info(f'Candlepin consumer registered successfully (uuid={consumer_uuid})') | ||
| return cert_pem, key_pem, consumer_uuid | ||
|
|
||
| def checkin(self, consumer_uuid, cert_pem, key_pem): | ||
| """PUT /consumers/{uuid} — reset inactivity timer. | ||
|
|
||
| Best-effort: logs a warning on failure but never raises so that a | ||
| transient Candlepin outage cannot abort a gather run. | ||
|
|
||
| Returns True on success, False on any failure. | ||
| """ | ||
| url = f'{self.base_url}/consumers/{consumer_uuid}' | ||
| try: | ||
| with self._temp_cert_files(cert_pem, key_pem) as (cert_path, key_path): | ||
| resp = requests.put( | ||
| url, | ||
| cert=(cert_path, key_path), | ||
| json={'facts': {'aap.last_checkin': datetime.now(timezone.utc).isoformat()}}, | ||
| headers={'Content-Type': 'application/json'}, | ||
| verify=self.verify, | ||
| proxies=self.proxies, | ||
| timeout=30, | ||
| ) | ||
| if resp.status_code in (200, 204): | ||
| logger.info(f'Candlepin check-in successful for consumer {consumer_uuid}') | ||
| return True | ||
| logger.warning(f'Candlepin check-in returned unexpected status {resp.status_code} for consumer {consumer_uuid}') | ||
| return False | ||
| except Exception as e: | ||
| logger.warning(f'Candlepin check-in failed for consumer {consumer_uuid}: {e}') | ||
| return False | ||
|
|
||
| def regenerate_cert(self, consumer_uuid, cert_pem, key_pem): | ||
| """POST /consumers/{uuid} — regenerate the identity certificate. | ||
|
|
||
| Returns ``(new_cert_pem, new_key_pem)`` on success. | ||
| Raises ``RuntimeError`` on API or parsing failure so the caller can | ||
| decide whether to fall back to service-account auth. | ||
| """ | ||
| url = f'{self.base_url}/consumers/{consumer_uuid}' | ||
| with self._temp_cert_files(cert_pem, key_pem) as (cert_path, key_path): | ||
| try: | ||
| resp = requests.post( | ||
| url, | ||
| cert=(cert_path, key_path), | ||
| verify=self.verify, | ||
| proxies=self.proxies, | ||
| timeout=120, | ||
| ) | ||
| except Exception as e: | ||
| raise RuntimeError(f'Candlepin regenerate_cert network error for consumer {consumer_uuid}: {e}') from e | ||
|
|
||
| if not resp.ok: | ||
| raise RuntimeError(f'Candlepin regenerate_cert failed with status {resp.status_code} for consumer {consumer_uuid}: {resp.text}') | ||
|
|
||
| try: | ||
| body = resp.json() | ||
| id_cert = body.get('idCert', {}) | ||
| new_cert_pem = id_cert.get('cert') | ||
| new_key_pem = id_cert.get('key') | ||
| except Exception as e: | ||
| raise RuntimeError(f'Candlepin regenerate_cert: could not parse response JSON: {e}') from e | ||
|
|
||
| if not new_cert_pem or not new_key_pem: | ||
| raise RuntimeError(f'Candlepin regenerate_cert: response did not contain idCert.cert / idCert.key for consumer {consumer_uuid}') | ||
|
|
||
| logger.info(f'Candlepin cert regenerated successfully for consumer {consumer_uuid}') | ||
| return new_cert_pem, new_key_pem | ||
|
|
||
| # ------------------------------------------------------------------ | ||
| # Private helpers | ||
| # ------------------------------------------------------------------ | ||
|
|
||
| @staticmethod | ||
| def _write_temp_pem(content): | ||
| """Write PEM content to a secure temp file. Returns (fd, path).""" | ||
| fd, path = tempfile.mkstemp(prefix='candlepin_', suffix='.pem') | ||
| try: | ||
| os.chmod(path, 0o600) | ||
| with os.fdopen(fd, 'w') as f: | ||
| f.write(content) | ||
| fd = None | ||
| except Exception: | ||
| if fd is not None: | ||
| try: | ||
| os.close(fd) | ||
| except OSError: | ||
| pass | ||
| if os.path.exists(path): | ||
| os.unlink(path) | ||
| raise | ||
| return path | ||
|
|
||
| @staticmethod | ||
| def _unlink_safe(path): | ||
| try: | ||
| if path and os.path.exists(path): | ||
| os.unlink(path) | ||
| except OSError as e: | ||
| logger.warning(f'Could not remove temp cert file {path}: {e}') | ||
|
|
||
| class _temp_cert_files: | ||
| """Context manager: writes cert + key to secure temp files, cleans up on exit.""" | ||
|
|
||
| def __init__(self, cert_pem, key_pem): | ||
| self._cert_pem = cert_pem | ||
| self._key_pem = key_pem | ||
| self._cert_path = None | ||
| self._key_path = None | ||
|
|
||
| def __enter__(self): | ||
| self._cert_path = CandlepinClient._write_temp_pem(self._cert_pem) | ||
| try: | ||
| self._key_path = CandlepinClient._write_temp_pem(self._key_pem) | ||
| except Exception: | ||
| CandlepinClient._unlink_safe(self._cert_path) | ||
| raise | ||
| return self._cert_path, self._key_path | ||
|
|
||
| def __exit__(self, *_): | ||
| CandlepinClient._unlink_safe(self._cert_path) | ||
| CandlepinClient._unlink_safe(self._key_path) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.