wnm 0.0.12__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.

Potentially problematic release.


This version of wnm might be problematic. Click here for more details.

wnm/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """A service to manage a cluster of decentralized Autonomi nodes"""
2
+
3
+ __version__ = "0.0.12"
wnm/__main__.py ADDED
@@ -0,0 +1,236 @@
1
+ import json
2
+ import logging
3
+ import os
4
+ import sys
5
+ import time
6
+
7
+ from sqlalchemy import insert, select
8
+
9
+ from wnm.config import (
10
+ LOCK_FILE,
11
+ S,
12
+ apply_config_updates,
13
+ config_updates,
14
+ machine_config,
15
+ options,
16
+ )
17
+ from wnm.decision_engine import DecisionEngine
18
+ from wnm.executor import ActionExecutor
19
+ from wnm.migration import survey_machine
20
+ from wnm.models import Node
21
+ from wnm.utils import (
22
+ get_antnode_version,
23
+ get_machine_metrics,
24
+ update_counters,
25
+ )
26
+
27
+ logging.basicConfig(level=logging.INFO)
28
+ # Info level logging for sqlalchemy is too verbose, only use when needed
29
+ logging.getLogger("sqlalchemy.engine.Engine").disabled = True
30
+
31
+
32
+ # A storage place for ant node data
33
+ Workers = []
34
+
35
+ # Detect ANM
36
+
37
+
38
+ # Make a decision about what to do (new implementation using DecisionEngine)
39
+ def choose_action(machine_config, metrics, dry_run):
40
+ """Plan and execute actions using DecisionEngine and ActionExecutor.
41
+
42
+ This function now acts as a thin wrapper around the new decision engine
43
+ and action executor classes.
44
+
45
+ Args:
46
+ machine_config: Machine configuration dictionary
47
+ metrics: Current system metrics
48
+ dry_run: If True, log actions without executing
49
+
50
+ Returns:
51
+ Dictionary with execution status
52
+ """
53
+ # Check records for expired status (must be done before planning)
54
+ if not dry_run:
55
+ metrics = update_counters(S, metrics, machine_config)
56
+
57
+ # Handle nodes with no version number (done before planning)
58
+ if metrics["nodes_no_version"] > 0:
59
+ if dry_run:
60
+ logging.warning("DRYRUN: Update NoVersion nodes")
61
+ else:
62
+ with S() as session:
63
+ no_version = session.execute(
64
+ select(Node.timestamp, Node.id, Node.binary)
65
+ .where(Node.version == "")
66
+ .order_by(Node.timestamp.asc())
67
+ ).all()
68
+ # Iterate through nodes with no version number
69
+ for check in no_version:
70
+ # Update version number from binary
71
+ version = get_antnode_version(check[2])
72
+ logging.info(
73
+ f"Updating version number for node {check[1]} to {version}"
74
+ )
75
+ with S() as session:
76
+ session.query(Node).filter(Node.id == check[1]).update(
77
+ {"version": version}
78
+ )
79
+ session.commit()
80
+
81
+ # Use the new DecisionEngine to plan actions
82
+ engine = DecisionEngine(machine_config, metrics)
83
+ actions = engine.plan_actions()
84
+
85
+ # Log the computed features for debugging
86
+ logging.info(json.dumps(engine.get_features(), indent=2))
87
+
88
+ # Use ActionExecutor to execute the planned actions
89
+ executor = ActionExecutor(S)
90
+ result = executor.execute(actions, machine_config, metrics, dry_run)
91
+
92
+ return result
93
+
94
+
95
+ def main():
96
+
97
+ # Are we already running
98
+ if os.path.exists(LOCK_FILE):
99
+ logging.warning("wnm still running")
100
+ sys.exit(1)
101
+
102
+ # We're starting, so lets create a lock file
103
+ try:
104
+ with open(LOCK_FILE, "w") as file:
105
+ file.write(str(int(time.time())))
106
+ except (PermissionError, OSError) as e:
107
+ logging.error(f"Unable to create lock file: {e}")
108
+ sys.exit(1)
109
+
110
+ # Config should have loaded the machine_config
111
+ if machine_config:
112
+ logging.info("Machine: " + json.dumps(machine_config))
113
+ else:
114
+ logging.error("Unable to load machine config, exiting")
115
+ sys.exit(1)
116
+ # Check for config updates
117
+ if config_updates:
118
+ logging.info("Update: " + json.dumps(config_updates))
119
+ if options.dry_run:
120
+ logging.warning("Dry run, not saving requested updates")
121
+ # Create a dictionary for the machine config
122
+ # Machine by default returns a parameter array,
123
+ # use the __json__ method to return a dict
124
+ local_config = json.loads(json.dumps(machine_config))
125
+ # Apply the local config with the requested updates
126
+ local_config.update(config_updates)
127
+ else:
128
+ # Store the config changes to the database
129
+ apply_config_updates(config_updates)
130
+ # Create a working dictionary for the machine config
131
+ # Machine by default returns a parameter array,
132
+ # use the __json__ method to return a dict
133
+ local_config = json.loads(json.dumps(machine_config))
134
+ else:
135
+ local_config = json.loads(json.dumps(machine_config))
136
+
137
+ metrics = get_machine_metrics(
138
+ S,
139
+ local_config["node_storage"],
140
+ local_config["hd_remove"],
141
+ local_config["crisis_bytes"],
142
+ )
143
+ logging.info(json.dumps(metrics, indent=2))
144
+
145
+ # Do we already have nodes
146
+ if metrics["total_nodes"] == 0:
147
+ # Are we migrating an anm server
148
+ if options.init and options.migrate_anm:
149
+ Workers = survey_machine(machine_config) or []
150
+ if Workers:
151
+ if options.dry_run:
152
+ logging.warning(f"DRYRUN: Not saving {len(Workers)} detected nodes")
153
+ else:
154
+ with S() as session:
155
+ session.execute(insert(Node), Workers)
156
+ session.commit()
157
+ # Reload metrics
158
+ metrics = get_machine_metrics(
159
+ S,
160
+ local_config["node_storage"],
161
+ local_config["hd_remove"],
162
+ local_config["crisis_bytes"],
163
+ )
164
+ logging.info(
165
+ "Found {counter} nodes defined".format(
166
+ counter=metrics["total_nodes"]
167
+ )
168
+ )
169
+ else:
170
+ logging.warning("Requested migration but no nodes found")
171
+ else:
172
+ logging.info("No nodes found")
173
+ else:
174
+ logging.info(
175
+ "Found {counter} nodes configured".format(counter=metrics["total_nodes"])
176
+ )
177
+
178
+ # Check for reports
179
+ if options.report:
180
+ from wnm.reports import generate_node_status_report, generate_node_status_details_report
181
+
182
+ # If survey action is specified, run it first
183
+ if options.force_action == "survey":
184
+ logging.info("Running survey before generating report")
185
+ executor = ActionExecutor(S)
186
+ survey_result = executor.execute_forced_action(
187
+ "survey",
188
+ local_config,
189
+ metrics,
190
+ service_name=options.service_name,
191
+ dry_run=options.dry_run,
192
+ )
193
+ logging.info(f"Survey result: {survey_result}")
194
+
195
+ # Generate the report
196
+ if options.report == "node-status":
197
+ report_output = generate_node_status_report(
198
+ S, options.service_name, options.report_format
199
+ )
200
+ elif options.report == "node-status-details":
201
+ report_output = generate_node_status_details_report(
202
+ S, options.service_name, options.report_format
203
+ )
204
+ else:
205
+ report_output = f"Unknown report type: {options.report}"
206
+
207
+ print(report_output)
208
+ os.remove(LOCK_FILE)
209
+ sys.exit(0)
210
+
211
+ # Check for forced actions
212
+ if options.force_action:
213
+ logging.info(f"Executing forced action: {options.force_action}")
214
+ executor = ActionExecutor(S)
215
+ this_action = executor.execute_forced_action(
216
+ options.force_action,
217
+ local_config,
218
+ metrics,
219
+ service_name=options.service_name,
220
+ dry_run=options.dry_run,
221
+ count=options.count if hasattr(options, 'count') else 1,
222
+ )
223
+ else:
224
+ this_action = choose_action(local_config, metrics, options.dry_run)
225
+
226
+ print("Action:", json.dumps(this_action, indent=2))
227
+
228
+ os.remove(LOCK_FILE)
229
+ sys.exit(1)
230
+
231
+
232
+ if __name__ == "__main__":
233
+ main()
234
+ # print(options.MemRemove)
235
+
236
+ print("End of program")
wnm/actions.py ADDED
@@ -0,0 +1,45 @@
1
+ """Action model for representing planned node operations.
2
+
3
+ This module defines the action types and data structures used by the decision engine
4
+ to represent planned operations on nodes.
5
+ """
6
+
7
+ from dataclasses import dataclass
8
+ from enum import Enum
9
+ from typing import Optional
10
+
11
+
12
+ class ActionType(Enum):
13
+ """Types of actions that can be performed on nodes."""
14
+
15
+ ADD_NODE = "add"
16
+ REMOVE_NODE = "remove"
17
+ UPGRADE_NODE = "upgrade"
18
+ START_NODE = "start"
19
+ STOP_NODE = "stop"
20
+ RESTART_NODE = "restart"
21
+ SURVEY_NODES = "survey"
22
+ RESURVEY_NODES = "resurvey"
23
+
24
+
25
+ @dataclass
26
+ class Action:
27
+ """Represents a planned action to be executed.
28
+
29
+ Attributes:
30
+ type: The type of action to perform
31
+ node_id: Optional node ID if action is node-specific
32
+ priority: Higher values indicate more urgent actions (default: 0)
33
+ reason: Human-readable explanation of why this action is needed
34
+ """
35
+
36
+ type: ActionType
37
+ node_id: Optional[int] = None
38
+ priority: int = 0
39
+ reason: str = ""
40
+
41
+ def __repr__(self) -> str:
42
+ """Return a string representation of the action."""
43
+ if self.node_id is not None:
44
+ return f"Action({self.type.value}, node={self.node_id}, reason={self.reason})"
45
+ return f"Action({self.type.value}, reason={self.reason})"
wnm/common.py ADDED
@@ -0,0 +1,23 @@
1
+ # Primary node for want of one
2
+ QUEEN = 1
3
+
4
+ # Donation address, default to faucet vault, can be overridden in config
5
+ DONATE = "0x00455d78f850b0358E8cea5be24d415E01E107CF"
6
+ # Faucet address, to allow faucet donation and another donate address
7
+ FAUCET = "0x00455d78f850b0358E8cea5be24d415E01E107CF"
8
+
9
+ # Keep these as strings so they can be grepped in logs
10
+ STOPPED = "STOPPED" # 0 Node is not responding to it's metrics port
11
+ RUNNING = "RUNNING" # 1 Node is responding to it's metrics port
12
+ UPGRADING = "UPGRADING" # 2 Upgrade in progress
13
+ DISABLED = "DISABLED" # -1 Do not start
14
+ RESTARTING = "RESTARTING" # 3 re/starting a server intionally
15
+ MIGRATING = "MIGRATING" # 4 Moving volumes in progress
16
+ REMOVING = "REMOVING" # 5 Removing node in progress
17
+ DEAD = "DEAD" # -86 Broken node to cleanup
18
+
19
+ # Magic numbers extracted from codebase
20
+ MIN_NODES_THRESHOLD = 0 # Minimum nodes before considering actions
21
+ PORT_MULTIPLIER = 1000 # Port calculation: PortStart * 1000 + node_id
22
+ METRICS_PORT_BASE = 13000 # Metrics port calculation: 13000 + node_id
23
+ DEFAULT_CRISIS_BYTES = 2 * 10**9 # Default crisis threshold in bytes (2GB)