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 Optional
19 from pathlib import Path
20 
21 import click
22 
23 from ..modules import ui
24 from ..modules import node as node_module
25 from ..modules.node import NodeStatus, getNodeStatus
26 from ..modules.user import initializeUserSession
27 from ..modules.utils import onBeforeCommandExecute, checkEnvironment
28 from ..modules.update import activateAutoUpdate
29 from ...utils import docker
30 from ...configuration import NodeConfiguration, InvalidConfiguration, ConfigurationNotFound
31 
32 
33 @click.command()
34 @click.option("--image", type = str, help = "Docker image url")
35 @onBeforeCommandExecute(node_module.initializeNodeConfiguration)
36 def start(image: Optional[str]) -> None:
37  nodeConfig = NodeConfiguration.load()
38 
39  if node_module.isRunning():
40  if not ui.clickPrompt(
41  "Node is already running. Do you wish to restart the Node? (Y/n)",
42  type = bool,
43  default = True,
44  show_default = False
45  ):
46  return
47 
48  node_module.stop(nodeConfig.id)
49 
50  if node_module.exists():
51  node_module.clean()
52 
53 
54  if image is not None:
55  nodeConfig.image = image # store forced image (flagged) so we can run autoupdate afterwards
56  nodeConfig.save()
57 
58  dockerImage = nodeConfig.image
59 
60  if node_module.shouldUpdate(dockerImage):
61  node_module.pull(dockerImage)
62 
63  node_module.start(dockerImage, nodeConfig)
64  docker.removeDanglingImages(node_module.getRepoFromImageUrl(dockerImage), node_module.getTagFromImageUrl(dockerImage))
65  activateAutoUpdate()
66 
67 
68 @click.command()
69 def stop() -> None:
70  nodeConfig = NodeConfiguration.load()
71 
72  if not node_module.isRunning():
73  ui.errorEcho("Node is already offline.")
74  return
75 
76  node_module.stop(nodeConfig.id)
77 
78 
79 @click.command()
80 @click.option("-y", "autoAccept", is_flag = True, help = "Accepts all prompts.")
81 @click.option("-n", "autoDecline", is_flag = True, help = "Declines all prompts.")
82 @onBeforeCommandExecute(node_module.initializeNodeConfiguration)
83 def update(autoAccept: bool, autoDecline: bool) -> None:
84  if autoAccept and autoDecline:
85  ui.errorEcho("Only one of the flags (\"-y\" or \"-n\") can be used at the same time.")
86  return
87 
88  nodeConfig = NodeConfiguration.load()
89  nodeStatus = node_module.getNodeStatus()
90 
91  if nodeStatus == NodeStatus.inactive:
92  ui.errorEcho("Node is not running. To update Node you need to start it first.")
93  return
94 
95  if nodeStatus == NodeStatus.reconnecting:
96  ui.errorEcho("Node is reconnecting. Cannot update now.")
97  return
98 
99  if nodeStatus == NodeStatus.busy and not autoAccept:
100  if autoDecline:
101  return
102 
103  if not ui.clickPrompt(
104  "Node is busy, do you wish to terminate the current execution to perform the update? (Y/n)",
105  type = bool,
106  default = True,
107  show_default = False
108  ):
109  return
110 
111  node_module.stop(nodeConfig.id)
112 
113  if not node_module.shouldUpdate(nodeConfig.image):
114  ui.successEcho("Node is already up to date.")
115  return
116 
117  ui.stdEcho("Fetching latest node image...")
118  node_module.pull(nodeConfig.image)
119 
120  if getNodeStatus() == NodeStatus.busy and not autoAccept:
121  if autoDecline:
122  return
123 
124  if not ui.clickPrompt(
125  "Node is busy, do you wish to terminate the current execution to perform the update? (Y/n)",
126  type = bool,
127  default = True,
128  show_default = False
129  ):
130  return
131 
132  node_module.stop(nodeConfig.id)
133 
134  ui.stdEcho("Updating node...")
135  node_module.start(nodeConfig.image, nodeConfig)
136 
137  docker.removeDanglingImages(
138  node_module.getRepoFromImageUrl(nodeConfig.image),
139  node_module.getTagFromImageUrl(nodeConfig.image)
140  )
141  activateAutoUpdate()
142 
143 
144 @click.command()
145 @click.option("--advanced", is_flag = True, help = "Configure node settings manually.")
146 def config(advanced: bool) -> None:
147  if node_module.isRunning():
148  if not ui.clickPrompt(
149  "Node is already running. Do you wish to stop the Node? (Y/n)",
150  type = bool,
151  default = True,
152  show_default = False
153  ):
154  ui.errorEcho("If you wish to reconfigure your node, use coretex node stop commands first.")
155  return
156 
157  try:
158  nodeConfig = NodeConfiguration.load()
159  node_module.stop(nodeConfig.id)
160  except (ConfigurationNotFound, InvalidConfiguration):
161  node_module.stop()
162 
163  try:
164  nodeConfig = NodeConfiguration.load()
165  if not ui.clickPrompt(
166  "Node configuration already exists. Would you like to update? (Y/n)",
167  type = bool,
168  default = True,
169  show_default = False
170  ):
171  return
172  except (ConfigurationNotFound, InvalidConfiguration):
173  pass
174 
175  nodeConfig = node_module.configureNode(advanced)
176  nodeConfig.save()
177  ui.previewNodeConfig(nodeConfig)
178 
179  ui.successEcho("Node successfully configured.")
180  activateAutoUpdate()
181 
182 
183 @click.command()
184 def status() -> None:
185  nodeStatus = getNodeStatus()
186  statusColors = {
187  "inactive": "red",
188  "active": 'green',
189  "busy": "cyan",
190  "reconnecting": "yellow"
191  }
192  statusEcho = click.style(nodeStatus.name, fg = statusColors[nodeStatus.name])
193  click.echo(f"Node is {statusEcho}.")
194 
195 
196 @click.command()
197 @click.option("--tail", "-n", type = int, help = "Shows N last logs.")
198 @click.option("--follow", "-f", is_flag = True, help = "Displays logs realtime.")
199 @click.option("--timestamps", "-t", is_flag = True, help = "Displays timestamps for logs.")
200 def logs(tail: Optional[int], follow: bool, timestamps: bool) -> None:
201  if not node_module.isRunning():
202  ui.errorEcho("There is no currently running Node on the machine.")
203  return
204 
205  node_module.showLogs(tail, follow, timestamps)
206 
207 
208 @click.group()
209 @onBeforeCommandExecute(docker.isDockerAvailable, excludeSubcommands = ["status"])
210 @onBeforeCommandExecute(initializeUserSession)
211 @onBeforeCommandExecute(node_module.checkResourceLimitations, excludeSubcommands = ["status"])
212 @onBeforeCommandExecute(checkEnvironment)
213 def node() -> None:
214  pass
215 
216 
217 node.add_command(start, "start")
218 node.add_command(stop, "stop")
219 node.add_command(update, "update")
220 node.add_command(config, "config")
221 node.add_command(status, "status")
222 node.add_command(logs, "logs")