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 +3 -0
- wnm/__main__.py +236 -0
- wnm/actions.py +45 -0
- wnm/common.py +23 -0
- wnm/config.py +673 -0
- wnm/decision_engine.py +388 -0
- wnm/executor.py +1299 -0
- wnm/firewall/__init__.py +13 -0
- wnm/firewall/base.py +71 -0
- wnm/firewall/factory.py +95 -0
- wnm/firewall/null_firewall.py +71 -0
- wnm/firewall/ufw_manager.py +118 -0
- wnm/migration.py +42 -0
- wnm/models.py +459 -0
- wnm/process_managers/__init__.py +23 -0
- wnm/process_managers/base.py +203 -0
- wnm/process_managers/docker_manager.py +371 -0
- wnm/process_managers/factory.py +83 -0
- wnm/process_managers/launchd_manager.py +592 -0
- wnm/process_managers/setsid_manager.py +340 -0
- wnm/process_managers/systemd_manager.py +529 -0
- wnm/reports.py +286 -0
- wnm/utils.py +407 -0
- wnm/wallets.py +177 -0
- wnm-0.0.12.dist-info/METADATA +367 -0
- wnm-0.0.12.dist-info/RECORD +29 -0
- wnm-0.0.12.dist-info/WHEEL +5 -0
- wnm-0.0.12.dist-info/entry_points.txt +2 -0
- wnm-0.0.12.dist-info/top_level.txt +1 -0
wnm/__init__.py
ADDED
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)
|