nrfcloud-utils 3.0.12__tar.gz → 3.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {nrfcloud_utils-3.0.12 → nrfcloud_utils-3.2.0}/PKG-INFO +11 -12
- {nrfcloud_utils-3.0.12 → nrfcloud_utils-3.2.0}/pyproject.toml +34 -23
- {nrfcloud_utils-3.0.12 → nrfcloud_utils-3.2.0}/src/nrfcloud_utils/claim_and_provision_device.py +14 -11
- {nrfcloud_utils-3.0.12 → nrfcloud_utils-3.2.0}/src/nrfcloud_utils/claim_devices.py +11 -8
- nrfcloud_utils-3.2.0/src/nrfcloud_utils/cli.py +74 -0
- {nrfcloud_utils-3.0.12 → nrfcloud_utils-3.2.0}/src/nrfcloud_utils/cli_helpers.py +35 -1
- {nrfcloud_utils-3.0.12 → nrfcloud_utils-3.2.0}/src/nrfcloud_utils/create_ca_cert.py +11 -7
- {nrfcloud_utils-3.0.12 → nrfcloud_utils-3.2.0}/src/nrfcloud_utils/create_device_credentials.py +11 -12
- {nrfcloud_utils-3.0.12 → nrfcloud_utils-3.2.0}/src/nrfcloud_utils/create_proxy_jwt.py +11 -6
- {nrfcloud_utils-3.0.12 → nrfcloud_utils-3.2.0}/src/nrfcloud_utils/device_credentials_installer.py +47 -16
- {nrfcloud_utils-3.0.12 → nrfcloud_utils-3.2.0}/src/nrfcloud_utils/gather_attestation_tokens.py +12 -9
- {nrfcloud_utils-3.0.12 → nrfcloud_utils-3.2.0}/src/nrfcloud_utils/modem_credentials_parser.py +139 -27
- nrfcloud_utils-3.2.0/src/nrfcloud_utils/nrf93_onboard.py +293 -0
- {nrfcloud_utils-3.0.12 → nrfcloud_utils-3.2.0}/src/nrfcloud_utils/nrf_cloud_device_mgmt.py +7 -2
- {nrfcloud_utils-3.0.12 → nrfcloud_utils-3.2.0}/src/nrfcloud_utils/nrf_cloud_diap.py +1 -1
- {nrfcloud_utils-3.0.12 → nrfcloud_utils-3.2.0}/src/nrfcloud_utils/nrf_cloud_onboard.py +12 -12
- {nrfcloud_utils-3.0.12 → nrfcloud_utils-3.2.0}/LICENSE.txt +0 -0
- {nrfcloud_utils-3.0.12 → nrfcloud_utils-3.2.0}/README.md +0 -0
- {nrfcloud_utils-3.0.12 → nrfcloud_utils-3.2.0}/src/nrfcloud_utils/__init__.py +0 -0
- {nrfcloud_utils-3.0.12 → nrfcloud_utils-3.2.0}/src/nrfcloud_utils/ca_certs.py +0 -0
|
@@ -1,27 +1,26 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nrfcloud-utils
|
|
3
|
-
Version: 3.0
|
|
3
|
+
Version: 3.2.0
|
|
4
4
|
Summary: Scripts and utilities for working with the nRF Cloud
|
|
5
|
-
License: BSD-3-Clause
|
|
5
|
+
License-Expression: BSD-3-Clause
|
|
6
6
|
License-File: LICENSE.txt
|
|
7
7
|
Author: Nordic Semiconductor ASA
|
|
8
8
|
Requires-Python: >=3.10,<4.0
|
|
9
|
-
Classifier: License :: OSI Approved :: BSD License
|
|
10
9
|
Classifier: Programming Language :: Python :: 3
|
|
11
10
|
Classifier: Programming Language :: Python :: 3.10
|
|
12
11
|
Classifier: Programming Language :: Python :: 3.11
|
|
13
12
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
13
|
Classifier: Programming Language :: Python :: 3.13
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
-
Requires-Dist: cbor2 (>=5.4.2.post1
|
|
17
|
-
Requires-Dist: cffi (>=2.0.0
|
|
18
|
-
Requires-Dist: cryptography (>=46.0.5
|
|
19
|
-
Requires-Dist: inquirer (>=3.4.0
|
|
20
|
-
Requires-Dist: nrfcredstore (>=2.
|
|
21
|
-
Requires-Dist: pyjwt (>=2.3.0
|
|
22
|
-
Requires-Dist: requests (>=2.32.0
|
|
23
|
-
Requires-Dist: semver (>=3.0.0
|
|
24
|
-
Requires-Dist: urllib3 (>=2.
|
|
15
|
+
Requires-Dist: cbor2 (>=5.4.2.post1)
|
|
16
|
+
Requires-Dist: cffi (>=2.0.0)
|
|
17
|
+
Requires-Dist: cryptography (>=46.0.5)
|
|
18
|
+
Requires-Dist: inquirer (>=3.4.0)
|
|
19
|
+
Requires-Dist: nrfcredstore (>=2.1.0)
|
|
20
|
+
Requires-Dist: pyjwt (>=2.3.0)
|
|
21
|
+
Requires-Dist: requests (>=2.32.0)
|
|
22
|
+
Requires-Dist: semver (>=3.0.0)
|
|
23
|
+
Requires-Dist: urllib3 (>=2.7.0)
|
|
25
24
|
Project-URL: Repository, https://github.com/nRFCloud/utils
|
|
26
25
|
Description-Content-Type: text/markdown
|
|
27
26
|
|
|
@@ -2,36 +2,33 @@
|
|
|
2
2
|
requires = ["poetry-core"]
|
|
3
3
|
build-backend = "poetry.core.masonry.api"
|
|
4
4
|
|
|
5
|
-
[
|
|
5
|
+
[project]
|
|
6
6
|
name = "nrfcloud-utils"
|
|
7
|
-
version = "3.0
|
|
7
|
+
version = "3.2.0"
|
|
8
8
|
description = "Scripts and utilities for working with the nRF Cloud"
|
|
9
|
-
authors = ["Nordic Semiconductor ASA"]
|
|
10
9
|
license = "BSD-3-Clause"
|
|
11
10
|
readme = "README.md"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
{
|
|
11
|
+
requires-python = ">=3.10,<4.0"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Nordic Semiconductor ASA" }
|
|
14
|
+
]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"cbor2>=5.4.2.post1",
|
|
17
|
+
"cryptography>=46.0.5",
|
|
18
|
+
"cffi>=2.0.0",
|
|
19
|
+
"requests>=2.32.0",
|
|
20
|
+
"urllib3>=2.7.0",
|
|
21
|
+
"semver>=3.0.0",
|
|
22
|
+
"pyjwt>=2.3.0",
|
|
23
|
+
"inquirer>=3.4.0",
|
|
24
|
+
"nrfcredstore>=2.1.0",
|
|
15
25
|
]
|
|
16
26
|
|
|
17
|
-
[
|
|
18
|
-
|
|
19
|
-
cbor2 = "^5.4.2.post1"
|
|
20
|
-
cryptography = "^46.0.5"
|
|
21
|
-
cffi = "^2.0.0"
|
|
22
|
-
requests = "^2.32.0"
|
|
23
|
-
urllib3 = "^2.2.2"
|
|
24
|
-
semver = "^3.0.0"
|
|
25
|
-
pyjwt = "^2.3.0"
|
|
26
|
-
inquirer = "^3.4.0"
|
|
27
|
-
nrfcredstore = "^2.0.9"
|
|
28
|
-
|
|
29
|
-
[tool.poetry.group.dev.dependencies]
|
|
30
|
-
pytest = "^7.3.1"
|
|
31
|
-
pytest-cov = "^4.0.0"
|
|
32
|
-
pytest-watch = "^4.2.0"
|
|
27
|
+
[project.urls]
|
|
28
|
+
repository = "https://github.com/nRFCloud/utils"
|
|
33
29
|
|
|
34
|
-
[
|
|
30
|
+
[project.scripts]
|
|
31
|
+
nrfcloud-utils = "nrfcloud_utils.cli:run"
|
|
35
32
|
claim_and_provision_device = "nrfcloud_utils.claim_and_provision_device:run"
|
|
36
33
|
claim_devices = "nrfcloud_utils.claim_devices:run"
|
|
37
34
|
create_ca_cert = "nrfcloud_utils.create_ca_cert:run"
|
|
@@ -42,6 +39,20 @@ gather_attestation_tokens = "nrfcloud_utils.gather_attestation_tokens:run"
|
|
|
42
39
|
modem_credentials_parser = "nrfcloud_utils.modem_credentials_parser:run"
|
|
43
40
|
nrf_cloud_device_mgmt = "nrfcloud_utils.nrf_cloud_device_mgmt:run"
|
|
44
41
|
nrf_cloud_onboard = "nrfcloud_utils.nrf_cloud_onboard:run"
|
|
42
|
+
nrf93_onboard = "nrfcloud_utils.nrf93_onboard:run"
|
|
43
|
+
|
|
44
|
+
[dependency-groups]
|
|
45
|
+
dev = [
|
|
46
|
+
"pytest>=7.3.1",
|
|
47
|
+
"pytest-cov>=4.0.0",
|
|
48
|
+
"pytest-watch>=4.2.0",
|
|
49
|
+
"poetry (>=2.4.1,<3.0.0)", # used for development today. might be migrated to uv in the future.
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
[tool.poetry]
|
|
53
|
+
packages = [
|
|
54
|
+
{ include = "nrfcloud_utils", from = "src" }
|
|
55
|
+
]
|
|
45
56
|
|
|
46
57
|
[tool.pytest.ini_options]
|
|
47
58
|
addopts = ["--import-mode=importlib"]
|
{nrfcloud_utils-3.0.12 → nrfcloud_utils-3.2.0}/src/nrfcloud_utils/claim_and_provision_device.py
RENAMED
|
@@ -7,7 +7,7 @@ import sys
|
|
|
7
7
|
import time
|
|
8
8
|
import json
|
|
9
9
|
import argparse
|
|
10
|
-
import
|
|
10
|
+
import logging
|
|
11
11
|
from nrfcloud_utils import (
|
|
12
12
|
ca_certs,
|
|
13
13
|
nrf_cloud_onboard,
|
|
@@ -15,8 +15,8 @@ from nrfcloud_utils import (
|
|
|
15
15
|
nrf_cloud_diap,
|
|
16
16
|
create_device_credentials
|
|
17
17
|
)
|
|
18
|
-
from nrfcloud_utils.cli_helpers import
|
|
19
|
-
from nrfcloud_utils.cli_helpers import CMD_TERM_DICT, CMD_TYPE_AUTO, CMD_TYPE_AT, CMD_TYPE_AT_SHELL,
|
|
18
|
+
from nrfcloud_utils.cli_helpers import setup_logging
|
|
19
|
+
from nrfcloud_utils.cli_helpers import CMD_TERM_DICT, CMD_TYPE_AUTO, CMD_TYPE_AT, CMD_TYPE_AT_SHELL, parser_add_comms_args
|
|
20
20
|
from cryptography import x509
|
|
21
21
|
from cryptography.hazmat.primitives import serialization
|
|
22
22
|
from nrfcredstore.comms import Comms
|
|
@@ -27,9 +27,10 @@ logger = logging.getLogger(__name__)
|
|
|
27
27
|
IMEI_LEN = 15
|
|
28
28
|
DEV_ID_MAX_LEN = 64
|
|
29
29
|
|
|
30
|
-
def
|
|
30
|
+
def get_parser():
|
|
31
31
|
parser = argparse.ArgumentParser(description="nRF Cloud Claim and Provision",
|
|
32
|
-
formatter_class=argparse.ArgumentDefaultsHelpFormatter
|
|
32
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
33
|
+
add_help=False)
|
|
33
34
|
parser_add_comms_args(parser)
|
|
34
35
|
parser.add_argument("--dv", type=int, help="Number of days cert is valid",
|
|
35
36
|
default=(10 * 365))
|
|
@@ -81,11 +82,13 @@ def parse_args(in_args):
|
|
|
81
82
|
choices=['debug', 'info', 'warning', 'error', 'critical'],
|
|
82
83
|
help='Set the logging level'
|
|
83
84
|
)
|
|
85
|
+
return parser
|
|
86
|
+
|
|
87
|
+
def parse_args(in_args):
|
|
88
|
+
_p = get_parser()
|
|
89
|
+
parser = argparse.ArgumentParser(parents=[_p], description=_p.description, formatter_class=_p.formatter_class)
|
|
84
90
|
args = parser.parse_args(in_args)
|
|
85
|
-
|
|
86
|
-
logging.basicConfig(level=args.log_level.upper())
|
|
87
|
-
else:
|
|
88
|
-
coloredlogs.install(level=args.log_level.upper(), fmt='%(levelname)-8s %(message)s')
|
|
91
|
+
setup_logging(level=args.log_level, use_color=not args.plain)
|
|
89
92
|
return args
|
|
90
93
|
|
|
91
94
|
def error_exit(err_msg):
|
|
@@ -132,7 +135,7 @@ def main(in_args):
|
|
|
132
135
|
error_exit(f'Device ID must not exceed {DEV_ID_MAX_LEN} characters')
|
|
133
136
|
|
|
134
137
|
if 'CN=' in args.csr_attr:
|
|
135
|
-
error_exit(
|
|
138
|
+
error_exit('Do not include CN in --csr_attr. The device ID will be used as the CN')
|
|
136
139
|
|
|
137
140
|
# load local CA cert and key if needed; assume not needed if using provisioning tags
|
|
138
141
|
if args.provisioning_tags is None:
|
|
@@ -195,7 +198,7 @@ def main(in_args):
|
|
|
195
198
|
logger.info(f'Unclaiming device {dev_uuid}...')
|
|
196
199
|
api_res = nrf_cloud_diap.unclaim_device(args.api_key, dev_uuid)
|
|
197
200
|
if api_res.status_code == 204:
|
|
198
|
-
logger.info(
|
|
201
|
+
logger.info('...success')
|
|
199
202
|
else:
|
|
200
203
|
nrf_cloud_diap.print_api_result("Unclaim device response", api_res)
|
|
201
204
|
logger.info("Device may not have been claimed before, continuing...")
|
|
@@ -7,9 +7,9 @@ import io
|
|
|
7
7
|
import sys
|
|
8
8
|
import csv
|
|
9
9
|
import argparse
|
|
10
|
-
import
|
|
10
|
+
import logging
|
|
11
11
|
from nrfcloud_utils import nrf_cloud_diap
|
|
12
|
-
from nrfcloud_utils.cli_helpers import
|
|
12
|
+
from nrfcloud_utils.cli_helpers import setup_logging
|
|
13
13
|
|
|
14
14
|
logger = logging.getLogger(__name__)
|
|
15
15
|
|
|
@@ -17,9 +17,10 @@ args = None
|
|
|
17
17
|
IMEI_LEN = 15
|
|
18
18
|
MAX_CSV_ROWS = 1000
|
|
19
19
|
|
|
20
|
-
def
|
|
20
|
+
def get_parser():
|
|
21
21
|
parser = argparse.ArgumentParser(description="nRF Cloud Claim Devices",
|
|
22
|
-
formatter_class=argparse.ArgumentDefaultsHelpFormatter
|
|
22
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
23
|
+
add_help=False)
|
|
23
24
|
|
|
24
25
|
parser.add_argument("--csv", type=str,
|
|
25
26
|
help="Filepath to attestation token CSV file",
|
|
@@ -39,11 +40,13 @@ def parse_args(in_args):
|
|
|
39
40
|
choices=['debug', 'info', 'warning', 'error', 'critical'],
|
|
40
41
|
help='Set the logging level'
|
|
41
42
|
)
|
|
43
|
+
return parser
|
|
44
|
+
|
|
45
|
+
def parse_args(in_args):
|
|
46
|
+
_p = get_parser()
|
|
47
|
+
parser = argparse.ArgumentParser(parents=[_p], description=_p.description, formatter_class=_p.formatter_class)
|
|
42
48
|
args = parser.parse_args(in_args)
|
|
43
|
-
|
|
44
|
-
logging.basicConfig(level=args.log_level.upper())
|
|
45
|
-
else:
|
|
46
|
-
coloredlogs.install(level=args.log_level.upper(), fmt='%(levelname)-8s %(message)s')
|
|
49
|
+
setup_logging(level=args.log_level, use_color=not args.plain)
|
|
47
50
|
return args
|
|
48
51
|
|
|
49
52
|
def bulk_claim(api_key, array_of_claims):
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2025 Nordic Semiconductor ASA
|
|
4
|
+
#
|
|
5
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
6
|
+
import argparse
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from nrfcloud_utils import (
|
|
10
|
+
claim_and_provision_device,
|
|
11
|
+
claim_devices,
|
|
12
|
+
create_ca_cert,
|
|
13
|
+
create_device_credentials,
|
|
14
|
+
create_proxy_jwt,
|
|
15
|
+
device_credentials_installer,
|
|
16
|
+
gather_attestation_tokens,
|
|
17
|
+
modem_credentials_parser,
|
|
18
|
+
nrf_cloud_device_mgmt,
|
|
19
|
+
nrf_cloud_onboard,
|
|
20
|
+
nrf93_onboard,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
_MODULES = {
|
|
24
|
+
"claim_and_provision_device": claim_and_provision_device,
|
|
25
|
+
"claim_devices": claim_devices,
|
|
26
|
+
"create_ca_cert": create_ca_cert,
|
|
27
|
+
"create_device_credentials": create_device_credentials,
|
|
28
|
+
"create_proxy_jwt": create_proxy_jwt,
|
|
29
|
+
"device_credentials_installer": device_credentials_installer,
|
|
30
|
+
"gather_attestation_tokens": gather_attestation_tokens,
|
|
31
|
+
"modem_credentials_parser": modem_credentials_parser,
|
|
32
|
+
"nrf_cloud_device_mgmt": nrf_cloud_device_mgmt,
|
|
33
|
+
"nrf_cloud_onboard": nrf_cloud_onboard,
|
|
34
|
+
"nrf93_onboard": nrf93_onboard,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _build_parser():
|
|
39
|
+
parser = argparse.ArgumentParser(
|
|
40
|
+
prog="nrfcloud-utils",
|
|
41
|
+
description="Scripts and utilities for working with nRF Cloud",
|
|
42
|
+
)
|
|
43
|
+
subparsers = parser.add_subparsers(dest="command", metavar="command", required=False)
|
|
44
|
+
for name, mod in _MODULES.items():
|
|
45
|
+
_p = mod.get_parser()
|
|
46
|
+
subparsers.add_parser(
|
|
47
|
+
name,
|
|
48
|
+
parents=[_p],
|
|
49
|
+
description=_p.description,
|
|
50
|
+
formatter_class=_p.formatter_class,
|
|
51
|
+
help=_p.description,
|
|
52
|
+
)
|
|
53
|
+
return parser
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def run():
|
|
57
|
+
parser = _build_parser()
|
|
58
|
+
# parse_args() handles --help at both levels and validates the subcommand name
|
|
59
|
+
# and its args (using each module's get_parser() for the subparser definition).
|
|
60
|
+
# We then re-dispatch to the module's own main() so setup_logging and other
|
|
61
|
+
# module init runs exactly as it does when the command is called standalone.
|
|
62
|
+
args = parser.parse_args()
|
|
63
|
+
|
|
64
|
+
if not args.command:
|
|
65
|
+
parser.print_help()
|
|
66
|
+
sys.exit(0)
|
|
67
|
+
|
|
68
|
+
cmd = args.command
|
|
69
|
+
sys.argv = [cmd] + sys.argv[2:]
|
|
70
|
+
_MODULES[cmd].main(sys.argv[1:])
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
run()
|
|
@@ -10,6 +10,7 @@ import platform
|
|
|
10
10
|
import os
|
|
11
11
|
import csv
|
|
12
12
|
import logging
|
|
13
|
+
import coloredlogs
|
|
13
14
|
|
|
14
15
|
MAX_CSV_ROWS = 1000
|
|
15
16
|
|
|
@@ -20,6 +21,39 @@ full_encoding = 'mbcs' if is_windows else 'ascii'
|
|
|
20
21
|
|
|
21
22
|
logger = logging.getLogger(__name__)
|
|
22
23
|
|
|
24
|
+
def setup_logging(level='info', fmt='%(levelname)-8s %(message)s', use_color=True):
|
|
25
|
+
"""
|
|
26
|
+
Configure colored logging with dark-terminal-friendly colors.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
level: logging level as string (debug, info, warning, error, critical)
|
|
30
|
+
fmt: log format string
|
|
31
|
+
use_color: whether to use colored output (False for --plain mode)
|
|
32
|
+
"""
|
|
33
|
+
log_level = getattr(logging, level.upper(), logging.INFO)
|
|
34
|
+
|
|
35
|
+
if use_color:
|
|
36
|
+
# Define colors for dark terminals with better contrast
|
|
37
|
+
level_styles = {
|
|
38
|
+
'debug': {'color': 'cyan'},
|
|
39
|
+
'info': {'color': 'green'},
|
|
40
|
+
'warning': {'color': 'yellow'},
|
|
41
|
+
'error': {'color': 'red'},
|
|
42
|
+
'critical': {'color': 'red', 'bold': True},
|
|
43
|
+
}
|
|
44
|
+
field_styles = {
|
|
45
|
+
'asctime': {'color': 'white'},
|
|
46
|
+
'levelname': {'color': 'white', 'bold': True},
|
|
47
|
+
}
|
|
48
|
+
coloredlogs.install(
|
|
49
|
+
level=log_level,
|
|
50
|
+
fmt=fmt,
|
|
51
|
+
level_styles=level_styles,
|
|
52
|
+
field_styles=field_styles
|
|
53
|
+
)
|
|
54
|
+
else:
|
|
55
|
+
logging.basicConfig(level=log_level, format=fmt)
|
|
56
|
+
|
|
23
57
|
def write_file(pathname, filename, bytes):
|
|
24
58
|
"""
|
|
25
59
|
save bytes to file
|
|
@@ -28,7 +62,7 @@ def write_file(pathname, filename, bytes):
|
|
|
28
62
|
if not path.isdir(pathname):
|
|
29
63
|
try:
|
|
30
64
|
makedirs(pathname, exist_ok=True)
|
|
31
|
-
except OSError
|
|
65
|
+
except OSError:
|
|
32
66
|
raise RuntimeError(f"Error creating file path [{pathname}]")
|
|
33
67
|
|
|
34
68
|
full_path = path.join(pathname, filename)
|
|
@@ -22,13 +22,14 @@ from cryptography.x509 import (
|
|
|
22
22
|
SubjectKeyIdentifier,
|
|
23
23
|
)
|
|
24
24
|
|
|
25
|
-
from nrfcloud_utils.cli_helpers import write_file
|
|
26
|
-
import
|
|
25
|
+
from nrfcloud_utils.cli_helpers import write_file, setup_logging
|
|
26
|
+
import logging
|
|
27
27
|
|
|
28
28
|
logger = logging.getLogger(__name__)
|
|
29
29
|
|
|
30
|
-
def
|
|
31
|
-
parser = argparse.ArgumentParser(description="Create CA Certificate"
|
|
30
|
+
def get_parser():
|
|
31
|
+
parser = argparse.ArgumentParser(description="Create CA Certificate",
|
|
32
|
+
add_help=False)
|
|
32
33
|
parser.add_argument("-c", type=str, help="2 character country code", default="NO")
|
|
33
34
|
parser.add_argument("--st", type=str, help="State or Province", default="")
|
|
34
35
|
parser.add_argument("-l", type=str, help="Locality", default="")
|
|
@@ -50,10 +51,13 @@ def parse_args(in_args):
|
|
|
50
51
|
choices=['debug', 'info', 'warning', 'error', 'critical'],
|
|
51
52
|
help='Set the logging level'
|
|
52
53
|
)
|
|
54
|
+
return parser
|
|
55
|
+
|
|
56
|
+
def parse_args(in_args):
|
|
57
|
+
_p = get_parser()
|
|
58
|
+
parser = argparse.ArgumentParser(parents=[_p], description=_p.description, formatter_class=_p.formatter_class)
|
|
53
59
|
args = parser.parse_args(in_args)
|
|
54
|
-
level
|
|
55
|
-
fmt = '%(levelname)-8s %(message)s'
|
|
56
|
-
coloredlogs.install(level=level, fmt=fmt)
|
|
60
|
+
setup_logging(level=args.log_level)
|
|
57
61
|
return args
|
|
58
62
|
|
|
59
63
|
|
{nrfcloud_utils-3.0.12 → nrfcloud_utils-3.2.0}/src/nrfcloud_utils/create_device_credentials.py
RENAMED
|
@@ -15,22 +15,18 @@ from cryptography.x509.oid import NameOID
|
|
|
15
15
|
from cryptography import x509
|
|
16
16
|
import uuid
|
|
17
17
|
from cryptography.x509 import (
|
|
18
|
-
Name,
|
|
19
|
-
NameAttribute,
|
|
20
|
-
BasicConstraints,
|
|
21
18
|
KeyUsage,
|
|
22
|
-
AuthorityKeyIdentifier,
|
|
23
|
-
SubjectKeyIdentifier,
|
|
24
19
|
)
|
|
25
20
|
|
|
26
|
-
from nrfcloud_utils.cli_helpers import write_file, save_onboarding_csv
|
|
21
|
+
from nrfcloud_utils.cli_helpers import write_file, save_onboarding_csv, setup_logging
|
|
27
22
|
from nrfcloud_utils import ca_certs
|
|
28
|
-
import
|
|
23
|
+
import logging
|
|
29
24
|
|
|
30
25
|
logger = logging.getLogger(__name__)
|
|
31
26
|
|
|
32
|
-
def
|
|
33
|
-
parser = argparse.ArgumentParser(description="Create Device Credentials"
|
|
27
|
+
def get_parser():
|
|
28
|
+
parser = argparse.ArgumentParser(description="Create Device Credentials",
|
|
29
|
+
add_help=False)
|
|
34
30
|
parser.add_argument("--ca", type=str, required=True, help="Filepath to your CA cert PEM", default="")
|
|
35
31
|
parser.add_argument("--ca-key", type=str, required=True, help="Filepath to your CA's private key PEM", default="")
|
|
36
32
|
parser.add_argument("-c", type=str, help="2 character country code; required if CSR is not provided", default="NO")
|
|
@@ -60,10 +56,13 @@ def parse_args(in_args):
|
|
|
60
56
|
choices=['debug', 'info', 'warning', 'error', 'critical'],
|
|
61
57
|
help='Set the logging level'
|
|
62
58
|
)
|
|
59
|
+
return parser
|
|
60
|
+
|
|
61
|
+
def parse_args(in_args):
|
|
62
|
+
_p = get_parser()
|
|
63
|
+
parser = argparse.ArgumentParser(parents=[_p], description=_p.description, formatter_class=_p.formatter_class)
|
|
63
64
|
args = parser.parse_args(in_args)
|
|
64
|
-
level
|
|
65
|
-
fmt = '%(levelname)-8s %(message)s'
|
|
66
|
-
coloredlogs.install(level=level, fmt=fmt)
|
|
65
|
+
setup_logging(level=args.log_level)
|
|
67
66
|
if len(args.csr) == 0 and len(args.cn) == 0:
|
|
68
67
|
args.cn = str(uuid.uuid4())
|
|
69
68
|
return args
|
|
@@ -9,13 +9,15 @@ import sys
|
|
|
9
9
|
import jwt
|
|
10
10
|
from datetime import datetime, timezone, timedelta
|
|
11
11
|
from os import path
|
|
12
|
-
import
|
|
12
|
+
import logging
|
|
13
|
+
from nrfcloud_utils.cli_helpers import setup_logging
|
|
13
14
|
|
|
14
15
|
logger = logging.getLogger(__name__)
|
|
15
16
|
|
|
16
|
-
def
|
|
17
|
+
def get_parser():
|
|
17
18
|
parser = argparse.ArgumentParser(description="Create JWT for proxy (cloud-to-cloud) requests to nRF Cloud",
|
|
18
|
-
formatter_class=argparse.ArgumentDefaultsHelpFormatter
|
|
19
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
20
|
+
add_help=False)
|
|
19
21
|
|
|
20
22
|
parser.add_argument("--key", type=str, required=True,
|
|
21
23
|
help="Required filepath to the ES256 private key PEM (Service Key) used for JWT signing. \
|
|
@@ -42,10 +44,13 @@ def parse_args(in_args):
|
|
|
42
44
|
choices=['debug', 'info', 'warning', 'error', 'critical'],
|
|
43
45
|
help='Set the logging level'
|
|
44
46
|
)
|
|
47
|
+
return parser
|
|
48
|
+
|
|
49
|
+
def parse_args(in_args):
|
|
50
|
+
_p = get_parser()
|
|
51
|
+
parser = argparse.ArgumentParser(parents=[_p], description=_p.description, formatter_class=_p.formatter_class)
|
|
45
52
|
args = parser.parse_args(in_args)
|
|
46
|
-
level
|
|
47
|
-
fmt = '%(levelname)-8s %(message)s'
|
|
48
|
-
coloredlogs.install(level=level, fmt=fmt)
|
|
53
|
+
setup_logging(level=args.log_level)
|
|
49
54
|
return args
|
|
50
55
|
|
|
51
56
|
def create_nrf_cloud_jwt(prv_key_bytes, team_id, dev_id, days_valid):
|
{nrfcloud_utils-3.0.12 → nrfcloud_utils-3.2.0}/src/nrfcloud_utils/device_credentials_installer.py
RENAMED
|
@@ -10,9 +10,9 @@ import os
|
|
|
10
10
|
import sys
|
|
11
11
|
import uuid
|
|
12
12
|
import semver
|
|
13
|
-
import
|
|
13
|
+
import logging
|
|
14
14
|
from nrfcloud_utils import create_device_credentials, ca_certs, modem_credentials_parser
|
|
15
|
-
from nrfcloud_utils.cli_helpers import write_file, save_devinfo_csv, save_onboarding_csv,
|
|
15
|
+
from nrfcloud_utils.cli_helpers import write_file, save_devinfo_csv, save_onboarding_csv, full_encoding, setup_logging
|
|
16
16
|
from nrfcloud_utils.cli_helpers import CMD_TERM_DICT, CMD_TYPE_AUTO, CMD_TYPE_AT, CMD_TYPE_AT_SHELL, CMD_TYPE_TLS_SHELL, parser_add_comms_args
|
|
17
17
|
from nrfcredstore.command_interface import ATCommandInterface, TLSCredShellInterface
|
|
18
18
|
from nrfcredstore.comms import Comms
|
|
@@ -27,9 +27,10 @@ DEV_ID_MAX_LEN = 64
|
|
|
27
27
|
MIN_REQD_MFW_VER = "1.3.0"
|
|
28
28
|
MIN_REQD_MFW_VER_FOR_VERIFY = "1.3.2"
|
|
29
29
|
|
|
30
|
-
def
|
|
30
|
+
def get_parser():
|
|
31
31
|
parser = argparse.ArgumentParser(description="Device Credentials Installer",
|
|
32
|
-
formatter_class=argparse.ArgumentDefaultsHelpFormatter
|
|
32
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
33
|
+
add_help=False)
|
|
33
34
|
parser_add_comms_args(parser)
|
|
34
35
|
parser.add_argument("--dv", type=int, help="Number of days cert is valid",
|
|
35
36
|
default=(10 * 365))
|
|
@@ -105,11 +106,13 @@ def parse_args(in_args):
|
|
|
105
106
|
choices=['debug', 'info', 'warning', 'error', 'critical'],
|
|
106
107
|
help='Set the logging level'
|
|
107
108
|
)
|
|
109
|
+
return parser
|
|
110
|
+
|
|
111
|
+
def parse_args(in_args):
|
|
112
|
+
_p = get_parser()
|
|
113
|
+
parser = argparse.ArgumentParser(parents=[_p], description=_p.description, formatter_class=_p.formatter_class)
|
|
108
114
|
args = parser.parse_args(in_args)
|
|
109
|
-
|
|
110
|
-
logging.basicConfig(level=args.log_level.upper())
|
|
111
|
-
else:
|
|
112
|
-
coloredlogs.install(level=args.log_level.upper(), fmt='%(levelname)-8s %(message)s')
|
|
115
|
+
setup_logging(level=args.log_level, use_color=not args.plain)
|
|
113
116
|
return args
|
|
114
117
|
|
|
115
118
|
def parse_mfw_ver(ver_str):
|
|
@@ -380,17 +383,32 @@ def main(in_args):
|
|
|
380
383
|
# write CA cert(s) to device
|
|
381
384
|
nrf_ca_cert_text = format_cred(ca_certs.get_ca_certs(args.coap, stage=args.stage))
|
|
382
385
|
|
|
383
|
-
logger.info(
|
|
384
|
-
|
|
386
|
+
logger.info('Writing CA cert(s) to device...')
|
|
387
|
+
try:
|
|
388
|
+
cred_if.write_credential(args.sectag, 0, nrf_ca_cert_text)
|
|
389
|
+
except Exception as e:
|
|
390
|
+
logger.error('Failed to write CA certificate to device')
|
|
391
|
+
logger.debug(f'Error details: {e}')
|
|
392
|
+
raise
|
|
385
393
|
|
|
386
394
|
# write dev cert to device
|
|
387
|
-
logger.info(
|
|
388
|
-
|
|
395
|
+
logger.info('Writing dev cert to device...')
|
|
396
|
+
try:
|
|
397
|
+
cred_if.write_credential(args.sectag, 1, dev_text)
|
|
398
|
+
except Exception as e:
|
|
399
|
+
logger.error('Failed to write device certificate to device')
|
|
400
|
+
logger.debug(f'Error details: {e}')
|
|
401
|
+
raise
|
|
389
402
|
|
|
390
403
|
# If the private key was locally generated, write it to the device
|
|
391
404
|
if prv_text is not None:
|
|
392
|
-
logger.info(
|
|
393
|
-
|
|
405
|
+
logger.info('Writing private key to device...')
|
|
406
|
+
try:
|
|
407
|
+
cred_if.write_credential(args.sectag, 2, prv_text)
|
|
408
|
+
except Exception as e:
|
|
409
|
+
logger.error('Failed to write private key to device')
|
|
410
|
+
logger.debug(f'Error details: {e}')
|
|
411
|
+
raise
|
|
394
412
|
|
|
395
413
|
if args.verify:
|
|
396
414
|
logger.info('Verifying credentials...')
|
|
@@ -451,7 +469,14 @@ def verify_credential(cred_if, sec_tag, cred_type, cred = None, get_hash = False
|
|
|
451
469
|
logger.error('Invalid credential string')
|
|
452
470
|
return False
|
|
453
471
|
|
|
454
|
-
|
|
472
|
+
try:
|
|
473
|
+
present, hash = cred_if.check_credential_exists(sec_tag, cred_type, get_hash = get_hash)
|
|
474
|
+
except IndexError as e:
|
|
475
|
+
logger.error(f'...Failed to verify {cred_type_name}: Modem returned malformed response')
|
|
476
|
+
logger.debug(f'Error details: {e}')
|
|
477
|
+
logger.error('This may indicate the credential was not properly written to the device.')
|
|
478
|
+
logger.error('Try re-running the provisioning process.')
|
|
479
|
+
return False
|
|
455
480
|
|
|
456
481
|
if not present:
|
|
457
482
|
logger.error(f'...{cred_type_name} not found')
|
|
@@ -462,7 +487,13 @@ def verify_credential(cred_if, sec_tag, cred_type, cred = None, get_hash = False
|
|
|
462
487
|
return False
|
|
463
488
|
|
|
464
489
|
if verify_hash:
|
|
465
|
-
|
|
490
|
+
try:
|
|
491
|
+
expected_hash = cred_if.calculate_expected_hash(cred)
|
|
492
|
+
except (IndexError, Exception) as e:
|
|
493
|
+
logger.error(f'...Failed to calculate hash for {cred_type_name}')
|
|
494
|
+
logger.debug(f'Error details: {e}')
|
|
495
|
+
return False
|
|
496
|
+
|
|
466
497
|
if hash != expected_hash:
|
|
467
498
|
logger.error(f'{cred_type_name} - SHA mismatch:')
|
|
468
499
|
logger.error(f'\tDevice : {hash}')
|
{nrfcloud_utils-3.0.12 → nrfcloud_utils-3.2.0}/src/nrfcloud_utils/gather_attestation_tokens.py
RENAMED
|
@@ -8,20 +8,21 @@ import sys
|
|
|
8
8
|
import csv
|
|
9
9
|
import argparse
|
|
10
10
|
from nrfcloud_utils import modem_credentials_parser
|
|
11
|
-
from nrfcloud_utils.cli_helpers import
|
|
12
|
-
from nrfcloud_utils.cli_helpers import CMD_TERM_DICT, CMD_TYPE_AUTO, CMD_TYPE_AT, CMD_TYPE_AT_SHELL,
|
|
11
|
+
from nrfcloud_utils.cli_helpers import setup_logging
|
|
12
|
+
from nrfcloud_utils.cli_helpers import CMD_TERM_DICT, CMD_TYPE_AUTO, CMD_TYPE_AT, CMD_TYPE_AT_SHELL, parser_add_comms_args
|
|
13
13
|
from nrfcredstore.comms import Comms
|
|
14
14
|
from nrfcredstore.command_interface import ATCommandInterface
|
|
15
15
|
from datetime import datetime, timezone
|
|
16
|
-
import
|
|
16
|
+
import logging
|
|
17
17
|
|
|
18
18
|
logger = logging.getLogger(__name__)
|
|
19
19
|
|
|
20
20
|
IMEI_LEN = 15
|
|
21
21
|
|
|
22
|
-
def
|
|
22
|
+
def get_parser():
|
|
23
23
|
parser = argparse.ArgumentParser(description="Gather Attestation Tokens",
|
|
24
|
-
formatter_class=argparse.ArgumentDefaultsHelpFormatter
|
|
24
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
25
|
+
add_help=False)
|
|
25
26
|
parser_add_comms_args(parser)
|
|
26
27
|
parser.add_argument("--csv", type=str,
|
|
27
28
|
help="Filepath to attestation token CSV file",
|
|
@@ -40,11 +41,13 @@ def parse_args(in_args):
|
|
|
40
41
|
choices=['debug', 'info', 'warning', 'error', 'critical'],
|
|
41
42
|
help='Set the logging level'
|
|
42
43
|
)
|
|
44
|
+
return parser
|
|
45
|
+
|
|
46
|
+
def parse_args(in_args):
|
|
47
|
+
_p = get_parser()
|
|
48
|
+
parser = argparse.ArgumentParser(parents=[_p], description=_p.description, formatter_class=_p.formatter_class)
|
|
43
49
|
args = parser.parse_args(in_args)
|
|
44
|
-
|
|
45
|
-
logging.basicConfig(level=args.log_level.upper())
|
|
46
|
-
else:
|
|
47
|
-
coloredlogs.install(level=args.log_level.upper(), fmt='%(levelname)-8s %(message)s')
|
|
50
|
+
setup_logging(level=args.log_level, use_color=not args.plain)
|
|
48
51
|
return args
|
|
49
52
|
|
|
50
53
|
def check_if_device_exists_in_csv(csv_filename, uuid, delete_duplicates):
|
{nrfcloud_utils-3.0.12 → nrfcloud_utils-3.2.0}/src/nrfcloud_utils/modem_credentials_parser.py
RENAMED
|
@@ -6,15 +6,14 @@
|
|
|
6
6
|
|
|
7
7
|
import argparse
|
|
8
8
|
import sys
|
|
9
|
-
from os import path
|
|
10
|
-
from os import makedirs
|
|
11
9
|
from cbor2 import loads
|
|
12
10
|
import base64
|
|
13
11
|
import hashlib
|
|
14
|
-
import
|
|
12
|
+
import logging
|
|
13
|
+
import textwrap
|
|
15
14
|
from cryptography.hazmat.primitives import serialization
|
|
16
15
|
from cryptography import x509
|
|
17
|
-
from nrfcloud_utils.cli_helpers import write_file
|
|
16
|
+
from nrfcloud_utils.cli_helpers import write_file, setup_logging
|
|
18
17
|
|
|
19
18
|
logger = logging.getLogger(__name__)
|
|
20
19
|
|
|
@@ -43,8 +42,9 @@ header_key_type_dict = {
|
|
|
43
42
|
-5: 'nordic_base_rd_key'
|
|
44
43
|
}
|
|
45
44
|
|
|
46
|
-
def
|
|
47
|
-
parser = argparse.ArgumentParser(description="Modem Credentials Parser"
|
|
45
|
+
def get_parser():
|
|
46
|
+
parser = argparse.ArgumentParser(description="Modem Credentials Parser",
|
|
47
|
+
add_help=False)
|
|
48
48
|
parser.add_argument("-k", "--keygen", type=str, help="base64url string: KEYGEN output", default="")
|
|
49
49
|
parser.add_argument("-a", "--attest", type=str, help="base64url string: ATTESTTOKEN output", default="")
|
|
50
50
|
parser.add_argument("-s", "--save", action='store_true', help="Save PEM file(s): <UUID>_<sec_tag>_<type>.pem")
|
|
@@ -58,21 +58,137 @@ def parse_args(in_args):
|
|
|
58
58
|
parser.add_argument("-P", "--plain",
|
|
59
59
|
help="bool: Plain output (no colors)",
|
|
60
60
|
action='store_true', default=False)
|
|
61
|
+
return parser
|
|
62
|
+
|
|
63
|
+
def parse_args(in_args):
|
|
64
|
+
_p = get_parser()
|
|
65
|
+
parser = argparse.ArgumentParser(parents=[_p], description=_p.description, formatter_class=_p.formatter_class)
|
|
61
66
|
args = parser.parse_args(in_args)
|
|
62
|
-
|
|
63
|
-
logging.basicConfig(level=args.log_level.upper())
|
|
64
|
-
else:
|
|
65
|
-
coloredlogs.install(level=args.log_level.upper(), fmt='%(levelname)-8s %(message)s')
|
|
67
|
+
setup_logging(level=args.log_level, use_color=not args.plain)
|
|
66
68
|
return args
|
|
67
69
|
|
|
68
70
|
def base64_decode(string):
|
|
69
71
|
"""
|
|
70
72
|
add padding before decoding.
|
|
71
73
|
"""
|
|
72
|
-
padding
|
|
74
|
+
# Base64url data can omit padding; only add what is required.
|
|
75
|
+
padding = (-len(string)) % 4
|
|
73
76
|
string = string + ("=" * padding)
|
|
74
77
|
return base64.urlsafe_b64decode(string)
|
|
75
78
|
|
|
79
|
+
def _decode_tlv_length(data, offset):
|
|
80
|
+
"""Decode DER length and return (value_length, header_length)."""
|
|
81
|
+
first = data[offset]
|
|
82
|
+
if first < 0x80:
|
|
83
|
+
return first, 1
|
|
84
|
+
|
|
85
|
+
nbytes = first & 0x7F
|
|
86
|
+
if nbytes == 0:
|
|
87
|
+
raise ValueError("Indefinite DER length is not supported")
|
|
88
|
+
|
|
89
|
+
value_len = 0
|
|
90
|
+
for i in range(nbytes):
|
|
91
|
+
value_len = (value_len << 8) | data[offset + 1 + i]
|
|
92
|
+
return value_len, 1 + nbytes
|
|
93
|
+
|
|
94
|
+
def _encode_tlv_length(value_len):
|
|
95
|
+
"""Encode DER length bytes for value_len."""
|
|
96
|
+
if value_len < 0x80:
|
|
97
|
+
return bytes([value_len])
|
|
98
|
+
|
|
99
|
+
out = []
|
|
100
|
+
value = value_len
|
|
101
|
+
while value > 0:
|
|
102
|
+
out.append(value & 0xFF)
|
|
103
|
+
value >>= 8
|
|
104
|
+
out.reverse()
|
|
105
|
+
return bytes([0x80 | len(out), *out])
|
|
106
|
+
|
|
107
|
+
def _normalize_ecdsa_csr_der(der_bytes):
|
|
108
|
+
"""
|
|
109
|
+
Normalize CSR DER that encodes ecdsa-with-SHA256 with NULL params.
|
|
110
|
+
|
|
111
|
+
Older modem output may include signatureAlgorithm = SEQUENCE(OID, NULL).
|
|
112
|
+
For ECDSA-with-SHA* this must be encoded without parameters.
|
|
113
|
+
"""
|
|
114
|
+
# ecdsa-with-SHA256 OID (1.2.840.10045.4.3.2)
|
|
115
|
+
ecdsa_sha256_oid_tlv = b"\x06\x08\x2A\x86\x48\xCE\x3D\x04\x03\x02"
|
|
116
|
+
|
|
117
|
+
if len(der_bytes) < 8 or der_bytes[0] != 0x30:
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
outer_len, outer_len_hdr = _decode_tlv_length(der_bytes, 1)
|
|
122
|
+
except (IndexError, ValueError):
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
outer_start = 1 + outer_len_hdr
|
|
126
|
+
outer_end = outer_start + outer_len
|
|
127
|
+
if outer_end != len(der_bytes):
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
# certificationRequestInfo
|
|
131
|
+
cri_tag_idx = outer_start
|
|
132
|
+
if der_bytes[cri_tag_idx] != 0x30:
|
|
133
|
+
return None
|
|
134
|
+
try:
|
|
135
|
+
cri_len, cri_len_hdr = _decode_tlv_length(der_bytes, cri_tag_idx + 1)
|
|
136
|
+
except (IndexError, ValueError):
|
|
137
|
+
return None
|
|
138
|
+
cri_start = cri_tag_idx
|
|
139
|
+
cri_value_start = cri_tag_idx + 1 + cri_len_hdr
|
|
140
|
+
cri_end = cri_value_start + cri_len
|
|
141
|
+
|
|
142
|
+
# signatureAlgorithm
|
|
143
|
+
sig_alg_tag_idx = cri_end
|
|
144
|
+
if sig_alg_tag_idx >= outer_end or der_bytes[sig_alg_tag_idx] != 0x30:
|
|
145
|
+
return None
|
|
146
|
+
try:
|
|
147
|
+
sig_alg_len, sig_alg_len_hdr = _decode_tlv_length(der_bytes, sig_alg_tag_idx + 1)
|
|
148
|
+
except (IndexError, ValueError):
|
|
149
|
+
return None
|
|
150
|
+
sig_alg_value_start = sig_alg_tag_idx + 1 + sig_alg_len_hdr
|
|
151
|
+
sig_alg_end = sig_alg_value_start + sig_alg_len
|
|
152
|
+
if sig_alg_end > outer_end:
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
sig_alg_value = der_bytes[sig_alg_value_start:sig_alg_end]
|
|
156
|
+
# Only normalize the exact legacy ECDSA+NULL form.
|
|
157
|
+
if not (sig_alg_value.startswith(ecdsa_sha256_oid_tlv) and sig_alg_value.endswith(b"\x05\x00")):
|
|
158
|
+
return None
|
|
159
|
+
if len(sig_alg_value) != len(ecdsa_sha256_oid_tlv) + 2:
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
# signature BIT STRING (keep as-is)
|
|
163
|
+
sig_tag_idx = sig_alg_end
|
|
164
|
+
if sig_tag_idx >= outer_end or der_bytes[sig_tag_idx] != 0x03:
|
|
165
|
+
return None
|
|
166
|
+
try:
|
|
167
|
+
sig_len, sig_len_hdr = _decode_tlv_length(der_bytes, sig_tag_idx + 1)
|
|
168
|
+
except (IndexError, ValueError):
|
|
169
|
+
return None
|
|
170
|
+
sig_value_start = sig_tag_idx + 1 + sig_len_hdr
|
|
171
|
+
sig_end = sig_value_start + sig_len
|
|
172
|
+
if sig_end != outer_end:
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
cri_tlv = der_bytes[cri_start:cri_end]
|
|
176
|
+
normalized_sig_alg = b"\x30" + _encode_tlv_length(len(ecdsa_sha256_oid_tlv)) + ecdsa_sha256_oid_tlv
|
|
177
|
+
sig_tlv = der_bytes[sig_tag_idx:sig_end]
|
|
178
|
+
|
|
179
|
+
new_outer_value = cri_tlv + normalized_sig_alg + sig_tlv
|
|
180
|
+
return b"\x30" + _encode_tlv_length(len(new_outer_value)) + new_outer_value
|
|
181
|
+
|
|
182
|
+
def _csr_der_to_pem(der_bytes):
|
|
183
|
+
"""Convert CSR DER bytes to PEM without changing the original DER payload."""
|
|
184
|
+
b64 = base64.b64encode(der_bytes).decode("ascii")
|
|
185
|
+
wrapped = "\n".join(textwrap.wrap(b64, 64))
|
|
186
|
+
return (
|
|
187
|
+
"-----BEGIN CERTIFICATE REQUEST-----\n"
|
|
188
|
+
+ wrapped
|
|
189
|
+
+ "\n-----END CERTIFICATE REQUEST-----\n"
|
|
190
|
+
).encode("ascii")
|
|
191
|
+
|
|
76
192
|
def format_uuid(hex_str):
|
|
77
193
|
return '{0}-{1}-{2}-{3}-{4}'.format(hex_str[:8], hex_str[8:12],
|
|
78
194
|
hex_str[12:16], hex_str[16:20],
|
|
@@ -178,30 +294,26 @@ def parse_keygen_output(keygen_str):
|
|
|
178
294
|
body = body_cose[0]
|
|
179
295
|
|
|
180
296
|
# Decode base64url to binary
|
|
181
|
-
|
|
297
|
+
payload_body_bytes = base64_decode(body)
|
|
298
|
+
body_bytes = payload_body_bytes
|
|
182
299
|
|
|
183
|
-
# This can be either a CSR or device public key
|
|
184
300
|
try:
|
|
185
|
-
# Try to load CSR, if it fails, assume public key
|
|
186
301
|
csr = x509.load_der_x509_csr(body_bytes)
|
|
302
|
+
except:
|
|
303
|
+
logger.warning("normalizing CSR")
|
|
304
|
+
normalized = _normalize_ecdsa_csr_der(body_bytes)
|
|
305
|
+
logger.warning(f"keygen_str: {keygen_str}, normalized: {base64.urlsafe_b64encode(normalized)}")
|
|
306
|
+
csr = x509.load_der_x509_csr(normalized)
|
|
187
307
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
pub_key = serialization.load_der_public_key(body_bytes)
|
|
191
|
-
pub_key_bytes = pub_key.public_bytes(serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo)
|
|
192
|
-
|
|
193
|
-
else:
|
|
194
|
-
# CSR loaded, logger.info it
|
|
195
|
-
csr_pem_bytes = csr.public_bytes(serialization.Encoding.PEM)
|
|
196
|
-
csr_pem_list = str(csr_pem_bytes.decode()).split('\n')
|
|
197
|
-
logger.info(csr_pem_bytes.decode().replace('\n', '\\n'))
|
|
308
|
+
csr_pem_bytes = csr.public_bytes(serialization.Encoding.PEM)
|
|
309
|
+
logger.info(csr_pem_bytes.decode().replace('\n', '\\n'))
|
|
198
310
|
|
|
199
|
-
|
|
200
|
-
|
|
311
|
+
# Extract public key
|
|
312
|
+
pub_key_bytes = csr.public_key().public_bytes(serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo)
|
|
201
313
|
|
|
202
314
|
logger.info("Device public key: {}".format(pub_key_bytes.decode().replace('\n', '\\n')))
|
|
203
315
|
|
|
204
|
-
payload_digest = hashlib.sha256(
|
|
316
|
+
payload_digest = hashlib.sha256(payload_body_bytes).hexdigest()
|
|
205
317
|
logger.info(f"SHA256 Digest: {payload_digest}")
|
|
206
318
|
|
|
207
319
|
# Get optional cose
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2026 Nordic Semiconductor ASA
|
|
4
|
+
#
|
|
5
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import re
|
|
9
|
+
import sys
|
|
10
|
+
import logging
|
|
11
|
+
import requests
|
|
12
|
+
import coloredlogs
|
|
13
|
+
from nrfcredstore.comms import Comms
|
|
14
|
+
from nrfcredstore.command_interface import ATCommandInterface
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
_TAG_PATTERN = re.compile(r'^[a-zA-Z0-9_.,@\/:#-]{0,799}$')
|
|
19
|
+
|
|
20
|
+
def _valid_tag(value):
|
|
21
|
+
if not _TAG_PATTERN.match(value):
|
|
22
|
+
raise argparse.ArgumentTypeError(
|
|
23
|
+
f"Invalid tag {value!r}. Must match /[a-zA-Z0-9_.,@\\/:#-]{{0,799}}/"
|
|
24
|
+
)
|
|
25
|
+
return value
|
|
26
|
+
|
|
27
|
+
DEV_STAGE_DICT = {'dev': '.dev.',
|
|
28
|
+
'prod': '.',
|
|
29
|
+
'': '.'}
|
|
30
|
+
dev_stage_key = 'prod'
|
|
31
|
+
|
|
32
|
+
API_URL_START = 'https://api'
|
|
33
|
+
API_URL_END = 'nrfcloud.com/v1/'
|
|
34
|
+
api_url = API_URL_START + DEV_STAGE_DICT[dev_stage_key] + API_URL_END
|
|
35
|
+
|
|
36
|
+
def get_nrf93m1_uuid(cred_if):
|
|
37
|
+
"""Get or create device UUID for nRF93M1."""
|
|
38
|
+
result = cred_if.at_command('AT%DEVICEUUID', wait_for_result=False)
|
|
39
|
+
if not result:
|
|
40
|
+
logger.error('Failed to send AT%DEVICEUUID command')
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
# Expect response like: %DEVICEUUID: 988234bd-a066-a101-656e-684d6f5adad6
|
|
44
|
+
retval, output = cred_if.comms.expect_response("OK", "ERROR", "%DEVICEUUID:")
|
|
45
|
+
if not retval:
|
|
46
|
+
logger.error('Failed to get device UUID from nRF93M1')
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
# Parse UUID from output
|
|
50
|
+
lines = [line.strip() for line in output.split("\n") if "%DEVICEUUID:" in line]
|
|
51
|
+
if not lines:
|
|
52
|
+
logger.error('UUID not found in response')
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
# Extract UUID (format: "%DEVICEUUID: <uuid>" or "%DEVICEUUID: creating device uuid...")
|
|
56
|
+
uuid_line = lines[-1] # Get the last line with UUID
|
|
57
|
+
if ':' in uuid_line:
|
|
58
|
+
uuid_str = uuid_line.split(':', 1)[1].strip()
|
|
59
|
+
# Check if it's not a status message
|
|
60
|
+
if 'creating' not in uuid_str.lower() and len(uuid_str) > 30:
|
|
61
|
+
logger.debug(f'Retrieved device UUID: {uuid_str}')
|
|
62
|
+
return uuid_str
|
|
63
|
+
|
|
64
|
+
logger.error('Failed to parse UUID from response')
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
def get_nrf93m1_identity_key(cred_if):
|
|
68
|
+
"""Get or create identity key for nRF93M1."""
|
|
69
|
+
result = cred_if.at_command('AT%CLOUDACCESSKEY', wait_for_result=False)
|
|
70
|
+
if not result:
|
|
71
|
+
logger.error('Failed to send AT%CLOUDACCESSKEY command')
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
# Expect response like: %CLOUDACCESSKEY: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
|
|
75
|
+
retval, output = cred_if.comms.expect_response("OK", "ERROR", "%CLOUDACCESSKEY:")
|
|
76
|
+
if not retval:
|
|
77
|
+
logger.error('Failed to get identity key from nRF93M1')
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
# Parse identity key from output
|
|
81
|
+
lines = [line.strip() for line in output.split("\n") if "%CLOUDACCESSKEY:" in line]
|
|
82
|
+
if not lines:
|
|
83
|
+
logger.error('Identity key not found in response')
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
# Extract identity key (format: "%CLOUDACCESSKEY: <base64_key>")
|
|
87
|
+
identity_key_line = lines[-1] # Get the last line with key
|
|
88
|
+
if ':' in identity_key_line:
|
|
89
|
+
identity_key_str = identity_key_line.split(':', 1)[1].strip()
|
|
90
|
+
# Check if it's not a status message
|
|
91
|
+
if 'creating' not in identity_key_str.lower() and len(identity_key_str) > 50:
|
|
92
|
+
logger.debug(f'Retrieved device identity key: {identity_key_str}')
|
|
93
|
+
return identity_key_str
|
|
94
|
+
|
|
95
|
+
logger.error('Failed to parse identity key from response')
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
def get_parser():
|
|
99
|
+
parser = argparse.ArgumentParser(
|
|
100
|
+
description="nRF93M1 - Onboard Device using Registration token JWT",
|
|
101
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
102
|
+
add_help=False
|
|
103
|
+
)
|
|
104
|
+
parser.add_argument("--port", type=str, required=True,
|
|
105
|
+
help="Serial port for the nRF93M1 device (e.g., /dev/ttyACM0 or COM3)")
|
|
106
|
+
parser.add_argument("--baudrate", type=int, help="Serial baudrate", default=115200)
|
|
107
|
+
parser.add_argument("--api-key", type=str, required=True,
|
|
108
|
+
help="nRF Cloud API key", default="")
|
|
109
|
+
parser.add_argument('--log-level', default='info',
|
|
110
|
+
choices=['debug', 'info', 'warning', 'error', 'critical'],
|
|
111
|
+
help='Set the logging level')
|
|
112
|
+
parser.add_argument("--stage", type=str,
|
|
113
|
+
choices=['prod', 'dev'],
|
|
114
|
+
help="For internal (Nordic) use only", default="")
|
|
115
|
+
parser.add_argument("--tags", type=_valid_tag, nargs="+", default=["nRF93M1-EK"],
|
|
116
|
+
metavar="TAG",
|
|
117
|
+
help="Tags to assign to the device on nRF Cloud. "
|
|
118
|
+
"Each tag must match /[a-zA-Z0-9_.,@\\/:#-]{0,799}/")
|
|
119
|
+
return parser
|
|
120
|
+
|
|
121
|
+
def parse_args(in_args):
|
|
122
|
+
_p = get_parser()
|
|
123
|
+
parser = argparse.ArgumentParser(parents=[_p], description=_p.description, formatter_class=_p.formatter_class)
|
|
124
|
+
args = parser.parse_args(in_args)
|
|
125
|
+
|
|
126
|
+
# Setup logging
|
|
127
|
+
if hasattr(args, 'log_level'):
|
|
128
|
+
coloredlogs.install(level=args.log_level.upper(), fmt='%(levelname)-8s %(message)s')
|
|
129
|
+
else:
|
|
130
|
+
coloredlogs.install(level='INFO', fmt='%(levelname)-8s %(message)s')
|
|
131
|
+
|
|
132
|
+
return args
|
|
133
|
+
|
|
134
|
+
def set_dev_stage(stage = ''):
|
|
135
|
+
global api_url
|
|
136
|
+
global dev_stage_key
|
|
137
|
+
|
|
138
|
+
if stage in DEV_STAGE_DICT.keys():
|
|
139
|
+
dev_stage_key = stage
|
|
140
|
+
api_url = '{}{}{}'.format(API_URL_START, DEV_STAGE_DICT[dev_stage_key], API_URL_END)
|
|
141
|
+
else:
|
|
142
|
+
logger.error('Invalid stage')
|
|
143
|
+
|
|
144
|
+
return api_url
|
|
145
|
+
|
|
146
|
+
def fetch_tenant_id(api_key):
|
|
147
|
+
hdr = {'Authorization': 'Bearer ' + api_key}
|
|
148
|
+
req = api_url + "account"
|
|
149
|
+
response = requests.get(req, headers=hdr)
|
|
150
|
+
if not response.ok:
|
|
151
|
+
logger.error(f'Failed to fetch tenant ID: HTTP {response.status_code}')
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
account_info = response.json()
|
|
156
|
+
except ValueError:
|
|
157
|
+
logger.error('Failed to parse account response JSON')
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
tenant_id = account_info.get('team', {}).get('tenantId')
|
|
161
|
+
if not tenant_id:
|
|
162
|
+
logger.error('tenantId not found in account response')
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
return tenant_id
|
|
166
|
+
|
|
167
|
+
def gen_registration_jwt(cred_if, tenant_id):
|
|
168
|
+
result = cred_if.at_command(f'AT%REGJWT="{tenant_id}"', wait_for_result=False)
|
|
169
|
+
if not result:
|
|
170
|
+
logger.error('Failed to send AT%REGJWT command')
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
retval, output = cred_if.comms.expect_response("OK", "ERROR", "%REGJWT:")
|
|
174
|
+
if not retval:
|
|
175
|
+
logger.error('Failed to get registration JWT from nRF93M1')
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
lines = [line.strip() for line in output.split("\n") if line.strip().startswith("%REGJWT:")]
|
|
179
|
+
if not lines:
|
|
180
|
+
logger.error('Registration JWT not found in response')
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
jwt_str = lines[-1].split(':', 1)[1].strip()
|
|
184
|
+
if not jwt_str:
|
|
185
|
+
logger.error('Registration JWT is empty')
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
logger.debug('Retrieved registration JWT from device')
|
|
189
|
+
return jwt_str
|
|
190
|
+
|
|
191
|
+
def onboard_device(api_key, dev_id, sub_type, tags, fw_types, onboarding_token):
|
|
192
|
+
hdr = {
|
|
193
|
+
'Authorization': 'Bearer ' + api_key,
|
|
194
|
+
'Accept': 'application/json',
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
req = api_url + "devices/" + dev_id
|
|
198
|
+
|
|
199
|
+
payload = {
|
|
200
|
+
'onboardingToken': onboarding_token,
|
|
201
|
+
'subType': sub_type,
|
|
202
|
+
'tags': tags,
|
|
203
|
+
'supportedFirmwareTypes': fw_types,
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return requests.post(req, json=payload, headers=hdr)
|
|
207
|
+
|
|
208
|
+
def main(in_args):
|
|
209
|
+
args = parse_args(in_args)
|
|
210
|
+
|
|
211
|
+
if args.stage:
|
|
212
|
+
set_dev_stage(args.stage)
|
|
213
|
+
|
|
214
|
+
logger.info('nRF93M1 - Onboard Device using Registration token JWT')
|
|
215
|
+
logger.info(f'Connecting to device on {args.port}...')
|
|
216
|
+
|
|
217
|
+
# Initialize serial communication
|
|
218
|
+
try:
|
|
219
|
+
serial_interface = Comms(
|
|
220
|
+
port=args.port,
|
|
221
|
+
baudrate=args.baudrate,
|
|
222
|
+
)
|
|
223
|
+
except Exception as e:
|
|
224
|
+
logger.error(f'Failed to open serial port: {e}')
|
|
225
|
+
sys.exit(1)
|
|
226
|
+
|
|
227
|
+
# Create AT command interface
|
|
228
|
+
cred_if = ATCommandInterface(serial_interface)
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
# Verify device is responsive
|
|
232
|
+
logger.info('Checking device connectivity...')
|
|
233
|
+
resp = cred_if.at_command('AT', wait_for_result=True)
|
|
234
|
+
if not resp:
|
|
235
|
+
logger.error('No response from device. Check connection and try again.')
|
|
236
|
+
sys.exit(1)
|
|
237
|
+
logger.info('Device is responsive')
|
|
238
|
+
|
|
239
|
+
# Get device UUID
|
|
240
|
+
logger.info('Retrieving device UUID...')
|
|
241
|
+
dev_id = get_nrf93m1_uuid(cred_if)
|
|
242
|
+
if not dev_id:
|
|
243
|
+
logger.error('[Failed] Device UUID')
|
|
244
|
+
sys.exit(2)
|
|
245
|
+
logger.info('[OK] Device UUID')
|
|
246
|
+
|
|
247
|
+
# Get identity key
|
|
248
|
+
logger.info('Retrieving identity key...')
|
|
249
|
+
identity_key_base64 = get_nrf93m1_identity_key(cred_if)
|
|
250
|
+
if not identity_key_base64:
|
|
251
|
+
logger.error('[Failed] Device identity key')
|
|
252
|
+
sys.exit(3)
|
|
253
|
+
logger.info('[OK] Device identity key')
|
|
254
|
+
|
|
255
|
+
# Based on the stage specified, we query the tenantID from nRF Cloud using the user's API key.
|
|
256
|
+
logger.info('Retrieving tenant ID from nRF Cloud account...')
|
|
257
|
+
tenant_id = fetch_tenant_id(args.api_key)
|
|
258
|
+
if not tenant_id:
|
|
259
|
+
logger.error('[Failed] Tenant ID')
|
|
260
|
+
sys.exit(4)
|
|
261
|
+
logger.info(f'[OK] Tenant ID')
|
|
262
|
+
|
|
263
|
+
# Generate the registration JWT on the device using the tenantId
|
|
264
|
+
logger.info('Generating registration JWT...')
|
|
265
|
+
registration_jwt = gen_registration_jwt(cred_if, tenant_id)
|
|
266
|
+
if not registration_jwt:
|
|
267
|
+
logger.error('[Failed] Registration JWT')
|
|
268
|
+
sys.exit(5)
|
|
269
|
+
logger.info('[OK] Registration JWT')
|
|
270
|
+
|
|
271
|
+
# Onboard the device
|
|
272
|
+
logger.info('Onboarding device to nRF Cloud...')
|
|
273
|
+
sub_type = "nRF93M1"
|
|
274
|
+
fw_types = ["MODEM"]
|
|
275
|
+
onboard_response = onboard_device(args.api_key, dev_id, sub_type, args.tags, fw_types, registration_jwt)
|
|
276
|
+
if not onboard_response.ok:
|
|
277
|
+
logger.error(f'Failed to onboard device: HTTP {onboard_response.status_code} - {onboard_response.text}')
|
|
278
|
+
sys.exit(6)
|
|
279
|
+
logger.info('[OK] Device onboarded successfully')
|
|
280
|
+
|
|
281
|
+
except KeyboardInterrupt:
|
|
282
|
+
logger.warning('Interrupted by user')
|
|
283
|
+
sys.exit(130)
|
|
284
|
+
except Exception as e:
|
|
285
|
+
logger.error(f'Unexpected error: {e}', exc_info=True)
|
|
286
|
+
sys.exit(99)
|
|
287
|
+
|
|
288
|
+
def run():
|
|
289
|
+
main(sys.argv[1:])
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
if __name__ == '__main__':
|
|
293
|
+
run()
|
|
@@ -182,9 +182,10 @@ class nRFCloudDevice:
|
|
|
182
182
|
def url_encode(token):
|
|
183
183
|
return urllib.parse.quote_plus(token)
|
|
184
184
|
|
|
185
|
-
def
|
|
185
|
+
def get_parser():
|
|
186
186
|
parser = argparse.ArgumentParser(description="nRF Cloud Manage FOTA Update Jobs",
|
|
187
|
-
formatter_class=argparse.ArgumentDefaultsHelpFormatter
|
|
187
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
188
|
+
add_help=False)
|
|
188
189
|
parser.add_argument("--api-key",
|
|
189
190
|
help="nRF Cloud API key",
|
|
190
191
|
type=str, required=True, default="")
|
|
@@ -218,7 +219,11 @@ def parse_args(in_args):
|
|
|
218
219
|
parser.add_argument("--desc",
|
|
219
220
|
help="The description of the created updated.",
|
|
220
221
|
type=str, required=False, default="")
|
|
222
|
+
return parser
|
|
221
223
|
|
|
224
|
+
def parse_args(in_args):
|
|
225
|
+
_p = get_parser()
|
|
226
|
+
parser = argparse.ArgumentParser(parents=[_p], description=_p.description, formatter_class=_p.formatter_class)
|
|
222
227
|
args = parser.parse_args(in_args)
|
|
223
228
|
return args
|
|
224
229
|
|
|
@@ -10,15 +10,12 @@ import requests
|
|
|
10
10
|
import csv
|
|
11
11
|
import time
|
|
12
12
|
import json
|
|
13
|
-
import requests
|
|
14
13
|
import os
|
|
15
14
|
import io
|
|
16
|
-
import
|
|
17
|
-
from os import path
|
|
15
|
+
import logging
|
|
18
16
|
from os import makedirs
|
|
19
|
-
from ast import literal_eval
|
|
20
17
|
from enum import Enum
|
|
21
|
-
from nrfcloud_utils.cli_helpers import write_file
|
|
18
|
+
from nrfcloud_utils.cli_helpers import write_file, setup_logging
|
|
22
19
|
|
|
23
20
|
logger = logging.getLogger(__name__)
|
|
24
21
|
|
|
@@ -52,9 +49,10 @@ DEV_LIST_ID_IDX = 0
|
|
|
52
49
|
DEV_LIST_RES_IDX = 1
|
|
53
50
|
BULK_OP_REQ_ID = "bulkOpsRequestId"
|
|
54
51
|
|
|
55
|
-
def
|
|
52
|
+
def get_parser():
|
|
56
53
|
parser = argparse.ArgumentParser(description="nRF Cloud Device Onboarding",
|
|
57
|
-
formatter_class=argparse.ArgumentDefaultsHelpFormatter
|
|
54
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
55
|
+
add_help=False)
|
|
58
56
|
parser.add_argument("--api-key", type=str, required=True,
|
|
59
57
|
help="nRF Cloud API key", default="")
|
|
60
58
|
parser.add_argument("--chk", action='store_true', default=False,
|
|
@@ -85,11 +83,13 @@ def parse_args(in_args):
|
|
|
85
83
|
parser.add_argument("-P", "--plain",
|
|
86
84
|
help="bool: Plain output (no colors)",
|
|
87
85
|
action='store_true', default=False)
|
|
86
|
+
return parser
|
|
87
|
+
|
|
88
|
+
def parse_args(in_args):
|
|
89
|
+
_p = get_parser()
|
|
90
|
+
parser = argparse.ArgumentParser(parents=[_p], description=_p.description, formatter_class=_p.formatter_class)
|
|
88
91
|
args = parser.parse_args(in_args)
|
|
89
|
-
|
|
90
|
-
logging.basicConfig(level=args.log_level.upper())
|
|
91
|
-
else:
|
|
92
|
-
coloredlogs.install(level=args.log_level.upper(), fmt='%(levelname)-8s %(message)s')
|
|
92
|
+
setup_logging(level=args.log_level, use_color=not args.plain)
|
|
93
93
|
return args
|
|
94
94
|
|
|
95
95
|
def set_dev_stage(stage = ''):
|
|
@@ -353,7 +353,7 @@ def check_file_path(file):
|
|
|
353
353
|
if not os.path.exists(path):
|
|
354
354
|
try:
|
|
355
355
|
makedirs(path, exist_ok=True)
|
|
356
|
-
except OSError
|
|
356
|
+
except OSError:
|
|
357
357
|
logger.error("Error creating file path: " + path)
|
|
358
358
|
return ''
|
|
359
359
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|