hcs-cli 0.1.320__py3-none-any.whl → 0.1.322__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_cli/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
- __version__ = "0.1.320"
1
+ __version__ = "0.1.322"
2
2
 
3
3
  from . import service as service # noqa: F401
hcs_cli/cmds/api.py CHANGED
@@ -3,8 +3,7 @@ import sys
3
3
 
4
4
  import click
5
5
  from hcs_core.ctxp import CtxpException, profile
6
- from hcs_core.sglib.client_util import hdc_service_client, is_regional_service, regional_service_client
7
- from hcs_core.util.query_util import PageRequest
6
+ from hcs_cli.service import api as hcs_api
8
7
 
9
8
 
10
9
  def _has_stdin_data():
@@ -143,12 +142,10 @@ def api(
143
142
  if not path.startswith("/"):
144
143
  raise click.UsageError("Path must start with a '/'. Please provide a valid context path. Provided path=" + path)
145
144
 
146
- service_path = path.split("/")[1]
147
- api_path = path[len(service_path) + 1 :]
148
- if is_regional_service(service_path):
149
- client = regional_service_client(service_path, region=region)
145
+ if region or hdc:
146
+ handler = hcs_api.with_context(hdc=hdc, region=region)
150
147
  else:
151
- client = hdc_service_client(service_path, hdc=hdc)
148
+ handler = hcs_api
152
149
 
153
150
  if header:
154
151
  headers = {h.split(":")[0].strip(): h.split(":")[1].strip() for h in header}
@@ -158,28 +155,13 @@ def api(
158
155
  # print( f"{method} {api_path} text={raw_data} json={json_data}" )
159
156
 
160
157
  if method == "GET":
161
- if all_pages:
162
-
163
- def _get_page(query_string):
164
- url = api_path
165
- if url.find("?") > 0:
166
- url += "&" + query_string
167
- else:
168
- url += "?" + query_string
169
- ret = client.get(url)
170
- return ret
171
-
172
- response = PageRequest(_get_page, size=100, limit=0).get()
173
- else:
174
- response = client.get(api_path, headers=headers, raise_on_404=raise_on_404)
158
+ return handler.get(path, headers=headers, all_pages=all_pages, raise_on_404=raise_on_404)
175
159
  elif method == "POST":
176
- response = client.post(api_path, text=raw_data, json=json_data, headers=headers)
160
+ return handler.post(path, headers=headers, text=raw_data, json=json_data)
177
161
  elif method == "PUT":
178
- response = client.put(api_path, text=raw_data, json=json_data, headers=headers)
162
+ return handler.put(path, headers=headers, text=raw_data, json=json_data)
179
163
  elif method == "DELETE":
180
- response = client.delete(api_path, headers=headers, raise_on_404=raise_on_404)
164
+ return handler.delete(path, headers=headers, raise_on_404=raise_on_404)
181
165
  elif method == "PATCH":
182
- response = client.patch(api_path, text=raw_data, json=json_data, headers=headers)
183
- else:
184
- raise click.UsageError(f"Unsupported HTTP method: {method}")
185
- return response
166
+ return handler.patch(path, headers=headers, text=raw_data, json=json_data)
167
+ raise click.UsageError(f"Unsupported HTTP method: {method}")
@@ -15,10 +15,17 @@ limitations under the License.
15
15
 
16
16
  import click
17
17
 
18
- from hcs_cli.service.clouddriver import provider
18
+ from hcs_cli.service import clouddriver as cd
19
19
 
20
20
 
21
21
  @click.command()
22
- def summary(**kwargs):
23
- """Get summary"""
24
- return provider.summary()
22
+ @click.argument("name", type=str, required=True)
23
+ def action(name: str, **kwargs):
24
+ """Get perform an action"""
25
+ return cd.action(name, kwargs)
26
+
27
+
28
+ @click.command()
29
+ def health(**kwargs):
30
+ """Get health"""
31
+ return cd.health()
@@ -75,7 +75,7 @@ stringData:
75
75
  JcSvTMU2HOuQ/q6vFZq3PRbvroMeXlw2WJrqO3vu04SZwYiP8dcrlHTXQJh+Mp9T
76
76
  mQ==
77
77
  -----END CERTIFICATE-----
78
- mqtt.ca-key: |-
78
+ mqtt.ca-key: |-
79
79
  -----BEGIN PRIVATE KEY-----
80
80
  MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDUNJx2Cf0noNHc
81
81
  DXB9AFPSFMkkpwxb0wtqaCLiB7v5zkF7CVBl5LnecVk7CM8bJY62H90Lv8Zv/2sl
@@ -104,7 +104,7 @@ stringData:
104
104
  boPQ+RK1VKj5UFkUKz5LiohLmCBEQ7sh1HBS2Vje8MFtg0P138sRlbMCvdAEARTZ
105
105
  uQfAKLns7QFf9W1C60AVAQ==
106
106
  -----END PRIVATE KEY-----
107
- mqtt.client-cert: |-
107
+ mqtt.client-cert: |-
108
108
  -----BEGIN CERTIFICATE-----
109
109
  MIIFKDCCAxCgAwIBAgIFALjj4O4wDQYJKoZIhvcNAQELBQAwdTELMAkGA1UEBhMC
110
110
  VVMxCzAJBgNVBAgMAkNBMR8wHQYDVQQKDBZIb3Jpem9uIENsb3VkIFNlcnZpY2Vz
@@ -204,7 +204,7 @@ stringData:
204
204
  JcSvTMU2HOuQ/q6vFZq3PRbvroMeXlw2WJrqO3vu04SZwYiP8dcrlHTXQJh+Mp9T
205
205
  mQ==
206
206
  -----END CERTIFICATE-----
207
- mqtt.client-key: |-
207
+ mqtt.client-key: |-
208
208
  -----BEGIN PRIVATE KEY-----
209
209
  MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC40aC4pyy0+++D
210
210
  kD1cPRsMWO4ANZ8L4LaaHcg/e/1NbiExd/QlZESNJnnIyD29qY1h9WGEVDHtInsl
hcs_cli/cmds/hoc/stats.py CHANGED
@@ -1,6 +1,8 @@
1
1
  import click
2
- import time
2
+ import datetime
3
3
  import json
4
+ import time
5
+
4
6
  from hcs_cli.service.hoc import diagnostic
5
7
  from hcs_cli.service import tsctl
6
8
 
@@ -22,25 +24,49 @@ def aggregate_connects(from_: int, to: int, wait: bool, verbose: bool):
22
24
  return "", 1
23
25
 
24
26
  if wait:
25
- print("Waiting for task to complete...")
26
- totalWaitTime = 0
27
- while True:
28
- time.sleep(10)
29
- taskStatus = tsctl.task.lastlog(result.namespace, result.group, result.taskKey)
30
- if "state" in taskStatus and taskStatus.state == "Error":
31
- print(f"Task is in {taskStatus.state} state - {taskStatus.error} ")
32
- print(json.dumps(taskStatus, indent=2))
33
- break
34
- if "state" in taskStatus and taskStatus.state == "Success":
35
- print("Task completed successfully.")
36
- print(json.dumps(taskStatus, indent=2))
37
- break
38
- if ("state" in taskStatus and taskStatus.state == "") or "state" not in taskStatus:
39
- print("Task is still in progress...")
40
- totalWaitTime += 10
41
- if totalWaitTime >= 600:
42
- print("Waited for 10 minutes, exiting.")
43
- break
44
- continue
27
+ wait_for_task(result)
28
+
29
+
30
+ @click.command(name="stats-aggregate-connects-day-before")
31
+ @click.option("--wait", type=bool, required=False, is_flag=True, default=False)
32
+ @click.option("--verbose", type=bool, required=False, is_flag=True, default=False)
33
+ def aggregate_connects_day_before(wait: bool, verbose: bool):
34
+ dt = datetime.datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0)
35
+ dt_2_days_ago = dt - datetime.timedelta(days=2)
36
+ dt_1_day_ago = dt - datetime.timedelta(days=1)
37
+
38
+ print(f"Aggregating connect events from {dt_2_days_ago} to {dt_1_day_ago}")
39
+ from_ = int(dt_2_days_ago.timestamp())
40
+ to = int(dt_1_day_ago.timestamp())
41
+ payload = {"from": from_, "to": to}
42
+ result = diagnostic.aggregateConnects(payload, verbose=verbose)
43
+ click.echo(result)
44
+ if not result:
45
+ return "", 1
46
+ if wait:
47
+ wait_for_task(result)
48
+
49
+
50
+ def wait_for_task(result: dict):
51
+ print("Waiting for task to complete...")
52
+ totalWaitTime = 0
53
+ while True:
54
+ time.sleep(10)
55
+ taskStatus = tsctl.task.lastlog(result.namespace, result.group, result.taskKey)
56
+ if "state" in taskStatus and taskStatus.state == "Error":
57
+ print(f"Task is in {taskStatus.state} state - {taskStatus.error} ")
45
58
  print(json.dumps(taskStatus, indent=2))
46
59
  break
60
+ if "state" in taskStatus and taskStatus.state == "Success":
61
+ print("Task completed successfully.")
62
+ print(json.dumps(taskStatus, indent=2))
63
+ break
64
+ if ("state" in taskStatus and taskStatus.state == "") or "state" not in taskStatus:
65
+ print("Task is still in progress...")
66
+ totalWaitTime += 10
67
+ if totalWaitTime >= 600:
68
+ print("Waited for 10 minutes, exiting.")
69
+ break
70
+ continue
71
+ print(json.dumps(taskStatus, indent=2))
72
+ break
@@ -37,11 +37,15 @@ def template(id: str, org: str, **kwargs):
37
37
  if not plan:
38
38
  return None, 1
39
39
 
40
- template = hcs.template.get(org_id=org_id, id=id)
40
+ template_data = hcs.template.get(org_id=org_id, id=id)
41
41
 
42
- plan["meta"]["maxCapacity"] = template["sparePolicy"]["limit"]
42
+ plan["meta"]["maxCapacity"] = template_data["sparePolicy"]["limit"]
43
43
  calendar = plan["calendar"]
44
44
  for key in calendar:
45
45
  calendar[key]["calculatedCapacity"] = calendar[key]["idealCapacity"]
46
46
  calendar[key]["idealCapacity"] = calendar[key]["forecastCapacity"]
47
- edit_plan(plan, template["name"])
47
+
48
+ # fetch usage data for the usage chart
49
+ usage = hcs.scm.template_usage(org_id, id)
50
+
51
+ edit_plan(plan, template_data["name"], usage)
hcs_cli/cmds/task.py CHANGED
@@ -17,6 +17,7 @@ import click
17
17
  import hcs_core.sglib.cli_options as cli
18
18
  import yumako
19
19
  from hcs_core.ctxp import CtxpException, recent, util
20
+ from datetime import datetime
20
21
 
21
22
  from hcs_cli.service import task
22
23
 
@@ -222,6 +223,7 @@ def use(namespace: str, group: str, smart_path: str, reset: bool, **kwargs):
222
223
  required=False,
223
224
  help="Filter tasks by state, for one-time tasks only. Comma separated values. E.g. Init,Running,Canceled,Error,Success",
224
225
  )
226
+ @click.option("--v1", is_flag=True, default=False, required=False, help="Use v1 task API.")
225
227
  @click.option("--v1log", is_flag=True, default=False, required=False, help="Fetch last log for v1 tasks.")
226
228
  @cli.limit
227
229
  @cli.search
@@ -476,7 +478,7 @@ def retrigger(org: str, namespace: str, group: str, smart_path: str, execution_i
476
478
  return task.retrigger(org_id, namespace, group, key, execution_id)
477
479
 
478
480
 
479
- @task_cmd_group.command()
481
+ @task_cmd_group.command(hidden=True)
480
482
  @cli.org_id
481
483
  @click.option("--namespace", "-n", type=str, required=False)
482
484
  @click.option("--group", "-g", type=str, required=False)
@@ -496,6 +498,51 @@ def resubmit(org: str, namespace: str, group: str, smart_path: str, confirm: boo
496
498
  return task.resubmit(org_id, namespace, group, key)
497
499
 
498
500
 
501
+ @task_cmd_group.command()
502
+ @cli.org_id
503
+ @click.option("--namespace", "-n", type=str, required=False)
504
+ @click.option("--group", "-g", type=str, required=False)
505
+ @click.option(
506
+ "--reference", "-r", type=str, required=True, help="Reference task to copy parameters from, format: [[<namespace>/]<group>/]<key>."
507
+ )
508
+ @click.option(
509
+ "--at", type=str, required=False, help="Schedule time, exact time 'YYYY-MM-DD HH:MM:SS', or delta time '1h30m', default is now."
510
+ )
511
+ @click.argument("smart_path", type=str, required=True)
512
+ def duplicate(org: str, namespace: str, group: str, smart_path: str, at: str, **kwargs):
513
+ """Duplicate an existing task to run once, immediately or at a specific time."""
514
+ namespace, group, key = _parse_task_param(namespace, group, smart_path)
515
+ org_id = cli.get_org_id(org)
516
+ ret = task.get(org_id, namespace, group, key, **kwargs)
517
+ if not ret:
518
+ return f"Task {namespace}/{group}/{key} not found.", 1
519
+
520
+ # Clone the task
521
+ new_task = ret.model_copy()
522
+ new_task.key = None # Clear the key to let the system generate a new one
523
+ new_task.log = None # Clear the log for the new task
524
+ new_task.meta["_duplicatedFrom"] = key # Track the source task
525
+
526
+ # Handle schedule based on 'at' parameter
527
+ if at and at.lower() != "now":
528
+ now_ms = int(datetime.now().timestamp() * 1000)
529
+ try:
530
+ # Try to parse as exact time (YYYY-MM-DD HH:MM:SS)
531
+ scheduled_dt = datetime.strptime(at, "%Y-%m-%d %H:%M:%S")
532
+ scheduled_ms = int(scheduled_dt.timestamp() * 1000)
533
+ except ValueError:
534
+ # Parse as delta time (e.g., 1h30m)
535
+ delta_seconds = yumako.time.duration(at)
536
+ scheduled_ms = now_ms + int(delta_seconds * 1000)
537
+
538
+ if scheduled_ms > now_ms:
539
+ new_task.schedule = task.Schedule(initialDelayMs=scheduled_ms)
540
+ else:
541
+ new_task.schedule = None # run immediately
542
+
543
+ return task.create(org_id, namespace, new_task)
544
+
545
+
499
546
  def _parse_task_param(namespace: str, group: str, smart_path: str):
500
547
  if smart_path:
501
548
  parts = smart_path.split("/")
@@ -62,6 +62,7 @@
62
62
  "properties": {
63
63
  "azureCreateResourceGroup": true,
64
64
  "new-task-engine": true,
65
- "simulateAgentStatus": "AVAILABLE"
65
+ "simulateAgentStatus": "AVAILABLE",
66
+ "countinuousErrorThreshold": 3
66
67
  }
67
68
  }
@@ -56,8 +56,9 @@
56
56
  }
57
57
  },
58
58
  "properties": {
59
- "new-task-engine": false,
59
+ "new-task-engine": true,
60
60
  "sg-lcm-enforced-provision": true,
61
- "autonomous": true
61
+ "autonomous": true,
62
+ "countinuousErrorThreshold": 2
62
63
  }
63
64
  }
hcs_cli/service/api.py ADDED
@@ -0,0 +1,104 @@
1
+ from typing import Optional
2
+ from hcs_core.sglib.client_util import hdc_service_client, is_regional_service, regional_service_client
3
+ from hcs_core.util.query_util import PageRequest
4
+
5
+
6
+ def _parse_path(path: str, hdc: str = None, region: str = None):
7
+ service_path = path.split("/")[1]
8
+ api_path = path[len(service_path) + 1 :]
9
+ if is_regional_service(service_path):
10
+ client = regional_service_client(service_path, region=region)
11
+ else:
12
+ client = hdc_service_client(service_path, hdc=hdc)
13
+ return api_path, client
14
+
15
+
16
+ def _get(
17
+ path: str, headers: Optional[dict] = None, all_pages: bool = False, raise_on_404: bool = False, hdc: str = None, region: str = None
18
+ ):
19
+ api_path, client = _parse_path(path, hdc=hdc, region=region)
20
+ if all_pages:
21
+
22
+ def _get_page(query_string):
23
+ url = api_path
24
+ if url.find("?") > 0:
25
+ url += "&" + query_string
26
+ else:
27
+ url += "?" + query_string
28
+ ret = client.get(url)
29
+ return ret
30
+
31
+ return PageRequest(_get_page, size=100, limit=0).get()
32
+ else:
33
+ return client.get(api_path, headers=headers, raise_on_404=raise_on_404)
34
+
35
+
36
+ def _post(
37
+ path: str, headers: Optional[dict] = None, text: Optional[str] = None, json: Optional[dict] = None, hdc: str = None, region: str = None
38
+ ):
39
+ api_path, client = _parse_path(path, hdc=hdc, region=region)
40
+ return client.post(api_path, text=text, json=json, headers=headers)
41
+
42
+
43
+ def _put(
44
+ path: str, headers: Optional[dict] = None, text: Optional[str] = None, json: Optional[dict] = None, hdc: str = None, region: str = None
45
+ ):
46
+ api_path, client = _parse_path(path, hdc=hdc, region=region)
47
+ return client.put(api_path, text=text, json=json, headers=headers)
48
+
49
+
50
+ def _delete(path: str, headers: Optional[dict] = None, raise_on_404: bool = False, hdc: str = None, region: str = None):
51
+ api_path, client = _parse_path(path, hdc=hdc, region=region)
52
+ return client.delete(api_path, headers=headers, raise_on_404=raise_on_404)
53
+
54
+
55
+ def _patch(
56
+ path: str, headers: Optional[dict] = None, text: Optional[str] = None, json: Optional[dict] = None, hdc: str = None, region: str = None
57
+ ):
58
+ api_path, client = _parse_path(path, hdc=hdc, region=region)
59
+ return client.patch(api_path, text=text, json=json, headers=headers)
60
+
61
+
62
+ class APIContext:
63
+ """Context-aware API wrapper that maintains hdc and region for all requests."""
64
+
65
+ def __init__(self, hdc: str = None, region: str = None):
66
+ self.hdc = hdc
67
+ self.region = region
68
+
69
+ def get(self, path: str, headers: Optional[dict] = None, all_pages: bool = False, raise_on_404: bool = False):
70
+ return _get(path, headers=headers, all_pages=all_pages, raise_on_404=raise_on_404, hdc=self.hdc, region=self.region)
71
+
72
+ def post(self, path: str, headers: Optional[dict] = None, text: Optional[str] = None, json: Optional[dict] = None):
73
+ return _post(path, headers=headers, text=text, json=json, hdc=self.hdc, region=self.region)
74
+
75
+ def put(self, path: str, headers: Optional[dict] = None, text: Optional[str] = None, json: Optional[dict] = None):
76
+ return _put(path, headers=headers, text=text, json=json, hdc=self.hdc, region=self.region)
77
+
78
+ def delete(self, path: str, headers: Optional[dict] = None, raise_on_404: bool = False):
79
+ return _delete(path, headers=headers, raise_on_404=raise_on_404, hdc=self.hdc, region=self.region)
80
+
81
+ def patch(self, path: str, headers: Optional[dict] = None, text: Optional[str] = None, json: Optional[dict] = None):
82
+ return _patch(path, headers=headers, text=text, json=json, hdc=self.hdc, region=self.region)
83
+
84
+
85
+ def with_context(hdc: str = None, region: str = None):
86
+ """Create a context-aware API wrapper with the specified hdc and region.
87
+
88
+ Args:
89
+ hdc: HDC name for global services
90
+ region: Region name for regional services
91
+
92
+ Returns:
93
+ APIContext: An object with get(), post(), put(), delete(), patch() methods
94
+ """
95
+ return APIContext(hdc=hdc, region=region)
96
+
97
+
98
+ _no_context_api = APIContext()
99
+
100
+ get = _no_context_api.get
101
+ post = _no_context_api.post
102
+ put = _no_context_api.put
103
+ delete = _no_context_api.delete
104
+ patch = _no_context_api.patch
@@ -0,0 +1,2 @@
1
+ from .service import action as action
2
+ from .service import health as health
@@ -0,0 +1,29 @@
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 logging
17
+
18
+ from hcs_core.sglib.client_util import hdc_service_client
19
+
20
+ log = logging.getLogger(__name__)
21
+ _client = hdc_service_client("clouddriver")
22
+
23
+
24
+ def action(name: str, params: dict):
25
+ return _client.post(f"/v1/action/{name}", params)
26
+
27
+
28
+ def health():
29
+ return _client.get("/v1/health")
hcs_cli/service/task.py CHANGED
@@ -205,6 +205,12 @@ def resubmit(org_id: str, namespace: str, group: str, key: str, **kwargs):
205
205
  return _tsctl_client.post(url, body)
206
206
 
207
207
 
208
+ def create(org_id: str, namespace: str, task: TaskModel, **kwargs):
209
+ url = f"/v1/namespaces/{namespace}/tasks?org_id={org_id}"
210
+ body = TaskModel.model_dump_json(task)
211
+ return _tsctl_client.post(url, body)
212
+
213
+
208
214
  def logs(org_id: str, namespace: str, group: str, key: str, search: str, **kwargs):
209
215
  search_parts = []
210
216
  if org_id: