hcs-core 0.1.250__py3-none-any.whl → 0.1.316__py3-none-any.whl
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.
- hcs_core/__init__.py +1 -0
- hcs_core/ctxp/__init__.py +12 -4
- hcs_core/ctxp/_init.py +94 -22
- hcs_core/ctxp/built_in_cmds/_ut.py +4 -3
- hcs_core/ctxp/built_in_cmds/context.py +16 -1
- hcs_core/ctxp/built_in_cmds/profile.py +30 -11
- hcs_core/ctxp/cli_options.py +34 -13
- hcs_core/ctxp/cli_processor.py +33 -20
- hcs_core/ctxp/cmd_util.py +87 -0
- hcs_core/ctxp/config.py +1 -1
- hcs_core/ctxp/context.py +82 -3
- hcs_core/ctxp/data_util.py +56 -20
- hcs_core/ctxp/dispatcher.py +82 -0
- hcs_core/ctxp/duration.py +65 -0
- hcs_core/ctxp/extension.py +7 -6
- hcs_core/ctxp/fn_util.py +57 -0
- hcs_core/ctxp/fstore.py +39 -22
- hcs_core/ctxp/jsondot.py +259 -78
- hcs_core/ctxp/logger.py +7 -6
- hcs_core/ctxp/profile.py +53 -21
- hcs_core/ctxp/profile_store.py +1 -0
- hcs_core/ctxp/recent.py +3 -3
- hcs_core/ctxp/state.py +4 -3
- hcs_core/ctxp/task_schd.py +168 -0
- hcs_core/ctxp/telemetry.py +145 -0
- hcs_core/ctxp/template_util.py +21 -0
- hcs_core/ctxp/timeutil.py +11 -0
- hcs_core/ctxp/util.py +194 -33
- hcs_core/ctxp/var_template.py +3 -4
- hcs_core/plan/__init__.py +11 -5
- hcs_core/plan/base_provider.py +1 -0
- hcs_core/plan/core.py +29 -26
- hcs_core/plan/dag.py +15 -12
- hcs_core/plan/helper.py +4 -2
- hcs_core/plan/kop.py +21 -8
- hcs_core/plan/provider/dev/dummy.py +3 -3
- hcs_core/sglib/auth.py +137 -95
- hcs_core/sglib/cli_options.py +20 -5
- hcs_core/sglib/client_util.py +230 -62
- hcs_core/sglib/csp.py +73 -6
- hcs_core/sglib/ez_client.py +139 -41
- hcs_core/sglib/hcs_client.py +3 -9
- hcs_core/sglib/init.py +17 -0
- hcs_core/sglib/login_support.py +22 -83
- hcs_core/sglib/payload_util.py +3 -1
- hcs_core/sglib/requtil.py +38 -0
- hcs_core/sglib/utils.py +107 -0
- hcs_core/util/check_license.py +0 -2
- hcs_core/util/duration.py +6 -3
- hcs_core/util/job_view.py +35 -15
- hcs_core/util/pki_util.py +48 -1
- hcs_core/util/query_util.py +54 -8
- hcs_core/util/scheduler.py +3 -3
- hcs_core/util/ssl_util.py +1 -1
- hcs_core/util/versions.py +15 -12
- hcs_core-0.1.316.dist-info/METADATA +54 -0
- hcs_core-0.1.316.dist-info/RECORD +69 -0
- {hcs_core-0.1.250.dist-info → hcs_core-0.1.316.dist-info}/WHEEL +1 -2
- hcs_core-0.1.250.dist-info/METADATA +0 -36
- hcs_core-0.1.250.dist-info/RECORD +0 -59
- hcs_core-0.1.250.dist-info/top_level.txt +0 -1
hcs_core/sglib/ez_client.py
CHANGED
|
@@ -13,16 +13,45 @@ See the License for the specific language governing permissions and
|
|
|
13
13
|
limitations under the License.
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
import os
|
|
19
|
+
import sys
|
|
20
|
+
import threading
|
|
17
21
|
from http.client import HTTPResponse
|
|
22
|
+
from typing import Callable, Optional, Type
|
|
23
|
+
|
|
18
24
|
import httpx
|
|
19
|
-
from
|
|
20
|
-
import
|
|
21
|
-
|
|
25
|
+
from authlib.integrations.httpx_client import OAuth2Client
|
|
26
|
+
from pydantic import BaseModel
|
|
27
|
+
|
|
22
28
|
from hcs_core.ctxp import jsondot
|
|
23
29
|
|
|
24
30
|
log = logging.getLogger(__name__)
|
|
25
31
|
|
|
32
|
+
_print_http_error = True
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def parse(resp: httpx.Response):
|
|
36
|
+
if not resp.content:
|
|
37
|
+
return None
|
|
38
|
+
content_type = resp.headers["Content-Type"]
|
|
39
|
+
if content_type.startswith("text"):
|
|
40
|
+
return resp.text
|
|
41
|
+
if len(resp.content) > 3:
|
|
42
|
+
try:
|
|
43
|
+
data = resp.json()
|
|
44
|
+
if isinstance(data, list):
|
|
45
|
+
return [jsondot.dotify(obj) for obj in data]
|
|
46
|
+
elif isinstance(data, dict):
|
|
47
|
+
return jsondot.dotify(data)
|
|
48
|
+
except:
|
|
49
|
+
log.info("--- Fail parsing json. Dump content ---")
|
|
50
|
+
log.info(resp.content)
|
|
51
|
+
raise
|
|
52
|
+
else:
|
|
53
|
+
return None
|
|
54
|
+
|
|
26
55
|
|
|
27
56
|
def _raise_on_4xx_5xx(response: httpx.Response):
|
|
28
57
|
if not response.is_success:
|
|
@@ -97,66 +126,135 @@ def on404ReturnNone(func):
|
|
|
97
126
|
raise
|
|
98
127
|
|
|
99
128
|
|
|
129
|
+
def _raise_http_error(e: httpx.HTTPStatusError):
|
|
130
|
+
if _print_http_error:
|
|
131
|
+
print(e.response.text, file=sys.stderr)
|
|
132
|
+
raise e
|
|
133
|
+
|
|
134
|
+
|
|
100
135
|
class EzClient:
|
|
101
|
-
def __init__(self, base_url: str, oauth_client: OAuth2Client = None,
|
|
136
|
+
def __init__(self, base_url: str = None, oauth_client: OAuth2Client = None, lazy_init: Callable = None) -> None:
|
|
102
137
|
# self._client = httpx.Client(base_url=base_url, timeout=30, event_hooks=event_hooks)
|
|
103
|
-
|
|
104
|
-
self._client_impl =
|
|
105
|
-
self.
|
|
138
|
+
|
|
139
|
+
self._client_impl = None
|
|
140
|
+
self._lazy_init = lazy_init
|
|
141
|
+
self._lock = threading.Lock()
|
|
142
|
+
|
|
143
|
+
if lazy_init:
|
|
144
|
+
if base_url:
|
|
145
|
+
raise ValueError("Cannot use both base_url and lazy_init")
|
|
146
|
+
if oauth_client:
|
|
147
|
+
raise ValueError("Cannot use both oauth_client and lazy_init")
|
|
148
|
+
else:
|
|
149
|
+
if not base_url:
|
|
150
|
+
raise ValueError("base_url must be provided if lazy_init is not used")
|
|
151
|
+
if not oauth_client:
|
|
152
|
+
raise ValueError("oauth_client must be provided if lazy_init is not used")
|
|
153
|
+
self._init_client(base_url, oauth_client)
|
|
154
|
+
|
|
155
|
+
def _init_client(self, base_url: str, client: OAuth2Client):
|
|
156
|
+
if base_url.endswith("/"):
|
|
157
|
+
base_url = base_url[:-1]
|
|
158
|
+
client.base_url = base_url
|
|
159
|
+
client.timeout = int(os.environ.get("HCS_TIMEOUT", 30))
|
|
160
|
+
request_hooks = client.event_hooks["request"]
|
|
161
|
+
response_hooks = client.event_hooks["response"]
|
|
162
|
+
if _log_request not in request_hooks:
|
|
163
|
+
request_hooks.append(_log_request)
|
|
164
|
+
if _log_response not in response_hooks:
|
|
165
|
+
response_hooks.append(_log_response)
|
|
166
|
+
if _raise_on_4xx_5xx not in response_hooks:
|
|
167
|
+
response_hooks.append(_raise_on_4xx_5xx)
|
|
168
|
+
self._client_impl = client
|
|
106
169
|
|
|
107
170
|
def _client(self):
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
response_hooks.append(_raise_on_4xx_5xx)
|
|
120
|
-
self._client_impl = client
|
|
121
|
-
return self._client_impl
|
|
122
|
-
|
|
123
|
-
def post(self, url: str, json: dict = None, text: str = None, files=None, headers: dict = None):
|
|
124
|
-
resp = self._client().post(url, json=json, content=text, files=files, headers=headers)
|
|
125
|
-
return _parse_resp(resp)
|
|
171
|
+
self._lock.acquire()
|
|
172
|
+
try:
|
|
173
|
+
if self._lazy_init:
|
|
174
|
+
base_url, client = self._lazy_init()
|
|
175
|
+
self._init_client(base_url, client)
|
|
176
|
+
self._lazy_init = None
|
|
177
|
+
|
|
178
|
+
self._client_impl.ensure_token()
|
|
179
|
+
return self._client_impl
|
|
180
|
+
finally:
|
|
181
|
+
self._lock.release()
|
|
126
182
|
|
|
127
|
-
def
|
|
183
|
+
def post(
|
|
184
|
+
self,
|
|
185
|
+
url: str,
|
|
186
|
+
json: dict = None,
|
|
187
|
+
text: str = None,
|
|
188
|
+
files=None,
|
|
189
|
+
headers: dict = None,
|
|
190
|
+
type: Optional[Type[BaseModel]] = None,
|
|
191
|
+
timeout: float = 30.0,
|
|
192
|
+
):
|
|
193
|
+
# import json as jsonlib
|
|
194
|
+
# print("->", self._client().base_url, url, jsonlib.dumps(json, indent=4))
|
|
128
195
|
try:
|
|
129
|
-
resp = self._client().
|
|
130
|
-
|
|
196
|
+
resp = self._client().post(url, json=json, content=text, files=files, headers=headers, timeout=timeout)
|
|
197
|
+
except httpx.HTTPStatusError as e:
|
|
198
|
+
_raise_http_error(e)
|
|
199
|
+
data = _parse_resp(resp)
|
|
200
|
+
if data and type:
|
|
201
|
+
try:
|
|
202
|
+
return type.model_validate(data)
|
|
203
|
+
except:
|
|
204
|
+
log.info("--- Fail converting model. Dump content ---")
|
|
205
|
+
log.info(data)
|
|
206
|
+
raise
|
|
207
|
+
return data
|
|
208
|
+
|
|
209
|
+
def get(self, url: str, headers: dict = None, raise_on_404: bool = False, type: Optional[Type[BaseModel]] = None):
|
|
210
|
+
try:
|
|
211
|
+
# print("->", self._client().base_url, url)
|
|
212
|
+
resp = self._client().get(url, headers=headers)
|
|
213
|
+
data = _parse_resp(resp)
|
|
214
|
+
if data and type:
|
|
215
|
+
try:
|
|
216
|
+
return type.model_validate(data)
|
|
217
|
+
except:
|
|
218
|
+
log.info("--- Fail converting model. Dump content ---")
|
|
219
|
+
log.info(data)
|
|
220
|
+
raise
|
|
221
|
+
return data
|
|
131
222
|
except httpx.HTTPStatusError as e:
|
|
132
223
|
if _is_404(e):
|
|
133
224
|
if raise_on_404:
|
|
134
|
-
|
|
225
|
+
_raise_http_error(e)
|
|
135
226
|
else:
|
|
136
227
|
pass
|
|
137
228
|
else:
|
|
138
|
-
|
|
229
|
+
_raise_http_error(e)
|
|
139
230
|
|
|
140
|
-
def patch(self, url: str, json: dict):
|
|
141
|
-
|
|
142
|
-
|
|
231
|
+
def patch(self, url: str, json: dict = None, text=None, headers: dict = None):
|
|
232
|
+
try:
|
|
233
|
+
resp = self._client().patch(url, json=json, content=text, headers=headers)
|
|
234
|
+
return _parse_resp(resp)
|
|
235
|
+
except httpx.HTTPStatusError as e:
|
|
236
|
+
_raise_http_error(e)
|
|
237
|
+
raise
|
|
143
238
|
|
|
144
|
-
def delete(self, url: str, raise_on_404: bool = False):
|
|
239
|
+
def delete(self, url: str, headers: dict = None, raise_on_404: bool = False):
|
|
145
240
|
try:
|
|
146
|
-
resp = self._client().delete(url)
|
|
241
|
+
resp = self._client().delete(url, headers=headers)
|
|
147
242
|
return _parse_resp(resp)
|
|
148
243
|
except httpx.HTTPStatusError as e:
|
|
149
244
|
if _is_404(e):
|
|
150
245
|
if raise_on_404:
|
|
151
|
-
|
|
246
|
+
_raise_http_error(e)
|
|
152
247
|
else:
|
|
153
248
|
pass
|
|
154
249
|
else:
|
|
155
|
-
|
|
250
|
+
_raise_http_error(e)
|
|
156
251
|
|
|
157
|
-
def put(self, url: str, json: dict):
|
|
158
|
-
|
|
159
|
-
|
|
252
|
+
def put(self, url: str, json: dict = None, text=None, headers: dict = None):
|
|
253
|
+
try:
|
|
254
|
+
resp = self._client().put(url, json=json, content=text, headers=headers)
|
|
255
|
+
return _parse_resp(resp)
|
|
256
|
+
except httpx.HTTPStatusError as e:
|
|
257
|
+
_raise_http_error(e)
|
|
160
258
|
|
|
161
259
|
def close(self):
|
|
162
260
|
self._client().close()
|
hcs_core/sglib/hcs_client.py
CHANGED
|
@@ -13,15 +13,9 @@ See the License for the specific language governing permissions and
|
|
|
13
13
|
limitations under the License.
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
|
-
from hcs_core.ctxp import profile
|
|
17
|
-
from .ez_client import EzClient
|
|
18
16
|
from . import auth as auth
|
|
17
|
+
from .ez_client import EzClient
|
|
19
18
|
|
|
20
19
|
|
|
21
|
-
def hcs_client(url: str) -> EzClient:
|
|
22
|
-
|
|
23
|
-
url = profile.current().hcs.url
|
|
24
|
-
if url.endswith("/"):
|
|
25
|
-
url = url[:-1]
|
|
26
|
-
|
|
27
|
-
return EzClient(url, lazy_oauth_client=auth.oauth_client)
|
|
20
|
+
def hcs_client(url: str, custom_auth: dict = None) -> EzClient:
|
|
21
|
+
return EzClient(base_url=url, oauth_client=auth.oauth_client(custom_auth))
|
hcs_core/sglib/init.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import atexit
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
log = logging.getLogger(__name__)
|
|
5
|
+
_sg_client = None
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# Work around issue:
|
|
9
|
+
# http11.py, line 211, in close: AttributeError: 'NoneType' object has no attribute 'CLOSED'
|
|
10
|
+
def _on_exit():
|
|
11
|
+
if _sg_client:
|
|
12
|
+
_sg_client.close()
|
|
13
|
+
|
|
14
|
+
log.info("Exit")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
atexit.register(_on_exit)
|
hcs_core/sglib/login_support.py
CHANGED
|
@@ -16,17 +16,16 @@ limitations under the License.
|
|
|
16
16
|
# nanw @ 2023
|
|
17
17
|
# PKCE with authlib: https://docs.authlib.org/en/latest/specs/rfc7636.html
|
|
18
18
|
|
|
19
|
+
import logging
|
|
20
|
+
import platform
|
|
21
|
+
import secrets
|
|
19
22
|
import threading
|
|
20
|
-
import time
|
|
21
|
-
from typing import Callable
|
|
22
23
|
import webbrowser
|
|
23
|
-
import
|
|
24
|
-
import
|
|
25
|
-
|
|
24
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
25
|
+
from urllib.parse import parse_qs, urlparse
|
|
26
|
+
|
|
26
27
|
from authlib.integrations.httpx_client import OAuth2Client
|
|
27
28
|
from authlib.oauth2.rfc7636 import create_s256_code_challenge
|
|
28
|
-
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
29
|
-
from urllib.parse import urlparse, parse_qs
|
|
30
29
|
|
|
31
30
|
log = logging.getLogger(__name__)
|
|
32
31
|
|
|
@@ -35,10 +34,12 @@ _callback_url = "/hcs-cli/oauth/callback"
|
|
|
35
34
|
|
|
36
35
|
_public_client_ids = {
|
|
37
36
|
"production": "ldjmWbBAUcSB1w3HSbzoYcdKoloYFqT2dWK",
|
|
37
|
+
# "production": "WbvEYXK4abljPJUuY2zODLpOaX5ddH2uhMX", #collie mobile
|
|
38
38
|
"staging": "BbwHlgWt0vFwPUZMJTb5IKltynXjDmb46IO",
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
_auth_code_event = threading.Event()
|
|
42
|
+
_auth_code_event.value = None
|
|
42
43
|
_state = None
|
|
43
44
|
|
|
44
45
|
_auth_success_html = """
|
|
@@ -62,10 +63,10 @@ _auth_success_html = """
|
|
|
62
63
|
</style>
|
|
63
64
|
</head>
|
|
64
65
|
<body>
|
|
65
|
-
<h3>You have successfully logged into
|
|
66
|
+
<h3>You have successfully logged into Omnissa Horizon Cloud Service.</h3>
|
|
66
67
|
<p>You can close this window, and return to the terminal.</p>
|
|
67
68
|
<br/>
|
|
68
|
-
<p><a href="https://github.com/euc-eng/hcs-cli">HCS CLI</a> is in
|
|
69
|
+
<p><a href="https://github.com/euc-eng/hcs-cli/blob/dev/README.md">HCS CLI</a> is in tech preview. <a href="https://github.com/euc-eng/hcs-cli/blob/main/doc/hcs-cli-cheatsheet.md">Cheatsheet</a>:</p>
|
|
69
70
|
<code>
|
|
70
71
|
# To get the login details: <br/>
|
|
71
72
|
hcs login -d <br/><br/>
|
|
@@ -167,9 +168,18 @@ def do_oauth2_pkce(csp_url: str, client_id: str, org_id: str):
|
|
|
167
168
|
webbrowser.open(authorization_url, new=0, autoraise=True)
|
|
168
169
|
|
|
169
170
|
try:
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
171
|
+
# On Windows, wait() without timeout doesn't respond to CTRL+C properly
|
|
172
|
+
# Use a polling loop with timeout to allow KeyboardInterrupt to be caught
|
|
173
|
+
if platform.system() == "Windows":
|
|
174
|
+
while not _auth_code_event.wait(timeout=0.5):
|
|
175
|
+
pass # Keep waiting until the event is set
|
|
176
|
+
else:
|
|
177
|
+
_auth_code_event.wait()
|
|
178
|
+
except KeyboardInterrupt:
|
|
179
|
+
log.info("Aborted by user")
|
|
180
|
+
return
|
|
181
|
+
except Exception as e:
|
|
182
|
+
log.error(f"Login error: {e}")
|
|
173
183
|
return
|
|
174
184
|
# Once the user is redirected back to your app with an authorization code, exchange it for an access token
|
|
175
185
|
authorization_code = _auth_code_event.value
|
|
@@ -206,77 +216,6 @@ def login_via_browser(csp_url: str, org_id: str):
|
|
|
206
216
|
return do_oauth2_pkce(csp_url, client_id, org_id)
|
|
207
217
|
|
|
208
218
|
|
|
209
|
-
def create_oauth_client(
|
|
210
|
-
oauth_token: dict, csp_url: str, fn_on_new_oauth_token: Callable = None, fn_custom_refresh: Callable = None
|
|
211
|
-
):
|
|
212
|
-
client_id = identify_client_id(csp_url)
|
|
213
|
-
|
|
214
|
-
if not fn_custom_refresh and client_id:
|
|
215
|
-
client = OAuth2Client(
|
|
216
|
-
client_id=client_id,
|
|
217
|
-
client_secret="", # Required by CSP auth
|
|
218
|
-
token=oauth_token,
|
|
219
|
-
token_endpoint=csp_url + "/csp/gateway/am/api/auth/token",
|
|
220
|
-
token_endpoint_auth_method="client_secret_basic",
|
|
221
|
-
update_token=fn_on_new_oauth_token,
|
|
222
|
-
)
|
|
223
|
-
else:
|
|
224
|
-
# This is the case for production today, no csp-oauth-app yet,
|
|
225
|
-
# and auth with API token case.
|
|
226
|
-
client = OAuth2Client(
|
|
227
|
-
# client_id=client_id,
|
|
228
|
-
# client_secret="", # Required by CSP auth
|
|
229
|
-
token=oauth_token,
|
|
230
|
-
# token_endpoint=csp_url + "/csp/gateway/am/api/auth/token",
|
|
231
|
-
# token_endpoint_auth_method="client_secret_basic",
|
|
232
|
-
# update_token=fn_on_new_oauth_token,
|
|
233
|
-
)
|
|
234
|
-
# start our own token refresh timer. Refresh using provided method
|
|
235
|
-
if oauth_token and fn_on_new_oauth_token:
|
|
236
|
-
_register_custom_refresher(client, oauth_token, fn_on_new_oauth_token, fn_custom_refresh)
|
|
237
|
-
else:
|
|
238
|
-
# not even initialized.
|
|
239
|
-
pass
|
|
240
|
-
return client
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
_scheduler: ThreadPoolScheduler = None
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
def _register_custom_refresher(
|
|
247
|
-
client: OAuth2Client, oauth_token, fn_on_new_oauth_token: Callable, fn_custom_refresh: Callable
|
|
248
|
-
):
|
|
249
|
-
global _scheduler
|
|
250
|
-
if not _scheduler:
|
|
251
|
-
_scheduler = ThreadPoolScheduler()
|
|
252
|
-
|
|
253
|
-
def get_refresh_delay(token):
|
|
254
|
-
return token["expires_at"] - time.time() - 30
|
|
255
|
-
|
|
256
|
-
def refresh_impl():
|
|
257
|
-
if client.is_closed:
|
|
258
|
-
return
|
|
259
|
-
new_token = fn_custom_refresh()
|
|
260
|
-
client.token = new_token
|
|
261
|
-
fn_on_new_oauth_token(new_token)
|
|
262
|
-
delay = get_refresh_delay(new_token)
|
|
263
|
-
_scheduler.submit(refresh_impl, delay)
|
|
264
|
-
|
|
265
|
-
delay = get_refresh_delay(oauth_token)
|
|
266
|
-
_scheduler.submit(refresh_impl, delay)
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
def refresh_oauth_token(old_oauth_token: dict, csp_url: str):
|
|
270
|
-
with create_oauth_client(old_oauth_token, csp_url) as client:
|
|
271
|
-
token_endpoint = csp_url + "/csp/gateway/am/api/auth/token"
|
|
272
|
-
new_token = client.refresh_token(token_endpoint)
|
|
273
|
-
if not new_token:
|
|
274
|
-
raise Exception("CSP auth refresh failed.")
|
|
275
|
-
if "cspErrorCode" in new_token:
|
|
276
|
-
raise Exception(f"CSP auth failed: {new_token.get('message')}")
|
|
277
|
-
return new_token
|
|
278
|
-
|
|
279
|
-
|
|
280
219
|
# def _test():
|
|
281
220
|
# import jwt
|
|
282
221
|
# import json
|
hcs_core/sglib/payload_util.py
CHANGED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
from hcs_core.ctxp import jsondot
|
|
6
|
+
|
|
7
|
+
log = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def parse(resp: httpx.Response):
|
|
11
|
+
if not resp.content:
|
|
12
|
+
return None
|
|
13
|
+
content_type = resp.headers["Content-Type"]
|
|
14
|
+
if content_type.startswith("text"):
|
|
15
|
+
return resp.text
|
|
16
|
+
if len(resp.content) > 3:
|
|
17
|
+
try:
|
|
18
|
+
data = resp.json()
|
|
19
|
+
if isinstance(data, list):
|
|
20
|
+
return [jsondot.dotify(obj) for obj in data]
|
|
21
|
+
elif isinstance(data, dict):
|
|
22
|
+
return jsondot.dotify(data)
|
|
23
|
+
except:
|
|
24
|
+
log.info("--- Fail parsing json. Dump content ---")
|
|
25
|
+
log.info(resp.content)
|
|
26
|
+
raise
|
|
27
|
+
else:
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def on404ReturnNone(func):
|
|
32
|
+
try:
|
|
33
|
+
resp = func()
|
|
34
|
+
return parse(resp)
|
|
35
|
+
except httpx.HTTPStatusError as e:
|
|
36
|
+
if e.response.status_code == 404:
|
|
37
|
+
return None
|
|
38
|
+
raise
|
hcs_core/sglib/utils.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
import time
|
|
4
|
+
import zipfile
|
|
5
|
+
|
|
6
|
+
from hcs_core.ctxp import jsondot
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _print_output(text, name):
|
|
10
|
+
if len(text) == 0:
|
|
11
|
+
print(name + ": <EMPTY>")
|
|
12
|
+
else:
|
|
13
|
+
print(name + ":")
|
|
14
|
+
lines = text.splitlines()
|
|
15
|
+
for line in lines:
|
|
16
|
+
print(">> " + line)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def run(cmd, check=True):
|
|
20
|
+
print("CMD: " + cmd)
|
|
21
|
+
parts = cmd.split(" ")
|
|
22
|
+
proc = subprocess.run(
|
|
23
|
+
parts,
|
|
24
|
+
shell=False,
|
|
25
|
+
check=False,
|
|
26
|
+
text=True,
|
|
27
|
+
stdout=subprocess.PIPE,
|
|
28
|
+
stderr=subprocess.STDOUT,
|
|
29
|
+
encoding="utf-8",
|
|
30
|
+
)
|
|
31
|
+
if check and proc.returncode != 0:
|
|
32
|
+
_print_output(proc.stdout, "STDOUT")
|
|
33
|
+
_print_output(proc.stderr, "STDERR")
|
|
34
|
+
proc.check_returncode()
|
|
35
|
+
return proc
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _run_and_capture_output(cmd):
|
|
39
|
+
proc = subprocess.run(cmd, shell=True, check=False, text=True, capture_output=True, encoding="utf-8")
|
|
40
|
+
if proc.returncode != 0:
|
|
41
|
+
_print_output(proc.stdout, "STDOUT")
|
|
42
|
+
_print_output(proc.stderr, "STDERR")
|
|
43
|
+
proc.check_returncode()
|
|
44
|
+
return proc.stdout.strip()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def run_govc_guest_script(script, args):
|
|
48
|
+
print("Running guest script: " + script + " " + args)
|
|
49
|
+
cmd = f'govc guest.start {script} "{args} >/tmp/script-out 2>/tmp/script-err"'
|
|
50
|
+
output = _run_and_capture_output(cmd)
|
|
51
|
+
|
|
52
|
+
# wait for the process to finish
|
|
53
|
+
cmd = f"govc guest.ps -X -x -json -p {output}"
|
|
54
|
+
output = _run_and_capture_output(cmd)
|
|
55
|
+
ret = jsondot.parse(output)
|
|
56
|
+
|
|
57
|
+
output = _run_and_capture_output("govc guest.download /tmp/script-out -")
|
|
58
|
+
_print_output(output, "STDOUT")
|
|
59
|
+
output = _run_and_capture_output("govc guest.download /tmp/script-err -")
|
|
60
|
+
if len(output) > 0:
|
|
61
|
+
_print_output(output, "STDERR")
|
|
62
|
+
|
|
63
|
+
if ret.ProcessInfo[0].ExitCode != 0:
|
|
64
|
+
raise Exception("Fail running script: " + script)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def waitForVmPowerOff(iPath):
|
|
68
|
+
|
|
69
|
+
def current_millis():
|
|
70
|
+
return int(round(time.time() * 1000))
|
|
71
|
+
|
|
72
|
+
start_time = current_millis()
|
|
73
|
+
while current_millis() - start_time < 5 * 60 * 1000:
|
|
74
|
+
p = subprocess.run(
|
|
75
|
+
"govc vm.info -vm.ipath=" + iPath,
|
|
76
|
+
stdout=subprocess.PIPE,
|
|
77
|
+
stderr=subprocess.STDOUT,
|
|
78
|
+
shell=True,
|
|
79
|
+
)
|
|
80
|
+
if str(p.stdout).find("poweredOff") > 0:
|
|
81
|
+
return
|
|
82
|
+
time.sleep(5)
|
|
83
|
+
raise Exception("Timeout customization. Check log in deployer VM.")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def zipdir(src_dir, target_file):
|
|
87
|
+
with zipfile.ZipFile(target_file, "w", zipfile.ZIP_DEFLATED) as file:
|
|
88
|
+
for root, dirs, files in os.walk(src_dir):
|
|
89
|
+
for name in files:
|
|
90
|
+
filePath = os.path.join(root, name)
|
|
91
|
+
arcname = filePath[len(src_dir) :]
|
|
92
|
+
file.write(filePath, arcname)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def dos2unix(file):
|
|
96
|
+
file = os.path.abspath(file)
|
|
97
|
+
content = ""
|
|
98
|
+
outsize = 0
|
|
99
|
+
with open(file, "rb") as infile:
|
|
100
|
+
content = infile.read()
|
|
101
|
+
with open(file, "wb") as output:
|
|
102
|
+
for line in content.splitlines():
|
|
103
|
+
outsize += len(line) + 1
|
|
104
|
+
output.write(line + b"\n")
|
|
105
|
+
stripped_bytes = len(content) - outsize
|
|
106
|
+
if stripped_bytes != 0:
|
|
107
|
+
print("dos2unix: Stripped %s bytes. %s" % (stripped_bytes, file))
|
hcs_core/util/check_license.py
CHANGED
hcs_core/util/duration.py
CHANGED
|
@@ -14,7 +14,7 @@ limitations under the License.
|
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
16
|
import re
|
|
17
|
-
from datetime import
|
|
17
|
+
from datetime import datetime, timedelta, timezone
|
|
18
18
|
|
|
19
19
|
PATTERN = re.compile("^(([0-9]+)D)?(([0-9]+)H)?(([0-9]+)M)?(([0-9]+)S)?$")
|
|
20
20
|
|
|
@@ -116,10 +116,13 @@ def to_utc(dt):
|
|
|
116
116
|
|
|
117
117
|
|
|
118
118
|
def stale(when) -> str:
|
|
119
|
+
return format_timedelta(from_now(when), True)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def from_now(when) -> timedelta:
|
|
119
123
|
when_utc = to_utc(when)
|
|
120
124
|
now_utc = datetime.now().astimezone(timezone.utc)
|
|
121
|
-
|
|
122
|
-
return format_timedelta(diff, True)
|
|
125
|
+
return now_utc - when_utc
|
|
123
126
|
|
|
124
127
|
|
|
125
128
|
def _test():
|