diff --git a/README.md b/README.md index d78d441..53eda3f 100644 --- a/README.md +++ b/README.md @@ -20,9 +20,9 @@ pip install . ``` # Configuration -The `manifester_settings.yaml` file is used to configure manifester via [DynaConf](https://github.com/rochacbruno/dynaconf/). +The `manifester_settings.yaml` file is used to configure manifester via [DynaConf](https://github.com/rochacbruno/dynaconf/). -Multiple types of manifests can be configured in the `manifest_category` section of `manifester_settings.yaml`. These types can be differentiated based on the Satellite version of the subscription allocation, the names and quantities of the subscriptions to be added to the manifest, and whether [Simple Content Access](https://access.redhat.com/documentation/en-us/subscription_central/2021/html-single/getting_started_with_simple_content_access/index) is enabled on the manifest. +Multiple types of manifests can be configured in the `manifest_category` section of `manifester_settings.yaml`. These types can be differentiated based on the Satellite version of the subscription allocation, the names and quantities of the subscriptions to be added to the manifest, and whether [Simple Content Access](https://access.redhat.com/documentation/en-us/subscription_central/2021/html-single/getting_started_with_simple_content_access/index) is enabled on the manifest. The value of the `name` setting for each subscription in a manifest must exactly match the name of a subscription available in the account which was used to generate the offline token. One method for determining the subscription names available in an account is to register a system to RHSM and then run `subscription manager list --available` on that system. A planned future feature of Manifester is a CLI command that will return a list of available subscriptions. @@ -32,4 +32,4 @@ Currently, the only action supported by the manifester CLI is generating a manif ``` manifester get-manifest --manifest-category --allocation_name ``` - Two options are available for this subcommand. The `--manifest_category` option is required and must match one of the manifest categories defined in `manifester_settings.yaml`. The `--allocation_name` option specifies the name of the subscription allocation in RHSM and is also used in the file name of the manifest archive exported by Manifester. If no value is supplied for `--allocation_name`, a string of 10 random alphabetic characters will be used for the allocation name. + Two options are available for this subcommand. The `--manifest_category` option is required and must match one of the manifest categories defined in `manifester_settings.yaml`. The `--allocation_name` option specifies the name of the subscription allocation in RHSM and is also used in the file name of the manifest archive exported by Manifester. If no value is supplied for `--allocation_name`, a string of 10 random alphabetic characters will be used for the allocation name. diff --git a/manifester/commands.py b/manifester/commands.py index b11c47a..258c561 100644 --- a/manifester/commands.py +++ b/manifester/commands.py @@ -1,9 +1,8 @@ import click -from manifester import logger as mlog -from manifester.settings import settings from manifester import Manifester + # To do: add a command for returning subscription pools @click.group def cli(): @@ -11,8 +10,14 @@ def cli(): @cli.command() -@click.option("--manifest_category", type=str, help="Category of manifest (golden_ticket or robottelo_automation by default)") -@click.option("--allocation_name", type=str, help="Name of upstream subscription allocation") +@click.option( + "--manifest_category", + type=str, + help="Category of manifest (golden_ticket or robottelo_automation by default)", +) +@click.option( + "--allocation_name", type=str, help="Name of upstream subscription allocation" +) def get_manifest(manifest_category, allocation_name): manifester = Manifester(manifest_category, allocation_name) manifester.create_subscription_allocation() diff --git a/manifester/helpers.py b/manifester/helpers.py index 5849297..08b492c 100644 --- a/manifester/helpers.py +++ b/manifester/helpers.py @@ -9,7 +9,7 @@ def simple_retry(cmd, cmd_args=None, cmd_kwargs=None, max_timeout=240, _cur_time cmd_kwargs = cmd_kwargs if cmd_kwargs else {} # If additional debug information is needed, the following log entry can be modified to # include the data being passed by adding {cmd_kwargs=} to the f-string. Please do so - # with caution as some data (notably the offline token) should be treated as a secret. + # with caution as some data (notably the offline token) should be treated as a secret. logger.debug(f"Sending request to endpoint {cmd_args}") response = cmd(*cmd_args, **cmd_kwargs) logger.debug(f"Response status code is {response.status_code}") diff --git a/manifester/manifester.py b/manifester/manifester.py index accf53e..47277c4 100644 --- a/manifester/manifester.py +++ b/manifester/manifester.py @@ -12,17 +12,23 @@ class Manifester: def __init__(self, manifest_category, allocation_name=None, **kwargs): - self.allocation_name = allocation_name or "".join(random.sample(string.ascii_letters, 10)) - self.offline_token = kwargs.get("offline_token", settings.offline_token) - manifest_data = settings.manifest_category.get(manifest_category) - self.subscription_data = manifest_data.subscription_data - self.sat_version = kwargs.get("sat_version", manifest_data.sat_version) + self.allocation_name = allocation_name or "".join( + random.sample(string.ascii_letters, 10) + ) + self.manifest_data = settings.manifest_category.get(manifest_category) + self.offline_token = kwargs.get( + "offline_token", self.manifest_data.get("offline_token", settings.offline_token) + ) + self.subscription_data = self.manifest_data.subscription_data + self.sat_version = kwargs.get("sat_version", self.manifest_data.sat_version) self.token_request_data = { "grant_type": "refresh_token", "client_id": "rhsm-api", "refresh_token": self.offline_token, } - self.simple_content_access = kwargs.get("simple_content_access", manifest_data.simple_content_access) + self.simple_content_access = kwargs.get( + "simple_content_access", self.manifest_data.simple_content_access + ) self._access_token = None self._subscription_pools = None @@ -33,7 +39,7 @@ def access_token(self): logger.debug("Generating access token") token_data = simple_retry( requests.post, - cmd_args=["https://sso.redhat.com/auth/realms/redhat-external/protocol/openid-connect/token"], + cmd_args=[f"{self.manifest_data.url.token_request}"], cmd_kwargs=token_request_data, ).json() self._access_token = token_data["access_token"] @@ -42,6 +48,7 @@ def access_token(self): def create_subscription_allocation(self): allocation_data = { "headers": {"Authorization": f"Bearer {self.access_token}"}, + "proxies": self.manifest_data.get("proxies", settings.proxies), "params": { "name": f"{self.allocation_name}", "version": f"{self.sat_version}", @@ -50,12 +57,13 @@ def create_subscription_allocation(self): } self.allocation = simple_retry( requests.post, - cmd_args=["https://api.access.redhat.com/management/v1/allocations"], + cmd_args=[f"{self.manifest_data.url.allocations}"], cmd_kwargs=allocation_data, ).json() self.allocation_uuid = self.allocation["body"]["uuid"] logger.info( - f"Subscription allocation created with name {self.allocation_name} and UUID {self.allocation_uuid}" + f"Subscription allocation created with name {self.allocation_name} " + f"and UUID {self.allocation_uuid}" ) return self.allocation_uuid @@ -65,81 +73,116 @@ def subscription_pools(self): _offset = 0 data = { "headers": {"Authorization": f"Bearer {self.access_token}"}, + "proxies": self.manifest_data.get("proxies", settings.proxies), "params": {"offset": _offset}, } self._subscription_pools = simple_retry( requests.get, - cmd_args=[f"https://api.access.redhat.com/management/v1/allocations/{self.allocation_uuid}/pools"], + cmd_args=[ + f"{self.manifest_data.url.allocations}/{self.allocation_uuid}/pools" + ], cmd_kwargs=data, ).json() _results = len(self._subscription_pools["body"]) - # The endpoint used in the above API call can return a maximum of 50 subscription pools. For - # organizations with more than 50 subscription pools, the loop below works around this limit by - # repeating calls with a progressively larger value for the `offset` parameter. + # The endpoint used in the above API call can return a maximum of 50 subscription pools. + # For organizations with more than 50 subscription pools, the loop below works around + # this limit by repeating calls with a progressively larger value for the `offset` + # parameter. while _results == 50: _offset += 50 - logger.debug(f"Fetching additional subscription pools with an offset of {_offset}.") + logger.debug( + f"Fetching additional subscription pools with an offset of {_offset}." + ) data = { "headers": {"Authorization": f"Bearer {self.access_token}"}, + "proxies": self.manifest_data.get("proxies", settings.proxies), "params": {"offset": _offset}, } offset_pools = simple_retry( requests.get, - cmd_args=[f"https://api.access.redhat.com/management/v1/allocations/{self.allocation_uuid}/pools"], + cmd_args=[ + f"{self.manifest_data.url.allocations}/{self.allocation_uuid}/pools" + ], cmd_kwargs=data, ).json() self._subscription_pools["body"] += offset_pools["body"] _results = len(offset_pools["body"]) total_pools = len(self._subscription_pools["body"]) - logger.debug(f"Total subscription pools available for this allocation: {total_pools}") + logger.debug( + f"Total subscription pools available for this allocation: {total_pools}" + ) return self._subscription_pools def add_entitlements_to_allocation(self, pool_id, entitlement_quantity): data = { "headers": {"Authorization": f"Bearer {self.access_token}"}, + "proxies": self.manifest_data.get("proxies", settings.proxies), "params": {"pool": f"{pool_id}", "quantity": f"{entitlement_quantity}"}, } add_entitlements = simple_retry( requests.post, - cmd_args=[f"https://api.access.redhat.com/management/v1/allocations/{self.allocation_uuid}/entitlements"], + cmd_args=[ + f"{self.manifest_data.url.allocations}/{self.allocation_uuid}/entitlements" + ], cmd_kwargs=data, ) return add_entitlements def verify_allocation_entitlements(self, entitlement_quantity, subscription_name): - logger.info(f"Verifying the entitlement quantity of {subscription_name} on the allocation.") + logger.info( + f"Verifying the entitlement quantity of {subscription_name} on the allocation." + ) data = { "headers": {"Authorization": f"Bearer {self.access_token}"}, + "proxies": self.manifest_data.get("proxies", settings.proxies), "params": {"include": "entitlements"}, } self.entitlement_data = simple_retry( requests.get, - cmd_args=[f"https://api.access.redhat.com/management/v1/allocations/{self.allocation_uuid}"], + cmd_args=[f"{self.manifest_data.url.allocation}/{self.allocation_uuid}"], cmd_kwargs=data, ).json() - current_entitlement = [d for d in self.entitlement_data["body"]["entitlementsAttached"]["value"] if d["subscriptionName"] == subscription_name] + current_entitlement = [ + d + for d in self.entitlement_data["body"]["entitlementsAttached"]["value"] + if d["subscriptionName"] == subscription_name + ] if not current_entitlement: return logger.debug(f"Current entitlement is {current_entitlement}") self.attached_quantity = current_entitlement[0]["entitlementQuantity"] if self.attached_quantity == entitlement_quantity: - logger.debug(f"Operation successful. Attached {self.attached_quantity} entitlements.") + logger.debug( + f"Operation successful. Attached {self.attached_quantity} entitlements." + ) return True elif self.attached_quantity < entitlement_quantity: - logger.debug(f"{self.attached_quantity} of {entitlement_quantity} attached. Trying again.") + logger.debug( + f"{self.attached_quantity} of {entitlement_quantity} attached. Trying again." + ) return else: - logger.warning(f"Something went wrong. Attached quantity {self.attached_quantity} is greater than requested quantity {entitlement_quantity}.") + logger.warning( + f"Something went wrong. Attached quantity {self.attached_quantity} is greater than " + f"requested quantity {entitlement_quantity}." + ) return True def process_subscription_pools(self, subscription_pools, subscription_data): logger.debug(f"Finding a matching pool for {subscription_data['name']}.") - matching = [d for d in subscription_pools["body"] if d["subscriptionName"] == subscription_data["name"]] - logger.debug(f"The following pools are matches for this subscription: {matching}") + matching = [ + d + for d in subscription_pools["body"] + if d["subscriptionName"] == subscription_data["name"] + ] + logger.debug( + f"The following pools are matches for this subscription: {matching}" + ) for match in matching: if match["entitlementsAvailable"] > subscription_data["quantity"]: logger.debug( - f"Pool {match['id']} is a match for this subscription and has {match['entitlementsAvailable']} entitlements available." + f"Pool {match['id']} is a match for this subscription and has " + f"{match['entitlementsAvailable']} entitlements available." ) add_entitlements = self.add_entitlements_to_allocation( pool_id=match["id"], @@ -156,61 +199,80 @@ def process_subscription_pools(self, subscription_pools, subscription_data): # If no entitlements of a given subscription are # attached, refresh the pools and try again if not self.attached_quantity: - self._subscription_pools=None + self._subscription_pools = None # self.subscription_pools self.process_subscription_pools( subscription_pools=self.subscription_pools, - subscription_data=subscription_data + subscription_data=subscription_data, ) # If non-zero but insufficient entitlements are # attached, find the difference between the # attached quantity and the desired quantity, refresh # the pools, and try again else: - logger.debug(f"Received response status {add_entitlements.status_code}. Trying to find another pool.") - self._subscription_pools=None + logger.debug( + f"Received response status {add_entitlements.status_code}." + "Trying to find another pool." + ) + self._subscription_pools = None subscription_data["quantity"] -= self.attached_quantity self.process_subscription_pools( subscription_pools=self.subscription_pools, subscription_data=subscription_data, ) else: - logger.debug(f"Successfully added {subscription_data['quantity']} entitlements of {subscription_data['name']} to the allocation.") + logger.debug( + f"Successfully added {subscription_data['quantity']} entitlements of " + f"{subscription_data['name']} to the allocation." + ) break elif add_entitlements.status_code == 200: logger.debug( - f"Successfully added {subscription_data['quantity']} entitlements of {subscription_data['name']} to the allocation." + f"Successfully added {subscription_data['quantity']} entitlements of " + f"{subscription_data['name']} to the allocation." ) break else: - raise Exception(f"Something went wrong while adding entitlements. Received response status {add_entitlements.status_code}.") + raise Exception( + "Something went wrong while adding entitlements. Received response status " + f"{add_entitlements.status_code}." + ) def trigger_manifest_export(self): - headers = {"headers": {"Authorization": f"Bearer {self.access_token}"}} - limit_exceeded = False + data = { + "headers": {"Authorization": f"Bearer {self.access_token}"}, + "proxies": self.manifest_data.get("proxies", settings.proxies), + } # Should this use the XDG Base Directory Specification? local_file = Path(f"manifests/{self.allocation_name}_manifest.zip") local_file.parent.mkdir(parents=True, exist_ok=True) - logger.info(f"Triggering manifest export job for subscription allocation {self.allocation_name}") + logger.info( + f"Triggering manifest export job for subscription allocation {self.allocation_name}" + ) trigger_export_job = simple_retry( requests.get, cmd_args=[ - f"https://api.access.redhat.com/management/v1/allocations/{self.allocation_uuid}/export" + f"{self.manifest_data.url.allocations}/{self.allocation_uuid}/export" ], - cmd_kwargs=headers, + cmd_kwargs=data, ).json() export_job_id = trigger_export_job["body"]["exportJobID"] export_job = simple_retry( requests.get, - cmd_args=[f"https://api.access.redhat.com/management/v1/allocations/{self.allocation_uuid}/exportJob/{export_job_id}"], - cmd_kwargs=headers, + cmd_args=[ + f"{self.manifest_data.url.allocations}/{self.allocation_uuid}/exportJob/{export_job_id}" + ], + cmd_kwargs=data, ) request_count = 1 + limit_exceeded = False while export_job.status_code != 200: export_job = simple_retry( requests.get, - cmd_args=[f"https://api.access.redhat.com/management/v1/allocations/{self.allocation_uuid}/exportJob/{export_job_id}"], - cmd_kwargs=headers, + cmd_args=[ + f"{self.manifest_data.url.allocations}/{self.allocation_uuid}/exportJob/{export_job_id}" + ], + cmd_kwargs=data, ) logger.debug( f"Attempting to export manifest. Attempt number: {request_count}" @@ -218,7 +280,8 @@ def trigger_manifest_export(self): if request_count > 50: limit_exceeded = True logger.info( - f"Manifest export job status check limit exceeded. This may indicate an upstream issue with Red Hat Subscription Management." + "Manifest export job status check limit exceeded. This may indicate an " + "upstream issue with Red Hat Subscription Management." ) break request_count += 1 @@ -229,10 +292,11 @@ def trigger_manifest_export(self): manifest = simple_retry( requests.get, cmd_args=[f"{export_href}"], - cmd_kwargs=headers, + cmd_kwargs=data, ) logger.info( - f"Writing manifest for subscription allocation {self.allocation_name} to location {local_file}" + f"Writing manifest for subscription allocation {self.allocation_name} to location " + f"{local_file}" ) local_file.write_bytes(manifest.content) return manifest diff --git a/manifester/settings.py b/manifester/settings.py index 1259d79..37b6712 100644 --- a/manifester/settings.py +++ b/manifester/settings.py @@ -1,6 +1,6 @@ import os - from pathlib import Path + from dynaconf import Dynaconf from dynaconf import Validator @@ -14,7 +14,7 @@ settings_path = MANIFESTER_DIRECTORY.joinpath("manifester_settings.yaml") validators = [ - Validator("offline_token", must_exist=True), + # Validator("offline_token", must_exist=True), Validator("simple_content_access", default="enabled"), ] settings = Dynaconf( diff --git a/manifester_settings.yaml.example b/manifester_settings.yaml.example index a247905..033413d 100644 --- a/manifester_settings.yaml.example +++ b/manifester_settings.yaml.example @@ -1,12 +1,15 @@ #rhsm-manifester settings log_level: "info" offline_token: "" +proxies: {"https": ""} manifest_category: golden_ticket: + # An offline token can be generated at https://access.redhat.com/management/api + offline_token: "" # Value of sat_version setting should be in the form 'sat-6.10' sat_version: "sat-6.10" # golden_ticket manifests should not use a quantity higher than 1 for any subscription - # unless doing so is required for a test. + # unless doing so is required for a test. subscription_data: # name should be an exact match of the subscription name as listed on the Customer Portal - name: "Software Collections and Developer Toolset" @@ -14,11 +17,20 @@ manifest_category: - name: "Red Hat Ansible Automation Platform, Standard (100 Managed Nodes)" quantity: 1 simple_content_access: "enabled" + url: + token_request: "https://sso.redhat.com/auth/realms/redhat-external/protocol/openid-connect/token" + allocations: "https://api.access.redhat.com/management/v1/allocations" + proxies: {"https": ""} robottelo_automation: + offline_token: "" sat_version: "sat-6.10" subscription_data: - name: "Software Collections and Developer Toolset" quantity: 3 - name: "Red Hat Ansible Automation Platform, Standard (100 Managed Nodes)" quantity: 2 - simple_content_access: "disabled" \ No newline at end of file + simple_content_access: "disabled" + url: + token_request: "https://sso.redhat.com/auth/realms/redhat-external/protocol/openid-connect/token" + allocations: "https://api.access.redhat.com/management/v1/allocations" + proxies: {"https": ""}