machine_access_control 0.2.3__tar.gz → 0.4.0__tar.gz
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.
- {machine_access_control-0.2.3 → machine_access_control-0.4.0}/PKG-INFO +8 -6
- {machine_access_control-0.2.3 → machine_access_control-0.4.0}/pyproject.toml +7 -7
- {machine_access_control-0.2.3 → machine_access_control-0.4.0}/src/dm_mac/models/machine.py +159 -38
- {machine_access_control-0.2.3 → machine_access_control-0.4.0}/src/dm_mac/slack_handler.py +24 -24
- {machine_access_control-0.2.3 → machine_access_control-0.4.0}/src/dm_mac/views/prometheus.py +19 -36
- {machine_access_control-0.2.3 → machine_access_control-0.4.0}/LICENSE +0 -0
- {machine_access_control-0.2.3 → machine_access_control-0.4.0}/README.rst +0 -0
- {machine_access_control-0.2.3 → machine_access_control-0.4.0}/src/dm_mac/__init__.py +0 -0
- {machine_access_control-0.2.3 → machine_access_control-0.4.0}/src/dm_mac/cli_utils.py +0 -0
- {machine_access_control-0.2.3 → machine_access_control-0.4.0}/src/dm_mac/models/__init__.py +0 -0
- {machine_access_control-0.2.3 → machine_access_control-0.4.0}/src/dm_mac/models/users.py +0 -0
- {machine_access_control-0.2.3 → machine_access_control-0.4.0}/src/dm_mac/neongetter.py +0 -0
- {machine_access_control-0.2.3 → machine_access_control-0.4.0}/src/dm_mac/py.typed +0 -0
- {machine_access_control-0.2.3 → machine_access_control-0.4.0}/src/dm_mac/utils.py +0 -0
- {machine_access_control-0.2.3 → machine_access_control-0.4.0}/src/dm_mac/views/__init__.py +0 -0
- {machine_access_control-0.2.3 → machine_access_control-0.4.0}/src/dm_mac/views/api.py +0 -0
- {machine_access_control-0.2.3 → machine_access_control-0.4.0}/src/dm_mac/views/machine.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: machine_access_control
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Decatur Makers Machine Access Control package
|
|
5
|
-
Home-page: https://github.com/jantman/machine_access_control
|
|
6
5
|
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
7
|
Author: Jason Antman
|
|
8
8
|
Author-email: jason@jasonantman.com
|
|
9
9
|
Requires-Python: >=3.12,<4.0
|
|
@@ -12,18 +12,20 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
-
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Requires-Dist: aiohttp (>=3.12.14,<4.0.0)
|
|
16
17
|
Requires-Dist: asyncio (>=3.4.3,<4.0.0)
|
|
17
|
-
Requires-Dist: filelock (>=3.
|
|
18
|
+
Requires-Dist: filelock (>=3.16.1,<4.0.0)
|
|
18
19
|
Requires-Dist: humanize (>=4.11.0,<5.0.0)
|
|
19
20
|
Requires-Dist: jsonschema (>=4.23.0,<5.0.0)
|
|
20
21
|
Requires-Dist: prometheus-client (>=0.20.0,<0.21.0)
|
|
21
22
|
Requires-Dist: quart (>=0.19.8,<0.20.0)
|
|
22
|
-
Requires-Dist: requests (>=2.32.
|
|
23
|
+
Requires-Dist: requests (>=2.32.4,<3.0.0)
|
|
23
24
|
Requires-Dist: slack-bolt (>=1.21.3,<2.0.0)
|
|
24
25
|
Requires-Dist: slack-sdk (>=3.33.5,<4.0.0)
|
|
25
26
|
Project-URL: Changelog, https://github.com/jantman/machine_access_control/releases
|
|
26
27
|
Project-URL: Documentation, https://github.com/jantman/machine_access_control
|
|
28
|
+
Project-URL: Homepage, https://github.com/jantman/machine_access_control
|
|
27
29
|
Project-URL: Repository, https://github.com/jantman/machine_access_control
|
|
28
30
|
Description-Content-Type: text/x-rst
|
|
29
31
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "machine_access_control"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.0"
|
|
4
4
|
description = "Decatur Makers Machine Access Control package"
|
|
5
5
|
authors = ["Jason Antman <jason@jasonantman.com>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -25,13 +25,13 @@ Changelog = "https://github.com/jantman/machine_access_control/releases"
|
|
|
25
25
|
[tool.poetry.dependencies]
|
|
26
26
|
python = "^3.12"
|
|
27
27
|
jsonschema = "^4.23.0"
|
|
28
|
-
requests = "^2.32.
|
|
29
|
-
filelock = "^3.
|
|
28
|
+
requests = "^2.32.4"
|
|
29
|
+
filelock = "^3.16.1"
|
|
30
30
|
prometheus-client = "^0.20.0"
|
|
31
31
|
quart = "^0.19.8"
|
|
32
32
|
asyncio = "^3.4.3"
|
|
33
33
|
slack-bolt = "^1.21.3"
|
|
34
|
-
aiohttp = "^3.
|
|
34
|
+
aiohttp = "^3.12.14"
|
|
35
35
|
slack-sdk = "^3.33.5"
|
|
36
36
|
humanize = "^4.11.0"
|
|
37
37
|
|
|
@@ -40,7 +40,7 @@ Pygments = ">=2.10.0"
|
|
|
40
40
|
black = ">=21.10b0"
|
|
41
41
|
coverage = {extras = ["toml"], version = ">=6.2"}
|
|
42
42
|
darglint = ">=1.8.1"
|
|
43
|
-
flake8 = ">=
|
|
43
|
+
flake8 = ">=7.1.1"
|
|
44
44
|
flake8-bandit = ">=2.1.2"
|
|
45
45
|
flake8-bugbear = ">=21.9.2"
|
|
46
46
|
flake8-docstrings = ">=1.6.0"
|
|
@@ -49,7 +49,7 @@ furo = ">=2021.11.12"
|
|
|
49
49
|
isort = ">=5.10.1"
|
|
50
50
|
mypy = ">=0.930"
|
|
51
51
|
nox = ">=2024.4.15"
|
|
52
|
-
nox-poetry = ">= 1.0
|
|
52
|
+
nox-poetry = ">= 1.2.0"
|
|
53
53
|
pep8-naming = ">=0.12.1"
|
|
54
54
|
pre-commit = ">=2.16.0"
|
|
55
55
|
pre-commit-hooks = ">=4.1.0"
|
|
@@ -65,7 +65,7 @@ responses = "^0.25.3"
|
|
|
65
65
|
types-requests = "^2.32.0.20240712"
|
|
66
66
|
faker = "^26.3.0"
|
|
67
67
|
pytest-html = "^4.1.1"
|
|
68
|
-
poetry-plugin-export = "^1.
|
|
68
|
+
poetry-plugin-export = "^1.9.0"
|
|
69
69
|
freezegun = "^1.5.1"
|
|
70
70
|
|
|
71
71
|
[tool.isort]
|
|
@@ -53,6 +53,18 @@ CONFIG_SCHEMA: Dict[str, Any] = {
|
|
|
53
53
|
"but log and display a warning if the "
|
|
54
54
|
"operator is not authorized.",
|
|
55
55
|
},
|
|
56
|
+
"always_enabled": {
|
|
57
|
+
"type": "boolean",
|
|
58
|
+
"description": "If set, machine is always enabled and "
|
|
59
|
+
"does not require RFID authentication. "
|
|
60
|
+
"Displays 'Always On' and relay is always "
|
|
61
|
+
"on unless Oopsed or Locked.",
|
|
62
|
+
},
|
|
63
|
+
"alias": {
|
|
64
|
+
"type": "string",
|
|
65
|
+
"description": "Optional human-friendly alias for the machine. "
|
|
66
|
+
"Used in Slack messages and logs instead of the machine name.",
|
|
67
|
+
},
|
|
56
68
|
},
|
|
57
69
|
"additionalProperties": False,
|
|
58
70
|
"description": "Unique machine name, alphanumeric _ and - only.",
|
|
@@ -69,6 +81,8 @@ class Machine:
|
|
|
69
81
|
name: str,
|
|
70
82
|
authorizations_or: List[str],
|
|
71
83
|
unauthorized_warn_only: bool = False,
|
|
84
|
+
always_enabled: bool = False,
|
|
85
|
+
alias: Optional[str] = None,
|
|
72
86
|
):
|
|
73
87
|
"""Initialize a new MachineState instance."""
|
|
74
88
|
#: The name of the machine
|
|
@@ -78,6 +92,10 @@ class Machine:
|
|
|
78
92
|
#: Whether to allow anyone to operate machine regardless of
|
|
79
93
|
#: authorization, just logging/displaying a warning if unauthorized
|
|
80
94
|
self.unauthorized_warn_only: bool = unauthorized_warn_only
|
|
95
|
+
#: Whether machine is always enabled without RFID authentication
|
|
96
|
+
self.always_enabled: bool = always_enabled
|
|
97
|
+
#: Optional human-friendly alias for the machine
|
|
98
|
+
self.alias: Optional[str] = alias
|
|
81
99
|
#: state of the machine
|
|
82
100
|
self.state: "MachineState" = MachineState(self)
|
|
83
101
|
|
|
@@ -135,6 +153,11 @@ class Machine:
|
|
|
135
153
|
return
|
|
136
154
|
await slack.log_unoops(self, source)
|
|
137
155
|
|
|
156
|
+
@property
|
|
157
|
+
def display_name(self) -> str:
|
|
158
|
+
"""Return the display name for this machine (alias if present, else name)."""
|
|
159
|
+
return self.alias if self.alias else self.name
|
|
160
|
+
|
|
138
161
|
@property
|
|
139
162
|
def as_dict(self) -> Dict[str, Any]:
|
|
140
163
|
"""Return a dict representation of this machine."""
|
|
@@ -142,6 +165,8 @@ class Machine:
|
|
|
142
165
|
"name": self.name,
|
|
143
166
|
"authorizations_or": self.authorizations_or,
|
|
144
167
|
"unauthorized_warn_only": self.unauthorized_warn_only,
|
|
168
|
+
"always_enabled": self.always_enabled,
|
|
169
|
+
"alias": self.alias,
|
|
145
170
|
}
|
|
146
171
|
|
|
147
172
|
|
|
@@ -152,6 +177,7 @@ class MachinesConfig:
|
|
|
152
177
|
"""Initialize MachinesConfig."""
|
|
153
178
|
logger.debug("Initializing MachinesConfig")
|
|
154
179
|
self.machines_by_name: Dict[str, Machine] = {}
|
|
180
|
+
self.machines_by_alias: Dict[str, Machine] = {}
|
|
155
181
|
self.machines: List[Machine] = []
|
|
156
182
|
mdict: Dict[str, Any]
|
|
157
183
|
mname: str
|
|
@@ -159,8 +185,16 @@ class MachinesConfig:
|
|
|
159
185
|
mach: Machine = Machine(name=mname, **mdict)
|
|
160
186
|
self.machines.append(mach)
|
|
161
187
|
self.machines_by_name[mach.name] = mach
|
|
188
|
+
if mach.alias:
|
|
189
|
+
self.machines_by_alias[mach.alias] = mach
|
|
162
190
|
self.load_time: float = time()
|
|
163
191
|
|
|
192
|
+
def get_machine(self, name_or_alias: str) -> Optional[Machine]:
|
|
193
|
+
"""Get a machine by name or alias."""
|
|
194
|
+
return self.machines_by_name.get(name_or_alias) or self.machines_by_alias.get(
|
|
195
|
+
name_or_alias
|
|
196
|
+
)
|
|
197
|
+
|
|
164
198
|
def _load_and_validate_config(self) -> Dict[str, Dict[str, Any]]:
|
|
165
199
|
"""Load and validate the config file."""
|
|
166
200
|
config: Dict[str, Dict[str, Any]] = cast(
|
|
@@ -187,6 +221,8 @@ class MachineState:
|
|
|
187
221
|
|
|
188
222
|
LOCKOUT_DISPLAY_TEXT: str = "Down for\nmaintenance"
|
|
189
223
|
|
|
224
|
+
ALWAYS_ON_DISPLAY_TEXT: str = "Always On"
|
|
225
|
+
|
|
190
226
|
STATUS_LED_BRIGHTNESS: float = 0.5
|
|
191
227
|
|
|
192
228
|
def __init__(self, machine: Machine, load_state: bool = True):
|
|
@@ -290,29 +326,37 @@ class MachineState:
|
|
|
290
326
|
async def _handle_reboot(self) -> None:
|
|
291
327
|
"""Handle when the ESP32 (MCU) has rebooted since last checkin.
|
|
292
328
|
|
|
293
|
-
This logs out the current user if logged in and
|
|
294
|
-
|
|
329
|
+
This logs out the current user if logged in and resets the machine state.
|
|
330
|
+
For always-enabled machines, restores the always-on state.
|
|
295
331
|
"""
|
|
296
332
|
logging.getLogger("AUTH").warning(
|
|
297
|
-
"Machine %s rebooted; resetting relay and RFID state",
|
|
333
|
+
"Machine %s rebooted; resetting relay and RFID state",
|
|
334
|
+
self.machine.display_name,
|
|
298
335
|
)
|
|
299
336
|
# locking handled in update()
|
|
300
|
-
self.relay_desired_state = False
|
|
301
337
|
self.current_user = None
|
|
302
|
-
|
|
303
|
-
self.
|
|
304
|
-
|
|
338
|
+
# Restore always-enabled state if applicable
|
|
339
|
+
if self.machine.always_enabled:
|
|
340
|
+
self.relay_desired_state = True
|
|
341
|
+
self.display_text = self.ALWAYS_ON_DISPLAY_TEXT
|
|
342
|
+
self.status_led_rgb = (0.0, 1.0, 0.0)
|
|
343
|
+
self.status_led_brightness = self.STATUS_LED_BRIGHTNESS
|
|
344
|
+
else:
|
|
345
|
+
self.relay_desired_state = False
|
|
346
|
+
self.display_text = self.DEFAULT_DISPLAY_TEXT
|
|
347
|
+
self.status_led_rgb = (0.0, 0.0, 0.0)
|
|
348
|
+
self.status_led_brightness = 0.0
|
|
305
349
|
# log to Slack, if enabled
|
|
306
350
|
slack: Optional["SlackHandler"] = current_app.config.get("SLACK_HANDLER")
|
|
307
351
|
if not slack:
|
|
308
352
|
# Slack integration is not enabled
|
|
309
353
|
return
|
|
310
|
-
await slack.admin_log(f"Machine {self.machine.
|
|
354
|
+
await slack.admin_log(f"Machine {self.machine.display_name} has rebooted.")
|
|
311
355
|
|
|
312
356
|
def lockout(self) -> None:
|
|
313
357
|
"""Lock-out the machine."""
|
|
314
358
|
logging.getLogger("OOPS").warning(
|
|
315
|
-
"Machine %s was locked out for maintenance.", self.machine.
|
|
359
|
+
"Machine %s was locked out for maintenance.", self.machine.display_name
|
|
316
360
|
)
|
|
317
361
|
with self._lock:
|
|
318
362
|
self.is_locked_out = True
|
|
@@ -325,19 +369,29 @@ class MachineState:
|
|
|
325
369
|
def unlock(self) -> None:
|
|
326
370
|
"""Un-lock-out the machine."""
|
|
327
371
|
logging.getLogger("OOPS").warning(
|
|
328
|
-
"Machine %s was removed from maintenance lock-out.",
|
|
372
|
+
"Machine %s was removed from maintenance lock-out.",
|
|
373
|
+
self.machine.display_name,
|
|
329
374
|
)
|
|
330
375
|
with self._lock:
|
|
331
376
|
self.is_locked_out = False
|
|
332
|
-
self.relay_desired_state = False
|
|
333
377
|
self.current_user = None
|
|
334
|
-
|
|
335
|
-
self.
|
|
336
|
-
|
|
378
|
+
# Restore always-enabled state if applicable
|
|
379
|
+
if self.machine.always_enabled:
|
|
380
|
+
self.relay_desired_state = True
|
|
381
|
+
self.display_text = self.ALWAYS_ON_DISPLAY_TEXT
|
|
382
|
+
self.status_led_rgb = (0.0, 1.0, 0.0)
|
|
383
|
+
self.status_led_brightness = self.STATUS_LED_BRIGHTNESS
|
|
384
|
+
else:
|
|
385
|
+
self.relay_desired_state = False
|
|
386
|
+
self.display_text = self.DEFAULT_DISPLAY_TEXT
|
|
387
|
+
self.status_led_rgb = (0.0, 0.0, 0.0)
|
|
388
|
+
self.status_led_brightness = 0.0
|
|
337
389
|
|
|
338
390
|
def oops(self, do_locking: bool = True) -> None:
|
|
339
391
|
"""Oops the machine."""
|
|
340
|
-
logging.getLogger("OOPS").warning(
|
|
392
|
+
logging.getLogger("OOPS").warning(
|
|
393
|
+
"Machine %s was Oopsed.", self.machine.display_name
|
|
394
|
+
)
|
|
341
395
|
locker = self._lock if do_locking else nullcontext()
|
|
342
396
|
with locker:
|
|
343
397
|
self.is_oopsed = True
|
|
@@ -350,16 +404,23 @@ class MachineState:
|
|
|
350
404
|
def unoops(self, do_locking: bool = True) -> None:
|
|
351
405
|
"""Un-oops the machine."""
|
|
352
406
|
logging.getLogger("OOPS").warning(
|
|
353
|
-
"Machine %s was un-Oopsed.", self.machine.
|
|
407
|
+
"Machine %s was un-Oopsed.", self.machine.display_name
|
|
354
408
|
)
|
|
355
409
|
locker = self._lock if do_locking else nullcontext()
|
|
356
410
|
with locker:
|
|
357
411
|
self.is_oopsed = False
|
|
358
|
-
self.relay_desired_state = False
|
|
359
412
|
self.current_user = None
|
|
360
|
-
|
|
361
|
-
self.
|
|
362
|
-
|
|
413
|
+
# Restore always-enabled state if applicable
|
|
414
|
+
if self.machine.always_enabled:
|
|
415
|
+
self.relay_desired_state = True
|
|
416
|
+
self.display_text = self.ALWAYS_ON_DISPLAY_TEXT
|
|
417
|
+
self.status_led_rgb = (0.0, 1.0, 0.0)
|
|
418
|
+
self.status_led_brightness = self.STATUS_LED_BRIGHTNESS
|
|
419
|
+
else:
|
|
420
|
+
self.relay_desired_state = False
|
|
421
|
+
self.display_text = self.DEFAULT_DISPLAY_TEXT
|
|
422
|
+
self.status_led_rgb = (0.0, 0.0, 0.0)
|
|
423
|
+
self.status_led_brightness = 0
|
|
363
424
|
|
|
364
425
|
async def update(
|
|
365
426
|
self,
|
|
@@ -398,7 +459,21 @@ class MachineState:
|
|
|
398
459
|
if oops:
|
|
399
460
|
await self._handle_oops(users)
|
|
400
461
|
self.last_update = time()
|
|
401
|
-
|
|
462
|
+
# Handle always-enabled machines - track RFID but maintain always-on state
|
|
463
|
+
if (
|
|
464
|
+
self.machine.always_enabled
|
|
465
|
+
and not self.is_oopsed
|
|
466
|
+
and not self.is_locked_out
|
|
467
|
+
):
|
|
468
|
+
self.relay_desired_state = True
|
|
469
|
+
self.display_text = self.ALWAYS_ON_DISPLAY_TEXT
|
|
470
|
+
self.status_led_rgb = (0.0, 1.0, 0.0)
|
|
471
|
+
self.status_led_brightness = self.STATUS_LED_BRIGHTNESS
|
|
472
|
+
self.last_update = time()
|
|
473
|
+
# Track RFID changes for logging/auditing purposes
|
|
474
|
+
if rfid_value != self.rfid_value:
|
|
475
|
+
await self._handle_rfid_tracking_always_enabled(users, rfid_value)
|
|
476
|
+
elif rfid_value != self.rfid_value:
|
|
402
477
|
if rfid_value is None:
|
|
403
478
|
await self._handle_rfid_remove()
|
|
404
479
|
else:
|
|
@@ -417,7 +492,7 @@ class MachineState:
|
|
|
417
492
|
ustr = f" Current user is: {user.full_name}."
|
|
418
493
|
uname = user.full_name
|
|
419
494
|
logging.getLogger("OOPS").warning(
|
|
420
|
-
"Machine %s was Oopsed.%s", self.machine.
|
|
495
|
+
"Machine %s was Oopsed.%s", self.machine.display_name, ustr
|
|
421
496
|
)
|
|
422
497
|
# locking handled in update()
|
|
423
498
|
self.oops(do_locking=False)
|
|
@@ -437,12 +512,12 @@ class MachineState:
|
|
|
437
512
|
"""Handle RFID card removed."""
|
|
438
513
|
logging.getLogger("AUTH").info(
|
|
439
514
|
"RFID logout on %s by %s; session duration %d seconds",
|
|
440
|
-
self.machine.
|
|
515
|
+
self.machine.display_name,
|
|
441
516
|
self.current_user.full_name if self.current_user else self.rfid_value,
|
|
442
517
|
time() - cast(float, self.rfid_present_since),
|
|
443
518
|
)
|
|
444
519
|
log_str: str = (
|
|
445
|
-
f"RFID logout on {self.machine.
|
|
520
|
+
f"RFID logout on {self.machine.display_name} by "
|
|
446
521
|
+ (self.current_user.full_name if self.current_user else "unknown")
|
|
447
522
|
+ "; session duration "
|
|
448
523
|
+ naturaldelta(time() - cast(float, self.rfid_present_since))
|
|
@@ -473,13 +548,13 @@ class MachineState:
|
|
|
473
548
|
if not user:
|
|
474
549
|
logging.getLogger("AUTH").warning(
|
|
475
550
|
"RFID login attempt on %s by unknown fob %s",
|
|
476
|
-
self.machine.
|
|
551
|
+
self.machine.display_name,
|
|
477
552
|
rfid_value,
|
|
478
553
|
)
|
|
479
554
|
if self.is_oopsed or self.is_locked_out:
|
|
480
555
|
if slack:
|
|
481
556
|
await slack.admin_log(
|
|
482
|
-
f"RFID login attempt on {self.machine.
|
|
557
|
+
f"RFID login attempt on {self.machine.display_name} "
|
|
483
558
|
"by unknown fob when oopsed or locked out."
|
|
484
559
|
)
|
|
485
560
|
return
|
|
@@ -488,7 +563,7 @@ class MachineState:
|
|
|
488
563
|
self.status_led_brightness = self.STATUS_LED_BRIGHTNESS
|
|
489
564
|
if slack:
|
|
490
565
|
await slack.admin_log(
|
|
491
|
-
f"RFID login attempt on {self.machine.
|
|
566
|
+
f"RFID login attempt on {self.machine.display_name} by unknown fob"
|
|
492
567
|
)
|
|
493
568
|
return
|
|
494
569
|
# ok, we have a known user
|
|
@@ -496,26 +571,26 @@ class MachineState:
|
|
|
496
571
|
if self.is_oopsed:
|
|
497
572
|
logging.getLogger("AUTH").warning(
|
|
498
573
|
"RFID login attempt while oopsed on %s by %s",
|
|
499
|
-
self.machine.
|
|
574
|
+
self.machine.display_name,
|
|
500
575
|
logname,
|
|
501
576
|
)
|
|
502
577
|
# don't change anything
|
|
503
578
|
if slack:
|
|
504
579
|
await slack.admin_log(
|
|
505
|
-
f"RFID login attempt on {self.machine.
|
|
580
|
+
f"RFID login attempt on {self.machine.display_name} by "
|
|
506
581
|
f"{user.full_name} when oopsed."
|
|
507
582
|
)
|
|
508
583
|
return
|
|
509
584
|
if self.is_locked_out:
|
|
510
585
|
logging.getLogger("AUTH").warning(
|
|
511
586
|
"RFID login attempt while locked out on %s by %s",
|
|
512
|
-
self.machine.
|
|
587
|
+
self.machine.display_name,
|
|
513
588
|
logname,
|
|
514
589
|
)
|
|
515
590
|
# don't change anything
|
|
516
591
|
if slack:
|
|
517
592
|
await slack.admin_log(
|
|
518
|
-
f"RFID login attempt on {self.machine.
|
|
593
|
+
f"RFID login attempt on {self.machine.display_name} by "
|
|
519
594
|
f"{user.full_name} when machine locked-out."
|
|
520
595
|
)
|
|
521
596
|
return
|
|
@@ -524,7 +599,7 @@ class MachineState:
|
|
|
524
599
|
"User %s (%s) authorized for %s; session start",
|
|
525
600
|
user.full_name,
|
|
526
601
|
user.account_id,
|
|
527
|
-
self.machine.
|
|
602
|
+
self.machine.display_name,
|
|
528
603
|
)
|
|
529
604
|
self.current_user = user
|
|
530
605
|
self.relay_desired_state = True
|
|
@@ -533,7 +608,7 @@ class MachineState:
|
|
|
533
608
|
self.status_led_brightness = self.STATUS_LED_BRIGHTNESS
|
|
534
609
|
if slack:
|
|
535
610
|
await slack.admin_log(
|
|
536
|
-
f"RFID login on {self.machine.
|
|
611
|
+
f"RFID login on {self.machine.display_name} by authorized user "
|
|
537
612
|
f"{user.full_name}"
|
|
538
613
|
)
|
|
539
614
|
else:
|
|
@@ -541,7 +616,7 @@ class MachineState:
|
|
|
541
616
|
"User %s (%s) UNAUTHORIZED for %s",
|
|
542
617
|
user.full_name,
|
|
543
618
|
user.account_id,
|
|
544
|
-
self.machine.
|
|
619
|
+
self.machine.display_name,
|
|
545
620
|
)
|
|
546
621
|
self.relay_desired_state = False
|
|
547
622
|
self.display_text = "Unauthorized"
|
|
@@ -549,10 +624,56 @@ class MachineState:
|
|
|
549
624
|
self.status_led_brightness = self.STATUS_LED_BRIGHTNESS
|
|
550
625
|
if slack:
|
|
551
626
|
await slack.admin_log(
|
|
552
|
-
f"rejected RFID login on {self.machine.
|
|
627
|
+
f"rejected RFID login on {self.machine.display_name} by "
|
|
553
628
|
f"UNAUTHORIZED user {user.full_name}"
|
|
554
629
|
)
|
|
555
630
|
|
|
631
|
+
async def _handle_rfid_tracking_always_enabled(
|
|
632
|
+
self, users: UsersConfig, rfid_value: Optional[str]
|
|
633
|
+
) -> None:
|
|
634
|
+
"""Track RFID changes for always-enabled machines without changing state.
|
|
635
|
+
|
|
636
|
+
This method logs RFID insertions and removals for auditing purposes while
|
|
637
|
+
maintaining the always-on state of the machine.
|
|
638
|
+
"""
|
|
639
|
+
# locking handled in update()
|
|
640
|
+
if rfid_value is None:
|
|
641
|
+
# RFID removed
|
|
642
|
+
logging.getLogger("AUTH").info(
|
|
643
|
+
"RFID removed on always-enabled machine %s (was %s); session duration %d seconds",
|
|
644
|
+
self.machine.display_name,
|
|
645
|
+
self.current_user.full_name if self.current_user else self.rfid_value,
|
|
646
|
+
(
|
|
647
|
+
time() - cast(float, self.rfid_present_since)
|
|
648
|
+
if self.rfid_present_since
|
|
649
|
+
else 0
|
|
650
|
+
),
|
|
651
|
+
)
|
|
652
|
+
self.rfid_value = None
|
|
653
|
+
self.rfid_present_since = None
|
|
654
|
+
self.current_user = None
|
|
655
|
+
# State remains always-on (relay/display/LED not changed)
|
|
656
|
+
else:
|
|
657
|
+
# RFID inserted
|
|
658
|
+
self.rfid_present_since = time()
|
|
659
|
+
self.rfid_value = rfid_value
|
|
660
|
+
user: Optional[User] = users.users_by_fob.get(rfid_value)
|
|
661
|
+
if user:
|
|
662
|
+
self.current_user = user
|
|
663
|
+
logging.getLogger("AUTH").info(
|
|
664
|
+
"RFID inserted on always-enabled machine %s by %s (%s)",
|
|
665
|
+
self.machine.display_name,
|
|
666
|
+
user.full_name,
|
|
667
|
+
rfid_value,
|
|
668
|
+
)
|
|
669
|
+
else:
|
|
670
|
+
logging.getLogger("AUTH").warning(
|
|
671
|
+
"RFID inserted on always-enabled machine %s by unknown fob %s",
|
|
672
|
+
self.machine.display_name,
|
|
673
|
+
rfid_value,
|
|
674
|
+
)
|
|
675
|
+
# State remains always-on (relay/display/LED not changed)
|
|
676
|
+
|
|
556
677
|
async def _user_is_authorized(
|
|
557
678
|
self, user: User, slack: Optional["SlackHandler"] = None
|
|
558
679
|
) -> bool:
|
|
@@ -563,7 +684,7 @@ class MachineState:
|
|
|
563
684
|
"User %s (%s) authorized for %s based on %s",
|
|
564
685
|
user.full_name,
|
|
565
686
|
user.account_id,
|
|
566
|
-
self.machine.
|
|
687
|
+
self.machine.display_name,
|
|
567
688
|
auth,
|
|
568
689
|
)
|
|
569
690
|
return True
|
|
@@ -573,12 +694,12 @@ class MachineState:
|
|
|
573
694
|
"unauthorized_warn_only==True",
|
|
574
695
|
user.full_name,
|
|
575
696
|
user.account_id,
|
|
576
|
-
self.machine.
|
|
697
|
+
self.machine.display_name,
|
|
577
698
|
)
|
|
578
699
|
if slack:
|
|
579
700
|
await slack.admin_log(
|
|
580
701
|
f"WARNING - Authorizing user {user.full_name} for "
|
|
581
|
-
f"{self.machine.
|
|
702
|
+
f"{self.machine.display_name} based on unauthorized_warn_only "
|
|
582
703
|
"setting for machine. User is NOT authorized for this "
|
|
583
704
|
"machine."
|
|
584
705
|
)
|
|
@@ -160,11 +160,11 @@ class SlackHandler:
|
|
|
160
160
|
msg.channel_id,
|
|
161
161
|
)
|
|
162
162
|
return None
|
|
163
|
-
if msg.command[0] == "oops" and len(msg.command)
|
|
163
|
+
if msg.command[0] == "oops" and len(msg.command) >= 2:
|
|
164
164
|
return await self.oops(msg, say)
|
|
165
|
-
elif msg.command[0] == "lock" and len(msg.command)
|
|
165
|
+
elif msg.command[0] == "lock" and len(msg.command) >= 2:
|
|
166
166
|
return await self.lock(msg, say)
|
|
167
|
-
elif msg.command[0] == "clear" and len(msg.command)
|
|
167
|
+
elif msg.command[0] == "clear" and len(msg.command) >= 2:
|
|
168
168
|
return await self.clear(msg, say)
|
|
169
169
|
await say(self.HELP_RESPONSE)
|
|
170
170
|
|
|
@@ -175,7 +175,7 @@ class SlackHandler:
|
|
|
175
175
|
mname: str
|
|
176
176
|
mach: Machine
|
|
177
177
|
for mname, mach in sorted(mconf.machines_by_name.items()):
|
|
178
|
-
resp +=
|
|
178
|
+
resp += mach.display_name + ": "
|
|
179
179
|
if mach.state.is_oopsed or mach.state.is_locked_out:
|
|
180
180
|
if mach.state.is_oopsed:
|
|
181
181
|
resp += "Oopsed "
|
|
@@ -199,44 +199,44 @@ class SlackHandler:
|
|
|
199
199
|
|
|
200
200
|
async def oops(self, msg: Message, say: AsyncSay) -> None:
|
|
201
201
|
"""Set oops status on a machine."""
|
|
202
|
-
mname: str = msg.command[1]
|
|
202
|
+
mname: str = " ".join(msg.command[1:])
|
|
203
203
|
mconf: MachinesConfig = self.quart.config["MACHINES"]
|
|
204
|
-
mach: Optional[Machine] = mconf.
|
|
204
|
+
mach: Optional[Machine] = mconf.get_machine(mname)
|
|
205
205
|
if not mach:
|
|
206
206
|
await say(
|
|
207
|
-
f"Invalid machine name '{mname}'. Use status command to "
|
|
207
|
+
f"Invalid machine name or alias '{mname}'. Use status command to "
|
|
208
208
|
f"list all machines."
|
|
209
209
|
)
|
|
210
210
|
return
|
|
211
211
|
if mach.state.is_oopsed:
|
|
212
|
-
await say(f"Machine {
|
|
212
|
+
await say(f"Machine {mach.display_name} is already oopsed.")
|
|
213
213
|
return
|
|
214
214
|
await mach.oops(slack=self)
|
|
215
215
|
|
|
216
216
|
async def lock(self, msg: Message, say: AsyncSay) -> None:
|
|
217
217
|
"""Set lock status on a machine."""
|
|
218
|
-
mname: str = msg.command[1]
|
|
218
|
+
mname: str = " ".join(msg.command[1:])
|
|
219
219
|
mconf: MachinesConfig = self.quart.config["MACHINES"]
|
|
220
|
-
mach: Optional[Machine] = mconf.
|
|
220
|
+
mach: Optional[Machine] = mconf.get_machine(mname)
|
|
221
221
|
if not mach:
|
|
222
222
|
await say(
|
|
223
|
-
f"Invalid machine name '{mname}'. Use status command to "
|
|
223
|
+
f"Invalid machine name or alias '{mname}'. Use status command to "
|
|
224
224
|
f"list all machines."
|
|
225
225
|
)
|
|
226
226
|
return
|
|
227
227
|
if mach.state.is_locked_out:
|
|
228
|
-
await say(f"Machine {
|
|
228
|
+
await say(f"Machine {mach.display_name} is already locked-out.")
|
|
229
229
|
return
|
|
230
230
|
await mach.lockout(slack=self)
|
|
231
231
|
|
|
232
232
|
async def clear(self, msg: Message, say: AsyncSay) -> None:
|
|
233
233
|
"""Clear oops and lock status on a machine."""
|
|
234
|
-
mname: str = msg.command[1]
|
|
234
|
+
mname: str = " ".join(msg.command[1:])
|
|
235
235
|
mconf: MachinesConfig = self.quart.config["MACHINES"]
|
|
236
|
-
mach: Optional[Machine] = mconf.
|
|
236
|
+
mach: Optional[Machine] = mconf.get_machine(mname)
|
|
237
237
|
if not mach:
|
|
238
238
|
await say(
|
|
239
|
-
f"Invalid machine name '{mname}'. Use status command to "
|
|
239
|
+
f"Invalid machine name or alias '{mname}'. Use status command to "
|
|
240
240
|
f"list all machines."
|
|
241
241
|
)
|
|
242
242
|
return
|
|
@@ -248,7 +248,7 @@ class SlackHandler:
|
|
|
248
248
|
await mach.unlock(slack=self)
|
|
249
249
|
acted = True
|
|
250
250
|
if not acted:
|
|
251
|
-
await say(f"Machine {
|
|
251
|
+
await say(f"Machine {mach.display_name} is not oopsed or locked-out.")
|
|
252
252
|
|
|
253
253
|
async def log_unoops(self, machine: Machine, source: str) -> None:
|
|
254
254
|
"""
|
|
@@ -262,13 +262,13 @@ class SlackHandler:
|
|
|
262
262
|
create_task(
|
|
263
263
|
self.app.client.chat_postMessage(
|
|
264
264
|
channel=self.control_channel_id,
|
|
265
|
-
text=f"Machine {machine.
|
|
265
|
+
text=f"Machine {machine.display_name} un-oopsed via {source}.",
|
|
266
266
|
)
|
|
267
267
|
)
|
|
268
268
|
create_task(
|
|
269
269
|
self.app.client.chat_postMessage(
|
|
270
270
|
channel=self.oops_channel_id,
|
|
271
|
-
text=f"Machine {machine.
|
|
271
|
+
text=f"Machine {machine.display_name} oops has been cleared.",
|
|
272
272
|
)
|
|
273
273
|
)
|
|
274
274
|
|
|
@@ -286,13 +286,13 @@ class SlackHandler:
|
|
|
286
286
|
create_task(
|
|
287
287
|
self.app.client.chat_postMessage(
|
|
288
288
|
channel=self.control_channel_id,
|
|
289
|
-
text=f"Machine {machine.
|
|
289
|
+
text=f"Machine {machine.display_name} oopsed via {source} by {user_name}.",
|
|
290
290
|
)
|
|
291
291
|
)
|
|
292
292
|
create_task(
|
|
293
293
|
self.app.client.chat_postMessage(
|
|
294
294
|
channel=self.oops_channel_id,
|
|
295
|
-
text=f"Machine {machine.
|
|
295
|
+
text=f"Machine {machine.display_name} has been Oops'ed!",
|
|
296
296
|
)
|
|
297
297
|
)
|
|
298
298
|
|
|
@@ -308,13 +308,13 @@ class SlackHandler:
|
|
|
308
308
|
create_task(
|
|
309
309
|
self.app.client.chat_postMessage(
|
|
310
310
|
channel=self.control_channel_id,
|
|
311
|
-
text=f"Machine {machine.
|
|
311
|
+
text=f"Machine {machine.display_name} locked-out cleared via {source}.",
|
|
312
312
|
)
|
|
313
313
|
)
|
|
314
314
|
create_task(
|
|
315
315
|
self.app.client.chat_postMessage(
|
|
316
316
|
channel=self.oops_channel_id,
|
|
317
|
-
text=f"Machine {machine.
|
|
317
|
+
text=f"Machine {machine.display_name} is no longer locked-out for "
|
|
318
318
|
f"maintenance.",
|
|
319
319
|
)
|
|
320
320
|
)
|
|
@@ -331,13 +331,13 @@ class SlackHandler:
|
|
|
331
331
|
create_task(
|
|
332
332
|
self.app.client.chat_postMessage(
|
|
333
333
|
channel=self.control_channel_id,
|
|
334
|
-
text=f"Machine {machine.
|
|
334
|
+
text=f"Machine {machine.display_name} locked-out via {source}.",
|
|
335
335
|
)
|
|
336
336
|
)
|
|
337
337
|
create_task(
|
|
338
338
|
self.app.client.chat_postMessage(
|
|
339
339
|
channel=self.oops_channel_id,
|
|
340
|
-
text=f"Machine {machine.
|
|
340
|
+
text=f"Machine {machine.display_name} is locked-out for maintenance.",
|
|
341
341
|
)
|
|
342
342
|
)
|
|
343
343
|
|
{machine_access_control-0.2.3 → machine_access_control-0.4.0}/src/dm_mac/views/prometheus.py
RENAMED
|
@@ -134,52 +134,35 @@ class PromCustomCollector:
|
|
|
134
134
|
)
|
|
135
135
|
m: Machine
|
|
136
136
|
for m in mconf.machines:
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
)
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
)
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
{"machine_name": m.name}, 1 if m.state.rfid_value else 0
|
|
153
|
-
)
|
|
154
|
-
m_rfid_present_since.add_metric(
|
|
155
|
-
{"machine_name": m.name}, m.state.rfid_present_since or 0
|
|
156
|
-
)
|
|
157
|
-
current_amps.add_metric({"machine_name": m.name}, m.state.current_amps)
|
|
158
|
-
m_user.add_metric(
|
|
159
|
-
{"machine_name": m.name}, 1 if m.state.current_user else 0
|
|
160
|
-
)
|
|
161
|
-
uptime.add_metric({"machine_name": m.name}, m.state.uptime)
|
|
162
|
-
wifi_db.add_metric({"machine_name": m.name}, m.state.wifi_signal_db or 0)
|
|
163
|
-
wifi_percent.add_metric(
|
|
164
|
-
{"machine_name": m.name}, m.state.wifi_signal_percent or 0
|
|
165
|
-
)
|
|
166
|
-
temp_c.add_metric(
|
|
167
|
-
{"machine_name": m.name}, m.state.internal_temperature_c or 0
|
|
168
|
-
)
|
|
137
|
+
labels = {"machine_name": m.name, "display_name": m.display_name}
|
|
138
|
+
relay_state.add_metric(labels, 1 if m.state.relay_desired_state else 0)
|
|
139
|
+
oops_state.add_metric(labels, 1 if m.state.is_oopsed else 0)
|
|
140
|
+
lockout_state.add_metric(labels, 1 if m.state.is_locked_out else 0)
|
|
141
|
+
unauth_state.add_metric(labels, 1 if m.unauthorized_warn_only else 0)
|
|
142
|
+
m_checkin.add_metric(labels, m.state.last_checkin or 0)
|
|
143
|
+
m_update.add_metric(labels, m.state.last_update or 0)
|
|
144
|
+
m_rfid_present.add_metric(labels, 1 if m.state.rfid_value else 0)
|
|
145
|
+
m_rfid_present_since.add_metric(labels, m.state.rfid_present_since or 0)
|
|
146
|
+
current_amps.add_metric(labels, m.state.current_amps)
|
|
147
|
+
m_user.add_metric(labels, 1 if m.state.current_user else 0)
|
|
148
|
+
uptime.add_metric(labels, m.state.uptime)
|
|
149
|
+
wifi_db.add_metric(labels, m.state.wifi_signal_db or 0)
|
|
150
|
+
wifi_percent.add_metric(labels, m.state.wifi_signal_percent or 0)
|
|
151
|
+
temp_c.add_metric(labels, m.state.internal_temperature_c or 0)
|
|
169
152
|
led.add_metric(
|
|
170
|
-
{
|
|
153
|
+
{**labels, "led_attribute": "red"},
|
|
171
154
|
m.state.status_led_rgb[0],
|
|
172
155
|
)
|
|
173
156
|
led.add_metric(
|
|
174
|
-
{
|
|
157
|
+
{**labels, "led_attribute": "green"},
|
|
175
158
|
m.state.status_led_rgb[1],
|
|
176
159
|
)
|
|
177
160
|
led.add_metric(
|
|
178
|
-
{
|
|
161
|
+
{**labels, "led_attribute": "blue"},
|
|
179
162
|
m.state.status_led_rgb[2],
|
|
180
163
|
)
|
|
181
164
|
led.add_metric(
|
|
182
|
-
{
|
|
165
|
+
{**labels, "led_attribute": "brightness"},
|
|
183
166
|
m.state.status_led_brightness,
|
|
184
167
|
)
|
|
185
168
|
yield mconf_load
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|