hcs-cli 0.1.317__py3-none-any.whl → 0.1.319__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.
Files changed (112) hide show
  1. hcs_cli/__init__.py +1 -1
  2. hcs_cli/cmds/advisor/html_utils.py +30 -26
  3. hcs_cli/cmds/advisor/recommendation_engine.py +7 -10
  4. hcs_cli/cmds/api.py +5 -3
  5. hcs_cli/cmds/daas/tenant/plan.py +1 -1
  6. hcs_cli/cmds/debug/start.py +2 -3
  7. hcs_cli/cmds/dev/fs/helper/credential_helper.py +2 -0
  8. hcs_cli/cmds/dev/fs/helper/k8s_util.py +0 -1
  9. hcs_cli/cmds/dev/fs/helper/license_info.py +36 -12
  10. hcs_cli/cmds/dev/fs/init.py +38 -5
  11. hcs_cli/cmds/dev/fs/profiler.py +0 -1
  12. hcs_cli/cmds/dev/fs/provided_files/akka.plan.yml +94 -250
  13. hcs_cli/cmds/dev/fs/provided_files/azsim.plan.yml +27 -34
  14. hcs_cli/cmds/dev/fs/provided_files/azure.plan.yml +294 -322
  15. hcs_cli/cmds/dev/fs/provided_files/mqtt-secret.yaml +188 -93
  16. hcs_cli/cmds/dev/fs/provided_files/mqtt-server-external.yaml +4 -5
  17. hcs_cli/cmds/dev/fs/provided_files/patch-mqtt-hostname.yml +3 -3
  18. hcs_cli/cmds/dev/fs/provided_files/patch-vernemq-ssl-depth.json +1 -1
  19. hcs_cli/cmds/dev/fs/tailor.py +7 -12
  20. hcs_cli/cmds/dev/mqtt.py +1 -2
  21. hcs_cli/cmds/dev/onboard.py +35 -1
  22. hcs_cli/cmds/dev/util/mqtt_helper.py +0 -1
  23. hcs_cli/cmds/hoc/search.py +39 -9
  24. hcs_cli/cmds/hst/clean.py +2 -1
  25. hcs_cli/cmds/inventory/assign.py +72 -28
  26. hcs_cli/cmds/inventory/deassign.py +35 -18
  27. hcs_cli/cmds/inventory/logoff.py +86 -8
  28. hcs_cli/cmds/inventory/session.py +2 -2
  29. hcs_cli/cmds/scm/operator.py +2 -2
  30. hcs_cli/cmds/scm/plan.py +131 -3
  31. hcs_cli/cmds/task.py +2 -4
  32. hcs_cli/cmds/template/expand.py +65 -0
  33. hcs_cli/cmds/template/list_usage.py +2 -2
  34. hcs_cli/cmds/template/usage.py +20 -7
  35. hcs_cli/cmds/vm/list.py +0 -1
  36. hcs_cli/config/hcs-deployments.yaml +52 -52
  37. hcs_cli/main.py +0 -2
  38. hcs_cli/payload/akka.blueprint.yml +95 -243
  39. hcs_cli/payload/app/manual.json +19 -19
  40. hcs_cli/payload/edge/akka.json +6 -6
  41. hcs_cli/payload/edge/vsphere.json +6 -6
  42. hcs_cli/payload/hoc/lcm-capcalc.json.template +43 -0
  43. hcs_cli/payload/hoc/no-spare.json.template +1 -1
  44. hcs_cli/payload/inventory/assign.json +14 -16
  45. hcs_cli/payload/inventory/deassign.json +11 -11
  46. hcs_cli/payload/lcm/akka.json +31 -33
  47. hcs_cli/payload/lcm/azure-dummy-nt.json +64 -66
  48. hcs_cli/payload/lcm/azure-dummy.json +64 -66
  49. hcs_cli/payload/lcm/azure-real.json +13 -11
  50. hcs_cli/payload/lcm/edge-proxy.json +34 -36
  51. hcs_cli/payload/lcm/zero-dedicated.json +34 -36
  52. hcs_cli/payload/lcm/zero-delay-1m-per-vm.json +53 -69
  53. hcs_cli/payload/lcm/zero-fail-delete-template.json +43 -0
  54. hcs_cli/payload/lcm/zero-fail-destroy-onthread.json +38 -40
  55. hcs_cli/payload/lcm/zero-fail-destroy.json +38 -40
  56. hcs_cli/payload/lcm/zero-fail-prepare-onthread.json +38 -40
  57. hcs_cli/payload/lcm/zero-fail-prepare.json +38 -40
  58. hcs_cli/payload/lcm/zero-fail-vm-onthread.json +58 -74
  59. hcs_cli/payload/lcm/zero-fail-vm.json +58 -74
  60. hcs_cli/payload/lcm/zero-floating.json +34 -36
  61. hcs_cli/payload/lcm/zero-manual.json +33 -35
  62. hcs_cli/payload/lcm/zero-multisession.json +34 -36
  63. hcs_cli/payload/lcm/zero-nanw.json +31 -33
  64. hcs_cli/payload/lcm/zero-new-5k-delay.json +69 -78
  65. hcs_cli/payload/lcm/zero-new-5k.json +36 -38
  66. hcs_cli/payload/lcm/zero-new-snapshot.json +37 -39
  67. hcs_cli/payload/lcm/zero-new.json +37 -39
  68. hcs_cli/payload/lcm/zero-reuse-vm-id.json +33 -35
  69. hcs_cli/payload/lcm/zero-with-max-id-offset.json +32 -34
  70. hcs_cli/payload/lcm/zero.json +59 -73
  71. hcs_cli/payload/provider/ad-stes-vsphere.json +26 -26
  72. hcs_cli/payload/provider/akka.json +12 -12
  73. hcs_cli/payload/provider/azure.json +14 -14
  74. hcs_cli/payload/provider/edgeproxy.json +12 -12
  75. hcs_cli/payload/provider/vsphere.json +14 -14
  76. hcs_cli/payload/scm/starter.json +22 -23
  77. hcs_cli/payload/synt/core/p01-dummy-success.json +11 -15
  78. hcs_cli/payload/synt/core/p02-dummy-fail.json +12 -15
  79. hcs_cli/payload/synt/core/p03-dummy-exception.json +12 -15
  80. hcs_cli/payload/synt/core/p04-dummy-success-repeat.json +12 -15
  81. hcs_cli/payload/synt/core/p05-dummy-fail-repeat.json +13 -16
  82. hcs_cli/payload/synt/core/p06-dummy-exception-repeat.json +13 -16
  83. hcs_cli/payload/synt/core/p07-dummy-delay.json +12 -15
  84. hcs_cli/payload/synt/core/p08-dummy-property.json +12 -15
  85. hcs_cli/payload/synt/ext/p20-connect-success.json +12 -15
  86. hcs_cli/payload/synt/ext/p21-connect-fail.json +12 -15
  87. hcs_cli/payload/synt/ext/p30-ssl-success.json +12 -15
  88. hcs_cli/payload/synt/ext/p31-ssl-fail.json +13 -16
  89. hcs_cli/payload/synt/ext/p40-http-success.json +12 -15
  90. hcs_cli/payload/synt/ext/p41-http-fail.json +12 -15
  91. hcs_cli/payload/synt/ext/p42-http-status-code.json +14 -20
  92. hcs_cli/payload/synt/ext1/p10-ping-success.json +13 -16
  93. hcs_cli/payload/synt/ext1/p11-ping-fail.json +12 -15
  94. hcs_cli/payload/synt/ext1/p12-ping-success-repeat.json +14 -17
  95. hcs_cli/provider/hcs/cert.py +0 -1
  96. hcs_cli/provider/hcs/edge.py +1 -1
  97. hcs_cli/provider/hcs/uag.py +6 -1
  98. hcs_cli/service/hoc/diagnostic.py +0 -3
  99. hcs_cli/service/inventory/__init__.py +1 -1
  100. hcs_cli/service/inventory/session.py +23 -7
  101. hcs_cli/service/lcm/vm.py +0 -1
  102. hcs_cli/service/task.py +0 -1
  103. hcs_cli/support/debug_util.py +0 -1
  104. hcs_cli/support/plan_util.py +0 -1
  105. hcs_cli/support/predefined_payload.py +4 -1
  106. hcs_cli/support/template_util.py +0 -1
  107. hcs_cli/support/test_utils.py +2 -2
  108. hcs_cli/support/test_utils2.py +536 -0
  109. {hcs_cli-0.1.317.dist-info → hcs_cli-0.1.319.dist-info}/METADATA +24 -17
  110. {hcs_cli-0.1.317.dist-info → hcs_cli-0.1.319.dist-info}/RECORD +112 -108
  111. {hcs_cli-0.1.317.dist-info → hcs_cli-0.1.319.dist-info}/WHEEL +0 -0
  112. {hcs_cli-0.1.317.dist-info → hcs_cli-0.1.319.dist-info}/entry_points.txt +0 -0
@@ -13,47 +13,91 @@ See the License for the specific language governing permissions and
13
13
  limitations under the License.
14
14
  """
15
15
 
16
- import json
17
- import sys
18
16
  import uuid
19
17
 
20
18
  import click
21
19
  import hcs_core.sglib.cli_options as cli
22
20
 
23
- from hcs_cli.service import auth, inventory, portal
21
+ from hcs_cli.service import admin, auth, inventory, lcm, portal
24
22
  from hcs_cli.support.param_util import parse_vm_path
25
23
 
26
24
 
27
25
  @click.command()
28
26
  @cli.org_id
29
- @click.option(
30
- "--file",
31
- "-f",
32
- type=click.File("rt"),
33
- default=sys.stdin,
34
- help="Specify the template file name. If not specified, STDIN will be used.",
35
- )
36
- @click.option("--vm", type=str, required=False)
37
- def assign(org: str, file, vm: str):
27
+ @click.argument("vm_path", type=str, required=False)
28
+ def assign(org: str, vm_path: str):
38
29
  """Assign user to VM"""
39
30
  org_id = cli.get_org_id(org)
31
+ template_id, vm_id = parse_vm_path(vm_path)
40
32
 
41
- with file:
42
- data = file.read()
43
-
44
- try:
45
- payload = json.loads(data)
46
- except Exception as e:
47
- msg = "Invalid payload: " + str(e)
48
- return msg, 1
49
-
50
- if vm:
51
- template, vm_id = parse_vm_path(vm)
52
- payload["templateIds"] = [template]
53
- payload["vmId"] = vm_id
33
+ template = admin.template.get(template_id, org_id)
34
+ if template:
35
+ edge_id = template["edgeDeploymentId"]
36
+ else:
37
+ # try with lcm
38
+ template = lcm.template.get(template_id, org_id)
39
+ edge_id = template["edgeGateway"]["id"]
40
+ if not template:
41
+ return "Template not found: " + template_id, 1
42
+ payload = {
43
+ "id": "dspec1",
44
+ "userId": "user1",
45
+ "clientId": "client-1",
46
+ "entitlementId": "entitlement-1",
47
+ "templateIds": [template_id],
48
+ "allocationPolicy": "ANY",
49
+ "sessionType": "DESKTOP",
50
+ "username": "user1",
51
+ "userPrincipalName": "aduser1@vmwhorizon.com",
52
+ "userSid": "S-1-5-21-2502306595",
53
+ "orgId": org_id,
54
+ "location": template["location"],
55
+ "vmId": vm_id,
56
+ "templateType": template["templateType"],
57
+ "resume": True,
58
+ "clientSessionId": "client-uuid-1",
59
+ }
60
+ # print(json.dumps(payload, indent=4))
61
+ inventory.assignV2(payload)
54
62
 
55
- payload["orgId"] = org_id
56
- return inventory.assign(payload)
63
+ # payload_manual_session = {
64
+ # "templateId": "string",
65
+ # "vmId": "string",
66
+ # "userId": "string",
67
+ # "agentSessionGuid": "string",
68
+ # "agentSessionId": "string",
69
+ # "agentStaticSessionGuid": "string",
70
+ # "sessionType": "DESKTOP",
71
+ # "templateType": "FLOATING",
72
+ # "clientId": "string",
73
+ # "sessionStatus": "string",
74
+ # "lastAssignedTime": "2025-12-16T20:56:08.988Z",
75
+ # "lastLoginTime": "2025-12-16T20:56:08.988Z",
76
+ # "lastStatusUpdateTime": "2025-12-16T20:56:08.988Z",
77
+ # "username": "string",
78
+ # "userPrincipalName": "string",
79
+ # "userSid": "string",
80
+ # "entitlementId": "string",
81
+ # "orgId": "string",
82
+ # "location": "string",
83
+ # "clientSessionId": "UUID string",
84
+ # "agentErrorCode": "AGENT_ERR_FAILURE",
85
+ # "staticCloudSessionId": "UUID string",
86
+ # "hibernated": true,
87
+ # "id": 0,
88
+ # "dspecId": "string"
89
+ # }
90
+ payload_update = {
91
+ "id": "dspec1",
92
+ "agentSessionGuid": "agent-session-guid-1",
93
+ "agentSessionId": "agent-session-1",
94
+ "agentStaticSessionGuid": "agent-static-session-guid-1",
95
+ "sessionStatus": "string",
96
+ "vmId": vm_id,
97
+ "edgeId": edge_id,
98
+ "agentErrorCode": "",
99
+ }
100
+ return inventory.update_session(org_id=org_id, template_id=template_id, payload=payload_update)
57
101
 
58
102
 
59
103
  @click.command()
@@ -84,7 +128,7 @@ def bulk_assign(org: str, pool_group: str, pool: str, num_users: int):
84
128
 
85
129
  num_users = int(num_users)
86
130
  if num_users > len(users["users"]):
87
- print(f'max available users in AD = {len(users["users"])}, set -n less than {len(users["users"])}')
131
+ print(f"max available users in AD = {len(users['users'])}, set -n less than {len(users['users'])}")
88
132
  return
89
133
  fu = users["users"][:num_users]
90
134
  dspecs = []
@@ -19,29 +19,46 @@ import sys
19
19
  import click
20
20
  import hcs_core.sglib.cli_options as cli
21
21
 
22
- from hcs_cli.service import inventory
22
+ from hcs_cli.service import admin, inventory, lcm
23
+ from hcs_cli.support.param_util import parse_vm_path
23
24
 
24
25
 
25
26
  @click.command()
26
27
  @cli.org_id
27
- @click.option(
28
- "--file",
29
- "-f",
30
- type=click.File("rt"),
31
- default=sys.stdin,
32
- help="Specify the template file name. If not specified, STDIN will be used.",
33
- )
34
- def deassign(org: str, file):
28
+ @click.argument("vm_path", type=str, required=False)
29
+ def deassign(org: str, vm_path: str):
35
30
  """Assign user to VM"""
36
31
  org_id = cli.get_org_id(org)
37
- with file:
38
- data = file.read()
32
+ template_id, vm_id = parse_vm_path(vm_path)
33
+ sessions = inventory.sessions(template_id, vm_id, cli.get_org_id(org))
39
34
 
40
- try:
41
- payload = json.loads(data)
42
- except Exception as e:
43
- msg = "Invalid payload: " + str(e)
44
- return msg, 1
35
+ template = admin.template.get(template_id, cli.get_org_id(org))
36
+ if template:
37
+ edge_deployment_id = template["edgeDeploymentId"]
38
+ else:
39
+ # try with lcm
40
+ template = lcm.template.get(template_id, cli.get_org_id(org))
41
+ if not template:
42
+ return "Template not found: " + template_id, 1
43
+ edge_deployment_id = template["edgeGateway"]["id"]
45
44
 
46
- payload["orgId"] = org_id
47
- return inventory.deassign(payload)
45
+ all_ret = []
46
+ for session in sessions:
47
+ payload = {
48
+ "vmHubName": template["hdc"]["vmHub"]["name"],
49
+ "edgeDeploymentId": edge_deployment_id,
50
+ "vmId": vm_id,
51
+ "entitlementId": session.get("entitlementId"),
52
+ "agentSessionGuid": session.get("agentSessionGuid"),
53
+ "agentSessionId": session.get("agentSessionId"),
54
+ "agentStaticSessionGuid": session.get("agentStaticSessionGuid"),
55
+ "id": session.get("dspecId"),
56
+ "userId": session.get("userId"),
57
+ "type": "CONNECTION_ENDED",
58
+ }
59
+ ret = inventory.deassign(template_id=template_id, org_id=org_id, payload=payload)
60
+ all_ret.append(ret)
61
+
62
+ if len(all_ret) == 1:
63
+ return all_ret[0]
64
+ return all_ret
@@ -16,17 +16,95 @@ limitations under the License.
16
16
  import click
17
17
  import hcs_core.sglib.cli_options as cli
18
18
 
19
- from hcs_cli.service import inventory
19
+ from hcs_cli.service import admin, inventory, lcm
20
20
  from hcs_cli.support.param_util import parse_vm_path
21
21
 
22
22
 
23
23
  @click.command()
24
24
  @cli.org_id
25
- @click.argument("vm_path", type=str, required=True)
25
+ @click.argument("vm_path", type=str, required=False)
26
26
  def logoff(org: str, vm_path: str):
27
- """Log off an user"""
28
- template, vm = parse_vm_path(vm_path)
29
- ret = inventory.logoff(template, vm, cli.get_org_id(org))
30
- if ret:
31
- return ret
32
- return ret, 1
27
+ """Logoff all sessions on the vm.
28
+
29
+ Example: hcs inventory logoff template_id/vm_id
30
+ """
31
+
32
+ org_id = cli.get_org_id(org)
33
+ template_id, vm_id = parse_vm_path(vm_path)
34
+ sessions = inventory.sessions(template_id, vm_id, cli.get_org_id(org))
35
+ if not sessions:
36
+ return
37
+ # [
38
+ # {
39
+ # "templateId": null,
40
+ # "vmId": "expool000",
41
+ # "userId": "c12b884b-c813-4f73-81f5-df2052de9bc6",
42
+ # "agentSessionGuid": null,
43
+ # "agentSessionId": null,
44
+ # "agentStaticSessionGuid": null,
45
+ # "sessionType": "DESKTOP",
46
+ # "templateType": "FLOATING",
47
+ # "clientId": null,
48
+ # "sessionStatus": "ASSIGNED",
49
+ # "lastAssignedTime": "2025-12-16T18:55:45.078+00:00",
50
+ # "lastLoginTime": null,
51
+ # "lastStatusUpdateTime": "2025-12-16T18:55:45.078+00:00",
52
+ # "username": "u1ad1",
53
+ # "userPrincipalName": "u1ad1@horizonv2dev2.local",
54
+ # "userSid": "S-1-5-21-1840667356-2516272024-4034330832-1104",
55
+ # "releaseSessionOnDeassign": false,
56
+ # "entitlementId": "6941a2bfe79a1b9cef167ad2",
57
+ # "orgId": "7c4f9042-6119-45b6-93f5-24f1a80e2c62",
58
+ # "location": null,
59
+ # "clientSessionId": null,
60
+ # "agentErrorCode": "",
61
+ # "staticCloudSessionId": "dc907d57-85d8-478e-9d0c-028c1acd87f2",
62
+ # "hibernated": null,
63
+ # "id": null,
64
+ # "dspecId": "5722ff4a-7e75-4248-89e9-741d8a7ae91c"
65
+ # }
66
+ # ]
67
+
68
+ template = admin.template.get(template_id, cli.get_org_id(org))
69
+ if template:
70
+ edge_deployment_id = template["edgeDeploymentId"]
71
+ else:
72
+ # try with lcm
73
+ template = lcm.template.get(template_id, cli.get_org_id(org))
74
+ if not template:
75
+ return "Template not found: " + template_id, 1
76
+ edge_deployment_id = template["edgeGateway"]["id"]
77
+
78
+ all_ret = []
79
+ for session in sessions:
80
+ logoff_payload_v1 = {
81
+ "edgeDeploymentId": edge_deployment_id,
82
+ "vmId": vm_id,
83
+ "userId": session.get("userId"),
84
+ "entitlementId": session.get("entitlementId"),
85
+ "vmHubName": template["hdc"]["vmHub"]["name"],
86
+ "sessionType": session.get("sessionType"),
87
+ }
88
+ ret1 = inventory.logoff(template_id=template_id, org_id=org_id, payload=logoff_payload_v1)
89
+ # print("PAYLOADv1: ", logoff_payload_v1)
90
+ # print("RETv1: ", ret1)
91
+ all_ret.append(ret1)
92
+
93
+ logoff_payload_v2 = [
94
+ {
95
+ "agentSessionGuid": session.get("agentSessionGuid"),
96
+ "userId": session.get("userId"),
97
+ "sessionType": session.get("sessionType"),
98
+ "vmHubName": template["hdc"]["vmHub"]["name"],
99
+ "edgeDeploymentId": edge_deployment_id,
100
+ "vmId": vm_id,
101
+ "dspecId": session.get("dspecId"),
102
+ "dtemplateId": template_id,
103
+ }
104
+ ]
105
+ ret2 = inventory.logoffV2(org_id=org_id, payload=logoff_payload_v2)
106
+ # print("PAYLOADv2: ", logoff_payload_v2)
107
+ # print("RETv2: ", ret2)
108
+ all_ret.append(ret2)
109
+
110
+ return all_ret
@@ -16,7 +16,7 @@ limitations under the License.
16
16
  import click
17
17
  import hcs_core.sglib.cli_options as cli
18
18
 
19
- from hcs_cli.service import inventory
19
+ from hcs_cli.service import admin, inventory
20
20
  from hcs_cli.support.param_util import parse_vm_path
21
21
 
22
22
 
@@ -27,7 +27,7 @@ def session():
27
27
 
28
28
  @session.command()
29
29
  @cli.org_id
30
- @click.argument("vm_path", type=str, required=True)
30
+ @click.argument("vm_path", type=str, required=False)
31
31
  def list(org: str, vm_path: str):
32
32
  """List sessions"""
33
33
  template, vm = parse_vm_path(vm_path)
@@ -60,8 +60,8 @@ def _print_error(text: str):
60
60
 
61
61
  def _print_human_readable_log(d: TaskModel):
62
62
  ret = d.log
63
- time_started = ret.time_started
64
- time_completed = ret.time_completed
63
+ time_started = ret.timeStarted
64
+ time_completed = ret.timeCompleted
65
65
 
66
66
  if time_completed:
67
67
  delta = timedelta(milliseconds=time_completed - time_started)
hcs_cli/cmds/scm/plan.py CHANGED
@@ -16,11 +16,13 @@ limitations under the License.
16
16
  import json
17
17
  import re
18
18
  import sys
19
+ from datetime import datetime, timedelta
19
20
  from os import path
20
21
 
21
22
  import click
22
23
  import hcs_core.sglib.cli_options as cli
23
- from hcs_core.ctxp import data_util, recent
24
+ import yumako
25
+ from hcs_core.ctxp import data_util, recent, util
24
26
  from hcs_core.sglib.client_util import wait_for_res_status
25
27
 
26
28
  import hcs_cli.service.scm as scm
@@ -33,6 +35,98 @@ def plan():
33
35
  pass
34
36
 
35
37
 
38
+ def _get_next_slot_name():
39
+ """
40
+ Get the current and next half-hour aligned slot names and time delta based on current UTC time.
41
+
42
+ Slots are half-hour aligned (HH:00 or HH:30).
43
+
44
+ Returns:
45
+ tuple: (current_slot_name, next_slot_name, timedelta_to_next_slot)
46
+ where slot names are in format 'weekday/HH:MM' (e.g., 'monday/18:00')
47
+ and timedelta_to_next_slot is a timedelta object
48
+ """
49
+ current_time_utc = datetime.utcnow()
50
+
51
+ # Weekday names mapping (0=Monday, 6=Sunday in Python's weekday())
52
+ weekday_names = ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"]
53
+
54
+ # Get current half-hour aligned slot (round down)
55
+ current_minute = current_time_utc.minute
56
+ if current_minute < 30:
57
+ # Current slot is this hour:00
58
+ current_slot_time = current_time_utc.replace(minute=0, second=0, microsecond=0)
59
+ else:
60
+ # Current slot is this hour:30
61
+ current_slot_time = current_time_utc.replace(minute=30, second=0, microsecond=0)
62
+
63
+ current_weekday_name = weekday_names[current_slot_time.weekday()]
64
+ current_time_str = current_slot_time.strftime("%H:%M")
65
+ current_slot_name = f"{current_weekday_name}/{current_time_str}"
66
+
67
+ # Get next half-hour aligned slot
68
+ if current_minute < 30:
69
+ # Next slot is this hour:30
70
+ next_time = current_time_utc.replace(minute=30, second=0, microsecond=0)
71
+ else:
72
+ # Next slot is next hour:00
73
+ next_time = current_time_utc + timedelta(hours=1)
74
+ next_time = next_time.replace(minute=0, second=0, microsecond=0)
75
+
76
+ next_weekday_name = weekday_names[next_time.weekday()]
77
+ next_time_str = next_time.strftime("%H:%M")
78
+ next_slot_name = f"{next_weekday_name}/{next_time_str}"
79
+
80
+ timedelta_to_next = next_time - current_time_utc
81
+
82
+ return current_slot_name, next_slot_name, timedelta_to_next
83
+
84
+
85
+ def _format_scm_plan_task_table(data):
86
+ fields_mapping = {
87
+ "location": "Location",
88
+ "_slot": "Slot",
89
+ "_timeCreatedStale": "Time Created",
90
+ }
91
+
92
+ current_slot_name, next_slot_name, timedelta_to_next = _get_next_slot_name()
93
+ time_str_to_next = yumako.time.display(timedelta_to_next.total_seconds())
94
+
95
+ for d in data:
96
+ d["_timeCreatedStale"] = yumako.time.stale(d["timeCreated"] / 1000)
97
+ meta = d["meta"]
98
+ input = d["input"]
99
+ my_slot_name = meta["scm_plan_day"] + "/" + meta["scm_plan_slot"]
100
+ d["_slot"] = my_slot_name
101
+
102
+ if d["worker"] == "com.vmware.horizon.sg.scm.task.CapacityOptimization":
103
+ d["_idealCapacity"] = input["idealCapacity"]
104
+ d["_forecastCapacity"] = input["forecastCapacity"]
105
+ d["_taskKey"] = d["key"]
106
+
107
+ if my_slot_name == next_slot_name:
108
+ d["_nextExecution"] = click.style(f"In {time_str_to_next}", fg="bright_blue")
109
+ else:
110
+ d["_nextExecution"] = ""
111
+ fields_mapping["_nextExecution"] = "Next"
112
+
113
+ if my_slot_name == current_slot_name:
114
+ # d['_lastExecutedStale'] = 'TODO'
115
+ # d['_lastExecutionStatus'] = 'TODO'
116
+ # fields_mapping['_lastExecutedStale'] = 'Executed'
117
+ # fields_mapping['_lastExecutionStatus'] = 'Status'
118
+ pass
119
+
120
+ fields_mapping["_idealCapacity"] = "Ideal"
121
+ fields_mapping["_forecastCapacity"] = "Forecast"
122
+ fields_mapping["_taskKey"] = "Task Key"
123
+ else:
124
+ # TODO other worker types
125
+ pass
126
+
127
+ return util.format_table(data, fields_mapping)
128
+
129
+
36
130
  @plan.command
37
131
  @click.option("--template", help="Filter plan by template.")
38
132
  @click.option("--task", help="Filter plan by task class name.")
@@ -99,7 +193,7 @@ def delete(org: str, name: str, confirm: bool, **kwargs):
99
193
 
100
194
  @plan.command
101
195
  @cli.org_id
102
- @cli.limit
196
+ @cli.limit(default=336)
103
197
  @click.option("--day", required=False, help="Search by day-identifier. Example: Monday")
104
198
  @click.option("--time", required=False, help="Search by time. Example: 13:30")
105
199
  @click.option("--slot", required=False, help="Search by time slot. Example: Mon/13:30")
@@ -111,6 +205,7 @@ def delete(org: str, name: str, confirm: bool, **kwargs):
111
205
  help="Search by task state, as comma-separated values. E.g. 'init,running,success,error', or 'all'.",
112
206
  )
113
207
  @click.argument("name", required=False)
208
+ @cli.formatter(_format_scm_plan_task_table)
114
209
  def tasks(org: str, limit: int, day: str, time: str, slot: str, name: str, state: str, **kwargs):
115
210
  """Get tasks of a named calendar plan."""
116
211
  org = cli.get_org_id(org)
@@ -134,6 +229,40 @@ def tasks(org: str, limit: int, day: str, time: str, slot: str, name: str, state
134
229
  ret = scm.plan.tasks(org_id=org, id=name, limit=limit, day=day, slot=time, states=state, **kwargs)
135
230
  if ret is None:
136
231
  return "", 1
232
+
233
+ # sort tasks by slot.
234
+ for t in ret:
235
+ meta = t["meta"]
236
+ d = meta["scm_plan_day"]
237
+ if d == "sunday":
238
+ d = "0"
239
+ elif d == "monday":
240
+ d = "1"
241
+ elif d == "tuesday":
242
+ d = "2"
243
+ elif d == "wednesday":
244
+ d = "3"
245
+ elif d == "thursday":
246
+ d = "4"
247
+ elif d == "friday":
248
+ d = "5"
249
+ elif d == "saturday":
250
+ d = "6"
251
+ else:
252
+ raise ValueError("Invalid day name in task meta: " + d)
253
+ t["_slot"] = d + "/" + meta["scm_plan_slot"]
254
+ ret = sorted(ret, key=lambda x: x["_slot"])
255
+ for t in ret:
256
+ del t["_slot"]
257
+
258
+ # identify the current task result
259
+ # current_slot_name, next_slot_name, timedelta_to_next = _get_next_slot_name()
260
+ # for t in ret:
261
+ # meta = t['meta']
262
+ # my_slot_name = meta['scm_plan_day'] + '/' + meta['scm_plan_slot']
263
+ # if current_slot_name == my_slot_name:
264
+ # break
265
+
137
266
  return ret
138
267
 
139
268
 
@@ -343,7 +472,6 @@ def run(org: str, name: str, slot: str, config: str, wait: str, **kwargs):
343
472
 
344
473
 
345
474
  def _wait_for_task(org_id: str, name: str, task_key: str, timeout: str):
346
-
347
475
  return wait_for_res_status(
348
476
  resource_name=name + "/" + task_key,
349
477
  fn_get=lambda: scm.plan.get_task(org_id, name, task_key),
hcs_cli/cmds/task.py CHANGED
@@ -13,8 +13,6 @@ See the License for the specific language governing permissions and
13
13
  limitations under the License.
14
14
  """
15
15
 
16
- import json
17
-
18
16
  import click
19
17
  import hcs_core.sglib.cli_options as cli
20
18
  import yumako
@@ -29,9 +27,9 @@ def _format_task_table(data):
29
27
  schedule = d.get("schedule")
30
28
  if schedule:
31
29
  if schedule.get("intervalMs"):
32
- recurring = f'Every {yumako.time.display(schedule["intervalMs"] / 1000)}'
30
+ recurring = f"Every {yumako.time.display(schedule['intervalMs'] / 1000)}"
33
31
  elif schedule.get("cronExpression"):
34
- recurring = f'{schedule["cronExpression"]}'
32
+ recurring = f"{schedule['cronExpression']}"
35
33
  else:
36
34
  recurring = "<No>"
37
35
  else:
@@ -0,0 +1,65 @@
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 click
17
+ import hcs_core.sglib.cli_options as cli
18
+ import hcs_core.util.duration as duration
19
+ from hcs_core.ctxp import recent
20
+
21
+ import hcs_cli.service.admin as admin
22
+
23
+
24
+ @click.command(hidden=True)
25
+ @click.option(
26
+ "--number",
27
+ "-n",
28
+ type=int,
29
+ required=False,
30
+ default=1,
31
+ help="Number of VMs to expand. Use negative number to shrink.",
32
+ )
33
+ @click.argument("template_id", type=str, required=False)
34
+ @cli.org_id
35
+ @cli.wait
36
+ def expand(number: int, template_id: str, org: str, wait: str, **kwargs):
37
+ """Update an existing template"""
38
+
39
+ org_id = cli.get_org_id(org)
40
+
41
+ template_id = recent.require("template", template_id)
42
+ template = admin.template.get(template_id, org_id)
43
+
44
+ if not template:
45
+ return "Template not found: " + template_id
46
+
47
+ spare_policy = template.get("sparePolicy", {})
48
+ patch = {
49
+ "sparePolicy": {
50
+ "min": spare_policy.get("min", 0) + number,
51
+ "max": spare_policy.get("max", 0) + number,
52
+ "limit": spare_policy.get("limit", 0) + number,
53
+ }
54
+ }
55
+ if patch["sparePolicy"]["min"] < 0:
56
+ patch["sparePolicy"]["min"] = 0
57
+ if patch["sparePolicy"]["max"] < 0:
58
+ patch["sparePolicy"]["max"] = 0
59
+ if patch["sparePolicy"]["limit"] < 0:
60
+ patch["sparePolicy"]["limit"] = 0
61
+
62
+ ret = admin.template.update(template_id, org_id, patch)
63
+ if wait != "0":
64
+ ret = admin.template.wait_for_ready(template_id, org_id, duration.to_seconds(wait))
65
+ return ret
@@ -111,8 +111,8 @@ def list_usage(org: str, **kwargs):
111
111
  "addedVmHours": summary.get("addedVmHours"),
112
112
  "offloadVmHours": summary.get("offloadVmHours"),
113
113
  "reducedVmHours": summary.get("reducedVmHours"),
114
- "historyVmUtilizationPercent": f'{summary.get("historyVmUtilizationPercent", 0)}%',
115
- "predictionVmUtilizationPercent": f'{summary.get("predictionVmUtilizationPercent", 0)}%',
114
+ "historyVmUtilizationPercent": f"{summary.get('historyVmUtilizationPercent', 0)}%",
115
+ "predictionVmUtilizationPercent": f"{summary.get('predictionVmUtilizationPercent', 0)}%",
116
116
  }
117
117
 
118
118
  ret2.append(item)
@@ -13,6 +13,7 @@ See the License for the specific language governing permissions and
13
13
  limitations under the License.
14
14
  """
15
15
 
16
+ import json
16
17
  import os
17
18
  import tempfile
18
19
  import time
@@ -32,17 +33,28 @@ def _timestamp_to_date(timestamp: int):
32
33
 
33
34
 
34
35
  @click.command(hidden=True)
36
+ @click.option(
37
+ "--local-plan-file",
38
+ type=str,
39
+ required=False,
40
+ help="Instead of reading from API, read from local file for the template usage data. Debug only.",
41
+ )
35
42
  @click.argument("id", type=str, required=False)
36
43
  @cli.org_id
37
- def usage(id: str, org: str, **kwargs):
44
+ def usage(id: str, org: str, local_plan_file: str = None, **kwargs):
38
45
  """Show usage visualization"""
39
46
 
40
47
  org_id = cli.get_org_id(org)
41
48
  id = recent.require("template", id)
42
49
 
43
- usage = scm.template_usage(org_id, id)
44
- if not usage:
45
- return "No usage data found", 1
50
+ if local_plan_file:
51
+ with open(local_plan_file, "r") as f:
52
+ plan_data = json.load(f)
53
+ usage = plan_data["meta"]
54
+ else:
55
+ usage = scm.template_usage(org_id, id)
56
+ if not usage:
57
+ return "No usage data found", 1
46
58
 
47
59
  x_axis = []
48
60
  consumed_capacity = []
@@ -61,8 +73,9 @@ def usage(id: str, org: str, **kwargs):
61
73
  x_axis.append(_timestamp_to_date(t))
62
74
  max_capacity = history["maxCapacity"][i]
63
75
  min_free = history["minFree"][i]
64
- consumed_capacity.append(max_capacity - min_free)
65
- spare_capacity.append(min_free)
76
+ # consumed_capacity.append(max_capacity - min_free)
77
+ consumed_capacity.append(history["poweredOnAssignedVms"][i])
78
+ spare_capacity.append(max_capacity - consumed_capacity[-1])
66
79
  no_spare_error.append(history["noSpare"][i])
67
80
 
68
81
  start_timestamp = prediction["startTimestamp"]
@@ -72,7 +85,7 @@ def usage(id: str, org: str, **kwargs):
72
85
  max_capacity = prediction["maxCapacity"][i]
73
86
  min_free = prediction["minFree"][i]
74
87
  ideal_capacity = prediction["idealCapacity"][i]
75
- consumed_capacity_predicated.append(ideal_capacity - min_free)
88
+ consumed_capacity_predicated.append(ideal_capacity)
76
89
  spare_capacity_predicated.append(min_free)
77
90
  optimized_capacity.append(ideal_capacity)
78
91
  no_spare_error_predicated.append(prediction["noSpare"][i])
hcs_cli/cmds/vm/list.py CHANGED
@@ -129,7 +129,6 @@ def list_vms(
129
129
 
130
130
  vms = []
131
131
  if template_id and template_id.lower() == "all":
132
-
133
132
  cloud = _to_lower(cloud)
134
133
 
135
134
  def to_search_condition(values):