hcs-cli 0.1.318__py3-none-any.whl → 0.1.320__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 (119) hide show
  1. hcs_cli/__init__.py +2 -2
  2. hcs_cli/cmds/advisor/html_utils.py +30 -26
  3. hcs_cli/cmds/advisor/recommendation_engine.py +7 -10
  4. hcs_cli/cmds/daas/tenant/plan.py +1 -1
  5. hcs_cli/cmds/debug/start.py +0 -1
  6. hcs_cli/cmds/dev/fs/clear.py +8 -0
  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/init.py +38 -5
  10. hcs_cli/cmds/dev/fs/profiler.py +0 -1
  11. hcs_cli/cmds/dev/fs/provided_files/akka.plan.yml +94 -250
  12. hcs_cli/cmds/dev/fs/provided_files/azsim.plan.yml +27 -34
  13. hcs_cli/cmds/dev/fs/provided_files/azure.plan.yml +294 -322
  14. hcs_cli/cmds/dev/fs/provided_files/mqtt-secret.yaml +188 -93
  15. hcs_cli/cmds/dev/fs/provided_files/mqtt-server-external.yaml +4 -5
  16. hcs_cli/cmds/dev/fs/provided_files/patch-mqtt-hostname.yml +3 -3
  17. hcs_cli/cmds/dev/fs/provided_files/patch-vernemq-ssl-depth.json +1 -1
  18. hcs_cli/cmds/dev/fs/tailor.py +7 -12
  19. hcs_cli/cmds/dev/mqtt.py +1 -2
  20. hcs_cli/cmds/dev/util/mqtt_helper.py +0 -1
  21. hcs_cli/cmds/hoc/search.py +39 -9
  22. hcs_cli/cmds/hoc/stats.py +46 -0
  23. hcs_cli/cmds/hst/clean.py +2 -1
  24. hcs_cli/cmds/inventory/assign.py +1 -3
  25. hcs_cli/cmds/inventory/deassign.py +1 -1
  26. hcs_cli/cmds/inventory/delete.py +48 -0
  27. hcs_cli/cmds/lcm/provider/create.py +11 -2
  28. hcs_cli/cmds/lcm/template/expand.py +46 -0
  29. hcs_cli/cmds/lcm/vm/delete.py +3 -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 +64 -19
  33. hcs_cli/cmds/template/list_usage.py +2 -2
  34. hcs_cli/cmds/template/update.py +2 -2
  35. hcs_cli/cmds/template/usage.py +20 -7
  36. hcs_cli/cmds/vm/delete.py +3 -2
  37. hcs_cli/cmds/vm/list.py +51 -40
  38. hcs_cli/cmds/vmm/rootca_migrate.py +1 -1
  39. hcs_cli/config/hcs-deployments.yaml +52 -52
  40. hcs_cli/main.py +0 -2
  41. hcs_cli/payload/akka.blueprint.yml +95 -243
  42. hcs_cli/payload/app/manual.json +19 -19
  43. hcs_cli/payload/edge/akka.json +6 -6
  44. hcs_cli/payload/edge/vsphere.json +6 -6
  45. hcs_cli/payload/hoc/lcm-capcalc.json.template +43 -0
  46. hcs_cli/payload/hoc/no-spare.json.template +1 -1
  47. hcs_cli/payload/inventory/assign.json +14 -16
  48. hcs_cli/payload/inventory/deassign.json +11 -11
  49. hcs_cli/payload/lcm/akka.json +31 -33
  50. hcs_cli/payload/lcm/azure-dummy.json +64 -66
  51. hcs_cli/payload/lcm/azure-real.json +13 -11
  52. hcs_cli/payload/lcm/edge-proxy.json +34 -36
  53. hcs_cli/payload/lcm/zero-dedicated.json +34 -36
  54. hcs_cli/payload/lcm/zero-delay-1m-per-vm.json +53 -69
  55. hcs_cli/payload/lcm/zero-fail-delete-template.json +43 -0
  56. hcs_cli/payload/lcm/zero-fail-destroy-onthread.json +38 -40
  57. hcs_cli/payload/lcm/zero-fail-destroy.json +38 -40
  58. hcs_cli/payload/lcm/zero-fail-prepare-onthread.json +38 -40
  59. hcs_cli/payload/lcm/zero-fail-prepare.json +38 -40
  60. hcs_cli/payload/lcm/zero-fail-vm-onthread.json +58 -74
  61. hcs_cli/payload/lcm/zero-fail-vm.json +58 -74
  62. hcs_cli/payload/lcm/zero-floating.json +34 -36
  63. hcs_cli/payload/lcm/zero-manual.json +33 -35
  64. hcs_cli/payload/lcm/zero-multisession.json +34 -36
  65. hcs_cli/payload/lcm/zero-nanw.json +31 -33
  66. hcs_cli/payload/lcm/zero-new-5k-delay.json +69 -78
  67. hcs_cli/payload/lcm/zero-new-5k.json +36 -38
  68. hcs_cli/payload/lcm/zero-new-snapshot.json +37 -39
  69. hcs_cli/payload/lcm/zero-new.json +37 -39
  70. hcs_cli/payload/lcm/zero-reuse-vm-id.json +33 -35
  71. hcs_cli/payload/lcm/zero-with-max-id-offset.json +32 -34
  72. hcs_cli/payload/lcm/zero.json +59 -73
  73. hcs_cli/payload/provider/ad-stes-vsphere.json +26 -26
  74. hcs_cli/payload/provider/akka.json +12 -12
  75. hcs_cli/payload/provider/azure.json +14 -14
  76. hcs_cli/payload/provider/edgeproxy.json +12 -12
  77. hcs_cli/payload/provider/vsphere.json +14 -14
  78. hcs_cli/payload/scm/starter.json +22 -23
  79. hcs_cli/payload/synt/core/p01-dummy-success.json +11 -15
  80. hcs_cli/payload/synt/core/p02-dummy-fail.json +12 -15
  81. hcs_cli/payload/synt/core/p03-dummy-exception.json +12 -15
  82. hcs_cli/payload/synt/core/p04-dummy-success-repeat.json +12 -15
  83. hcs_cli/payload/synt/core/p05-dummy-fail-repeat.json +13 -16
  84. hcs_cli/payload/synt/core/p06-dummy-exception-repeat.json +13 -16
  85. hcs_cli/payload/synt/core/p07-dummy-delay.json +12 -15
  86. hcs_cli/payload/synt/core/p08-dummy-property.json +12 -15
  87. hcs_cli/payload/synt/ext/p20-connect-success.json +12 -15
  88. hcs_cli/payload/synt/ext/p21-connect-fail.json +12 -15
  89. hcs_cli/payload/synt/ext/p30-ssl-success.json +12 -15
  90. hcs_cli/payload/synt/ext/p31-ssl-fail.json +13 -16
  91. hcs_cli/payload/synt/ext/p40-http-success.json +12 -15
  92. hcs_cli/payload/synt/ext/p41-http-fail.json +12 -15
  93. hcs_cli/payload/synt/ext/p42-http-status-code.json +14 -20
  94. hcs_cli/payload/synt/ext1/p10-ping-success.json +13 -16
  95. hcs_cli/payload/synt/ext1/p11-ping-fail.json +12 -15
  96. hcs_cli/payload/synt/ext1/p12-ping-success-repeat.json +14 -17
  97. hcs_cli/provider/hcs/cert.py +0 -1
  98. hcs_cli/provider/hcs/edge.py +1 -1
  99. hcs_cli/provider/hcs/uag.py +1 -1
  100. hcs_cli/service/admin/template.py +10 -1
  101. hcs_cli/service/hoc/diagnostic.py +11 -3
  102. hcs_cli/service/inventory/__init__.py +15 -2
  103. hcs_cli/service/inventory/vm.py +12 -0
  104. hcs_cli/service/lcm/template.py +9 -6
  105. hcs_cli/service/lcm/vm.py +0 -1
  106. hcs_cli/service/task.py +0 -1
  107. hcs_cli/service/template.py +1 -1
  108. hcs_cli/support/debug_util.py +0 -1
  109. hcs_cli/support/plan_util.py +0 -1
  110. hcs_cli/support/predefined_payload.py +4 -1
  111. hcs_cli/support/template_util.py +0 -1
  112. hcs_cli/support/test_utils.py +2 -2
  113. hcs_cli/support/test_utils2.py +536 -0
  114. hcs_cli/support/vm_table.py +2 -2
  115. {hcs_cli-0.1.318.dist-info → hcs_cli-0.1.320.dist-info}/METADATA +24 -17
  116. {hcs_cli-0.1.318.dist-info → hcs_cli-0.1.320.dist-info}/RECORD +118 -113
  117. hcs_cli/payload/lcm/azure-dummy-nt.json +0 -69
  118. {hcs_cli-0.1.318.dist-info → hcs_cli-0.1.320.dist-info}/WHEEL +0 -0
  119. {hcs_cli-0.1.318.dist-info → hcs_cli-0.1.320.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,48 @@
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
+ from hcs_core.ctxp import recent
18
+ import hcs_core.sglib.cli_options as cli
19
+
20
+ from hcs_cli.service import inventory
21
+ from hcs_cli.support.param_util import parse_vm_path
22
+
23
+
24
+ @click.command()
25
+ @cli.org_id
26
+ @click.argument("vm_path", type=str, required=False)
27
+ def delete(org: str, vm_path: str):
28
+ """Delete a VM by path, e.g., template1/vm1, or 'vm1,vm2,vm3'."""
29
+
30
+ org_id = cli.get_org_id(org)
31
+
32
+ if vm_path.find(",") > 0:
33
+ vm_ids = [v.strip() for v in vm_path.split(",")]
34
+ template_id = recent.require("template", None)
35
+ else:
36
+ template_id, vm_id = parse_vm_path(vm_path)
37
+ ret = inventory.get(template_id, vm_id, org_id)
38
+ if not ret:
39
+ return "", 1
40
+ vm_ids = [vm_id]
41
+
42
+ ret = {"_requested_ids": vm_ids, "_accepted_ids": [], "_deleted": 0}
43
+ vms = inventory.begin_deleting_vms_by_id(template_id, org_id, vm_ids)
44
+ for vm in vms:
45
+ ret["_accepted_ids"].append(vm["id"])
46
+ actual_deleted = inventory.finish_deleting_vms(template_id, org_id, vm_ids)
47
+ ret["_deleted"] = actual_deleted
48
+ return ret
@@ -63,6 +63,12 @@ from hcs_cli.support import constant
63
63
  required=False,
64
64
  help="",
65
65
  )
66
+ @click.option(
67
+ "--subscription-id",
68
+ type=str,
69
+ required=False,
70
+ help="",
71
+ )
66
72
  @click.option(
67
73
  "--file",
68
74
  "-f",
@@ -80,6 +86,7 @@ def create(
80
86
  client_id: str,
81
87
  client_secret: str,
82
88
  tenant_id: str,
89
+ subscription_id: str,
83
90
  file: str,
84
91
  org: str,
85
92
  **kwargs,
@@ -108,7 +115,7 @@ def create(
108
115
  "description": description,
109
116
  "credentialId": credential_id,
110
117
  }
111
- elif client_id or client_secret or tenant_id:
118
+ elif client_id or client_secret or tenant_id or subscription_id:
112
119
  if type != "AZURE":
113
120
  raise click.BadParameter("client-id is only supported for Azure provider.")
114
121
  if not client_id:
@@ -117,7 +124,8 @@ def create(
117
124
  raise click.BadParameter("client-secret is required for client-id.")
118
125
  if not tenant_id:
119
126
  raise click.BadParameter("tenant-id is required for Azure provider.")
120
-
127
+ if not subscription_id:
128
+ raise click.BadParameter("subscription-id is required for Azure provider.")
121
129
  data = {
122
130
  "id": id if id else _rand_id(8),
123
131
  "type": type,
@@ -126,6 +134,7 @@ def create(
126
134
  "description": description,
127
135
  "credentialId": credential_id,
128
136
  "tenantId": tenant_id,
137
+ "subscriptionId": subscription_id,
129
138
  "credentials": [
130
139
  {
131
140
  "clientId": client_id,
@@ -0,0 +1,46 @@
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
+
19
+ import hcs_cli.service.lcm as lcm
20
+ from hcs_cli.cmds.template.expand import expand_impl
21
+
22
+
23
+ @click.command(hidden=True)
24
+ @click.option(
25
+ "--number",
26
+ "-n",
27
+ type=int,
28
+ required=False,
29
+ default=0,
30
+ help="Number of VMs to expand. Use negative number to shrink.",
31
+ )
32
+ @click.option(
33
+ "--to",
34
+ "-t",
35
+ type=int,
36
+ required=False,
37
+ default=0,
38
+ help="Expected size of template.",
39
+ )
40
+ @click.argument("template_id", type=str, required=False)
41
+ @cli.org_id
42
+ @cli.wait
43
+ def expand(number: int, to: int, template_id: str, org: str, wait: str):
44
+ """Update an existing template"""
45
+
46
+ return expand_impl(number, to, template_id, org, wait, lcm)
@@ -24,8 +24,9 @@ from hcs_cli.support.param_util import parse_vm_path
24
24
  @click.argument("vm_path", type=str, required=False)
25
25
  @cli.org_id
26
26
  @cli.confirm
27
+ @cli.force
27
28
  @cli.wait
28
- def delete(vm_path: str, org: str, confirm: bool, wait: str, **kwargs):
29
+ def delete(vm_path: str, org: str, confirm: bool, force: bool, wait: str, **kwargs):
29
30
  """Delete VM"""
30
31
  template_id, vm_id = parse_vm_path(vm_path)
31
32
  org_id = cli.get_org_id(org)
@@ -37,7 +38,7 @@ def delete(vm_path: str, org: str, confirm: bool, wait: str, **kwargs):
37
38
  if not confirm:
38
39
  click.confirm(f"Delete VM {template_id}/{vm_id}?", abort=True)
39
40
 
40
- vm.delete(template_id, vm_id, org_id, **kwargs)
41
+ vm.delete(template_id, vm_id, org_id, force=force, **kwargs)
41
42
 
42
43
  if wait == "0":
43
44
  return
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:
@@ -27,39 +27,84 @@ import hcs_cli.service.admin as admin
27
27
  "-n",
28
28
  type=int,
29
29
  required=False,
30
- default=1,
30
+ default=0,
31
31
  help="Number of VMs to expand. Use negative number to shrink.",
32
32
  )
33
+ @click.option(
34
+ "--to",
35
+ "-t",
36
+ type=int,
37
+ required=False,
38
+ default=0,
39
+ help="Expected size of template.",
40
+ )
33
41
  @click.argument("template_id", type=str, required=False)
34
42
  @cli.org_id
35
43
  @cli.wait
36
- def expand(number: int, template_id: str, org: str, wait: str, **kwargs):
44
+ def expand(number: int, to: int, template_id: str, org: str, wait: str):
37
45
  """Update an existing template"""
46
+ return expand_impl(number, to, template_id, org, wait, admin)
47
+
48
+
49
+ def expand_impl(number: int, to: int, template_id: str, org: str, wait: str, service):
50
+ if to != 0 and number != 0:
51
+ return "Specify either --to or --number, not both.", 1
52
+ if to < 0:
53
+ return "--to must be non-negative.", 1
54
+ if to > 5000:
55
+ return "--to exceeds maximum limit of 5000.", 1
38
56
 
39
57
  org_id = cli.get_org_id(org)
40
58
 
41
59
  template_id = recent.require("template", template_id)
42
- template = admin.template.get(template_id, org_id)
60
+ template = service.template.get(template_id, org_id)
43
61
 
44
62
  if not template:
45
- return "Template not found: " + template_id
63
+ return "Template not found: " + template_id, 1
46
64
 
47
65
  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
- }
66
+ new_spare_policy = {
67
+ "min": spare_policy.get("min", 0),
68
+ "max": spare_policy.get("max", 0),
69
+ "limit": spare_policy.get("limit", 0),
54
70
  }
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)
71
+
72
+ if to != 0:
73
+ new_spare_policy["min"] = to
74
+ new_spare_policy["max"] = to
75
+ new_spare_policy["limit"] = to
76
+ else:
77
+ if number == 0:
78
+ number = 1 # default to expand by 1
79
+ new_spare_policy["min"] += number
80
+ new_spare_policy["max"] += number
81
+ new_spare_policy["limit"] += number
82
+
83
+ if new_spare_policy["limit"] < 0:
84
+ new_spare_policy["limit"] = 0
85
+ elif new_spare_policy["limit"] > 5000:
86
+ new_spare_policy["limit"] = 5000
87
+
88
+ if new_spare_policy["min"] < 0:
89
+ new_spare_policy["min"] = 0
90
+ elif new_spare_policy["min"] > new_spare_policy["limit"]:
91
+ new_spare_policy["min"] = new_spare_policy["limit"]
92
+ if new_spare_policy["max"] < 0:
93
+ new_spare_policy["max"] = 0
94
+ elif new_spare_policy["max"] > new_spare_policy["limit"]:
95
+ new_spare_policy["max"] = new_spare_policy["limit"]
96
+
97
+ if (
98
+ new_spare_policy["min"] == spare_policy.get("min", 0)
99
+ and new_spare_policy["max"] == spare_policy.get("max", 0)
100
+ and new_spare_policy["limit"] == spare_policy.get("limit", 0)
101
+ ):
102
+ # no change
103
+ ret = template
104
+ else:
105
+ patch = {"sparePolicy": new_spare_policy}
106
+ ret = service.template.patch(template_id, org_id, patch)
107
+
63
108
  if wait != "0":
64
- ret = admin.template.wait_for_ready(template_id, org_id, duration.to_seconds(wait))
109
+ ret = service.template.wait_for_ready(template_id, org_id, duration.to_seconds(wait))
65
110
  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)
@@ -43,7 +43,7 @@ def update(template_id: str, update, org: str, wait: str, **kwargs):
43
43
  template = admin.template.get(template_id, org_id)
44
44
 
45
45
  if not template:
46
- return "Template not found: " + template_id
46
+ return "Template not found: " + template_id, 1
47
47
 
48
48
  allowed_fields = ["name", "description", "powerPolicy", "sparePolicy", "applicationProperties", "flags"]
49
49
 
@@ -52,7 +52,7 @@ def update(template_id: str, update, org: str, wait: str, **kwargs):
52
52
  if not patch:
53
53
  return template
54
54
 
55
- ret = admin.template.update(template_id, org_id, patch)
55
+ ret = admin.template.patch(template_id, org_id, patch)
56
56
  if wait != "0":
57
57
  ret = admin.template.wait_for_ready(template_id, org_id, duration.to_seconds(wait))
58
58
  return ret
@@ -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/delete.py CHANGED
@@ -23,10 +23,11 @@ from hcs_cli.support.param_util import parse_vm_path
23
23
 
24
24
  @click.command()
25
25
  @click.argument("vm_path", type=str, required=False)
26
+ @cli.force
26
27
  @cli.org_id
27
28
  @cli.confirm
28
29
  @cli.wait
29
- def delete(vm_path: str, org: str, confirm: bool, wait: str, **kwargs):
30
+ def delete(vm_path: str, org: str, confirm: bool, force: bool, wait: str, **kwargs):
30
31
  """Delete a VM by ID"""
31
32
  org_id = cli.get_org_id(org)
32
33
  template_id, vm_id = parse_vm_path(vm_path)
@@ -41,7 +42,7 @@ def delete(vm_path: str, org: str, confirm: bool, wait: str, **kwargs):
41
42
  if not confirm:
42
43
  click.confirm(f"Delete vm {existing['id']} from template {template['name']} ({template['id']})?", abort=True)
43
44
 
44
- vm.delete()
45
+ vm.delete(force=force)
45
46
 
46
47
  if wait == "0":
47
48
  return