nutanix-shim-server 0.0.1__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.
- nutanix_shim_server-0.0.1/.gitignore +177 -0
- nutanix_shim_server-0.0.1/.pre-commit-config.yaml +12 -0
- nutanix_shim_server-0.0.1/.python-version +1 -0
- nutanix_shim_server-0.0.1/LICENSE +22 -0
- nutanix_shim_server-0.0.1/PKG-INFO +12 -0
- nutanix_shim_server-0.0.1/README.md +0 -0
- nutanix_shim_server-0.0.1/pyproject.toml +36 -0
- nutanix_shim_server-0.0.1/src/nutanix_shim_server/__init__.py +15 -0
- nutanix_shim_server-0.0.1/src/nutanix_shim_server/clustermgmt.py +283 -0
- nutanix_shim_server-0.0.1/src/nutanix_shim_server/core.py +0 -0
- nutanix_shim_server-0.0.1/src/nutanix_shim_server/networking.py +137 -0
- nutanix_shim_server-0.0.1/src/nutanix_shim_server/routes/__init__.py +0 -0
- nutanix_shim_server-0.0.1/src/nutanix_shim_server/routes/clustermgmt.py +92 -0
- nutanix_shim_server-0.0.1/src/nutanix_shim_server/routes/networking.py +46 -0
- nutanix_shim_server-0.0.1/src/nutanix_shim_server/routes/vmm.py +255 -0
- nutanix_shim_server-0.0.1/src/nutanix_shim_server/server.py +24 -0
- nutanix_shim_server-0.0.1/src/nutanix_shim_server/vmm.py +604 -0
- nutanix_shim_server-0.0.1/uv.lock +1223 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# Env files
|
|
2
|
+
**/*.env.*
|
|
3
|
+
|
|
4
|
+
# Byte-compiled / optimized / DLL files
|
|
5
|
+
__pycache__/
|
|
6
|
+
*.py[cod]
|
|
7
|
+
*$py.class
|
|
8
|
+
|
|
9
|
+
# C extensions
|
|
10
|
+
*.so
|
|
11
|
+
|
|
12
|
+
# Distribution / packaging
|
|
13
|
+
.Python
|
|
14
|
+
build/
|
|
15
|
+
develop-eggs/
|
|
16
|
+
dist/
|
|
17
|
+
downloads/
|
|
18
|
+
eggs/
|
|
19
|
+
.eggs/
|
|
20
|
+
lib/
|
|
21
|
+
lib64/
|
|
22
|
+
parts/
|
|
23
|
+
sdist/
|
|
24
|
+
var/
|
|
25
|
+
wheels/
|
|
26
|
+
share/python-wheels/
|
|
27
|
+
*.egg-info/
|
|
28
|
+
.installed.cfg
|
|
29
|
+
*.egg
|
|
30
|
+
MANIFEST
|
|
31
|
+
|
|
32
|
+
# PyInstaller
|
|
33
|
+
# Usually these files are written by a python script from a template
|
|
34
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
35
|
+
*.manifest
|
|
36
|
+
*.spec
|
|
37
|
+
|
|
38
|
+
# Installer logs
|
|
39
|
+
pip-log.txt
|
|
40
|
+
pip-delete-this-directory.txt
|
|
41
|
+
|
|
42
|
+
# Unit test / coverage reports
|
|
43
|
+
htmlcov/
|
|
44
|
+
.tox/
|
|
45
|
+
.nox/
|
|
46
|
+
.coverage
|
|
47
|
+
.coverage.*
|
|
48
|
+
.cache
|
|
49
|
+
nosetests.xml
|
|
50
|
+
coverage.xml
|
|
51
|
+
*.cover
|
|
52
|
+
*.py,cover
|
|
53
|
+
.hypothesis/
|
|
54
|
+
.pytest_cache/
|
|
55
|
+
cover/
|
|
56
|
+
|
|
57
|
+
# Translations
|
|
58
|
+
*.mo
|
|
59
|
+
*.pot
|
|
60
|
+
|
|
61
|
+
# Django stuff:
|
|
62
|
+
*.log
|
|
63
|
+
local_settings.py
|
|
64
|
+
db.sqlite3
|
|
65
|
+
db.sqlite3-journal
|
|
66
|
+
|
|
67
|
+
# Flask stuff:
|
|
68
|
+
instance/
|
|
69
|
+
.webassets-cache
|
|
70
|
+
|
|
71
|
+
# Scrapy stuff:
|
|
72
|
+
.scrapy
|
|
73
|
+
|
|
74
|
+
# Sphinx documentation
|
|
75
|
+
docs/_build/
|
|
76
|
+
|
|
77
|
+
# PyBuilder
|
|
78
|
+
.pybuilder/
|
|
79
|
+
target/
|
|
80
|
+
|
|
81
|
+
# Jupyter Notebook
|
|
82
|
+
.ipynb_checkpoints
|
|
83
|
+
|
|
84
|
+
# IPython
|
|
85
|
+
profile_default/
|
|
86
|
+
ipython_config.py
|
|
87
|
+
|
|
88
|
+
# pyenv
|
|
89
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
90
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
91
|
+
# .python-version
|
|
92
|
+
|
|
93
|
+
# pipenv
|
|
94
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
95
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
96
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
97
|
+
# install all needed dependencies.
|
|
98
|
+
#Pipfile.lock
|
|
99
|
+
|
|
100
|
+
# poetry
|
|
101
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
102
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
103
|
+
# commonly ignored for libraries.
|
|
104
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
105
|
+
#poetry.lock
|
|
106
|
+
|
|
107
|
+
# pdm
|
|
108
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
109
|
+
#pdm.lock
|
|
110
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
111
|
+
# in version control.
|
|
112
|
+
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
|
113
|
+
.pdm.toml
|
|
114
|
+
.pdm-python
|
|
115
|
+
.pdm-build/
|
|
116
|
+
|
|
117
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
118
|
+
__pypackages__/
|
|
119
|
+
|
|
120
|
+
# Celery stuff
|
|
121
|
+
celerybeat-schedule
|
|
122
|
+
celerybeat.pid
|
|
123
|
+
|
|
124
|
+
# SageMath parsed files
|
|
125
|
+
*.sage.py
|
|
126
|
+
|
|
127
|
+
# Environments
|
|
128
|
+
.env
|
|
129
|
+
.venv
|
|
130
|
+
env/
|
|
131
|
+
venv/
|
|
132
|
+
ENV/
|
|
133
|
+
env.bak/
|
|
134
|
+
venv.bak/
|
|
135
|
+
|
|
136
|
+
# Spyder project settings
|
|
137
|
+
.spyderproject
|
|
138
|
+
.spyproject
|
|
139
|
+
|
|
140
|
+
# vscode settings
|
|
141
|
+
.vscode
|
|
142
|
+
|
|
143
|
+
# local mlflow tracking
|
|
144
|
+
mlruns/
|
|
145
|
+
mlartifacts/
|
|
146
|
+
|
|
147
|
+
# Rope project settings
|
|
148
|
+
.ropeproject
|
|
149
|
+
|
|
150
|
+
# mkdocs documentation
|
|
151
|
+
/site
|
|
152
|
+
|
|
153
|
+
# mypy
|
|
154
|
+
.mypy_cache/
|
|
155
|
+
.dmypy.json
|
|
156
|
+
dmypy.json
|
|
157
|
+
|
|
158
|
+
# Pyre type checker
|
|
159
|
+
.pyre/
|
|
160
|
+
|
|
161
|
+
# pytype static type analyzer
|
|
162
|
+
.pytype/
|
|
163
|
+
|
|
164
|
+
# Cython debug symbols
|
|
165
|
+
cython_debug/
|
|
166
|
+
|
|
167
|
+
# PyCharm
|
|
168
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
169
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
170
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
171
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
172
|
+
#.idea/
|
|
173
|
+
|
|
174
|
+
NORCE_CAChain.crt
|
|
175
|
+
|
|
176
|
+
# argus files
|
|
177
|
+
.argus
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 NORCE Research
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nutanix-shim-server
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Server for the Foreman Nutanix shim plugin
|
|
5
|
+
Author-email: Miles Granger <mgra@norceresearch.no>
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
|
+
Requires-Dist: fastapi[standard]>=0.118.0
|
|
9
|
+
Requires-Dist: ntnx-clustermgmt-py-client~=4.0.0
|
|
10
|
+
Requires-Dist: ntnx-networking-py-client~=4.0.0
|
|
11
|
+
Requires-Dist: ntnx-prism-py-client~=4.0.0
|
|
12
|
+
Requires-Dist: ntnx-vmm-py-client~=4.0.0
|
|
File without changes
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "nutanix-shim-server"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "Server for the Foreman Nutanix shim plugin"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Miles Granger", email = "mgra@norceresearch.no" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"fastapi[standard]>=0.118.0",
|
|
12
|
+
"ntnx-clustermgmt-py-client~=4.0.0",
|
|
13
|
+
"ntnx-networking-py-client~=4.0.0",
|
|
14
|
+
"ntnx-prism-py-client~=4.0.0",
|
|
15
|
+
"ntnx-vmm-py-client~=4.0.0",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
nutanix-shim-server = "nutanix_shim_server:main"
|
|
20
|
+
|
|
21
|
+
[build-system]
|
|
22
|
+
requires = ["hatchling"]
|
|
23
|
+
build-backend = "hatchling.build"
|
|
24
|
+
|
|
25
|
+
[dependency-groups]
|
|
26
|
+
dev = [
|
|
27
|
+
"ipython>=8.34.0",
|
|
28
|
+
"pre-commit>=4.1.0",
|
|
29
|
+
"pyright>=1.1.396",
|
|
30
|
+
"pytest-mock>=3.14.0",
|
|
31
|
+
"pytest>=8.3.5",
|
|
32
|
+
"ruff>=0.13.2",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[tool.pytest.init_options]
|
|
36
|
+
addopts = "-v"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
from fastapi_cli.cli import main as fastapi_main
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main() -> None:
|
|
7
|
+
"""Entry point that wraps fastapi-cli with the app path pre-configured."""
|
|
8
|
+
# Insert 'run' command and app path, then append any user-provided args
|
|
9
|
+
sys.argv = [
|
|
10
|
+
"fastapi",
|
|
11
|
+
"run",
|
|
12
|
+
"nutanix_shim_server.server:app",
|
|
13
|
+
*sys.argv[1:],
|
|
14
|
+
]
|
|
15
|
+
fastapi_main()
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import dataclasses
|
|
5
|
+
from typing import Self, cast
|
|
6
|
+
import ntnx_clustermgmt_py_client as cm
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from IPython.terminal.embed import embed
|
|
10
|
+
except ImportError:
|
|
11
|
+
|
|
12
|
+
def embed():
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ClusterMgmt:
|
|
17
|
+
config: cm.Configuration
|
|
18
|
+
|
|
19
|
+
def __init__(self):
|
|
20
|
+
self.config = cm.Configuration()
|
|
21
|
+
self.config.host = os.environ["NUTANIX_HOST"]
|
|
22
|
+
self.config.scheme = "https"
|
|
23
|
+
self.config.set_api_key(os.environ["NUTANIX_API_KEY"])
|
|
24
|
+
self.config.max_retry_attempts = 3
|
|
25
|
+
self.config.backoff_factor = 3
|
|
26
|
+
self.config.verify_ssl = False # TODO: True
|
|
27
|
+
self.config.port = os.environ.get("NUTANIX_PORT", 9440)
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def client(self) -> cm.ApiClient:
|
|
31
|
+
if not hasattr(self, "_client"):
|
|
32
|
+
self._client = cm.ApiClient(self.config)
|
|
33
|
+
self._client.add_default_header(
|
|
34
|
+
header_name="Accept-Encoding", header_value="gzip, deflate, br"
|
|
35
|
+
)
|
|
36
|
+
return self._client
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def storage_containers_api(self) -> cm.StorageContainersApi:
|
|
40
|
+
if not hasattr(self, "_storage_containers_api"):
|
|
41
|
+
self._storage_containers_api = cm.StorageContainersApi(
|
|
42
|
+
api_client=self.client
|
|
43
|
+
)
|
|
44
|
+
return self._storage_containers_api
|
|
45
|
+
|
|
46
|
+
def list_storage_containers(self) -> list[StorageContainerMetadata]:
|
|
47
|
+
"""Return list of storage containers"""
|
|
48
|
+
resp: cm.ListStorageContainersApiResponse = self.storage_containers_api.list_storage_containers()
|
|
49
|
+
containers: None | list[cm.StorageContainer] = resp.data
|
|
50
|
+
if containers:
|
|
51
|
+
return [
|
|
52
|
+
StorageContainerMetadata.from_nutanix_storage_container(container)
|
|
53
|
+
for container in containers
|
|
54
|
+
]
|
|
55
|
+
return []
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def clusters_api(self) -> cm.ClustersApi:
|
|
59
|
+
if not hasattr(self, "_clusters_api"):
|
|
60
|
+
self._clusters_api = cm.ClustersApi(api_client=self.client)
|
|
61
|
+
return self._clusters_api
|
|
62
|
+
|
|
63
|
+
def list_clusters(self) -> list[ClusterMetadata]:
|
|
64
|
+
"""Return list of clusters"""
|
|
65
|
+
resp: cm.ListClustersApiResponse = self.clusters_api.list_clusters()
|
|
66
|
+
clusters: None | list[cm.Cluster] = resp.data
|
|
67
|
+
if clusters:
|
|
68
|
+
return [
|
|
69
|
+
ClusterMetadata.from_nutanix_cluster(cluster) for cluster in clusters
|
|
70
|
+
]
|
|
71
|
+
return []
|
|
72
|
+
|
|
73
|
+
def get_cluster_stats(self, cluster_ext_id: str) -> ClusterResourceStats:
|
|
74
|
+
"""Get resource usage statistics for a specific cluster
|
|
75
|
+
|
|
76
|
+
Gets CPU/memory capacity by aggregating from cluster hosts, and usage
|
|
77
|
+
stats from the cluster stats API.
|
|
78
|
+
"""
|
|
79
|
+
import datetime
|
|
80
|
+
|
|
81
|
+
# Get cluster stats for usage metrics
|
|
82
|
+
end_time = datetime.datetime.now(datetime.timezone.utc)
|
|
83
|
+
start_time = end_time - datetime.timedelta(hours=1)
|
|
84
|
+
|
|
85
|
+
stats_resp: cm.ClusterStatsApiResponse = self.clusters_api.get_cluster_stats(
|
|
86
|
+
extId=cluster_ext_id,
|
|
87
|
+
_startTime=start_time,
|
|
88
|
+
_endTime=end_time,
|
|
89
|
+
)
|
|
90
|
+
stats: cm.ClusterStats = stats_resp.data # type: ignore
|
|
91
|
+
|
|
92
|
+
# Get hosts to aggregate CPU and memory capacity
|
|
93
|
+
hosts_resp = self.clusters_api.list_hosts_by_cluster_id(clusterExtId=cluster_ext_id)
|
|
94
|
+
hosts: None | list[cm.Host] = hosts_resp.data # type: ignore
|
|
95
|
+
|
|
96
|
+
# Aggregate capacity from all hosts in the cluster
|
|
97
|
+
total_cpu_capacity_hz = 0
|
|
98
|
+
total_memory_capacity_bytes = 0
|
|
99
|
+
total_cpu_cores = 0
|
|
100
|
+
|
|
101
|
+
if hosts:
|
|
102
|
+
for host in hosts:
|
|
103
|
+
# Try cpu_capacity_hz first, fall back to calculating from frequency and cores
|
|
104
|
+
if host.cpu_capacity_hz:
|
|
105
|
+
total_cpu_capacity_hz += host.cpu_capacity_hz
|
|
106
|
+
elif host.cpu_frequency_hz and host.number_of_cpu_cores:
|
|
107
|
+
# Calculate capacity: frequency * number of cores
|
|
108
|
+
total_cpu_capacity_hz += host.cpu_frequency_hz * host.number_of_cpu_cores
|
|
109
|
+
|
|
110
|
+
if host.memory_size_bytes:
|
|
111
|
+
total_memory_capacity_bytes += host.memory_size_bytes
|
|
112
|
+
|
|
113
|
+
if host.number_of_cpu_cores:
|
|
114
|
+
total_cpu_cores += host.number_of_cpu_cores
|
|
115
|
+
|
|
116
|
+
return ClusterResourceStats.from_nutanix_cluster_stats(
|
|
117
|
+
stats, total_cpu_capacity_hz, total_memory_capacity_bytes, total_cpu_cores
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclasses.dataclass(frozen=True)
|
|
122
|
+
class ClusterMetadata:
|
|
123
|
+
name: str
|
|
124
|
+
ext_id: str
|
|
125
|
+
n_nodes: int
|
|
126
|
+
arch: str
|
|
127
|
+
vm_count: int
|
|
128
|
+
is_available: bool
|
|
129
|
+
|
|
130
|
+
@classmethod
|
|
131
|
+
def from_nutanix_cluster(cls, cluster: cm.Cluster) -> Self:
|
|
132
|
+
# nutanix typing is almost always "Unknown | None" - hence the casting
|
|
133
|
+
nodes = cast(cm.NodeReference, cluster.nodes)
|
|
134
|
+
config = cast(cm.ClusterConfigReference, cluster.config)
|
|
135
|
+
return cls(
|
|
136
|
+
name=cast(str, cluster.name),
|
|
137
|
+
n_nodes=cast(int, nodes.number_of_nodes),
|
|
138
|
+
ext_id=cast(str, cluster.ext_id),
|
|
139
|
+
arch=cast(str, config.cluster_arch),
|
|
140
|
+
vm_count=cast(int, cluster.vm_count),
|
|
141
|
+
is_available=cast(bool, config.is_available),
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@dataclasses.dataclass(frozen=True)
|
|
146
|
+
class StorageContainerMetadata:
|
|
147
|
+
"""
|
|
148
|
+
Metadata about a storage container.
|
|
149
|
+
|
|
150
|
+
Includes container ID, name, capacity information, and storage features.
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
ext_id: str
|
|
154
|
+
name: str
|
|
155
|
+
cluster_name: None | str
|
|
156
|
+
cluster_ext_id: None | str
|
|
157
|
+
max_capacity_bytes: None | int
|
|
158
|
+
logical_advertised_capacity_bytes: None | int
|
|
159
|
+
replication_factor: None | int
|
|
160
|
+
is_compression_enabled: None | bool
|
|
161
|
+
is_encrypted: None | bool
|
|
162
|
+
is_marked_for_removal: None | bool
|
|
163
|
+
|
|
164
|
+
@classmethod
|
|
165
|
+
def from_nutanix_storage_container(cls, container: cm.StorageContainer) -> Self:
|
|
166
|
+
"""Convert Nutanix SDK StorageContainer to our response model"""
|
|
167
|
+
return cls(
|
|
168
|
+
ext_id=cast(str, container.container_ext_id), # Note: SDK uses container_ext_id, not ext_id
|
|
169
|
+
name=cast(str, container.name),
|
|
170
|
+
cluster_name=container.cluster_name,
|
|
171
|
+
cluster_ext_id=container.cluster_ext_id,
|
|
172
|
+
max_capacity_bytes=container.max_capacity_bytes,
|
|
173
|
+
logical_advertised_capacity_bytes=container.logical_advertised_capacity_bytes,
|
|
174
|
+
replication_factor=container.replication_factor,
|
|
175
|
+
is_compression_enabled=container.is_compression_enabled,
|
|
176
|
+
is_encrypted=container.is_encrypted,
|
|
177
|
+
is_marked_for_removal=container.is_marked_for_removal,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@dataclasses.dataclass(frozen=True)
|
|
182
|
+
class ClusterResourceStats:
|
|
183
|
+
"""
|
|
184
|
+
Resource usage statistics for a cluster.
|
|
185
|
+
|
|
186
|
+
Includes CPU, memory, and storage capacity and usage information.
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
ext_id: str
|
|
190
|
+
# CPU stats
|
|
191
|
+
cpu_capacity_hz: int
|
|
192
|
+
cpu_usage_hz: int
|
|
193
|
+
cpu_usage_percent: float
|
|
194
|
+
cpu_cores_total: int
|
|
195
|
+
cpu_cores_usage: int
|
|
196
|
+
# Memory stats
|
|
197
|
+
memory_capacity_bytes: int
|
|
198
|
+
memory_usage_bytes: int
|
|
199
|
+
memory_usage_percent: float
|
|
200
|
+
# Storage stats
|
|
201
|
+
storage_capacity_bytes: int
|
|
202
|
+
storage_usage_bytes: int
|
|
203
|
+
storage_usage_percent: float
|
|
204
|
+
|
|
205
|
+
@classmethod
|
|
206
|
+
def from_nutanix_cluster_stats(
|
|
207
|
+
cls,
|
|
208
|
+
stats: cm.ClusterStats,
|
|
209
|
+
cpu_capacity_hz: int = 0,
|
|
210
|
+
memory_capacity_bytes: int = 0,
|
|
211
|
+
cpu_cores_total: int = 0,
|
|
212
|
+
) -> Self:
|
|
213
|
+
"""Convert Nutanix SDK ClusterStats to our response model
|
|
214
|
+
|
|
215
|
+
Note: Stats from Nutanix API can be time-series (lists) or scalar values.
|
|
216
|
+
We extract the latest/last value from lists if needed.
|
|
217
|
+
|
|
218
|
+
CPU and memory capacity, and CPU cores are passed in separately since
|
|
219
|
+
they must be aggregated from cluster hosts (not available in ClusterStats).
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
def extract_value(val, field_name="unknown"):
|
|
223
|
+
"""Extract scalar value from either a list or scalar
|
|
224
|
+
|
|
225
|
+
Handles:
|
|
226
|
+
- None -> 0
|
|
227
|
+
- Scalar values -> value
|
|
228
|
+
- Lists of TimeValuePair -> extract value from last pair
|
|
229
|
+
- Empty lists -> 0
|
|
230
|
+
"""
|
|
231
|
+
if val is None:
|
|
232
|
+
return 0
|
|
233
|
+
if isinstance(val, list):
|
|
234
|
+
if not val:
|
|
235
|
+
return 0
|
|
236
|
+
# Take the last value from time series
|
|
237
|
+
last_item = val[-1]
|
|
238
|
+
# If it's a TimeValuePair object, extract the value
|
|
239
|
+
if hasattr(last_item, 'value'):
|
|
240
|
+
return last_item.value if last_item.value is not None else 0
|
|
241
|
+
return last_item
|
|
242
|
+
# Handle TimeValuePair objects directly
|
|
243
|
+
if hasattr(val, 'value'):
|
|
244
|
+
return val.value if val.value is not None else 0
|
|
245
|
+
return val
|
|
246
|
+
|
|
247
|
+
# Extract usage values from stats (storage includes capacity too)
|
|
248
|
+
# CPU: Use hypervisor_cpu_usage_ppm to calculate usage from capacity
|
|
249
|
+
cpu_usage_ppm = extract_value(stats.hypervisor_cpu_usage_ppm, "hypervisor_cpu_usage_ppm")
|
|
250
|
+
cpu_usage_hz = int(cpu_capacity_hz * (cpu_usage_ppm / 1_000_000)) if cpu_capacity_hz else 0
|
|
251
|
+
cpu_usage_pct = (cpu_usage_hz / cpu_capacity_hz * 100) if cpu_capacity_hz else 0
|
|
252
|
+
|
|
253
|
+
# Estimate cores in use based on usage percentage
|
|
254
|
+
cpu_cores_usage = int(cpu_cores_total * (cpu_usage_pct / 100)) if cpu_cores_total else 0
|
|
255
|
+
|
|
256
|
+
# Memory: overall_memory_usage_bytes from stats, capacity from aggregated hosts
|
|
257
|
+
memory_usage = extract_value(stats.overall_memory_usage_bytes, "overall_memory_usage_bytes")
|
|
258
|
+
memory_usage_pct = (memory_usage / memory_capacity_bytes * 100) if memory_capacity_bytes else 0
|
|
259
|
+
|
|
260
|
+
# Storage: both capacity and usage from stats
|
|
261
|
+
storage_capacity = extract_value(stats.storage_capacity_bytes, "storage_capacity_bytes")
|
|
262
|
+
storage_usage = extract_value(stats.storage_usage_bytes, "storage_usage_bytes")
|
|
263
|
+
storage_usage_pct = (storage_usage / storage_capacity * 100) if storage_capacity else 0
|
|
264
|
+
|
|
265
|
+
return cls(
|
|
266
|
+
ext_id=cast(str, stats.ext_id),
|
|
267
|
+
cpu_capacity_hz=int(cpu_capacity_hz),
|
|
268
|
+
cpu_usage_hz=int(cpu_usage_hz),
|
|
269
|
+
cpu_usage_percent=round(cpu_usage_pct, 2),
|
|
270
|
+
cpu_cores_total=int(cpu_cores_total),
|
|
271
|
+
cpu_cores_usage=int(cpu_cores_usage),
|
|
272
|
+
memory_capacity_bytes=int(memory_capacity_bytes),
|
|
273
|
+
memory_usage_bytes=int(memory_usage),
|
|
274
|
+
memory_usage_percent=round(memory_usage_pct, 2),
|
|
275
|
+
storage_capacity_bytes=int(storage_capacity),
|
|
276
|
+
storage_usage_bytes=int(storage_usage),
|
|
277
|
+
storage_usage_percent=round(storage_usage_pct, 2),
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
if __name__ == "__main__":
|
|
282
|
+
mgmt = ClusterMgmt()
|
|
283
|
+
mgmt.list_clusters()
|
|
File without changes
|