manage-iocs 0.1.0__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.
- manage_iocs/__init__.py +11 -0
- manage_iocs/__main__.py +38 -0
- manage_iocs/_version.py +34 -0
- manage_iocs/commands.py +387 -0
- manage_iocs/utils.py +136 -0
- manage_iocs-0.1.0.dist-info/METADATA +60 -0
- manage_iocs-0.1.0.dist-info/RECORD +11 -0
- manage_iocs-0.1.0.dist-info/WHEEL +5 -0
- manage_iocs-0.1.0.dist-info/entry_points.txt +2 -0
- manage_iocs-0.1.0.dist-info/licenses/LICENSE +28 -0
- manage_iocs-0.1.0.dist-info/top_level.txt +1 -0
manage_iocs/__init__.py
ADDED
manage_iocs/__main__.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Interface for ``python -m manage_iocs``."""
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import sys
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
|
|
7
|
+
from . import commands
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_command_from_args(args: list[str]) -> Callable:
|
|
11
|
+
if len(args) < 2:
|
|
12
|
+
raise RuntimeError("No command provided!")
|
|
13
|
+
|
|
14
|
+
command = getattr(commands, args[1], None)
|
|
15
|
+
if not command or not inspect.isfunction(command):
|
|
16
|
+
raise RuntimeError(f"Unknown command: {args[1]}")
|
|
17
|
+
|
|
18
|
+
if not bool(inspect.signature(command).parameters):
|
|
19
|
+
return command
|
|
20
|
+
elif len(args) < 3:
|
|
21
|
+
raise RuntimeError(f"Command '{command.__name__}' requires additional arguments!")
|
|
22
|
+
|
|
23
|
+
# Return a lambda that calls the command with the additional args
|
|
24
|
+
# Assign it the same name as the original command for testing purposes
|
|
25
|
+
def command_w_args():
|
|
26
|
+
return command(*args[2:])
|
|
27
|
+
|
|
28
|
+
command_w_args.__name__ = command.__name__
|
|
29
|
+
|
|
30
|
+
return command_w_args
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def main():
|
|
34
|
+
get_command_from_args(sys.argv)()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
if __name__ == "__main__":
|
|
38
|
+
sys.exit(main())
|
manage_iocs/_version.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
TYPE_CHECKING = False
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
from typing import Union
|
|
17
|
+
|
|
18
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
20
|
+
else:
|
|
21
|
+
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
23
|
+
|
|
24
|
+
version: str
|
|
25
|
+
__version__: str
|
|
26
|
+
__version_tuple__: VERSION_TUPLE
|
|
27
|
+
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
30
|
+
|
|
31
|
+
__version__ = version = '0.1.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 1, 0)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = None
|
manage_iocs/commands.py
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import socket
|
|
3
|
+
import sys
|
|
4
|
+
import time as ttime
|
|
5
|
+
from subprocess import PIPE, Popen
|
|
6
|
+
|
|
7
|
+
from . import __version__, utils
|
|
8
|
+
|
|
9
|
+
EXTRA_PAD_WIDTH = 5
|
|
10
|
+
|
|
11
|
+
# Length added to string to account for ANSI color escape codes
|
|
12
|
+
ANSI_COLOR_ESC_CODE_LEN = 9
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def version():
|
|
16
|
+
"""Print the version of the manage-iocs package."""
|
|
17
|
+
|
|
18
|
+
print(f"Version: {__version__}")
|
|
19
|
+
return 0
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def help():
|
|
23
|
+
"""Display this help message."""
|
|
24
|
+
version()
|
|
25
|
+
print("Usage: manage-iocs [command] <ioc>")
|
|
26
|
+
print("Available commands:")
|
|
27
|
+
docs: dict[str, str] = {}
|
|
28
|
+
signatures: dict[str, list[str]] = {}
|
|
29
|
+
for func in inspect.getmembers(sys.modules[__name__], inspect.isfunction):
|
|
30
|
+
docs[func[0]] = str(func[1].__doc__)
|
|
31
|
+
signatures[func[0]] = list(inspect.signature(func[1]).parameters.keys())
|
|
32
|
+
|
|
33
|
+
usages = [
|
|
34
|
+
f" {name} " + " ".join(f"<{param}>" for param in params)
|
|
35
|
+
for name, params in signatures.items()
|
|
36
|
+
]
|
|
37
|
+
max_sig_len = max(len(sig) for sig in usages)
|
|
38
|
+
|
|
39
|
+
for usage, doc in zip(usages, docs.values(), strict=False):
|
|
40
|
+
print(f" {usage.ljust(max_sig_len + EXTRA_PAD_WIDTH)} - {doc}")
|
|
41
|
+
return 0
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@utils.requires_ioc_installed
|
|
45
|
+
def attach(ioc: str):
|
|
46
|
+
"""Connect to procServ telnet server for the given IOC."""
|
|
47
|
+
|
|
48
|
+
is_running, _ = utils.get_ioc_status(ioc)
|
|
49
|
+
if is_running != "Running":
|
|
50
|
+
raise RuntimeError(f"Cannot attach to IOC '{ioc}': IOC is not running!")
|
|
51
|
+
|
|
52
|
+
procserv_port = utils.get_ioc_procserv_port(ioc)
|
|
53
|
+
print(f"Attaching to IOC '{ioc}' at port {procserv_port}...")
|
|
54
|
+
proc = Popen(
|
|
55
|
+
["telnet", "localhost", str(procserv_port)],
|
|
56
|
+
stdin=PIPE,
|
|
57
|
+
stdout=PIPE,
|
|
58
|
+
)
|
|
59
|
+
return proc.wait()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def report():
|
|
63
|
+
"""Show config(s) of an all IOCs on localhost"""
|
|
64
|
+
base_hostname = socket.gethostname()
|
|
65
|
+
if "." in base_hostname:
|
|
66
|
+
base_hostname = base_hostname.split(".")[0]
|
|
67
|
+
|
|
68
|
+
iocs = [
|
|
69
|
+
ioc_config
|
|
70
|
+
for ioc_config in utils.find_iocs().values()
|
|
71
|
+
if ioc_config.host == "localhost"
|
|
72
|
+
or ioc_config.host == base_hostname
|
|
73
|
+
or ioc_config.host == socket.gethostname()
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
if len(iocs) == 0:
|
|
77
|
+
print("No IOCs found on configured to run on this host.")
|
|
78
|
+
print(f"Searched in: {utils.IOC_SEARCH_PATH}")
|
|
79
|
+
return 1
|
|
80
|
+
|
|
81
|
+
if len({ioc.procserv_port for ioc in iocs}) < len(iocs):
|
|
82
|
+
print("Warning: Detected multiple IOCs configured to use the same procServ port!")
|
|
83
|
+
elif len({ioc.name for ioc in iocs}) < len(iocs):
|
|
84
|
+
print("Warning: Detected multiple IOCs configured with the same name!")
|
|
85
|
+
|
|
86
|
+
max_base_len = max(len(str(ioc.path)) for ioc in iocs) + EXTRA_PAD_WIDTH
|
|
87
|
+
max_ioc_name_len = max(len(ioc.name) for ioc in iocs) + EXTRA_PAD_WIDTH
|
|
88
|
+
max_user_len = max(len(ioc.user) for ioc in iocs) + EXTRA_PAD_WIDTH
|
|
89
|
+
max_port_len = max(len(str(ioc.procserv_port)) for ioc in iocs) + EXTRA_PAD_WIDTH
|
|
90
|
+
max_exec_len = max(len(ioc.exec_path) for ioc in iocs) + max_base_len - EXTRA_PAD_WIDTH
|
|
91
|
+
|
|
92
|
+
header = (
|
|
93
|
+
f"{'BASE'.ljust(max_base_len)}| {'IOC'.ljust(max_ioc_name_len)}| "
|
|
94
|
+
f"{'USER'.ljust(max_user_len)}| {'PORT'.ljust(max_port_len)}| "
|
|
95
|
+
f"{'EXEC'.ljust(max_exec_len)}"
|
|
96
|
+
)
|
|
97
|
+
print(header)
|
|
98
|
+
print("-" * len(header))
|
|
99
|
+
for ioc in iocs:
|
|
100
|
+
ttime.sleep(0.01)
|
|
101
|
+
print(
|
|
102
|
+
f"{str(ioc.path).ljust(max_base_len)}| {ioc.name.ljust(max_ioc_name_len)}| "
|
|
103
|
+
f"{ioc.user.ljust(max_user_len)}| {str(ioc.procserv_port).ljust(max_port_len)}| "
|
|
104
|
+
f"{str(ioc.path / ioc.chdir / ioc.exec_path).ljust(max_exec_len)}"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@utils.requires_root
|
|
109
|
+
@utils.requires_ioc_installed
|
|
110
|
+
def disable(ioc: str):
|
|
111
|
+
"""Disable autostart for the given IOC."""
|
|
112
|
+
|
|
113
|
+
_, _, ret = utils.systemctl_passthrough("disable", ioc)
|
|
114
|
+
if ret == 0:
|
|
115
|
+
print(f"Autostart disabled for IOC '{ioc}'")
|
|
116
|
+
else:
|
|
117
|
+
raise RuntimeError(f"Failed to disable autostart for IOC '{ioc}'!")
|
|
118
|
+
return ret
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@utils.requires_root
|
|
122
|
+
@utils.requires_ioc_installed
|
|
123
|
+
def enable(ioc: str):
|
|
124
|
+
"""Enable autostart for the given IOC."""
|
|
125
|
+
|
|
126
|
+
_, _, ret = utils.systemctl_passthrough("enable", ioc)
|
|
127
|
+
if ret == 0:
|
|
128
|
+
print(f"Autostart enabled for IOC '{ioc}'")
|
|
129
|
+
else:
|
|
130
|
+
raise RuntimeError(f"Failed to enable autostart for IOC '{ioc}'!")
|
|
131
|
+
return ret
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@utils.requires_ioc_installed
|
|
135
|
+
def start(ioc: str):
|
|
136
|
+
"""Start the given IOC."""
|
|
137
|
+
|
|
138
|
+
_, _, ret = utils.systemctl_passthrough("start", ioc)
|
|
139
|
+
if ret == 0:
|
|
140
|
+
print(f"IOC '{ioc}' started successfully.")
|
|
141
|
+
else:
|
|
142
|
+
raise RuntimeError(f"Failed to start IOC '{ioc}'!")
|
|
143
|
+
return ret
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def startall():
|
|
147
|
+
"""Start all IOCs on this host."""
|
|
148
|
+
|
|
149
|
+
iocs = utils.find_installed_iocs().values()
|
|
150
|
+
ret = 0
|
|
151
|
+
for ioc in iocs:
|
|
152
|
+
ret += start(ioc.name)
|
|
153
|
+
return ret
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@utils.requires_ioc_installed
|
|
157
|
+
def stop(ioc: str):
|
|
158
|
+
"""Stop the given IOC."""
|
|
159
|
+
|
|
160
|
+
_, _, ret = utils.systemctl_passthrough("stop", ioc)
|
|
161
|
+
if ret == 0:
|
|
162
|
+
print(f"IOC '{ioc}' stopped successfully.")
|
|
163
|
+
else:
|
|
164
|
+
raise RuntimeError(f"Failed to stop IOC '{ioc}'!")
|
|
165
|
+
return ret
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def stopall():
|
|
169
|
+
"""Stop all IOCs on this host."""
|
|
170
|
+
|
|
171
|
+
iocs = utils.find_installed_iocs().values()
|
|
172
|
+
ret = 0
|
|
173
|
+
for ioc in iocs:
|
|
174
|
+
ret += stop(ioc.name)
|
|
175
|
+
return ret
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@utils.requires_root
|
|
179
|
+
def enableall():
|
|
180
|
+
"""Enable autostart for all IOCs on this host."""
|
|
181
|
+
|
|
182
|
+
iocs = utils.find_installed_iocs().values()
|
|
183
|
+
ret = 0
|
|
184
|
+
for ioc in iocs:
|
|
185
|
+
ret += enable(ioc.name)
|
|
186
|
+
return ret
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@utils.requires_root
|
|
190
|
+
def disableall():
|
|
191
|
+
"""Disable autostart for all IOCs on this host."""
|
|
192
|
+
|
|
193
|
+
iocs = utils.find_installed_iocs().values()
|
|
194
|
+
ret = 0
|
|
195
|
+
for ioc in iocs:
|
|
196
|
+
ret += disable(ioc.name)
|
|
197
|
+
return ret
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@utils.requires_ioc_installed
|
|
201
|
+
def restart(ioc: str):
|
|
202
|
+
"""Restart the given IOC."""
|
|
203
|
+
|
|
204
|
+
_, _, ret = utils.systemctl_passthrough("restart", ioc)
|
|
205
|
+
if ret == 0:
|
|
206
|
+
print(f"IOC '{ioc}' restarted successfully.")
|
|
207
|
+
else:
|
|
208
|
+
raise RuntimeError(f"Failed to restart IOC '{ioc}'!")
|
|
209
|
+
return ret
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@utils.requires_root
|
|
213
|
+
def uninstall(ioc: str):
|
|
214
|
+
"""Remove /etc/systemd/system/softioc-[ioc].service"""
|
|
215
|
+
|
|
216
|
+
_, _, ret = utils.systemctl_passthrough("stop", ioc)
|
|
217
|
+
if ret != 0:
|
|
218
|
+
raise RuntimeError(f"Failed to stop IOC '{ioc}' before uninstalling!")
|
|
219
|
+
_, _, ret = utils.systemctl_passthrough("disable", ioc)
|
|
220
|
+
if ret != 0:
|
|
221
|
+
raise RuntimeError(f"Failed to disable IOC '{ioc}' before uninstalling!")
|
|
222
|
+
_, _, ret = utils.systemctl_passthrough("uninstall", ioc)
|
|
223
|
+
if ret == 0:
|
|
224
|
+
print(f"IOC '{ioc}' uninstalled successfully.")
|
|
225
|
+
else:
|
|
226
|
+
raise RuntimeError(f"Failed to uninstall IOC '{ioc}'!")
|
|
227
|
+
return ret
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@utils.requires_root
|
|
231
|
+
def install(ioc: str):
|
|
232
|
+
"""Create /etc/systemd/system/softioc-[ioc].service"""
|
|
233
|
+
|
|
234
|
+
iocs = utils.find_installed_iocs()
|
|
235
|
+
if ioc in iocs:
|
|
236
|
+
raise RuntimeError(f"IOC '{ioc}' is already installed!")
|
|
237
|
+
|
|
238
|
+
procserv_ports = [ioc.procserv_port for ioc in utils.find_installed_iocs().values()]
|
|
239
|
+
if utils.find_iocs()[ioc].procserv_port in procserv_ports:
|
|
240
|
+
raise RuntimeError(
|
|
241
|
+
f"Cannot install IOC '{ioc}': procServ port "
|
|
242
|
+
f"{utils.find_iocs()[ioc].procserv_port} is already in use!"
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
service_file = utils.SYSTEMD_SERVICE_PATH / f"softioc-{ioc}.service"
|
|
246
|
+
ioc_config = utils.find_iocs()[ioc]
|
|
247
|
+
base_hostname = socket.gethostname()
|
|
248
|
+
if "." in base_hostname:
|
|
249
|
+
base_hostname = base_hostname.split(".")[0]
|
|
250
|
+
|
|
251
|
+
if ioc_config.host not in [base_hostname, "localhost", socket.gethostname()]:
|
|
252
|
+
raise RuntimeError(
|
|
253
|
+
f"Cannot install IOC '{ioc}' on this host; configured host is '{ioc_config.host}'!"
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
if ioc_config.user == "root":
|
|
257
|
+
raise RuntimeError(f"Refusing to install IOC '{ioc}' to run as user 'root'!")
|
|
258
|
+
|
|
259
|
+
with open(service_file, "w") as f:
|
|
260
|
+
f.write(
|
|
261
|
+
f"""
|
|
262
|
+
#
|
|
263
|
+
# Installed by manage-iocs
|
|
264
|
+
#
|
|
265
|
+
[Unit]
|
|
266
|
+
Description=IOC {ioc} via procServ
|
|
267
|
+
After=network.target remote_fs.target local_fs.target syslog.target time.target centrifydc.service
|
|
268
|
+
ConditionFileIsExecutable=/usr/bin/procServ
|
|
269
|
+
|
|
270
|
+
[Service]
|
|
271
|
+
User={ioc_config.user}
|
|
272
|
+
ExecStart=/usr/bin/procServ -f -q -c {ioc_config.path} -i ^D^C^] -p /var/run/softioc-{ioc}.pid \
|
|
273
|
+
-n {ioc} --restrict -L /var/log/softioc/{ioc}/{ioc}.log \
|
|
274
|
+
{ioc_config.procserv_port} {ioc_config.path}/{ioc_config.exec_path}
|
|
275
|
+
Environment="PROCPORT={ioc_config.procserv_port}"
|
|
276
|
+
Environment="HOSTNAME={ioc_config.host}"
|
|
277
|
+
Environment="IOCNAME={ioc}"
|
|
278
|
+
Environment="TOP={ioc_config.path}"
|
|
279
|
+
#Restart=on-failure
|
|
280
|
+
|
|
281
|
+
[Install]
|
|
282
|
+
WantedBy=multi-user.target
|
|
283
|
+
"""
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
_, stderr, ret = utils.systemctl_passthrough("install", ioc)
|
|
287
|
+
if ret == 0:
|
|
288
|
+
print(f"IOC '{ioc}' installed successfully.")
|
|
289
|
+
else:
|
|
290
|
+
raise RuntimeError(f"Failed to install IOC '{ioc}'!: {stderr.strip()}")
|
|
291
|
+
return ret
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def status():
|
|
295
|
+
"""Get the status of the given IOC."""
|
|
296
|
+
|
|
297
|
+
ret = 0
|
|
298
|
+
statuses: dict[str, tuple[str, bool]] = {}
|
|
299
|
+
installed_iocs = utils.find_installed_iocs().keys()
|
|
300
|
+
if len(installed_iocs) == 0:
|
|
301
|
+
print("No Installed IOCs found on this host.")
|
|
302
|
+
return 1
|
|
303
|
+
|
|
304
|
+
for installed_ioc in installed_iocs:
|
|
305
|
+
try:
|
|
306
|
+
statuses[installed_ioc] = utils.get_ioc_status(installed_ioc)
|
|
307
|
+
except RuntimeError:
|
|
308
|
+
pass # TODO: Handle this better?
|
|
309
|
+
|
|
310
|
+
max_ioc_name_len = max(len(ioc_name) for ioc_name in statuses.keys()) + EXTRA_PAD_WIDTH
|
|
311
|
+
max_status_len = max(len(status[0]) for status in statuses.values()) + EXTRA_PAD_WIDTH
|
|
312
|
+
max_enabled_len = len("Auto-Start")
|
|
313
|
+
|
|
314
|
+
print(f"{'IOC'.ljust(max_ioc_name_len)}{'Status'.ljust(max_status_len)}Auto-Start")
|
|
315
|
+
ttime.sleep(0.01)
|
|
316
|
+
print("-" * (max_ioc_name_len + max_status_len + max_enabled_len))
|
|
317
|
+
for ioc_name, (state, is_enabled) in statuses.items():
|
|
318
|
+
ttime.sleep(0.01)
|
|
319
|
+
if state == "Running":
|
|
320
|
+
state_str = f"\033[92m{state}\033[0m" # Green
|
|
321
|
+
elif state == "Stopped":
|
|
322
|
+
state_str = f"\033[91m{state}\033[0m" # Red
|
|
323
|
+
else:
|
|
324
|
+
state_str = f"\033[93m{state}\033[0m" # Yellow
|
|
325
|
+
print(
|
|
326
|
+
f"{ioc_name.ljust(max_ioc_name_len)}{state_str.ljust(max_status_len + ANSI_COLOR_ESC_CODE_LEN)}{'Enabled' if is_enabled else 'Disabled'}" # noqa: E501
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
return ret
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def nextport():
|
|
333
|
+
"""Find the next unused procServ port."""
|
|
334
|
+
|
|
335
|
+
used_ports = [ioc.procserv_port for ioc in utils.find_iocs().values()]
|
|
336
|
+
|
|
337
|
+
print(max(used_ports) + 1 if len(used_ports) > 0 else 4000)
|
|
338
|
+
return 0
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
@utils.requires_ioc_installed
|
|
342
|
+
def lastlog(ioc: str):
|
|
343
|
+
"""Display the output of the last IOC startup"""
|
|
344
|
+
|
|
345
|
+
log_file = utils.MANAGE_IOCS_LOG_PATH / f"{ioc}.log"
|
|
346
|
+
if not log_file.exists():
|
|
347
|
+
raise RuntimeError(f"No log file found for IOC '{ioc}' at '{log_file}'!")
|
|
348
|
+
|
|
349
|
+
with open(log_file) as f:
|
|
350
|
+
log = f.readlines()
|
|
351
|
+
start_index = 0
|
|
352
|
+
for i, line in reversed(list(enumerate(log))):
|
|
353
|
+
if line.strip() == f'@@@ Restarting child "{ioc}"':
|
|
354
|
+
start_index = i
|
|
355
|
+
break
|
|
356
|
+
if start_index == 0:
|
|
357
|
+
last_log = "".join(log)
|
|
358
|
+
else:
|
|
359
|
+
last_log = "".join(log[start_index:])
|
|
360
|
+
print(last_log)
|
|
361
|
+
return 0
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
@utils.requires_ioc_installed
|
|
365
|
+
@utils.requires_root
|
|
366
|
+
def rename(ioc: str, new_name: str):
|
|
367
|
+
"""Rename an installed IOC."""
|
|
368
|
+
|
|
369
|
+
state, is_enabled = utils.get_ioc_status(ioc)
|
|
370
|
+
uninstall(ioc)
|
|
371
|
+
|
|
372
|
+
ioc_config = utils.find_iocs()[ioc]
|
|
373
|
+
with open(ioc_config.path / "config", "w") as f:
|
|
374
|
+
f.write(f"NAME={new_name}\n")
|
|
375
|
+
f.write(f"HOST={ioc_config.host}\n")
|
|
376
|
+
f.write(f"PORT={ioc_config.procserv_port}\n")
|
|
377
|
+
f.write(f"USER={ioc_config.user}\n")
|
|
378
|
+
if ioc_config.exec_path:
|
|
379
|
+
f.write(f"EXEC={ioc_config.exec_path}\n")
|
|
380
|
+
if ioc_config.chdir and len(ioc_config.chdir) > 0:
|
|
381
|
+
f.write(f"CHDIR={ioc_config.chdir}\n")
|
|
382
|
+
|
|
383
|
+
install(new_name)
|
|
384
|
+
if is_enabled:
|
|
385
|
+
enable(new_name)
|
|
386
|
+
if state == "Running":
|
|
387
|
+
start(new_name)
|
manage_iocs/utils.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import os
|
|
3
|
+
import socket
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from subprocess import PIPE, Popen
|
|
8
|
+
|
|
9
|
+
IOC_SEARCH_PATH = [Path("/epics/iocs"), Path("/opt/epics/iocs"), Path("/opt/iocs")]
|
|
10
|
+
if "MANAGE_IOCS_SEARCH_PATH" in os.environ:
|
|
11
|
+
IOC_SEARCH_PATH.extend(
|
|
12
|
+
[Path(p) for p in os.environ["MANAGE_IOCS_SEARCH_PATH"].split(os.pathsep)]
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
SYSTEMD_SERVICE_PATH = Path("/etc/systemd/system")
|
|
16
|
+
MANAGE_IOCS_LOG_PATH = Path("/var/log/softioc")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class IOC:
|
|
21
|
+
name: str
|
|
22
|
+
user: str
|
|
23
|
+
procserv_port: int
|
|
24
|
+
path: Path
|
|
25
|
+
host: str
|
|
26
|
+
exec_path: str
|
|
27
|
+
chdir: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def read_config_file(config_path: Path) -> dict[str, str]:
|
|
31
|
+
"""Read config file for IOC"""
|
|
32
|
+
config: dict[str, str] = {}
|
|
33
|
+
with open(config_path) as f:
|
|
34
|
+
for line in f:
|
|
35
|
+
line = line.strip()
|
|
36
|
+
if line and not line.startswith("#"):
|
|
37
|
+
key, value = line.split("=", 1)
|
|
38
|
+
config[key.strip()] = value.strip()
|
|
39
|
+
return config
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def find_iocs() -> dict[str, IOC]:
|
|
43
|
+
"""Get a list of IOCs available in the search paths."""
|
|
44
|
+
iocs = {}
|
|
45
|
+
search_paths = IOC_SEARCH_PATH
|
|
46
|
+
for search_path in search_paths:
|
|
47
|
+
if os.path.exists(search_path):
|
|
48
|
+
for item in os.listdir(search_path):
|
|
49
|
+
if os.path.isdir(search_path / item) and os.path.exists(
|
|
50
|
+
search_path / item / "config"
|
|
51
|
+
):
|
|
52
|
+
config = read_config_file(search_path / item / "config")
|
|
53
|
+
iocs[item] = IOC(
|
|
54
|
+
name=item,
|
|
55
|
+
procserv_port=int(config["PORT"]),
|
|
56
|
+
path=search_path / item,
|
|
57
|
+
host=config.get("HOST", "localhost"),
|
|
58
|
+
user=config.get("USER", "iocuser"),
|
|
59
|
+
exec_path=config.get("EXEC", "st.cmd"),
|
|
60
|
+
chdir=config.get("CHDIR", "."),
|
|
61
|
+
)
|
|
62
|
+
return iocs
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def find_iocs_on_host() -> dict[str, IOC]:
|
|
66
|
+
"""Get a list of IOCs available on the given host."""
|
|
67
|
+
all_iocs = find_iocs()
|
|
68
|
+
return {
|
|
69
|
+
name: ioc
|
|
70
|
+
for name, ioc in all_iocs.items()
|
|
71
|
+
if ioc.host == socket.gethostname() or ioc.host == "localhost"
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def find_installed_iocs() -> dict[str, IOC]:
|
|
76
|
+
"""Get a list of IOCs that have systemd service files installed."""
|
|
77
|
+
iocs = {}
|
|
78
|
+
for ioc in find_iocs().values():
|
|
79
|
+
service_file = SYSTEMD_SERVICE_PATH / f"softioc-{ioc.name}.service"
|
|
80
|
+
if service_file.exists():
|
|
81
|
+
iocs[ioc.name] = ioc
|
|
82
|
+
return iocs
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def get_ioc_procserv_port(ioc: str) -> int:
|
|
86
|
+
"""Get the procServ port number for the given IOC."""
|
|
87
|
+
|
|
88
|
+
return find_iocs()[ioc].procserv_port
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def systemctl_passthrough(action: str, ioc: str) -> tuple[str, str, int]:
|
|
92
|
+
"""Helper to call systemctl with the given action and IOC name."""
|
|
93
|
+
proc = Popen(["systemctl", action, f"softioc-{ioc}.service"], stdin=PIPE, stdout=PIPE)
|
|
94
|
+
out, err = proc.communicate()
|
|
95
|
+
decoded_out = out.decode().strip() if out else ""
|
|
96
|
+
decoded_err = err.decode().strip() if err else ""
|
|
97
|
+
return decoded_out, decoded_err, proc.returncode
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def get_ioc_status(ioc_name: str) -> tuple[str, bool]:
|
|
101
|
+
"""Get the active and enabled status of the given IOC."""
|
|
102
|
+
|
|
103
|
+
state, err, _ = systemctl_passthrough("is-active", ioc_name)
|
|
104
|
+
|
|
105
|
+
# Convert to more user-friendly terms
|
|
106
|
+
if state == "active":
|
|
107
|
+
state = "Running"
|
|
108
|
+
elif state == "inactive":
|
|
109
|
+
state = "Stopped"
|
|
110
|
+
|
|
111
|
+
enabled, err, _ = systemctl_passthrough("is-enabled", ioc_name)
|
|
112
|
+
if enabled not in ("enabled", "disabled"):
|
|
113
|
+
raise RuntimeError(err)
|
|
114
|
+
|
|
115
|
+
return state.capitalize(), enabled == "enabled"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def requires_root(func: Callable):
|
|
119
|
+
@functools.wraps(func)
|
|
120
|
+
def wrapper(*args):
|
|
121
|
+
if os.geteuid() != 0:
|
|
122
|
+
raise PermissionError(f"Command {func.__name__} requires root privileges.")
|
|
123
|
+
return func(*args)
|
|
124
|
+
|
|
125
|
+
return wrapper
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def requires_ioc_installed(func: Callable):
|
|
129
|
+
@functools.wraps(func)
|
|
130
|
+
def wrapper(ioc: str, *args, **kwargs):
|
|
131
|
+
installed_iocs = find_installed_iocs()
|
|
132
|
+
if ioc not in installed_iocs:
|
|
133
|
+
raise RuntimeError(f"No IOC with name '{ioc}' is installed!")
|
|
134
|
+
return func(ioc, *args, **kwargs)
|
|
135
|
+
|
|
136
|
+
return wrapper
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: manage-iocs
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI utility for auto-creating phoebus engineering screens from EPICS db templates
|
|
5
|
+
Author-email: Jakub Wlodek <jwlodek@bnl.gov>
|
|
6
|
+
License: BSD 3-Clause License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2025, Brookhaven National Laboratory
|
|
9
|
+
|
|
10
|
+
Redistribution and use in source and binary forms, with or without
|
|
11
|
+
modification, are permitted provided that the following conditions are met:
|
|
12
|
+
|
|
13
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
14
|
+
list of conditions and the following disclaimer.
|
|
15
|
+
|
|
16
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
17
|
+
this list of conditions and the following disclaimer in the documentation
|
|
18
|
+
and/or other materials provided with the distribution.
|
|
19
|
+
|
|
20
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
21
|
+
contributors may be used to endorse or promote products derived from
|
|
22
|
+
this software without specific prior written permission.
|
|
23
|
+
|
|
24
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
25
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
26
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
27
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
28
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
29
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
30
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
31
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
32
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
33
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
34
|
+
|
|
35
|
+
Project-URL: GitHub, https://github.com/NSLS2/manage-iocs
|
|
36
|
+
Classifier: Development Status :: 3 - Alpha
|
|
37
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
41
|
+
Requires-Python: >=3.11
|
|
42
|
+
Description-Content-Type: text/markdown
|
|
43
|
+
License-File: LICENSE
|
|
44
|
+
Requires-Dist: pyyaml
|
|
45
|
+
Provides-Extra: dev
|
|
46
|
+
Requires-Dist: copier; extra == "dev"
|
|
47
|
+
Requires-Dist: pipdeptree; extra == "dev"
|
|
48
|
+
Requires-Dist: pre-commit; extra == "dev"
|
|
49
|
+
Requires-Dist: pyright; extra == "dev"
|
|
50
|
+
Requires-Dist: pytest; extra == "dev"
|
|
51
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
52
|
+
Requires-Dist: ruff; extra == "dev"
|
|
53
|
+
Requires-Dist: tox-direct; extra == "dev"
|
|
54
|
+
Requires-Dist: types-mock; extra == "dev"
|
|
55
|
+
Requires-Dist: import-linter; extra == "dev"
|
|
56
|
+
Dynamic: license-file
|
|
57
|
+
|
|
58
|
+
# Manage-IOCs [](https://github.com/NSLS2/manage-iocs/actions/workflows/ci.yml)
|
|
59
|
+
|
|
60
|
+
A re-write of the `manage-iocs` utility in pure python.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
manage_iocs/__init__.py,sha256=Ksms_WJF8LTkbm38gEpm1jBpGqcQ8NGvmb2ZJlOE1j8,198
|
|
2
|
+
manage_iocs/__main__.py,sha256=zdgO1if090egIEVwHFea4T73NuRIER-q8LAm1ioSyXE,1005
|
|
3
|
+
manage_iocs/_version.py,sha256=5jwwVncvCiTnhOedfkzzxmxsggwmTBORdFL_4wq0ZeY,704
|
|
4
|
+
manage_iocs/commands.py,sha256=wsvKlZ6xEj_JWyqfVg4lK1ovP4lpr1N4aoeKw3W5cWw,11874
|
|
5
|
+
manage_iocs/utils.py,sha256=gAQVC5F908LHxQ32wp_BXpUieZnbtrT2I9b7TYN55Fc,4309
|
|
6
|
+
manage_iocs-0.1.0.dist-info/licenses/LICENSE,sha256=kF_ROu6b6TScdLaB1UbUhxnQAya6_eS90L3YJWt8dlc,1545
|
|
7
|
+
manage_iocs-0.1.0.dist-info/METADATA,sha256=DgPZHuEfeVJG98U2Xq5VJ1YFHe8HXs2RIXzT557kA_c,3032
|
|
8
|
+
manage_iocs-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
9
|
+
manage_iocs-0.1.0.dist-info/entry_points.txt,sha256=GtfxlU1jaRqK2R3Zd7kux1m1ySTiMk6ftaElbbKUEuE,58
|
|
10
|
+
manage_iocs-0.1.0.dist-info/top_level.txt,sha256=ZWSNEmez5G5Em-4EPOM83S1ypgwVHDOFl4YVpt3r2-E,12
|
|
11
|
+
manage_iocs-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025, Brookhaven National Laboratory
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
manage_iocs
|