Skip to content
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

Refactor settings and reformat multiple files #8

Merged
merged 5 commits into from
Aug 1, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -32,4 +32,4 @@ Currently, the only action supported by the manifester CLI is generating a manif
```
manifester get-manifest --manifest-category <manifest category name> --allocation_name <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.
13 changes: 9 additions & 4 deletions manifester/commands.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
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():
pass


@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",
mshriver marked this conversation as resolved.
Show resolved Hide resolved
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()
Expand Down
2 changes: 1 addition & 1 deletion manifester/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Expand Down
136 changes: 96 additions & 40 deletions manifester/manifester.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a fauxfactory dependency for this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right now it is just a single call, but if more random data is needed later, that is a possibility. or the more performant

str(uuid4()).split("-")[-1]

)
self.manifest_data = settings.manifest_category.get(manifest_category)
self.offline_token = kwargs.get(
"offline_token", self.manifest_data.offline_token
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be a good idea to modify this so the offline token could be set at the top level or overridden at the manifest level. That way someone using the same token for each doesn't need to duplicate the setting for each manifest.

Suggested change
self.offline_token = kwargs.get(
"offline_token", self.manifest_data.offline_token
)
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

Expand All @@ -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"]
Expand All @@ -50,12 +56,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

Expand All @@ -69,29 +76,38 @@ def subscription_pools(self):
}
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}"},
"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):
Expand All @@ -101,45 +117,67 @@ def add_entitlements_to_allocation(self, pool_id, 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}"},
"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"],
Expand All @@ -156,60 +194,76 @@ 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}."
f"Trying to find another pool."
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
f"Trying to find another pool."
"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(
f"Something went wrong while adding entitlements. Received response status "
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
f"Something went wrong while adding entitlements. Received response status "
"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
# 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,
).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_args=[
f"{self.manifest_data.url.allocations}/{self.allocation_uuid}/exportJob/{export_job_id}"
],
cmd_kwargs=headers,
)
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_args=[
f"{self.manifest_data.url.allocations}/{self.allocation_uuid}/exportJob/{export_job_id}"
],
cmd_kwargs=headers,
)
logger.debug(
Expand All @@ -218,7 +272,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
Expand All @@ -232,7 +287,8 @@ def trigger_manifest_export(self):
cmd_kwargs=headers,
)
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
Expand Down
Loading