zscaler-sdk-python 0.6.2__tar.gz → 0.7.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.
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/PKG-INFO +3 -2
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/pyproject.toml +3 -2
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/__init__.py +3 -1
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/utils.py +19 -0
- zscaler_sdk_python-0.7.0/zscaler/zcon/__init__.py +486 -0
- zscaler_sdk_python-0.7.0/zscaler/zcon/activation.py +62 -0
- zscaler_sdk_python-0.7.0/zscaler/zcon/admin_and_role_management.py +490 -0
- zscaler_sdk_python-0.7.0/zscaler/zcon/client.py +88 -0
- zscaler_sdk_python-0.7.0/zscaler/zcon/ecgroups.py +179 -0
- zscaler_sdk_python-0.7.0/zscaler/zcon/locations.py +322 -0
- zscaler_sdk_python-0.7.0/zscaler/zcon/provisioning.py +228 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zia/__init__.py +7 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zia/admin_and_role_management.py +26 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/LICENSE.md +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/README.md +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/cache/__init__.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/cache/cache.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/cache/no_op_cache.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/cache/zscaler_cache.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/constants.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/errors/__init__.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/errors/error.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/errors/http_error.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/errors/zscaler_api_error.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/exceptions/__init__.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/exceptions/exceptions.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/logger.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/ratelimiter/__init__.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/ratelimiter/ratelimiter.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/user_agent.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zdx/__init__.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zdx/admin.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zdx/alerts.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zdx/apps.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zdx/devices.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zdx/filters.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zdx/inventory.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zdx/troubleshooting.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zdx/users.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zdx/zdx_client.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zia/activate.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zia/apptotal.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zia/audit_logs.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zia/authentication_settings.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zia/client.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zia/cloud_apps.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zia/device_management.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zia/dlp.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zia/errors.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zia/firewall.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zia/forwarding_control.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zia/isolation_profile.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zia/labels.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zia/locations.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zia/sandbox.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zia/security.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zia/ssl_inspection.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zia/traffic.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zia/url_categories.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zia/url_filtering.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zia/users.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zia/web_dlp.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zia/workload_groups.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zia/zpa_gateway.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zpa/README.md +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zpa/__init__.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zpa/app_segments.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zpa/app_segments_inspection.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zpa/app_segments_pra.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zpa/authdomains.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zpa/certificates.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zpa/client.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zpa/cloud_connector_groups.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zpa/connectors.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zpa/emergency_access.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zpa/errors.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zpa/idp.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zpa/inspection.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zpa/isolation.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zpa/lss.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zpa/machine_groups.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zpa/microtenants.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zpa/policies.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zpa/posture_profiles.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zpa/privileged_remote_access.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zpa/provisioning.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zpa/saml_attributes.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zpa/scim_attributes.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zpa/scim_groups.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zpa/segment_groups.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zpa/server_groups.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zpa/servers.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zpa/service_edges.py +0 -0
- {zscaler_sdk_python-0.6.2 → zscaler_sdk_python-0.7.0}/zscaler/zpa/trusted_networks.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: zscaler-sdk-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Official Python SDK for the Zscaler Products (Beta)
|
|
5
5
|
Home-page: https://github.com/zscaler/zscaler-sdk-python
|
|
6
6
|
License: MIT
|
|
@@ -22,6 +22,7 @@ Classifier: Topic :: Security
|
|
|
22
22
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
23
|
Provides-Extra: dev
|
|
24
24
|
Requires-Dist: aenum ; extra == "dev"
|
|
25
|
+
Requires-Dist: aiohttp (>=3.10.2)
|
|
25
26
|
Requires-Dist: arrow
|
|
26
27
|
Requires-Dist: black (>=24.3.0) ; extra == "dev"
|
|
27
28
|
Requires-Dist: certifi (>=2024.2.2)
|
|
@@ -32,7 +33,7 @@ Requires-Dist: flatdict
|
|
|
32
33
|
Requires-Dist: idna
|
|
33
34
|
Requires-Dist: okta (>=2.9.7)
|
|
34
35
|
Requires-Dist: pycryptodomex
|
|
35
|
-
Requires-Dist: pydash ; extra == "dev"
|
|
36
|
+
Requires-Dist: pydash (>=8.0.3) ; extra == "dev"
|
|
36
37
|
Requires-Dist: python-box (>=7.2.0,<8.0.0)
|
|
37
38
|
Requires-Dist: python-dateutil
|
|
38
39
|
Requires-Dist: pytz
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "zscaler-sdk-python"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.7.0"
|
|
4
4
|
description = "Official Python SDK for the Zscaler Products (Beta)"
|
|
5
5
|
authors = ["Zscaler, Inc. <devrel@zscaler.com>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -45,13 +45,14 @@ xmltodict = "*"
|
|
|
45
45
|
yarl = "*"
|
|
46
46
|
pycryptodomex = "*"
|
|
47
47
|
aenum = "*"
|
|
48
|
-
pydash = "
|
|
48
|
+
pydash = ">=8.0.3"
|
|
49
49
|
flake8 = "*"
|
|
50
50
|
pytz = "*"
|
|
51
51
|
black = ">=24.3.0"
|
|
52
52
|
# cryptography = ">=3.4,<43.0"
|
|
53
53
|
cryptography = ">=42.0.7"
|
|
54
54
|
okta = ">=2.9.7"
|
|
55
|
+
aiohttp = ">=3.10.2"
|
|
55
56
|
|
|
56
57
|
[tool.poetry.dev-dependencies]
|
|
57
58
|
black = ">=24.3.0"
|
|
@@ -29,7 +29,9 @@ __license__ = "MIT"
|
|
|
29
29
|
__contributors__ = [
|
|
30
30
|
"William Guilherme",
|
|
31
31
|
]
|
|
32
|
-
__version__ = "0.
|
|
32
|
+
__version__ = "0.7.0"
|
|
33
33
|
|
|
34
|
+
from zscaler.zdx import ZDXClientHelper # noqa
|
|
34
35
|
from zscaler.zia import ZIAClientHelper # noqa
|
|
35
36
|
from zscaler.zpa import ZPAClientHelper # noqa
|
|
37
|
+
from zscaler.zcon import ZCONClientHelper # noqa
|
|
@@ -80,7 +80,10 @@ def snake_to_camel(name: str):
|
|
|
80
80
|
"name_l10n_tag": "nameL10nTag",
|
|
81
81
|
"surrogate_ip": "surrogateIP",
|
|
82
82
|
"surrogate_ip_enforced_for_known_browsers": "surrogateIPEnforcedForKnownBrowsers",
|
|
83
|
+
"ec_vms": "ecVMs",
|
|
83
84
|
"is_incomplete_dr_config": "isIncompleteDRConfig",
|
|
85
|
+
"ipv6_enabled": "ipV6Enabled",
|
|
86
|
+
"valid_ssl_certificate": "validSSLCertificate",
|
|
84
87
|
"email_ids": "emailIds",
|
|
85
88
|
"page_size": "pageSize",
|
|
86
89
|
}
|
|
@@ -97,6 +100,22 @@ def recursive_snake_to_camel(data):
|
|
|
97
100
|
return data
|
|
98
101
|
|
|
99
102
|
|
|
103
|
+
def convert_keys(data, direction="to_camel"):
|
|
104
|
+
converter = camel_to_snake if direction == "to_snake" else snake_to_camel
|
|
105
|
+
|
|
106
|
+
if isinstance(data, (list, BoxList)):
|
|
107
|
+
return [convert_keys(inner_dict, direction=direction) for inner_dict in data]
|
|
108
|
+
elif isinstance(data, (dict, Box)):
|
|
109
|
+
new_dict = {}
|
|
110
|
+
for k in data.keys():
|
|
111
|
+
v = data[k]
|
|
112
|
+
new_key = converter(k)
|
|
113
|
+
new_dict[new_key] = convert_keys(v, direction=direction) if isinstance(v, (dict, list)) else v
|
|
114
|
+
return new_dict
|
|
115
|
+
else:
|
|
116
|
+
return data
|
|
117
|
+
|
|
118
|
+
|
|
100
119
|
def chunker(lst, n):
|
|
101
120
|
"""Yield successive n-sized chunks from lst."""
|
|
102
121
|
for i in range(0, len(lst), n):
|
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
# Copyright (c) 2023, Zscaler Inc.
|
|
4
|
+
#
|
|
5
|
+
# Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
# purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
# copyright notice and this permission notice appear in all copies.
|
|
8
|
+
#
|
|
9
|
+
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES OF
|
|
10
|
+
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
11
|
+
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
12
|
+
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
13
|
+
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
14
|
+
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
15
|
+
|
|
16
|
+
import datetime
|
|
17
|
+
import logging
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
import time
|
|
21
|
+
import uuid
|
|
22
|
+
from time import sleep
|
|
23
|
+
|
|
24
|
+
import requests
|
|
25
|
+
from box import Box, BoxList
|
|
26
|
+
|
|
27
|
+
from zscaler import __version__
|
|
28
|
+
from zscaler.cache.no_op_cache import NoOpCache
|
|
29
|
+
from zscaler.cache.zscaler_cache import ZscalerCache
|
|
30
|
+
from zscaler.errors.http_error import HTTPError, ZscalerAPIError
|
|
31
|
+
from zscaler.exceptions.exceptions import HTTPException, ZscalerAPIException
|
|
32
|
+
from zscaler.logger import setup_logging
|
|
33
|
+
from zscaler.ratelimiter.ratelimiter import RateLimiter
|
|
34
|
+
from zscaler.user_agent import UserAgent
|
|
35
|
+
from zscaler.utils import (
|
|
36
|
+
convert_keys_to_snake,
|
|
37
|
+
dump_request,
|
|
38
|
+
dump_response,
|
|
39
|
+
format_json_response,
|
|
40
|
+
obfuscate_api_key,
|
|
41
|
+
retry_with_backoff,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
from zscaler.zcon.client import ZCONClient
|
|
45
|
+
from zscaler.zcon.activation import ActivationAPI
|
|
46
|
+
from zscaler.zcon.admin_and_role_management import AdminAndRoleManagementAPI
|
|
47
|
+
from zscaler.zcon.provisioning import ProvisioningAPI
|
|
48
|
+
from zscaler.zcon.ecgroups import EcGroupAPI
|
|
49
|
+
from zscaler.zcon.locations import LocationAPI
|
|
50
|
+
|
|
51
|
+
# Setup the logger
|
|
52
|
+
setup_logging(logger_name="zscaler-sdk-python")
|
|
53
|
+
logger = logging.getLogger("zscaler-sdk-python")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ZCONClientHelper(ZCONClient):
|
|
57
|
+
"""
|
|
58
|
+
A Controller to access Endpoints in the ZCON API.
|
|
59
|
+
|
|
60
|
+
The ZCON object stores the session token and simplifies access to CRUD options within the ZCON platform.
|
|
61
|
+
|
|
62
|
+
Attributes:
|
|
63
|
+
api_key (str): The ZCON API key generated from the ZCON console.
|
|
64
|
+
username (str): The ZCON administrator username.
|
|
65
|
+
password (str): The ZCON administrator password.
|
|
66
|
+
cloud (str): The ZCON cloud for your tenancy, accepted values are:
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
_vendor = "Zscaler"
|
|
70
|
+
_product = "Zscaler CON Cloud"
|
|
71
|
+
_build = __version__
|
|
72
|
+
_env_base = "ZCON"
|
|
73
|
+
env_cloud = "zscaler"
|
|
74
|
+
|
|
75
|
+
def __init__(self, cloud=None, timeout=240, cache=None, fail_safe=False, **kw):
|
|
76
|
+
self.api_key = kw.get("api_key", os.getenv(f"{self._env_base}_API_KEY"))
|
|
77
|
+
self.username = kw.get("username", os.getenv(f"{self._env_base}_USERNAME"))
|
|
78
|
+
self.password = kw.get("password", os.getenv(f"{self._env_base}_PASSWORD"))
|
|
79
|
+
# The 'cloud' parameter should have precedence over environment variables
|
|
80
|
+
self.env_cloud = cloud or kw.get("cloud") or os.getenv(f"{self._env_base}_CLOUD")
|
|
81
|
+
if not self.env_cloud:
|
|
82
|
+
raise ValueError(
|
|
83
|
+
f"Cloud environment must be set via the 'cloud' argument or the {self._env_base}_CLOUD environment variable."
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# URL construction
|
|
87
|
+
self.url = f"https://connector.{self.env_cloud}.net/api/v1"
|
|
88
|
+
self.conv_box = True
|
|
89
|
+
self.sandbox_token = kw.get("sandbox_token") or os.getenv(f"{self._env_base}_SANDBOX_TOKEN")
|
|
90
|
+
self.timeout = timeout
|
|
91
|
+
self.fail_safe = fail_safe
|
|
92
|
+
cache_enabled = os.environ.get("ZSCALER_CLIENT_CACHE_ENABLED", "true").lower() == "true"
|
|
93
|
+
if cache is None:
|
|
94
|
+
if cache_enabled:
|
|
95
|
+
ttl = int(os.environ.get("ZSCALER_CLIENT_CACHE_DEFAULT_TTL", 3600))
|
|
96
|
+
tti = int(os.environ.get("ZSCALER_CLIENT_CACHE_DEFAULT_TTI", 1800))
|
|
97
|
+
self.cache = ZscalerCache(ttl=ttl, tti=tti)
|
|
98
|
+
else:
|
|
99
|
+
self.cache = NoOpCache()
|
|
100
|
+
else:
|
|
101
|
+
self.cache = cache
|
|
102
|
+
# Initialize user-agent
|
|
103
|
+
ua = UserAgent()
|
|
104
|
+
self.user_agent = ua.get_user_agent_string()
|
|
105
|
+
# Initialize rate limiter
|
|
106
|
+
# You may want to adjust these parameters as per your rate limit configuration
|
|
107
|
+
self.rate_limiter = RateLimiter(
|
|
108
|
+
get_limit=2, # Adjust as per actual limit
|
|
109
|
+
post_put_delete_limit=2, # Adjust as per actual limit
|
|
110
|
+
get_freq=2, # Adjust as per actual frequency (in seconds)
|
|
111
|
+
post_put_delete_freq=2, # Adjust as per actual frequency (in seconds)
|
|
112
|
+
)
|
|
113
|
+
self.headers = {
|
|
114
|
+
"Content-Type": "application/json",
|
|
115
|
+
"Accept": "application/json",
|
|
116
|
+
"User-Agent": self.user_agent,
|
|
117
|
+
}
|
|
118
|
+
self.session_timeout_offset = datetime.timedelta(minutes=5)
|
|
119
|
+
self.session_refreshed = None
|
|
120
|
+
self.auth_details = None
|
|
121
|
+
self.session_id = None
|
|
122
|
+
self.authenticate()
|
|
123
|
+
|
|
124
|
+
def extractJSessionIDFromHeaders(self, header):
|
|
125
|
+
session_id_str = header.get("Set-Cookie", "")
|
|
126
|
+
|
|
127
|
+
if not session_id_str:
|
|
128
|
+
raise ValueError("no Set-Cookie header received")
|
|
129
|
+
|
|
130
|
+
regex = re.compile(r"JSESSIONID=(.*?);")
|
|
131
|
+
result = regex.search(session_id_str)
|
|
132
|
+
|
|
133
|
+
if not result:
|
|
134
|
+
raise ValueError("couldn't find JSESSIONID in header value")
|
|
135
|
+
|
|
136
|
+
return result.group(1)
|
|
137
|
+
|
|
138
|
+
def is_session_expired(self):
|
|
139
|
+
if self.auth_details is None:
|
|
140
|
+
return True
|
|
141
|
+
now = datetime.datetime.now()
|
|
142
|
+
if self.auth_details["passwordExpiryTime"] > 0 and (
|
|
143
|
+
self.session_refreshed + datetime.timedelta(seconds=-self.session_timeout_offset) < now
|
|
144
|
+
):
|
|
145
|
+
return True
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
@retry_with_backoff(retries=5)
|
|
149
|
+
def authenticate(self) -> Box:
|
|
150
|
+
"""
|
|
151
|
+
Creates a ZCON authentication session.
|
|
152
|
+
"""
|
|
153
|
+
api_key_chars = list(self.api_key)
|
|
154
|
+
api_obf = obfuscate_api_key(api_key_chars)
|
|
155
|
+
|
|
156
|
+
payload = {
|
|
157
|
+
"apiKey": api_obf["key"],
|
|
158
|
+
"username": self.username,
|
|
159
|
+
"password": self.password,
|
|
160
|
+
"timestamp": api_obf["timestamp"],
|
|
161
|
+
}
|
|
162
|
+
resp = requests.request(
|
|
163
|
+
"POST",
|
|
164
|
+
self.url + "/auth",
|
|
165
|
+
json=payload,
|
|
166
|
+
headers=self.headers,
|
|
167
|
+
timeout=self.timeout,
|
|
168
|
+
)
|
|
169
|
+
if resp.status_code > 299:
|
|
170
|
+
return resp
|
|
171
|
+
self.session_refreshed = datetime.datetime.now()
|
|
172
|
+
self.session_id = self.extractJSessionIDFromHeaders(resp.headers)
|
|
173
|
+
self.auth_details = resp.json()
|
|
174
|
+
return resp
|
|
175
|
+
|
|
176
|
+
def deauthenticate(self):
|
|
177
|
+
"""
|
|
178
|
+
Ends the ZCON authentication session.
|
|
179
|
+
"""
|
|
180
|
+
logout_url = self.url + "/auth"
|
|
181
|
+
|
|
182
|
+
headers = self.headers.copy()
|
|
183
|
+
headers.update({"Cookie": f"JSESSIONID={self.session_id}"})
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
response = requests.delete(logout_url, headers=headers, timeout=self.timeout)
|
|
187
|
+
if response.status_code == 204:
|
|
188
|
+
self.session_id = None
|
|
189
|
+
self.auth_details = None
|
|
190
|
+
return True
|
|
191
|
+
else:
|
|
192
|
+
return False
|
|
193
|
+
except requests.RequestException as e:
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
def __enter__(self):
|
|
197
|
+
if self.is_session_expired():
|
|
198
|
+
resp = self.authenticate()
|
|
199
|
+
if resp.status_code > 299:
|
|
200
|
+
raise Exception(f"Error auth:{resp.json()}")
|
|
201
|
+
return self
|
|
202
|
+
|
|
203
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
204
|
+
logger.debug("deauthenticating...")
|
|
205
|
+
self.deauthenticate()
|
|
206
|
+
|
|
207
|
+
def send(self, method, path, json=None, params=None, data=None, headers=None):
|
|
208
|
+
"""
|
|
209
|
+
Send a request to the ZCON API.
|
|
210
|
+
|
|
211
|
+
Parameters:
|
|
212
|
+
- method (str): The HTTP method.
|
|
213
|
+
- path (str): API endpoint path.
|
|
214
|
+
- json (dict, optional): Request payload. Defaults to None.
|
|
215
|
+
Returns:
|
|
216
|
+
- Response: Response object from the request.
|
|
217
|
+
"""
|
|
218
|
+
api = self.url
|
|
219
|
+
url = f"{api}/{path.lstrip('/')}"
|
|
220
|
+
start_time = time.time()
|
|
221
|
+
# Update headers to include the user agent
|
|
222
|
+
headers_with_user_agent = self.headers.copy()
|
|
223
|
+
headers_with_user_agent["User-Agent"] = self.user_agent
|
|
224
|
+
# Generate a unique UUID for this request
|
|
225
|
+
request_uuid = uuid.uuid4()
|
|
226
|
+
if headers is not None:
|
|
227
|
+
headers_with_user_agent.update(headers)
|
|
228
|
+
dump_request(
|
|
229
|
+
logger,
|
|
230
|
+
url,
|
|
231
|
+
method,
|
|
232
|
+
json,
|
|
233
|
+
params,
|
|
234
|
+
headers_with_user_agent,
|
|
235
|
+
request_uuid,
|
|
236
|
+
body=True,
|
|
237
|
+
)
|
|
238
|
+
# Check cache before sending request
|
|
239
|
+
cache_key = self.cache.create_key(url, params)
|
|
240
|
+
if method == "GET" and self.cache.contains(cache_key):
|
|
241
|
+
resp = self.cache.get(cache_key)
|
|
242
|
+
dump_response(
|
|
243
|
+
logger=logger,
|
|
244
|
+
url=url,
|
|
245
|
+
method=method,
|
|
246
|
+
params=params,
|
|
247
|
+
resp=resp,
|
|
248
|
+
request_uuid=request_uuid,
|
|
249
|
+
start_time=start_time,
|
|
250
|
+
from_cache=True,
|
|
251
|
+
)
|
|
252
|
+
return resp
|
|
253
|
+
|
|
254
|
+
attempts = 0
|
|
255
|
+
while attempts < 5: # Trying a maximum of 5 times
|
|
256
|
+
try:
|
|
257
|
+
# If the token is None or expired, fetch a new token
|
|
258
|
+
if self.is_session_expired():
|
|
259
|
+
logger.warning("The provided sesion expired. Refreshing...")
|
|
260
|
+
self.authenticate()
|
|
261
|
+
resp = requests.request(
|
|
262
|
+
method=method,
|
|
263
|
+
url=url,
|
|
264
|
+
json=json,
|
|
265
|
+
data=data,
|
|
266
|
+
params=params,
|
|
267
|
+
headers=headers_with_user_agent,
|
|
268
|
+
timeout=self.timeout,
|
|
269
|
+
cookies={"JSESSIONID": self.session_id},
|
|
270
|
+
)
|
|
271
|
+
dump_response(
|
|
272
|
+
logger=logger,
|
|
273
|
+
url=url,
|
|
274
|
+
params=params,
|
|
275
|
+
method=method,
|
|
276
|
+
resp=resp,
|
|
277
|
+
request_uuid=request_uuid,
|
|
278
|
+
start_time=start_time,
|
|
279
|
+
)
|
|
280
|
+
if resp.status_code == 429: # HTTP Status code 429 indicates "Too Many Requests"
|
|
281
|
+
sleep_time = int(
|
|
282
|
+
resp.headers.get("Retry-After", 2)
|
|
283
|
+
) # Default to 60 seconds if 'Retry-After' header is missing
|
|
284
|
+
logger.warning(f"Rate limit exceeded. Retrying in {sleep_time} seconds.")
|
|
285
|
+
sleep(sleep_time)
|
|
286
|
+
attempts += 1
|
|
287
|
+
continue
|
|
288
|
+
else:
|
|
289
|
+
break
|
|
290
|
+
except requests.RequestException as e:
|
|
291
|
+
if attempts == 4: # If it's the last attempt, raise the exception
|
|
292
|
+
logger.error(f"Failed to send {method} request to {url} after 5 attempts. Error: {str(e)}")
|
|
293
|
+
raise e
|
|
294
|
+
else:
|
|
295
|
+
logger.warning(f"Failed to send {method} request to {url}. Retrying... Error: {str(e)}")
|
|
296
|
+
attempts += 1
|
|
297
|
+
sleep(5) # Sleep for 5 seconds before retrying
|
|
298
|
+
|
|
299
|
+
# If Non-GET call, clear the
|
|
300
|
+
if method != "GET":
|
|
301
|
+
self.cache.delete(cache_key)
|
|
302
|
+
|
|
303
|
+
# Detailed logging for request and response
|
|
304
|
+
try:
|
|
305
|
+
response_data = resp.json()
|
|
306
|
+
except ValueError: # Using ValueError for JSON decoding errors
|
|
307
|
+
response_data = resp.text
|
|
308
|
+
# check if call was succesful
|
|
309
|
+
if 200 > resp.status_code or resp.status_code > 299:
|
|
310
|
+
# create errors
|
|
311
|
+
try:
|
|
312
|
+
error = ZscalerAPIError(url, resp, response_data)
|
|
313
|
+
if self.fail_safe:
|
|
314
|
+
raise ZscalerAPIException(url, resp, response_data)
|
|
315
|
+
except ZscalerAPIException:
|
|
316
|
+
raise
|
|
317
|
+
except Exception:
|
|
318
|
+
error = HTTPError(url, resp, response_data)
|
|
319
|
+
if self.fail_safe:
|
|
320
|
+
logger.error(response_data)
|
|
321
|
+
raise HTTPException(url, resp, response_data)
|
|
322
|
+
logger.error(error)
|
|
323
|
+
# Cache the response if it's a successful GET request
|
|
324
|
+
if method == "GET" and resp.status_code == 200:
|
|
325
|
+
self.cache.add(cache_key, resp)
|
|
326
|
+
return resp
|
|
327
|
+
|
|
328
|
+
def get(self, path, json=None, params=None):
|
|
329
|
+
"""
|
|
330
|
+
Send a GET request to the ZCON API.
|
|
331
|
+
|
|
332
|
+
Parameters:
|
|
333
|
+
- path (str): API endpoint path.
|
|
334
|
+
- data (dict, optional): Request payload. Defaults to None.
|
|
335
|
+
Returns:
|
|
336
|
+
- Response: Response object from the request.
|
|
337
|
+
"""
|
|
338
|
+
|
|
339
|
+
should_wait, delay = self.rate_limiter.wait("GET")
|
|
340
|
+
if should_wait:
|
|
341
|
+
time.sleep(delay)
|
|
342
|
+
response = self.send("GET", path, json, params)
|
|
343
|
+
if not response.ok:
|
|
344
|
+
raise Exception(f"GET request failed with status {response.status_code}: {response.json()}")
|
|
345
|
+
return format_json_response(response, box_attrs=dict())
|
|
346
|
+
|
|
347
|
+
def post(self, path, json=None, params=None, data=None, headers=None):
|
|
348
|
+
should_wait, delay = self.rate_limiter.wait("POST")
|
|
349
|
+
if should_wait:
|
|
350
|
+
time.sleep(delay)
|
|
351
|
+
response = self.send("POST", path, json, params, data=data, headers=headers)
|
|
352
|
+
if not response.ok:
|
|
353
|
+
raise Exception(f"POST request failed with status {response.status_code}: {response.json()}")
|
|
354
|
+
return format_json_response(response, box_attrs=dict())
|
|
355
|
+
|
|
356
|
+
def put(self, path, json=None, params=None):
|
|
357
|
+
should_wait, delay = self.rate_limiter.wait("PUT")
|
|
358
|
+
if should_wait:
|
|
359
|
+
time.sleep(delay)
|
|
360
|
+
response = self.send("PUT", path, json, params)
|
|
361
|
+
|
|
362
|
+
# Handle 204 No Content separately
|
|
363
|
+
if response.status_code == 204:
|
|
364
|
+
return response
|
|
365
|
+
|
|
366
|
+
# Handle 200 OK with content
|
|
367
|
+
if response.status_code == 200 and response.headers.get("content-type", "").startswith("application/json"):
|
|
368
|
+
return format_json_response(response, box_attrs=dict())
|
|
369
|
+
|
|
370
|
+
# Raise an exception for any other unexpected status codes
|
|
371
|
+
if not response.ok:
|
|
372
|
+
raise Exception(f"PUT request failed with status {response.status_code}: {response.json()}")
|
|
373
|
+
|
|
374
|
+
return response
|
|
375
|
+
|
|
376
|
+
def delete(self, path, json=None, params=None):
|
|
377
|
+
should_wait, delay = self.rate_limiter.wait("DELETE")
|
|
378
|
+
if should_wait:
|
|
379
|
+
time.sleep(delay)
|
|
380
|
+
response = self.send("DELETE", path, json, params)
|
|
381
|
+
if not response.ok:
|
|
382
|
+
raise Exception(f"DELETE request failed with status {response.status_code}: {response.json()}")
|
|
383
|
+
# Since DELETE typically returns 204 No Content, we don't need to format the response.
|
|
384
|
+
return response
|
|
385
|
+
|
|
386
|
+
ERROR_MESSAGES = {
|
|
387
|
+
"UNEXPECTED_STATUS": "Unexpected status code {status_code} received for page {page}.",
|
|
388
|
+
"MISSING_DATA_KEY": "The key '{data_key_name}' was not found in the response for page {page}.",
|
|
389
|
+
"EMPTY_RESULTS": "No results found for page {page}.",
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
def get_paginated_data(self, path=None, data_key_name=None, data_per_page=5, expected_status_code=200):
|
|
393
|
+
"""
|
|
394
|
+
Fetch paginated data from the ZCON API.
|
|
395
|
+
...
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
- list: List of fetched items.
|
|
399
|
+
- str: Error message, if any occurred.
|
|
400
|
+
"""
|
|
401
|
+
|
|
402
|
+
page = 1
|
|
403
|
+
ret_data = []
|
|
404
|
+
error_message = None
|
|
405
|
+
|
|
406
|
+
while True:
|
|
407
|
+
required_url = f"{path}"
|
|
408
|
+
should_wait, delay = self.rate_limiter.wait("GET")
|
|
409
|
+
if should_wait:
|
|
410
|
+
time.sleep(delay)
|
|
411
|
+
|
|
412
|
+
# Now proceed with sending the request
|
|
413
|
+
response = self.send(
|
|
414
|
+
method="GET",
|
|
415
|
+
path=required_url,
|
|
416
|
+
params={"page": page, "pageSize": data_per_page},
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
if response.status_code != expected_status_code:
|
|
420
|
+
error_message = self.ERROR_MESSAGES["UNEXPECTED_STATUS"].format(status_code=response.status_code, page=page)
|
|
421
|
+
logger.error(error_message)
|
|
422
|
+
break
|
|
423
|
+
data_json = response.json()
|
|
424
|
+
if isinstance(data_json, list):
|
|
425
|
+
data = data_json
|
|
426
|
+
else:
|
|
427
|
+
data = data_json.get(data_key_name)
|
|
428
|
+
|
|
429
|
+
if data is None:
|
|
430
|
+
error_message = self.ERROR_MESSAGES["MISSING_DATA_KEY"].format(data_key_name=data_key_name, page=page)
|
|
431
|
+
logger.error(error_message)
|
|
432
|
+
break
|
|
433
|
+
|
|
434
|
+
if not data: # Checks for empty data
|
|
435
|
+
logger.info(self.ERROR_MESSAGES["EMPTY_RESULTS"].format(page=page))
|
|
436
|
+
break
|
|
437
|
+
|
|
438
|
+
ret_data.extend(convert_keys_to_snake(data))
|
|
439
|
+
|
|
440
|
+
# Check for more pages
|
|
441
|
+
if len(data) == 0 or isinstance(data_json, dict) and int(response.json().get("totalPages")) <= page + 1:
|
|
442
|
+
break
|
|
443
|
+
|
|
444
|
+
page += 1
|
|
445
|
+
|
|
446
|
+
return BoxList(ret_data), error_message
|
|
447
|
+
|
|
448
|
+
@property
|
|
449
|
+
def activation(self):
|
|
450
|
+
"""
|
|
451
|
+
The interface object for the :ref:`ZCON Activation Service <zcon-activation>`.
|
|
452
|
+
|
|
453
|
+
"""
|
|
454
|
+
return ActivationAPI(self)
|
|
455
|
+
|
|
456
|
+
@property
|
|
457
|
+
def admin_and_role_management(self):
|
|
458
|
+
"""
|
|
459
|
+
The interface object for the :ref:`ZCON Admin and Role Management interface <zcon-admin_and_role_management>`.
|
|
460
|
+
|
|
461
|
+
"""
|
|
462
|
+
return AdminAndRoleManagementAPI(self)
|
|
463
|
+
|
|
464
|
+
@property
|
|
465
|
+
def ecgroups(self):
|
|
466
|
+
"""
|
|
467
|
+
The interface object for the :ref:`ZCON EC Group Service <zcon-ecgroups>`.
|
|
468
|
+
|
|
469
|
+
"""
|
|
470
|
+
return EcGroupAPI(self)
|
|
471
|
+
|
|
472
|
+
@property
|
|
473
|
+
def locations(self):
|
|
474
|
+
"""
|
|
475
|
+
The interface object for the :ref:`ZCON Location Service <zcon-locations>`.
|
|
476
|
+
|
|
477
|
+
"""
|
|
478
|
+
return LocationAPI(self)
|
|
479
|
+
|
|
480
|
+
@property
|
|
481
|
+
def provisioning(self):
|
|
482
|
+
"""
|
|
483
|
+
The interface object for the :ref:`ZCON API Key Provisioning Service <zcon-provisioning>`.
|
|
484
|
+
|
|
485
|
+
"""
|
|
486
|
+
return ProvisioningAPI(self)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
# Copyright (c) 2023, Zscaler Inc.
|
|
4
|
+
#
|
|
5
|
+
# Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
# purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
# copyright notice and this permission notice appear in all copies.
|
|
8
|
+
#
|
|
9
|
+
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES OF
|
|
10
|
+
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
11
|
+
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
12
|
+
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
13
|
+
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
14
|
+
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
15
|
+
|
|
16
|
+
from box import Box
|
|
17
|
+
from zscaler.zcon.client import ZCONClient
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ActivationAPI:
|
|
21
|
+
def __init__(self, client: ZCONClient):
|
|
22
|
+
self.rest = client
|
|
23
|
+
|
|
24
|
+
def activate(self, force: bool = False) -> Box:
|
|
25
|
+
"""
|
|
26
|
+
Activate the configuration.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
force (bool): If set to True, forces the activation. Default is False.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
:obj:`Box`: The status code of the operation.
|
|
33
|
+
|
|
34
|
+
Examples:
|
|
35
|
+
Activate the configuration without forcing::
|
|
36
|
+
|
|
37
|
+
zcon.config.activate()
|
|
38
|
+
|
|
39
|
+
Forcefully activate the configuration::
|
|
40
|
+
|
|
41
|
+
zcon.config.activate(force=True)
|
|
42
|
+
|
|
43
|
+
"""
|
|
44
|
+
if force:
|
|
45
|
+
return self.rest.put("ecAdminActivateStatus/forcedActivate")
|
|
46
|
+
else:
|
|
47
|
+
return self.rest.put("ecAdminActivateStatus/activate")
|
|
48
|
+
|
|
49
|
+
def get_status(self):
|
|
50
|
+
"""
|
|
51
|
+
Get the status of the configuration.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
:obj:`Box`: The status of the configuration.
|
|
55
|
+
|
|
56
|
+
Examples:
|
|
57
|
+
Get the status of the configuration::
|
|
58
|
+
|
|
59
|
+
print(zcon.config.get_status())
|
|
60
|
+
|
|
61
|
+
"""
|
|
62
|
+
return self.rest.get("ecAdminActivateStatus")
|