hcs-core 0.1.289__tar.gz → 0.1.290__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.
Files changed (71) hide show
  1. {hcs_core-0.1.289 → hcs_core-0.1.290}/.gitignore +0 -1
  2. {hcs_core-0.1.289 → hcs_core-0.1.290}/PKG-INFO +2 -1
  3. hcs_core-0.1.290/hcs_core/__init__.py +1 -0
  4. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/ctxp/built_in_cmds/profile.py +4 -2
  5. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/ctxp/cli_options.py +9 -1
  6. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/ctxp/cli_processor.py +5 -2
  7. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/ctxp/data_util.py +14 -9
  8. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/ctxp/fstore.py +1 -1
  9. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/ctxp/profile.py +4 -2
  10. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/ctxp/util.py +23 -3
  11. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/plan/__init__.py +1 -0
  12. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/plan/core.py +13 -2
  13. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/plan/kop.py +1 -1
  14. hcs_core-0.1.290/hcs_core/sglib/auth.py +186 -0
  15. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/sglib/client_util.py +125 -58
  16. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/sglib/csp.py +70 -2
  17. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/sglib/ez_client.py +35 -20
  18. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/sglib/hcs_client.py +2 -6
  19. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/util/job_view.py +3 -2
  20. {hcs_core-0.1.289 → hcs_core-0.1.290}/pyproject.toml +1 -0
  21. hcs_core-0.1.289/hcs_core/__init__.py +0 -1
  22. hcs_core-0.1.289/hcs_core/sglib/auth.py +0 -187
  23. {hcs_core-0.1.289 → hcs_core-0.1.290}/README.md +0 -0
  24. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/ctxp/__init__.py +0 -0
  25. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/ctxp/_init.py +0 -0
  26. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/ctxp/built_in_cmds/__init__.py +0 -0
  27. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/ctxp/built_in_cmds/_ut.py +0 -0
  28. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/ctxp/built_in_cmds/context.py +0 -0
  29. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/ctxp/cmd_util.py +0 -0
  30. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/ctxp/config.py +0 -0
  31. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/ctxp/context.py +0 -0
  32. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/ctxp/dispatcher.py +0 -0
  33. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/ctxp/duration.py +0 -0
  34. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/ctxp/extension.py +0 -0
  35. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/ctxp/fn_util.py +0 -0
  36. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/ctxp/jsondot.py +0 -0
  37. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/ctxp/logger.py +0 -0
  38. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/ctxp/profile_store.py +0 -0
  39. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/ctxp/recent.py +0 -0
  40. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/ctxp/state.py +0 -0
  41. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/ctxp/task_schd.py +0 -0
  42. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/ctxp/template_util.py +0 -0
  43. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/ctxp/timeutil.py +0 -0
  44. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/ctxp/var_template.py +0 -0
  45. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/plan/actions.py +0 -0
  46. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/plan/base_provider.py +0 -0
  47. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/plan/context.py +0 -0
  48. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/plan/dag.py +0 -0
  49. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/plan/helper.py +0 -0
  50. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/plan/provider/__init__.py +0 -0
  51. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/plan/provider/dev/__init__.py +0 -0
  52. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/plan/provider/dev/_prepare.py +0 -0
  53. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/plan/provider/dev/dummy.py +0 -0
  54. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/plan/provider/dev/fibonacci.py +0 -0
  55. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/sglib/__init__.py +0 -0
  56. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/sglib/cli_options.py +0 -0
  57. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/sglib/init.py +0 -0
  58. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/sglib/login_support.py +0 -0
  59. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/sglib/payload_util.py +0 -0
  60. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/sglib/requtil.py +0 -0
  61. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/sglib/utils.py +0 -0
  62. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/util/__init__.py +0 -0
  63. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/util/check_license.py +0 -0
  64. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/util/duration.py +0 -0
  65. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/util/exit.py +0 -0
  66. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/util/hcs_constants.py +0 -0
  67. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/util/pki_util.py +0 -0
  68. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/util/query_util.py +0 -0
  69. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/util/scheduler.py +0 -0
  70. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/util/ssl_util.py +0 -0
  71. {hcs_core-0.1.289 → hcs_core-0.1.290}/hcs_core/util/versions.py +0 -0
@@ -169,7 +169,6 @@ state/
169
169
  t.py
170
170
  logs/*.log
171
171
  lab/
172
- *.plan.yml
173
172
  *.state.yml
174
173
 
175
174
  hcsenv/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hcs-core
3
- Version: 0.1.289
3
+ Version: 0.1.290
4
4
  Summary: Horizon Cloud Service CLI module.
5
5
  Project-URL: Homepage, https://github.com/euc-eng/hcs-cli
6
6
  Project-URL: Bug Tracker, https://github.com/euc-eng/hcs-cli/issues
@@ -26,6 +26,7 @@ Requires-Dist: psutil>=5.9.4
26
26
  Requires-Dist: pydantic>=2.0.0
27
27
  Requires-Dist: pyjwt>=2.8.0
28
28
  Requires-Dist: pyopenssl>=24.1.0
29
+ Requires-Dist: python-dotenv>=1.1.0
29
30
  Requires-Dist: pyyaml>=6.0.1
30
31
  Requires-Dist: questionary>=2.0.1
31
32
  Requires-Dist: rel>=0.4.7
@@ -0,0 +1 @@
1
+ __version__ = "0.1.290"
@@ -53,7 +53,7 @@ def use(name: str):
53
53
  if ret:
54
54
  if profile.use(ret) is None:
55
55
  panic("No such profile: " + name)
56
- return ret
56
+ return profile.current()
57
57
  else:
58
58
  # aborted
59
59
  return "", 1
@@ -103,9 +103,11 @@ def get(name: str):
103
103
 
104
104
 
105
105
  @profile_cmd_group.command()
106
- @click.argument("name")
106
+ @click.argument("name", required=False)
107
107
  def delete(name: str):
108
108
  """Delete a profile by name."""
109
+ if not name:
110
+ name = profile.name()
109
111
  profile.delete(name)
110
112
 
111
113
 
@@ -27,7 +27,7 @@ verbose = click.option(
27
27
  output = click.option(
28
28
  "--output",
29
29
  "-o",
30
- type=click.Choice(["json", "json-compact", "yaml", "text", "table"], case_sensitive=False),
30
+ type=click.Choice(["json", "json-compact", "yaml", "yml", "text", "table"], case_sensitive=False),
31
31
  default=None,
32
32
  hidden=True,
33
33
  help="Specify output format",
@@ -41,6 +41,14 @@ field = click.option(
41
41
  help="Specify fields to output, in comma separated field names.",
42
42
  )
43
43
 
44
+ exclude_field = click.option(
45
+ "--exclude-field",
46
+ type=str,
47
+ required=False,
48
+ hidden=True,
49
+ help="Specify fields to exclude from output, in comma separated field names.",
50
+ )
51
+
44
52
  wait = click.option(
45
53
  "--wait",
46
54
  "-w",
@@ -75,6 +75,7 @@ def _ensure_sub_group(current: Group, mod_path: Path):
75
75
  meta = _read_group_meta(mod_path)
76
76
  help = meta.get("help")
77
77
  extension = meta.get("extension")
78
+ hidden = meta.get("hidden", False)
78
79
 
79
80
  subgroup = current.commands.get(name)
80
81
  if subgroup and isinstance(subgroup, Group):
@@ -82,7 +83,7 @@ def _ensure_sub_group(current: Group, mod_path: Path):
82
83
  subgroup.mod_path = mod_path
83
84
  return subgroup
84
85
 
85
- subgroup = LazyGroup(extension=extension, name=name, help=help, mod_path=mod_path)
86
+ subgroup = LazyGroup(extension=extension, name=name, help=help, mod_path=mod_path, hidden=hidden)
86
87
  current.add_command(subgroup)
87
88
  return subgroup
88
89
 
@@ -166,13 +167,14 @@ def _import_cmd_file(mod_path: Path, parent: click.core.Group):
166
167
  # Create a new decorator that combines the individual decorators
167
168
  def _default_io(cmd: click.Command):
168
169
 
169
- from .cli_options import field, first, ids, output
170
+ from .cli_options import exclude_field, field, first, ids, output
170
171
 
171
172
  cmd = output(cmd)
172
173
  # cmd = cli_options.verbose(cmd)
173
174
  cmd = field(cmd)
174
175
  cmd = ids(cmd)
175
176
  cmd = first(cmd)
177
+ cmd = exclude_field(cmd)
176
178
  callback = cmd.callback
177
179
 
178
180
  def inner(*args, **kwargs):
@@ -182,6 +184,7 @@ def _default_io(cmd: click.Command):
182
184
  "field": kwargs.pop("field"),
183
185
  "ids": kwargs.pop("ids"),
184
186
  "first": kwargs.pop("first"),
187
+ "exclude_field": kwargs.pop("exclude_field"),
185
188
  }
186
189
  if io_args["output"] == "table":
187
190
 
@@ -1,4 +1,5 @@
1
1
  import json
2
+ import os
2
3
  import re
3
4
  from io import TextIOWrapper
4
5
  from os import chmod, path
@@ -260,22 +261,26 @@ def process_variables(obj: dict, fn_get_var=None, use_env: bool = True):
260
261
  fn_get_var = _fn_get_var
261
262
 
262
263
  if use_env:
263
- import os
264
264
 
265
265
  prev_fn_get_var = fn_get_var
266
266
 
267
267
  def _fn_get_var_from_env(name: str):
268
268
  if not name.startswith("env."):
269
269
  return prev_fn_get_var(name)
270
- v = None
271
270
  actual_name = name[4:]
272
- v = os.environ.get(actual_name, None)
273
- if v:
274
- return v, True
275
- v, found = prev_fn_get_var(name)
276
- if found:
277
- return v
278
- raise CtxpException(f"Environment variable '{actual_name}' is used in template, but not found. ")
271
+ if actual_name.endswith("?"):
272
+ # optional
273
+ actual_name = actual_name[:-1]
274
+ required = False
275
+ else:
276
+ # required
277
+ required = True
278
+
279
+ if actual_name not in os.environ:
280
+ if required:
281
+ raise CtxpException(f"Environment variable '{actual_name}' is used in template, but not found. ")
282
+ return None, True
283
+ return os.environ[actual_name], True
279
284
 
280
285
  fn_get_var = _fn_get_var_from_env
281
286
 
@@ -199,7 +199,7 @@ class fstore:
199
199
  outfile.write(str(data))
200
200
  elif format == "json-compact":
201
201
  jsondot.save(data=data, file=file_path, pretty=False, lock=True)
202
- elif format == "yaml":
202
+ elif format == "yaml" or format == "yml":
203
203
  import yaml
204
204
 
205
205
  with open(file_path, "w", encoding="utf-8") as outfile:
@@ -63,7 +63,7 @@ def create(name: str, data: dict, auto_use: bool = True, overwrite: bool = False
63
63
  save_data_file(data, file(name), "yaml", 0o600)
64
64
  if auto_use:
65
65
  use(name)
66
- return get(name)
66
+ return get(name, reload=True)
67
67
 
68
68
 
69
69
  def copy(from_name: str, to_name: str, overwrite: bool = True):
@@ -126,6 +126,8 @@ def use(name: str) -> str:
126
126
  return
127
127
 
128
128
  _set_active_profile_name(name)
129
+ global _data
130
+ _data = _load_data_file_with_env_overrides(name, None)
129
131
  return name
130
132
 
131
133
 
@@ -223,10 +225,10 @@ class auth:
223
225
  if json.dumps(data) == json.dumps(_auth_cache):
224
226
  return
225
227
  _auth_cache = deepcopy(data)
226
- file_name = auth._file_name()
227
228
  if os.environ.get("CTXP_AUTH_PERSIST", "true").lower() == "false":
228
229
  pass
229
230
  else:
231
+ file_name = auth._file_name()
230
232
  save_data_file(data, file_name, "yaml", 0o600)
231
233
 
232
234
  @staticmethod
@@ -57,6 +57,7 @@ def validate_error_return(reason, return_code):
57
57
  def print_output(data: Any, args: dict, file=sys.stdout):
58
58
  output = args.get("output", "json")
59
59
  fields = args.get("field")
60
+ exclude_field = args.get("exclude_field")
60
61
  ids = args.get("ids", False)
61
62
  first = args.get("first", False)
62
63
 
@@ -74,14 +75,17 @@ def print_output(data: Any, args: dict, file=sys.stdout):
74
75
  if fields:
75
76
  raise CtxpException("--ids and --fields should not be used together.")
76
77
  data = _convert_to_id_only(data)
77
- elif fields:
78
- data = _filter_fields(data, fields)
78
+ else:
79
+ if fields:
80
+ data = _filter_fields(data, fields)
81
+ if exclude_field:
82
+ data = _exclude_fields(data, exclude_field)
79
83
 
80
84
  if output is None or output == "json":
81
85
  text = json.dumps(data, default=vars, indent=4)
82
86
  elif output == "json-compact":
83
87
  text = json.dumps(data, default=vars)
84
- elif output == "yaml":
88
+ elif output == "yaml" or output == "yml":
85
89
  from . import jsondot
86
90
 
87
91
  text = yaml.dump(jsondot.plain(data), sort_keys=False)
@@ -168,6 +172,22 @@ def _filter_fields(obj: Any, fields: str):
168
172
  return _filter_obj(obj)
169
173
 
170
174
 
175
+ def _exclude_fields(obj: Any, fields_exclude: str):
176
+ parts = fields_exclude.split(",")
177
+
178
+ def _filter_obj(o):
179
+ if not isinstance(o, dict):
180
+ return o
181
+ for k in list(o.keys()):
182
+ if k in parts:
183
+ del o[k]
184
+ return o
185
+
186
+ if isinstance(obj, list):
187
+ return list(map(_filter_obj, obj))
188
+ return _filter_obj(obj)
189
+
190
+
171
191
  def panic(reason: Any = None, code: int = 1):
172
192
  if isinstance(reason, Exception):
173
193
  text = error_details(reason)
@@ -5,6 +5,7 @@ from .core import clear as clear
5
5
  from .core import destroy as destroy
6
6
  from .core import get_deployment_data as get_deployment_data
7
7
  from .core import graph as graph
8
+ from .core import resolve as resolve
8
9
  from .helper import PlanException as PlanException
9
10
  from .helper import PluginException as PluginException
10
11
  from .kop import attach_job_view as attach_job_view
@@ -1,5 +1,5 @@
1
1
  """
2
- Copyright 2023-2023 VMware Inc.
2
+ Copyright 2025-2025 Omnissa Inc.
3
3
  SPDX-License-Identifier: Apache-2.0
4
4
 
5
5
  Licensed under the Apache License, Version 2.0 (the "License");
@@ -93,6 +93,17 @@ def _prepare_data(data: dict, additional_context: dict, target_resource_name: st
93
93
  # return True
94
94
 
95
95
 
96
+ def resolve(data: dict, additional_context: dict = None, target_resource_name: str = None):
97
+ blueprint, state, state_file = _prepare_data(data, additional_context, None)
98
+ if target_resource_name:
99
+ if target_resource_name in blueprint["resource"]:
100
+ return blueprint["resource"][target_resource_name]["data"]
101
+ if target_resource_name in blueprint["runtime"]:
102
+ return blueprint["runtime"][target_resource_name]["data"]
103
+ raise PlanException("Target resource or runtime not found: " + target_resource_name)
104
+ return blueprint
105
+
106
+
96
107
  def apply(
97
108
  data: dict,
98
109
  additional_context: dict = None,
@@ -249,7 +260,7 @@ def _deploy_res(name, res, state):
249
260
  action = handler.decide(res_data, res_state)
250
261
 
251
262
  if action == actions.skip:
252
- kop.skip("Already deployed")
263
+ kop.skip(_get_res_text(handler, res_state))
253
264
  return
254
265
 
255
266
  if action == actions.recreate:
@@ -1,5 +1,5 @@
1
1
  """
2
- Copyright 2023-2023 VMware Inc.
2
+ Copyright 2025-2025 Omnissa Inc.
3
3
  SPDX-License-Identifier: Apache-2.0
4
4
 
5
5
  Licensed under the Apache License, Version 2.0 (the "License");
@@ -0,0 +1,186 @@
1
+ """
2
+ Copyright 2023-2023 VMware Inc.
3
+ SPDX-License-Identifier: Apache-2.0
4
+
5
+ Licensed under the Apache License, Version 2.0 (the "License");
6
+ you may not use this file except in compliance with the License.
7
+ You may obtain a copy of the License at
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
14
+ """
15
+
16
+ import hashlib
17
+ import json
18
+ import logging
19
+ import threading
20
+ import time
21
+
22
+ import jwt
23
+ from authlib.integrations.httpx_client import OAuth2Client
24
+
25
+ from hcs_core.ctxp import CtxpException, panic, profile
26
+ from hcs_core.ctxp.jsondot import dotdict, dotify
27
+
28
+ from .csp import CspClient
29
+
30
+ log = logging.getLogger(__name__)
31
+
32
+
33
+ def _is_auth_valid(auth_data):
34
+ leeway = 60
35
+ return auth_data and time.time() + leeway < auth_data["expires_at"]
36
+
37
+
38
+ _login_lock = threading.Lock()
39
+
40
+
41
+ def login(force_refresh: bool = False):
42
+ """Ensure login state, using credentials from the current profile. Return oauth token."""
43
+ return _populate_token_with_cache(profile.current().csp, force_refresh)
44
+
45
+
46
+ def refresh_oauth_token(old_oauth_token: dict, csp_url: str):
47
+ with OAuth2Client(token=old_oauth_token) as client:
48
+ log.debug("Refresh auth token...")
49
+ token_url = csp_url + "/csp/gateway/am/api/auth/token"
50
+ from .login_support import identify_client_id
51
+
52
+ csp_specific_req_not_oauth_standard = (identify_client_id(csp_url), "")
53
+ new_token = client.refresh_token(token_url, auth=csp_specific_req_not_oauth_standard)
54
+ log.debug(f"New auth token: {new_token}")
55
+ if not new_token:
56
+ raise Exception("CSP auth refresh failed.")
57
+ if "cspErrorCode" in new_token:
58
+ raise Exception(f"CSP auth failed: {new_token.get('message')}")
59
+
60
+ return new_token
61
+
62
+
63
+ def _has_credential(auth_config: dict):
64
+ return (
65
+ auth_config.get("apiToken")
66
+ or auth_config.get("api_token")
67
+ or auth_config.get("clientId")
68
+ or auth_config.get("client_id")
69
+ or auth_config.get("basic")
70
+ )
71
+
72
+
73
+ def get_auth_cache(auth_config: dict):
74
+ cache, hash, token = _get_auth_cache(auth_config)
75
+ return token
76
+
77
+
78
+ def _get_auth_cache(auth_config: dict):
79
+ text = json.dumps(auth_config, default=vars)
80
+ hash = hashlib.md5(text.encode("ascii"), usedforsecurity=False).hexdigest()
81
+ auth_cache = profile.auth.get()
82
+ token = auth_cache.get(hash, None)
83
+ if token and not _is_auth_valid(token):
84
+ token = None
85
+ return auth_cache, hash, token
86
+
87
+
88
+ def save_auth_cache(auth_config: dict, token: dict):
89
+ """Save the auth token to the profile auth cache."""
90
+ cache, hash, _ = _get_auth_cache(auth_config)
91
+ if not token:
92
+ if hash in cache:
93
+ del cache[hash]
94
+ return
95
+ if not token.get("expires_at"):
96
+ token["expires_at"] = int(time.time() + token["expires_in"])
97
+ cache[hash] = token
98
+ profile.auth.set(cache)
99
+
100
+
101
+ def _populate_token_with_cache(auth_config: dict, force_refresh: bool = False):
102
+
103
+ with _login_lock:
104
+ cache, hash, token = _get_auth_cache(auth_config)
105
+ if token and not force_refresh:
106
+ return token
107
+
108
+ # invalid token. Refresh or recreate it.
109
+ if token:
110
+ # try using refresh token if possible
111
+ if auth_config.get("provider", "vmwarecsp") == "vmwarecsp":
112
+ try:
113
+ token = refresh_oauth_token(token, auth_config.url)
114
+ except Exception as e:
115
+ log.debug(f"Failed to refresh OAuth token: {e}")
116
+ token = None
117
+ else:
118
+ # hcs auth-service. Does not support refresh token.
119
+ token = None
120
+
121
+ if not token:
122
+ if _has_credential(auth_config):
123
+ token = CspClient.create(**auth_config).oauth_token()
124
+ else:
125
+ if auth_config.get("browser"):
126
+ from .login_support import login_via_browser
127
+
128
+ token = login_via_browser(auth_config.url, auth_config.orgId)
129
+ if not token:
130
+ raise CtxpException("Browser auth failed.")
131
+ else:
132
+ raise CtxpException(
133
+ "Browser auth was never attempted and no client credentials or API token provided."
134
+ )
135
+
136
+ if not token.get("expires_at"):
137
+ token["expires_at"] = int(time.time() + token["expires_in"])
138
+
139
+ cache[hash] = token
140
+ profile.auth.set(cache)
141
+ return token
142
+
143
+
144
+ class CustomOAuth2Client(OAuth2Client):
145
+ def __init__(self, auth_config: dict):
146
+ super().__init__()
147
+ self.auth_config = auth_config
148
+
149
+ def ensure_token(self):
150
+ # pylint: disable=access-member-before-definition
151
+ if self.token is None or not super().ensure_active_token():
152
+ self.token = _populate_token_with_cache(self.auth_config)
153
+
154
+
155
+ def oauth_client(auth_config: dict = None):
156
+ if not auth_config:
157
+ auth_config = profile.current().csp
158
+ return CustomOAuth2Client(auth_config)
159
+
160
+
161
+ def details(get_org_details: bool = False) -> dotdict:
162
+ """Get the auth details, for the current profile"""
163
+ oauth_token = login()
164
+ if not oauth_token:
165
+ return
166
+ return details_from_token(oauth_token, get_org_details)
167
+
168
+
169
+ def details_from_token(oauth_token, get_org_details: bool = False):
170
+ decoded = jwt.decode(oauth_token["access_token"], options={"verify_signature": False})
171
+ org_id = decoded["context_name"]
172
+ ret = {"token": oauth_token, "jwt": decoded, "org": {"id": org_id}}
173
+
174
+ if get_org_details:
175
+ csp_client = CspClient(url=profile.current().csp.url, oauth_token=oauth_token)
176
+ try:
177
+ org_details = csp_client.get_org_details(org_id)
178
+ except Exception as e:
179
+ org_details = {"error": f"Fail retrieving org details: {e}"}
180
+ ret["org"].update(org_details)
181
+ return dotify(ret)
182
+
183
+
184
+ def get_org_id_from_token(oauth_token: str) -> str:
185
+ decoded = jwt.decode(oauth_token["access_token"], options={"verify_signature": False})
186
+ return decoded["context_name"]