Skip to content

Commit 863646d

Browse files
committed
Code changes for supporting Offline LAS Licensing in restricted mode.
Signed-off-by: lakshmj <[email protected]>
1 parent 0b9e77d commit 863646d

3 files changed

Lines changed: 77 additions & 15 deletions

File tree

examples/nslaslicense_offline.yaml

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
hosts: demo_netscalers
44
gather_facts: false
55
tasks:
6-
- name: Apply offline LAS license to NetScaler ADC
6+
- name: Apply offline LAS license to NetScaler ADC (standard mode)
77
delegate_to: localhost
88
netscaler.adc.nslaslicense_offline:
99
nsip: "{{ nsip }}"
@@ -19,3 +19,21 @@
1919
- name: Display license result
2020
ansible.builtin.debug:
2121
var: lic_result
22+
23+
- name: Apply offline LAS license to NetScaler ADC (restricted mode)
24+
delegate_to: localhost
25+
netscaler.adc.nslaslicense_offline:
26+
nsip: "{{ nsip }}"
27+
nitro_user: "{{ nitro_user }}"
28+
nitro_pass: "{{ nitro_pass }}"
29+
nitro_protocol: "{{ nitro_protocol | default('https') }}"
30+
validate_certs: false
31+
entitlement_name: "VPX 10000 Premium"
32+
is_fips: false
33+
las_secrets_json: /path/to/las_secrets.json
34+
restricted_mode: true
35+
register: lic_result_restricted
36+
37+
- name: Display restricted mode license result
38+
ansible.builtin.debug:
39+
var: lic_result_restricted

plugins/module_utils/las_utils.py

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,17 @@ def import_offline_activation_request(self, request_file, fingerprint, bearer, l
299299
loglines.append("ERROR: import_offline_activation_request: {0}".format(str(e)))
300300
return "EXCEPTION ERROR"
301301

302+
def import_restricted_offline_activation_request(self, lsid, pubkey, bearer, loglines):
303+
url = "{0}/support/{1}/importrestrictedofflineactivationrequest".format(self._base_url, self._ccid)
304+
headers = {"Content-Type": "application/json", "Authorization": "CWSAuth bearer={0}".format(bearer)}
305+
payload = {"ver": "1.0", "lsid": lsid, "pubkey": pubkey}
306+
try:
307+
result = self._post_json(url, headers, payload)
308+
return result.get("importrequesttoken", "")
309+
except Exception as e:
310+
loglines.append("ERROR: import_restricted_offline_activation_request: {0}".format(str(e)))
311+
return "EXCEPTION ERROR"
312+
302313
def generate_offline_activation(self, import_token, bearer, ent_name, loglines):
303314
url = "{0}/{1}/{2}/generateofflineactivation".format(self._base_url, self._ccid, self.endpoint)
304315
headers = {"Content-Type": "application/json", "Authorization": "CWSAuth bearer={0}".format(bearer)}
@@ -458,7 +469,8 @@ def get_offline_request_package(nitro, ip, username, password, local_dir, new_ap
458469
# ---------------------------------------------------------------------------
459470

460471

461-
def extract_lsguid(file_path, loglines):
472+
def extract_request_fields(file_path, loglines):
473+
"""Extract lsguid, lsid, and pubkey from the NS offline activation request tgz in one pass."""
462474
dest_dir = os.path.dirname(file_path)
463475
# Validate that file_path is within dest_dir to guard against path traversal.
464476
real_file_path = os.path.realpath(file_path)
@@ -504,8 +516,12 @@ def extract_lsguid(file_path, loglines):
504516
loglines.append("DEBUG: Could not remove temp file lasData.tgz: {0}".format(str(e)))
505517

506518
lsguid = data["lsguid"]
519+
inner = data.get("data", {})
520+
lsid = inner["lsid"]
521+
pubkey = inner["pubkey"]
507522
loglines.append("INFO: Extracted lsguid: {0}".format(lsguid))
508-
return lsguid
523+
loglines.append("INFO: Extracted lsid: {0}".format(lsid))
524+
return lsguid, lsid, pubkey
509525

510526

511527
# ---------------------------------------------------------------------------
@@ -558,7 +574,7 @@ def get_ent_name(request_pem, request_ed, is_fips, loglines):
558574
# ---------------------------------------------------------------------------
559575

560576

561-
def generate_offline_package(lsguid, request_file, output_file, ent_name, secret_file, loglines):
577+
def generate_offline_package(lsguid, request_file, output_file, ent_name, secret_file, loglines, restricted_mode=False, lsid=None, pubkey=None):
562578
client = LASClient(lsguid, secret_file)
563579

564580
bearer = client.validate_bearer_cache()
@@ -572,13 +588,16 @@ def generate_offline_package(lsguid, request_file, output_file, ent_name, secret
572588
loglines.append("ERROR: Failed to obtain bearer token from LAS")
573589
return None
574590

575-
fingerprint = client.get_fingerprint_for_lsguid(bearer, loglines)
576-
if "ERROR" in str(fingerprint):
577-
loglines.append("ERROR: Failed to get device fingerprint for lsguid {0}".format(lsguid))
578-
return None
579-
loglines.append("INFO: Device fingerprint in LAS: {0!r}".format(fingerprint))
591+
if restricted_mode:
592+
import_token = client.import_restricted_offline_activation_request(lsid, pubkey, bearer, loglines)
593+
else:
594+
fingerprint = client.get_fingerprint_for_lsguid(bearer, loglines)
595+
if "ERROR" in str(fingerprint):
596+
loglines.append("ERROR: Failed to get device fingerprint for lsguid {0}".format(lsguid))
597+
return None
598+
loglines.append("INFO: Device fingerprint in LAS: {0!r}".format(fingerprint))
599+
import_token = client.import_offline_activation_request(request_file, fingerprint, bearer, loglines)
580600

581-
import_token = client.import_offline_activation_request(request_file, fingerprint, bearer, loglines)
582601
if not import_token or "ERROR" in import_token:
583602
loglines.append("ERROR: Failed to import offline activation request")
584603
return None

plugins/modules/nslaslicense_offline.py

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,13 @@
7878
- The file must contain the keys C(ccid), C(client), C(password), C(las_endpoint), and C(cc_endpoint).
7979
type: str
8080
required: true
81+
restricted_mode:
82+
description:
83+
- Set to C(true) to use the restricted offline activation request flow.
84+
- When enabled, the module extracts C(lsid) and C(pubkey) from the activation request package
85+
and calls the restricted import endpoint instead of the standard file-upload endpoint.
86+
type: bool
87+
default: false
8188
"""
8289

8390
EXAMPLES = r"""
@@ -112,6 +119,19 @@
112119
nitro_pass: "{{ nitro_pass }}"
113120
entitlement_name: "MPX 8905 Standard"
114121
las_secrets_json: ./path/to/las_secrets.json
122+
123+
- name: Generate and apply offline LAS license using restricted mode (no file upload)
124+
delegate_to: localhost
125+
netscaler.adc.nslaslicense_offline:
126+
nsip: 10.102.201.230
127+
nitro_user: nsroot
128+
nitro_pass: "{{ nitro_pass }}"
129+
nitro_protocol: https
130+
validate_certs: false
131+
entitlement_name: "VPX 10000 Premium"
132+
is_fips: false
133+
las_secrets_json: ./path/to/las_secrets.json
134+
restricted_mode: true
115135
"""
116136

117137
RETURN = r"""
@@ -156,7 +176,7 @@
156176
apply_license_blob_ns,
157177
check_if_new_api,
158178
check_ns_version,
159-
extract_lsguid,
179+
extract_request_fields,
160180
generate_offline_package,
161181
get_offline_request_package,
162182
)
@@ -177,6 +197,7 @@ def main():
177197
entitlement_name=dict(required=True, type="str"),
178198
is_fips=dict(type="bool", default=False),
179199
las_secrets_json=dict(required=True, type="str", no_log=False),
200+
restricted_mode=dict(type="bool", default=False),
180201
)
181202

182203
module = AnsibleModule(
@@ -196,6 +217,7 @@ def main():
196217
ent_name = module.params["entitlement_name"]
197218
is_fips = module.params["is_fips"]
198219
las_secrets_json = module.params["las_secrets_json"]
220+
restricted_mode = module.params["restricted_mode"]
199221
if username != "nsroot":
200222
module.fail_json(msg="Only the 'nsroot' account is supported. Got: '{0}'".format(username), **result)
201223

@@ -289,20 +311,23 @@ def main():
289311
request_file = os.path.join(temp_dir, ns_file_name)
290312
loglines.append("INFO: Got request package: {0}".format(request_file))
291313

292-
# Extract lsguid (retry once on parse failure)
314+
# Extract lsguid (always) and lsid+pubkey (retry once on parse failure)
293315
try:
294-
lsguid = extract_lsguid(request_file, loglines)
316+
lsguid, lsid, pubkey = extract_request_fields(request_file, loglines)
295317
except Exception as e:
296318
loglines.append("WARNING: First parse attempt failed ({0}), re-downloading package".format(str(e)))
297319
ns_file_name = get_offline_request_package(nitro, ip, username, password, temp_dir, new_api, loglines)
298320
if not ns_file_name:
299321
module.fail_json(msg="Re-download of activation request package failed", **result)
300322
request_file = os.path.join(temp_dir, ns_file_name)
301-
lsguid = extract_lsguid(request_file, loglines)
323+
lsguid, lsid, pubkey = extract_request_fields(request_file, loglines)
302324

303325
# Generate offline token from LAS cloud
304326
output_file = "offline_token_{0}_activation.blob.tgz".format(ip)
305-
if generate_offline_package(lsguid, request_file, output_file, ent_name, las_secrets_json, loglines) is None:
327+
if generate_offline_package(
328+
lsguid, request_file, output_file, ent_name, las_secrets_json, loglines,
329+
restricted_mode=restricted_mode, lsid=lsid if restricted_mode else None, pubkey=pubkey if restricted_mode else None,
330+
) is None:
306331
module.fail_json(msg="Failed to generate offline license token from LAS", **result)
307332

308333
# Apply license blob to device

0 commit comments

Comments
 (0)