Coretex
node.py
1 # Copyright (C) 2023 Coretex LLC
2 
3 # This file is part of Coretex.ai
4 
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License as
7 # published by the Free Software Foundation, either version 3 of the
8 # License, or (at your option) any later version.
9 
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU Affero General Public License for more details.
14 
15 # You should have received a copy of the GNU Affero General Public License
16 # along with this program. If not, see <https://www.gnu.org/licenses/>.
17 
18 from typing import List, Optional, Tuple
19 from pathlib import Path
20 
21 from . import utils, config_defaults
22 from .base import BaseConfiguration, CONFIG_DIR
23 from ..utils import docker
24 from ..node import NodeMode
25 from ..networking import networkManager, NetworkRequestError
26 
27 
28 class NodeConfiguration(BaseConfiguration):
29 
30  @classmethod
31  def getConfigPath(cls) -> Path:
32  return CONFIG_DIR / "node_config.json"
33 
34  @property
35  def name(self) -> str:
36  return self.getValue("name", str)
37 
38  @name.setter
39  def name(self, value: str) -> None:
40  self._raw["name"] = value
41 
42  @property
43  def accessToken(self) -> str:
44  return self.getValue("accessToken", str)
45 
46  @accessToken.setter
47  def accessToken(self, value: str) -> None:
48  self._raw["accessToken"] = value
49 
50  @property
51  def storagePath(self) -> str:
52  return self.getValue("storagePath", str)
53 
54  @storagePath.setter
55  def storagePath(self, value: Optional[str]) -> None:
56  self._raw["storagePath"] = value
57 
58  @property
59  def image(self) -> str:
60  return self.getValue("image", str)
61 
62  @image.setter
63  def image(self, value: str) -> None:
64  self._raw["image"] = value
65 
66  @property
67  def allowGpu(self) -> bool:
68  return self.getValue("allowGpu", bool)
69 
70  @allowGpu.setter
71  def allowGpu(self, value: bool) -> None:
72  self._raw["allowGpu"] = value
73 
74  @property
75  def ram(self) -> int:
76  ram = self.getOptValue("ram", int)
77 
78  if ram is None:
79  ram = config_defaults.DEFAULT_RAM
80 
81  return ram
82 
83  @ram.setter
84  def ram(self, value: int) -> None:
85  self._raw["ram"] = value
86 
87  @property
88  def swap(self) -> int:
89  swap = self.getOptValue("swap", int)
90 
91  if swap is None:
92  swap = config_defaults.DEFAULT_SWAP_MEMORY
93 
94  return swap
95 
96  @swap.setter
97  def swap(self, value: int) -> None:
98  self._raw["swap"] = value
99 
100  @property
101  def sharedMemory(self) -> int:
102  sharedMemory = self.getOptValue("sharedMemory", int)
103 
104  if sharedMemory is None:
105  sharedMemory = config_defaults.DEFAULT_SHARED_MEMORY
106 
107  return sharedMemory
108 
109  @sharedMemory.setter
110  def sharedMemory(self, value: int) -> None:
111  self._raw["sharedMemory"] = value
112 
113  @property
114  def cpuCount(self) -> int:
115  cpuCount = self.getOptValue("cpuCount", int)
116 
117  if cpuCount is None:
118  cpuCount = config_defaults.DEFAULT_CPU_COUNT
119 
120  return cpuCount
121 
122  @cpuCount.setter
123  def cpuCount(self, value: int) -> None:
124  self._raw["cpuCount"] = value
125 
126  @property
127  def mode(self) -> int:
128  mode = self.getOptValue("mode", int)
129 
130  if mode is None:
131  mode = NodeMode.any
132 
133  return mode
134 
135  @mode.setter
136  def mode(self, value: int) -> None:
137  self._raw["mode"] = value
138 
139  @property
140  def id(self) -> int:
141  id = self.getOptValue("id", int)
142 
143  if id is None:
144  id = self.fetchNodeId()
145 
146  return id
147 
148  @id.setter
149  def id(self, value: int) -> None:
150  self._raw["id"] = value
151 
152  @property
153  def allowDocker(self) -> bool:
154  return self.getValue("allowDocker", bool)
155 
156  @allowDocker.setter
157  def allowDocker(self, value: bool) -> None:
158  self._raw["allowDocker"] = value
159 
160  @property
161  def secret(self) -> Optional[str]:
162  return self.getOptValue("secret", str)
163 
164  @secret.setter
165  def secret(self, value: Optional[str]) -> None:
166  self._raw["secret"] = value
167 
168  @property
169  def initScript(self) -> Optional[str]:
170  return self.getOptValue("initScript", str)
171 
172  @initScript.setter
173  def initScript(self, value: Optional[str]) -> None:
174  self._raw["initScript"] = value
175 
176  @property
177  def modelId(self) -> Optional[int]:
178  return self.getOptValue("modelId", int)
179 
180  @modelId.setter
181  def modelId(self, value: Optional[int]) -> None:
182  self._raw["modelId"] = value
183 
184  @property
185  def nearWalletId(self) -> Optional[str]:
186  return self.getOptValue("nearWalletId", str)
187 
188  @modelId.setter
189  def nearWalletId(self, value: Optional[str]) -> None:
190  self._raw["nearWalletId"] = value
191 
192  @property
193  def endpointInvocationPrice(self) -> Optional[float]:
194  return self.getOptValue("endpointInvocationPrice", float)
195 
196  @modelId.setter
197  def endpointInvocationPrice(self, value: Optional[float]) -> None:
198  self._raw["endpointInvocationPrice"] = value
199 
200  @property
201  def heartbeatInterval(self) -> int:
202  return self.getValue("heartbeatInterval", int, default = config_defaults.HEARTBEAT_INTERVAL)
203 
204  @heartbeatInterval.setter
205  def heartbeatInterval(self, value: int) -> None:
206  self._raw["heartbeatInterval"] = value
207 
208  def _isConfigValid(self) -> Tuple[bool, List[str]]:
209  isValid = True
210  errorMessages = []
211  cpuLimit, ramLimit = docker.getResourceLimits()
212  swapLimit = docker.getDockerSwapLimit()
213 
214  if not isinstance(self._raw.get("name"), str):
215  isValid = False
216  errorMessages.append("Invalid configuration. Missing required field \"name\".")
217 
218  if not isinstance(self._raw.get("image"), str):
219  isValid = False
220  errorMessages.append("Invalid configuration. Missing required field \"image\".")
221 
222  if not isinstance(self._raw.get("accessToken"), str):
223  isValid = False
224  errorMessages.append("Invalid configuration. Missing required field \"accessToken\".")
225 
226  validateRamField = utils.validateRamField(self, ramLimit)
227  if isinstance(validateRamField, tuple):
228  ram, message = validateRamField
229  errorMessages.append(message)
230  self.ram = ram
231 
232  validateCpuCount = utils.validateCpuCount(self, cpuLimit)
233  if isinstance(validateCpuCount, tuple):
234  cpuCount, message = validateCpuCount
235  errorMessages.append(message)
236  self.cpuCount = cpuCount
237 
238  validateSwapMemory = utils.validateSwapMemory(self, swapLimit)
239  if isinstance(validateSwapMemory, tuple):
240  swap, message = validateSwapMemory
241  errorMessages.append(message)
242  self.swap = swap
243 
244  return isValid, errorMessages
245 
246  def getInitScriptPath(self) -> Optional[Path]:
247  value = self._raw.get("initScript")
248 
249  if not isinstance(value, str):
250  return None
251 
252  if value == "":
253  return None
254 
255  path = Path(value).expanduser().absolute()
256  if not path.exists():
257  return None
258 
259  return path
260 
261  def fetchNodeId(self) -> int:
262  params = {
263  "name": f"={self.name}"
264  }
265 
266  response = networkManager.get("service/directory", params)
267  if response.hasFailed():
268  raise NetworkRequestError(response, "Failed to fetch node id.")
269 
270  responseJson = response.getJson(dict)
271  data = responseJson.get("data")
272 
273  if not isinstance(data, list):
274  raise TypeError(f"Invalid \"data\" type {type(data)}. Expected: \"list\"")
275 
276  if len(data) == 0:
277  raise ValueError(f"Node with name \"{self.name}\" not found.")
278 
279  nodeJson = data[0]
280  if not isinstance(nodeJson, dict):
281  raise TypeError(f"Invalid \"nodeJson\" type {type(nodeJson)}. Expected: \"dict\"")
282 
283  id = nodeJson.get("id")
284  if not isinstance(id, int):
285  raise TypeError(f"Invalid \"id\" type {type(id)}. Expected: \"int\"")
286 
287  self.id = int(id)
288  self.save()
289 
290  return int(id)