Skip to content

Commit e8c7b01

Browse files
5.2.1 (#77)
* 20241204 - Postman collection updated * Postman collection fix * 20250122 - Postman collection fixes * 20250130-01 DNS resolution check added * 20250130-02 Support for pre-existing NGINX Plus R33+ license tokens
1 parent 92a29fb commit e8c7b01

7 files changed

+108
-36
lines changed

USAGE-v5.2.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ The JSON schema is self explanatory. See also the [sample Postman collection](/c
1111

1212
- `.output.license` defines the JWT license to use for NGINX Plus R33+
1313
- `.output.license.endpoint` the usage reporting endpoint (defaults to `product.connect.nginx.com`). NGINX Instance Manager address can be used here
14-
- `.output.license.token` the JWT license token
14+
- `.output.license.token` the JWT license token. If this field is omitted, it is assumed that a `/etc/nginx/license.jwt` token already exists on the instance and it won't be replaced
1515
- `.output.license.ssl_verify` set to `false` to trust all SSL certificates (not recommended). Useful for reporting to NGINX Instance Manager without a local PKI.
1616
- `.output.license.grace_period` Set to 'true' to begin the 180-day reporting enforcement grace period. Reporting must begin or resume before the end of the grace period to ensure continued operation
1717
- `.output.type` defines how NGINX configuration will be returned:

contrib/postman/NGINX Declarative API.postman_collection.json

+57-7
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
{
22
"info": {
3-
"_postman_id": "c988529a-6e79-45c2-8c6a-b60b9aac92ba",
3+
"_postman_id": "3beaacd9-6fff-4a51-a64e-24efdad57e6a",
44
"name": "NGINX Declarative API",
55
"description": "Declarative REST API and GitOps automation layer for NGINX Instance Manager and NGINX One Console\n\n[https://github.com/f5devcentral/NGINX-Declarative-API/](https://github.com/f5devcentral/NGINX-Declarative-API/)",
66
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
77
"_exporter_id": "1667416",
8-
"_collection_link": "https://orange-rocket-1353.postman.co/workspace/NGINX-Declarative-API~8ba6e9c1-a04b-4484-8193-bbb142560553/collection/1667416-c988529a-6e79-45c2-8c6a-b60b9aac92ba?action=share&source=collection_link&creator=1667416"
8+
"_collection_link": "https://orange-rocket-1353.postman.co/workspace/NGINX-Declarative-API~8ba6e9c1-a04b-4484-8193-bbb142560553/collection/1667416-3beaacd9-6fff-4a51-a64e-24efdad57e6a?action=share&source=collection_link&creator=1667416"
99
},
1010
"item": [
1111
{
@@ -7293,7 +7293,18 @@
72937293
"method": "GET",
72947294
"header": [],
72957295
"url": {
7296-
"raw": ""
7296+
"raw": "https://apigw.nginx.lab/petstore/store/inventory",
7297+
"protocol": "https",
7298+
"host": [
7299+
"apigw",
7300+
"nginx",
7301+
"lab"
7302+
],
7303+
"path": [
7304+
"petstore",
7305+
"store",
7306+
"inventory"
7307+
]
72977308
}
72987309
},
72997310
"response": []
@@ -7332,7 +7343,24 @@
73327343
"method": "GET",
73337344
"header": [],
73347345
"url": {
7335-
"raw": ""
7346+
"raw": "https://apigw.nginx.lab/petstore/store/inventory?<script>alert();</script>",
7347+
"protocol": "https",
7348+
"host": [
7349+
"apigw",
7350+
"nginx",
7351+
"lab"
7352+
],
7353+
"path": [
7354+
"petstore",
7355+
"store",
7356+
"inventory"
7357+
],
7358+
"query": [
7359+
{
7360+
"key": "<script>alert();</script>",
7361+
"value": null
7362+
}
7363+
]
73367364
}
73377365
},
73387366
"response": []
@@ -7343,7 +7371,18 @@
73437371
"method": "GET",
73447372
"header": [],
73457373
"url": {
7346-
"raw": ""
7374+
"raw": "https://apigw.nginx.lab/petstore/user/login",
7375+
"protocol": "https",
7376+
"host": [
7377+
"apigw",
7378+
"nginx",
7379+
"lab"
7380+
],
7381+
"path": [
7382+
"petstore",
7383+
"user",
7384+
"login"
7385+
]
73477386
}
73487387
},
73497388
"response": []
@@ -7364,7 +7403,18 @@
73647403
"method": "GET",
73657404
"header": [],
73667405
"url": {
7367-
"raw": ""
7406+
"raw": "https://apigw.nginx.lab/petstore/user/login",
7407+
"protocol": "https",
7408+
"host": [
7409+
"apigw",
7410+
"nginx",
7411+
"lab"
7412+
],
7413+
"path": [
7414+
"petstore",
7415+
"user",
7416+
"login"
7417+
]
73687418
}
73697419
},
73707420
"response": []
@@ -11821,4 +11871,4 @@
1182111871
"type": "string"
1182211872
}
1182311873
]
11824-
}
11874+
}

src/V5_2_CreateConfig.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
import v5_2.DeclarationPatcher
2424
import v5_2.GitOps
2525
import v5_2.MiscUtils
26-
import v5_2.NMSOutput
26+
import v5_2.NIMOutput
2727
import v5_2.NGINXOneOutput
2828

2929
# NGINX App Protect helper functions
@@ -639,7 +639,7 @@ def createconfig(declaration: ConfigDeclaration, apiversion: str, runfromautosyn
639639
# NGINX auxiliary files for staged config
640640
auxFiles['rootDir'] = NcgConfig.config['nms']['config_dir']
641641

642-
finalReply = v5_2.NMSOutput.NMSOutput(d = d, declaration = declaration, apiversion = apiversion,
642+
finalReply = v5_2.NIMOutput.NIMOutput(d = d, declaration = declaration, apiversion = apiversion,
643643
b64HttpConf = b64HttpConf, b64StreamConf = b64StreamConf,
644644
configFiles = configFiles,
645645
auxFiles = auxFiles,

src/V5_2_NginxConfigDeclaration.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ class OutputNGINXOne(BaseModel, extra="forbid"):
136136

137137
class License(BaseModel, extra="forbid"):
138138
endpoint: str = "product.connect.nginx.com"
139-
token: str = ""
139+
token: Optional[str] = ""
140140
ssl_verify: bool = True
141141
grace_period: bool = False
142142

src/v5_2/MiscUtils.py

+19-20
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,12 @@
66
import json
77
import yaml
88
import uuid
9+
import socket
910

1011

12+
# Searches for a nested key in a dictionary and returns its value, or None if nothing was found.
13+
# key_lookup must be a string where each key is deparated by a given "separator" character, which by default is a dot
1114
def getDictKey(_dict: dict, key_lookup: str, separator='.'):
12-
"""
13-
Searches for a nested key in a dictionary and returns its value, or None if nothing was found.
14-
key_lookup must be a string where each key is deparated by a given "separator" character, which by default is a dot
15-
"""
1615
keys = key_lookup.split(separator)
1716
subdict = _dict
1817

@@ -23,38 +22,38 @@ def getDictKey(_dict: dict, key_lookup: str, separator='.'):
2322

2423
return subdict
2524

26-
"""
27-
Jinja2 regexp filter
28-
"""
25+
# Jinja2 regexp filter
2926
def regex_replace(s, find, replace):
3027
return re.sub(find, replace, s)
3128

32-
"""
33-
JSON/YAML detection
34-
"""
29+
# JSON/YAML detection
3530
def yaml_or_json(document: str):
3631
try:
3732
json.load(document)
3833
return 'json'
3934
except Exception:
4035
return 'yaml'
4136

42-
"""
43-
YAML to JSON conversion
44-
"""
37+
# YAML to JSON conversion
4538
def yaml_to_json(document: str):
4639
return json.dumps(yaml.safe_load(document))
4740

4841

49-
"""
50-
JSON TO YAML conversion
51-
"""
42+
# JSON TO YAML conversion
5243
def json_to_yaml(document: str):
5344
return yaml.dump(json.loads(document))
5445

5546

56-
"""
57-
Returns a unique ID
58-
"""
47+
# Returns a unique ID
5948
def getuniqueid():
60-
return uuid.uuid4()
49+
return uuid.uuid4()
50+
51+
52+
# Test DNS resolution
53+
# Returns {True,IP address} if successful and {False,error description} for NXDOMAIN/if DNS resolution failed
54+
def resolveFQDN(fqdn:str):
55+
try:
56+
return True,socket.gethostbyname(fqdn)
57+
except Exception as e:
58+
return False,e
59+

src/v5_2/NGINXOneOutput.py

+13-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import v5_2.DeclarationPatcher
2121
import v5_2.GitOps
2222
import v5_2.MiscUtils
23-
import v5_2.NMSOutput
2423

2524
# pydantic models
2625
from V5_2_NginxConfigDeclaration import *
@@ -55,6 +54,14 @@ def NGINXOneOutput(d, declaration: ConfigDeclaration, apiversion: str, b64HttpCo
5554
"content": f"invalid NGINX One URL {nOneUrlFromJson}"}},
5655
"headers": {'Content-Type': 'application/json'}}
5756

57+
# DNS resolution check
58+
dnsOutcome, dnsReply = v5_2.MiscUtils.resolveFQDN(urlCheck.netloc)
59+
if not dnsOutcome:
60+
return {"status_code": 400,
61+
"message": {"status_code": 400, "message": {"code": 400,
62+
"content": f"DNS resolution failed for {urlCheck.netloc}: {dnsReply}"}},
63+
"headers": {'Content-Type': 'application/json'}}
64+
5865
nOneUrl = f"{urlCheck.scheme}://{urlCheck.netloc}"
5966

6067
if nOneSynctime < 0:
@@ -170,10 +177,14 @@ def NGINXOneOutput(d, declaration: ConfigDeclaration, apiversion: str, b64HttpCo
170177

171178
# Append config files to staged configuration
172179
configFiles['files'].append(filesNginxMain)
173-
configFiles['files'].append(filesLicenseFile)
174180
configFiles['files'].append(filesHttpConf)
175181
configFiles['files'].append(filesStreamConf)
176182

183+
# If no R33+ license token was specified in the JSON declaration, it is assumed a token already exists
184+
# on the NGINX instances and it won't be overwritten
185+
if v5_2.MiscUtils.getDictKey(d, 'output.license.token') != "":
186+
configFiles['files'].append(filesLicenseFile)
187+
177188
# Staged config
178189
baseStagedConfig = {'aux': [ { 'files': configFiles } ] }
179190
#stagedConfig = {'conf_path': NcgConfig.config['nms']['config_dir'] + '/nginx.conf',

src/v5_2/NMSOutput.py src/v5_2/NIMOutput.py

+15-3
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import v5_2.DeclarationPatcher
2121
import v5_2.GitOps
2222
import v5_2.MiscUtils
23-
import v5_2.NMSOutput
23+
import v5_2.NIMOutput
2424

2525
# pydantic models
2626
from V5_2_NginxConfigDeclaration import *
@@ -34,7 +34,7 @@
3434
from NcgConfig import NcgConfig
3535
from NcgRedis import NcgRedis
3636

37-
def NMSOutput(d, declaration: ConfigDeclaration, apiversion: str, b64HttpConf: str,
37+
def NIMOutput(d, declaration: ConfigDeclaration, apiversion: str, b64HttpConf: str,
3838
b64StreamConf: str,configFiles = {}, auxFiles = {},
3939
runfromautosync: bool = False,
4040
configUid: str = ""):
@@ -54,6 +54,14 @@ def NMSOutput(d, declaration: ConfigDeclaration, apiversion: str, b64HttpConf: s
5454
"content": f"invalid NGINX Instance Manager URL {nmsUrlFromJson}"}},
5555
"headers": {'Content-Type': 'application/json'}}
5656

57+
# DNS resolution check
58+
dnsOutcome, dnsReply = v5_2.MiscUtils.resolveFQDN(urlCheck.netloc)
59+
if not dnsOutcome:
60+
return {"status_code": 400,
61+
"message": {"status_code": 400, "message": {"code": 400,
62+
"content": f"DNS resolution failed for {urlCheck.netloc}: {dnsReply}"}},
63+
"headers": {'Content-Type': 'application/json'}}
64+
5765
nmsUrl = f"{urlCheck.scheme}://{urlCheck.netloc}"
5866

5967
if nmsSynctime < 0:
@@ -180,10 +188,14 @@ def NMSOutput(d, declaration: ConfigDeclaration, apiversion: str, b64HttpConf: s
180188

181189
# Append config files to staged configuration
182190
configFiles['files'].append(filesNginxMain)
183-
configFiles['files'].append(filesLicenseFile)
184191
configFiles['files'].append(filesHttpConf)
185192
configFiles['files'].append(filesStreamConf)
186193

194+
# If no R33+ license token was specified in the JSON declaration, it is assumed a token already exists
195+
# on the NGINX instances and it won't be overwritten
196+
if v5_2.MiscUtils.getDictKey(d, 'output.license.token') != "":
197+
configFiles['files'].append(filesLicenseFile)
198+
187199
# Staged config
188200
baseStagedConfig = {'auxFiles': auxFiles, 'configFiles': configFiles}
189201
stagedConfig = {'auxFiles': auxFiles, 'configFiles': configFiles,

0 commit comments

Comments
 (0)