mirror of
https://github.com/ansible/awx.git
synced 2026-03-22 19:35:02 -02:30
change license uploading to parse RHSM manifests
Co-authored-by: Christian Adams <chadams@redhat.com>
This commit is contained in:
@@ -307,7 +307,7 @@ class BaseAccess(object):
|
||||
return True # User has access to both, permission check passed
|
||||
|
||||
def check_license(self, add_host_name=None, feature=None, check_expiration=True, quiet=False):
|
||||
validation_info = get_licenser().validate(new_cert=False)
|
||||
validation_info = get_licenser().validate()
|
||||
if validation_info.get('license_type', 'UNLICENSED') == 'open':
|
||||
return
|
||||
|
||||
@@ -345,7 +345,7 @@ class BaseAccess(object):
|
||||
report_violation(_("Host count exceeds available instances."))
|
||||
|
||||
def check_org_host_limit(self, data, add_host_name=None):
|
||||
validation_info = get_licenser().validate(new_cert=False)
|
||||
validation_info = get_licenser().validate()
|
||||
if validation_info.get('license_type', 'UNLICENSED') == 'open':
|
||||
return
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ from rest_framework.fields import FloatField
|
||||
|
||||
# Tower
|
||||
from awx.conf import fields, register, register_validate
|
||||
from awx.main.validators import validate_entitlement_cert
|
||||
|
||||
|
||||
logger = logging.getLogger('awx.main.conf')
|
||||
@@ -356,21 +355,6 @@ register(
|
||||
category_slug='jobs',
|
||||
)
|
||||
|
||||
register(
|
||||
'ENTITLEMENT_CERT',
|
||||
field_class=fields.CharField,
|
||||
allow_blank=True,
|
||||
default='',
|
||||
required=False,
|
||||
validators=[validate_entitlement_cert], # TODO: may need to use/modify `validate_certificate` validator
|
||||
label=_('RHSM Entitlement Public Certificate and Private Key'),
|
||||
help_text=_('Obtain a key pair via subscription-manager, or https://access.redhat.com. Refer to Ansible Tower docs for formatting key pair.'),
|
||||
category=_('SYSTEM'),
|
||||
category_slug='system',
|
||||
encrypted=True,
|
||||
)
|
||||
|
||||
|
||||
register(
|
||||
'AWX_RESOURCE_PROFILING_ENABLED',
|
||||
field_class=fields.BooleanField,
|
||||
|
||||
@@ -16,7 +16,7 @@ class Command(BaseCommand):
|
||||
|
||||
def handle(self, *args, **options):
|
||||
super(Command, self).__init__()
|
||||
license = get_licenser().validate(new_cert=False)
|
||||
license = get_licenser().validate()
|
||||
if options.get('data'):
|
||||
return json.dumps(license)
|
||||
return license.get('license_type', 'none')
|
||||
|
||||
@@ -901,7 +901,7 @@ class Command(BaseCommand):
|
||||
))
|
||||
|
||||
def check_license(self):
|
||||
license_info = get_licenser().validate(new_cert=False)
|
||||
license_info = get_licenser().validate()
|
||||
local_license_type = license_info.get('license_type', 'UNLICENSED')
|
||||
if license_info.get('license_key', 'UNLICENSED') == 'UNLICENSED':
|
||||
logger.error(LICENSE_NON_EXISTANT_MESSAGE)
|
||||
@@ -938,7 +938,7 @@ class Command(BaseCommand):
|
||||
logger.warning(LICENSE_MESSAGE % d)
|
||||
|
||||
def check_org_host_limit(self):
|
||||
license_info = get_licenser().validate(new_cert=False)
|
||||
license_info = get_licenser().validate()
|
||||
if license_info.get('license_type', 'UNLICENSED') == 'open':
|
||||
return
|
||||
|
||||
|
||||
@@ -9,16 +9,26 @@ The Licenser class can do the following:
|
||||
- Parse an Entitlement cert to generate license
|
||||
'''
|
||||
|
||||
import os
|
||||
import base64
|
||||
import configparser
|
||||
from datetime import datetime
|
||||
import collections
|
||||
import copy
|
||||
import tempfile
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import requests
|
||||
import time
|
||||
import zipfile
|
||||
|
||||
from dateutil.parser import parse as parse_date
|
||||
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
from cryptography import x509
|
||||
|
||||
# Django
|
||||
from django.conf import settings
|
||||
@@ -26,9 +36,6 @@ from django.conf import settings
|
||||
# AWX
|
||||
from awx.main.models import Host
|
||||
|
||||
# RHSM
|
||||
from rhsm import certificate
|
||||
|
||||
MAX_INSTANCES = 9999999
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -40,6 +47,26 @@ def rhsm_config():
|
||||
return config
|
||||
|
||||
|
||||
def validate_entitlement_manifest(data):
|
||||
buff = io.BytesIO()
|
||||
buff.write(base64.b64decode(data))
|
||||
z = zipfile.ZipFile(buff)
|
||||
buff = io.BytesIO()
|
||||
|
||||
export = z.open('consumer_export.zip').read()
|
||||
sig = z.open('signature').read()
|
||||
with open('/etc/tower/certs/candlepin-redhat-ca.crt', 'rb') as f:
|
||||
cert = x509.load_pem_x509_certificate(f.read(), backend=default_backend())
|
||||
key = cert.public_key()
|
||||
key.verify(sig, export, padding=padding.PKCS1v15(), algorithm=hashes.SHA256())
|
||||
|
||||
buff.write(export)
|
||||
z = zipfile.ZipFile(buff)
|
||||
for f in z.filelist:
|
||||
if f.filename.startswith('export/entitlements') and f.filename.endswith('.json'):
|
||||
return json.loads(z.open(f).read())
|
||||
|
||||
|
||||
class Licenser(object):
|
||||
# warn when there is a month (30 days) left on the license
|
||||
LICENSE_TIMEOUT = 60 * 60 * 24 * 30
|
||||
@@ -108,32 +135,15 @@ class Licenser(object):
|
||||
settings.LICENSE = {}
|
||||
|
||||
|
||||
def _generate_product_config(self):
|
||||
raw_cert = getattr(settings, 'ENTITLEMENT_CERT', None)
|
||||
# Fail early if no entitlement cert is available
|
||||
if not raw_cert or raw_cert == '':
|
||||
self._clear_license_setting()
|
||||
return
|
||||
|
||||
# Read certificate
|
||||
certinfo = certificate.create_from_pem(raw_cert)
|
||||
if not certinfo.is_valid():
|
||||
raise ValueError("Could not parse entitlement certificate")
|
||||
if certinfo.is_expired():
|
||||
raise ValueError("Certificate is expired")
|
||||
if not any(map(lambda x: x.id == '480', certinfo.products)):
|
||||
self._clear_license_setting()
|
||||
raise ValueError("Certificate is for another product")
|
||||
|
||||
def license_from_manifest(self, manifest):
|
||||
# Parse output for subscription metadata to build config
|
||||
license = dict()
|
||||
license['sku'] = certinfo.order.sku
|
||||
license['instance_count'] = int(certinfo.order.quantity_used)
|
||||
license['support_level'] = certinfo.order.service_level
|
||||
license['subscription_name'] = certinfo.order.name
|
||||
license['pool_id'] = certinfo.pool.id
|
||||
license['license_date'] = certinfo.end.strftime('%s')
|
||||
license['product_name'] = certinfo.order.name
|
||||
license['sku'] = manifest['pool']['productId']
|
||||
license['instance_count'] = manifest['pool']['quantity']
|
||||
license['subscription_name'] = manifest['pool']['productName']
|
||||
license['pool_id'] = manifest['pool']['id']
|
||||
license['license_date'] = parse_date(manifest['endDate']).strftime('%s')
|
||||
license['product_name'] = manifest['pool']['productName']
|
||||
license['valid_key'] = True
|
||||
license['license_type'] = 'enterprise'
|
||||
license['satellite'] = False
|
||||
@@ -269,7 +279,7 @@ class Licenser(object):
|
||||
ValidSub = collections.namedtuple('ValidSub', 'sku name support_level end_date trial quantity pool_id satellite')
|
||||
valid_subs = []
|
||||
for sub in json:
|
||||
satellite = sub['satellite']
|
||||
satellite = sub.get('satellite')
|
||||
if satellite:
|
||||
is_valid = self.is_appropriate_sat_sub(sub)
|
||||
else:
|
||||
@@ -344,12 +354,7 @@ class Licenser(object):
|
||||
)
|
||||
|
||||
|
||||
def validate(self, new_cert=False):
|
||||
|
||||
# Generate Config from Entitlement cert if it exists
|
||||
if new_cert:
|
||||
self._generate_product_config()
|
||||
|
||||
def validate(self):
|
||||
# Return license attributes with additional validation info.
|
||||
attrs = copy.deepcopy(self._attrs)
|
||||
type = attrs.get('license_type', 'none')
|
||||
@@ -368,7 +373,7 @@ class Licenser(object):
|
||||
attrs['available_instances'] = available_instances
|
||||
attrs['free_instances'] = max(0, available_instances - current_instances)
|
||||
|
||||
license_date = int(attrs.get('license_date', None) or 0)
|
||||
license_date = int(attrs.get('license_date', 0) or 0)
|
||||
current_date = int(time.time())
|
||||
time_remaining = license_date - current_date
|
||||
attrs['time_remaining'] = time_remaining
|
||||
|
||||
@@ -17,12 +17,6 @@ from rest_framework.exceptions import ParseError
|
||||
from awx.main.utils import parse_yaml_or_json
|
||||
|
||||
|
||||
def validate_entitlement_cert(data, min_keys=0, max_keys=None, min_certs=0, max_certs=None):
|
||||
# TODO: replace with more actual robust logic here to allow for multiple certificates in one cert file (because this is how entitlements do)
|
||||
# This is a temporary hack that should not be merged. Look at Ln:92
|
||||
pass
|
||||
|
||||
|
||||
def validate_pem(data, min_keys=0, max_keys=None, min_certs=0, max_certs=None):
|
||||
"""
|
||||
Validate the given PEM data is valid and contains the required numbers of
|
||||
|
||||
Reference in New Issue
Block a user