lockbot 2.9.0__tar.gz → 2.9.2__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.
- {lockbot-2.9.0/python/lockbot.egg-info → lockbot-2.9.2}/PKG-INFO +3 -1
- {lockbot-2.9.0 → lockbot-2.9.2}/README.md +2 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/pyproject.toml +1 -1
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/backend/app/bots/schemas.py +4 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/core/base_bot.py +68 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/core/device_bot.py +4 -1
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/core/handler.py +2 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/core/i18n/en.py +6 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/core/i18n/zh.py +6 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/core/io.py +2 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/core/node_bot.py +7 -2
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/core/queue_bot.py +10 -4
- {lockbot-2.9.0 → lockbot-2.9.2/python/lockbot.egg-info}/PKG-INFO +3 -1
- {lockbot-2.9.0 → lockbot-2.9.2}/LICENSE +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/MANIFEST.in +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/__init__.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/backend/__init__.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/backend/app/__init__.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/backend/app/admin/__init__.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/backend/app/admin/router.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/backend/app/audit/__init__.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/backend/app/audit/models.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/backend/app/audit/router.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/backend/app/audit/service.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/backend/app/auth/__init__.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/backend/app/auth/dependencies.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/backend/app/auth/models.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/backend/app/auth/router.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/backend/app/auth/schemas.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/backend/app/backup/__init__.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/backend/app/backup/router.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/backend/app/backup/scheduler.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/backend/app/backup/service.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/backend/app/bots/__init__.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/backend/app/bots/encryption.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/backend/app/bots/manager.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/backend/app/bots/models.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/backend/app/bots/router.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/backend/app/bots/webhook_handler.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/backend/app/config.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/backend/app/database.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/backend/app/logs/__init__.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/backend/app/main.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/backend/app/rate_limit.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/backend/app/settings/__init__.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/backend/app/settings/models.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/backend/app/settings/router.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/core/__init__.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/core/bot_instance.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/core/config.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/core/device_usage_alert.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/core/device_usage_utils.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/core/entry.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/core/env.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/core/i18n/__init__.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/core/message_adapter.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/core/msg_utils.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/core/platforms/__init__.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/core/platforms/infoflow.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/core/request.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/core/scheduler.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot/core/utils.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot.egg-info/SOURCES.txt +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot.egg-info/dependency_links.txt +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot.egg-info/requires.txt +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/python/lockbot.egg-info/top_level.txt +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/setup.cfg +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/tools/create_super_admin.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/tools/gen_keys.py +0 -0
- {lockbot-2.9.0 → lockbot-2.9.2}/tools/reset_super_admin_password.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lockbot
|
|
3
|
-
Version: 2.9.
|
|
3
|
+
Version: 2.9.2
|
|
4
4
|
Summary: Cluster resource management bot for IM platforms
|
|
5
5
|
Author-email: Jianbang Yang <yangjianbang112@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -53,6 +53,8 @@ Supports both standalone Flask deployment and a full platform mode with FastAPI
|
|
|
53
53
|
[](https://pypi.org/project/lockbot/)
|
|
54
54
|
[](https://github.com/DynamicHeart/lockbot/pkgs/container/lockbot)
|
|
55
55
|
|
|
56
|
+
<img src="./docs/demo.gif" width="600" />
|
|
57
|
+
|
|
56
58
|
## Features
|
|
57
59
|
|
|
58
60
|
- **Device Lock Bot** — Lock/unlock individual GPUs or devices on a cluster
|
|
@@ -10,6 +10,8 @@ Supports both standalone Flask deployment and a full platform mode with FastAPI
|
|
|
10
10
|
[](https://pypi.org/project/lockbot/)
|
|
11
11
|
[](https://github.com/DynamicHeart/lockbot/pkgs/container/lockbot)
|
|
12
12
|
|
|
13
|
+
<img src="./docs/demo.gif" width="600" />
|
|
14
|
+
|
|
13
15
|
## Features
|
|
14
16
|
|
|
15
17
|
- **Device Lock Bot** — Lock/unlock individual GPUs or devices on a cluster
|
|
@@ -68,6 +68,10 @@ def _validate_config_overrides(v: dict | None) -> dict | None:
|
|
|
68
68
|
if show_alias is not None and not isinstance(show_alias, bool):
|
|
69
69
|
errors.append("SHOW_NODE_ALIAS must be a boolean")
|
|
70
70
|
|
|
71
|
+
allow_alias = v.get("ALLOW_USER_ALIAS")
|
|
72
|
+
if allow_alias is not None and not isinstance(allow_alias, bool):
|
|
73
|
+
errors.append("ALLOW_USER_ALIAS must be a boolean")
|
|
74
|
+
|
|
71
75
|
max_dur = v.get("MAX_LOCK_DURATION")
|
|
72
76
|
default_dur = v.get("DEFAULT_DURATION")
|
|
73
77
|
if isinstance(max_dur, int) and isinstance(default_dur, int) and max_dur != -1 and default_dur > max_dur:
|
|
@@ -133,6 +133,74 @@ class BaseLockBot:
|
|
|
133
133
|
alias = aliases.get(node_key)
|
|
134
134
|
return f"{node_key}「{alias}」" if alias else node_key
|
|
135
135
|
|
|
136
|
+
def alias(self, user_id, command):
|
|
137
|
+
"""Set or clear a node alias. Usage: alias <node_key> [alias_value]"""
|
|
138
|
+
if not self.config.get_val("ALLOW_USER_ALIAS"):
|
|
139
|
+
return self.show_error(user_id, t("alias.disabled", config=self.config))
|
|
140
|
+
|
|
141
|
+
import re
|
|
142
|
+
|
|
143
|
+
m = re.match(r"^\s*alias\s+([\w\d_-]+)(?:\s+(.{1,15}))?\s*$", command)
|
|
144
|
+
if not m:
|
|
145
|
+
return self.show_error(user_id, t("alias.usage", config=self.config))
|
|
146
|
+
|
|
147
|
+
node_key = m.group(1)
|
|
148
|
+
alias_val = (m.group(2) or "").strip()
|
|
149
|
+
|
|
150
|
+
cluster_configs = self.config.get_val("CLUSTER_CONFIGS")
|
|
151
|
+
if node_key not in cluster_configs:
|
|
152
|
+
return self.show_error(
|
|
153
|
+
user_id,
|
|
154
|
+
t(
|
|
155
|
+
"alias.invalid_node",
|
|
156
|
+
config=self.config,
|
|
157
|
+
node_key=node_key,
|
|
158
|
+
valid_keys=str(list(cluster_configs.keys())),
|
|
159
|
+
),
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
aliases = self.config.get_val("NODE_ALIASES") or {}
|
|
163
|
+
if alias_val:
|
|
164
|
+
aliases[node_key] = alias_val
|
|
165
|
+
else:
|
|
166
|
+
aliases.pop(node_key, None)
|
|
167
|
+
self.config.set_val("NODE_ALIASES", aliases)
|
|
168
|
+
|
|
169
|
+
# Persist to DB if running under platform mode
|
|
170
|
+
self._persist_config_override("NODE_ALIASES", aliases)
|
|
171
|
+
|
|
172
|
+
if alias_val:
|
|
173
|
+
self.logger.info("user=%s set alias %s -> %s", user_id, node_key, alias_val)
|
|
174
|
+
msg = t("alias.set_ok", config=self.config, node_key=node_key, alias=alias_val)
|
|
175
|
+
else:
|
|
176
|
+
self.logger.info("user=%s cleared alias %s", user_id, node_key)
|
|
177
|
+
msg = t("alias.clear_ok", config=self.config, node_key=node_key)
|
|
178
|
+
return self.adapter.build_reply(msg, [user_id])
|
|
179
|
+
|
|
180
|
+
def _persist_config_override(self, key, value):
|
|
181
|
+
"""Persist a single config_override key to DB (best-effort)."""
|
|
182
|
+
try:
|
|
183
|
+
import json
|
|
184
|
+
|
|
185
|
+
from lockbot.backend.app.bots.models import Bot
|
|
186
|
+
from lockbot.backend.app.database import SessionLocal
|
|
187
|
+
|
|
188
|
+
bot_id = self.config.get_val("BOT_ID")
|
|
189
|
+
if not bot_id:
|
|
190
|
+
return
|
|
191
|
+
db = SessionLocal()
|
|
192
|
+
try:
|
|
193
|
+
bot = db.query(Bot).filter(Bot.id == int(bot_id)).first()
|
|
194
|
+
if bot:
|
|
195
|
+
overrides = json.loads(bot.config_overrides) if bot.config_overrides else {}
|
|
196
|
+
overrides[key] = value
|
|
197
|
+
bot.config_overrides = json.dumps(overrides, ensure_ascii=False)
|
|
198
|
+
db.commit()
|
|
199
|
+
finally:
|
|
200
|
+
db.close()
|
|
201
|
+
except Exception:
|
|
202
|
+
pass
|
|
203
|
+
|
|
136
204
|
def _whitelist_hint(self):
|
|
137
205
|
"""Return whitelist hint string if BOT_OWNER is configured, else empty string."""
|
|
138
206
|
owner = self.config.get_val("BOT_OWNER")
|
|
@@ -39,7 +39,7 @@ class DeviceBot(BaseLockBot):
|
|
|
39
39
|
_loader = staticmethod(create_or_load_device_state)
|
|
40
40
|
|
|
41
41
|
def supported_commands(self):
|
|
42
|
-
return ["lock", "slock", "unlock", "free", "kickout", "help", "h", "query"]
|
|
42
|
+
return ["lock", "slock", "unlock", "free", "kickout", "help", "h", "query", "alias"]
|
|
43
43
|
|
|
44
44
|
def parse_command(self, user_id, command_key, command, parsing_duration=False):
|
|
45
45
|
"""
|
|
@@ -493,6 +493,9 @@ class DeviceBot(BaseLockBot):
|
|
|
493
493
|
text += t("help.query_node_example", config=self.config, node=example_node)
|
|
494
494
|
text += "\n"
|
|
495
495
|
|
|
496
|
+
if self.config.get_val("ALLOW_USER_ALIAS"):
|
|
497
|
+
text += f"6. 备注: alias {example_node} <备注>\n\n"
|
|
498
|
+
|
|
496
499
|
return text
|
|
497
500
|
|
|
498
501
|
def _check_and_notify(self) -> float | None:
|
|
@@ -55,6 +55,8 @@ def execute_command(msg_data, bot):
|
|
|
55
55
|
return bot.query(user_id)
|
|
56
56
|
elif cmd in ("help", "h"):
|
|
57
57
|
return bot.print_help(user_id)
|
|
58
|
+
elif cmd == "alias":
|
|
59
|
+
return bot.alias(user_id, rcv_info)
|
|
58
60
|
else:
|
|
59
61
|
return bot.print_help(user_id, t("error.unknown_error", config=getattr(bot, "config", None), command=rcv_info))
|
|
60
62
|
return bot.print_help(user_id, t("error.unknown_error", config=getattr(bot, "config", None), command=rcv_info))
|
|
@@ -173,6 +173,12 @@ MESSAGES = {
|
|
|
173
173
|
"opkick.invalid_format": "Invalid opkick format. Usage: opkick <username> [node] [dev x,y]",
|
|
174
174
|
"opkick.notify_kicked": "⚠️ [Operator Release]\n\nNode: {node_key}\n",
|
|
175
175
|
"opkick.notify_kicked_reason": "Reason: {reason}\n\n",
|
|
176
|
+
# ── Alias command ──
|
|
177
|
+
"alias.usage": "Usage: alias <node_key> [label]\nExample: alias node0 10.10.10.56\nClear: alias node0",
|
|
178
|
+
"alias.invalid_node": "Node {node_key} does not exist. Available: {valid_keys}\n",
|
|
179
|
+
"alias.set_ok": "✅ Set {node_key} alias to「{alias}」\n\n",
|
|
180
|
+
"alias.clear_ok": "✅ Cleared alias for {node_key}\n\n",
|
|
181
|
+
"alias.disabled": "User alias command is not enabled for this bot",
|
|
176
182
|
# ── Webhook: bot not running ──
|
|
177
183
|
"webhook.bot_not_running": "⚠️ Bot {bot_name}(ID:{bot_id}) is not running. " # noqa: E501
|
|
178
184
|
"Please contact the owner @{owner_username} to start it.",
|
|
@@ -166,6 +166,12 @@ MESSAGES = {
|
|
|
166
166
|
"opkick.invalid_format": "opkick命令格式错误,正确格式: opkick <username> [node] [dev x,y]",
|
|
167
167
|
"opkick.notify_kicked": "⚠️【运维释放】\n\n节点: {node_key}\n",
|
|
168
168
|
"opkick.notify_kicked_reason": "原因: {reason}\n\n",
|
|
169
|
+
# ── Alias command ──
|
|
170
|
+
"alias.usage": "用法: alias <节点名> [备注]\n示例: alias node0 10.10.10.56\n清除: alias node0",
|
|
171
|
+
"alias.invalid_node": "节点 {node_key} 不存在,可用节点: {valid_keys}\n",
|
|
172
|
+
"alias.set_ok": "✅ 已设置 {node_key} 备注为「{alias}」\n\n",
|
|
173
|
+
"alias.clear_ok": "✅ 已清除 {node_key} 的备注\n\n",
|
|
174
|
+
"alias.disabled": "该机器人未开启用户自助设置备注功能",
|
|
169
175
|
# ── Webhook: bot not running ──
|
|
170
176
|
"webhook.bot_not_running": (
|
|
171
177
|
"⚠️ 机器人 {bot_name}(ID:{bot_id}) 尚未启动,请联系管理人 @{owner_username} 启动后再使用。"
|
|
@@ -154,6 +154,8 @@ def create_or_load_device_state(config=None):
|
|
|
154
154
|
clamped = apply_max_duration_limit(device_status["current_users"], max_duration, duration_whitelist)
|
|
155
155
|
clamped_user_ids.update(clamped)
|
|
156
156
|
default_device.update(**device_status)
|
|
157
|
+
# Always use config's dev_model as source of truth
|
|
158
|
+
default_device["dev_model"] = cluster_configs[node_key][dev_id]
|
|
157
159
|
|
|
158
160
|
if clamped_user_ids:
|
|
159
161
|
save_bot_state_to_file(default_state, config)
|
|
@@ -37,7 +37,7 @@ class NodeBot(BaseLockBot):
|
|
|
37
37
|
_loader = staticmethod(create_or_load_node_state)
|
|
38
38
|
|
|
39
39
|
def supported_commands(self):
|
|
40
|
-
return ["lock", "slock", "unlock", "free", "kickout", "help", "h", "query"]
|
|
40
|
+
return ["lock", "slock", "unlock", "free", "kickout", "help", "h", "query", "alias"]
|
|
41
41
|
|
|
42
42
|
def parse_command(self, user_id, command_key, command, parsing_duration=False):
|
|
43
43
|
"""
|
|
@@ -358,7 +358,12 @@ class NodeBot(BaseLockBot):
|
|
|
358
358
|
reply_info += t("help.section4_title", config=self.config)
|
|
359
359
|
reply_info += t("help.section5_title", config=self.config)
|
|
360
360
|
reply_info += t("help.query_at_bot", config=self.config)
|
|
361
|
-
reply_info += f" {example_node0}\n
|
|
361
|
+
reply_info += f" {example_node0}\n"
|
|
362
|
+
|
|
363
|
+
if self.config.get_val("ALLOW_USER_ALIAS"):
|
|
364
|
+
reply_info += f"6. 备注: alias {example_node0} <备注>\n"
|
|
365
|
+
|
|
366
|
+
reply_info += "\n"
|
|
362
367
|
return reply_info
|
|
363
368
|
|
|
364
369
|
def _check_and_notify(self) -> float | None:
|
|
@@ -26,7 +26,7 @@ class QueueBot(NodeBot):
|
|
|
26
26
|
"""
|
|
27
27
|
|
|
28
28
|
def supported_commands(self):
|
|
29
|
-
return ["lock", "unlock", "free", "kickout", "kicklock", "help", "h", "book", "take", "query"]
|
|
29
|
+
return ["lock", "unlock", "free", "kickout", "kicklock", "help", "h", "book", "take", "query", "alias"]
|
|
30
30
|
|
|
31
31
|
def lock(self, user_id, command):
|
|
32
32
|
"""
|
|
@@ -345,7 +345,12 @@ class QueueBot(NodeBot):
|
|
|
345
345
|
parts.append(t("help.section_help_title_queue", config=self.config))
|
|
346
346
|
parts.append(t("help.section_query_title_queue", config=self.config))
|
|
347
347
|
parts.append(t("help.query_at_bot", config=self.config))
|
|
348
|
-
parts.append(f" {example_node0}\n
|
|
348
|
+
parts.append(f" {example_node0}\n")
|
|
349
|
+
|
|
350
|
+
if self.config.get_val("ALLOW_USER_ALIAS"):
|
|
351
|
+
parts.append(f"8. 备注: alias {example_node0} <备注>\n")
|
|
352
|
+
|
|
353
|
+
parts.append("\n")
|
|
349
354
|
return "".join(parts)
|
|
350
355
|
|
|
351
356
|
def _check_and_notify(self) -> float | None:
|
|
@@ -522,11 +527,12 @@ class QueueBot(NodeBot):
|
|
|
522
527
|
usage_info = ""
|
|
523
528
|
for node_key, node_status in nodes.items():
|
|
524
529
|
if node_filter is None or node_key == node_filter:
|
|
530
|
+
display_name = self._display_node(node_key)
|
|
525
531
|
if node_status["status"] == "idle":
|
|
526
|
-
usage_info += "{:} {}\n".format(
|
|
532
|
+
usage_info += "{:} {}\n".format(display_name, t("status.idle", config=self.config))
|
|
527
533
|
else:
|
|
528
534
|
for user_idx, user_info in enumerate(node_status["current_users"]):
|
|
529
|
-
node_name = f"{
|
|
535
|
+
node_name = f"{display_name}" if user_idx == 0 else ""
|
|
530
536
|
uid = user_info["user_id"]
|
|
531
537
|
duration = format_duration(
|
|
532
538
|
remaining_duration(user_info["start_time"], user_info["duration"]),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lockbot
|
|
3
|
-
Version: 2.9.
|
|
3
|
+
Version: 2.9.2
|
|
4
4
|
Summary: Cluster resource management bot for IM platforms
|
|
5
5
|
Author-email: Jianbang Yang <yangjianbang112@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -53,6 +53,8 @@ Supports both standalone Flask deployment and a full platform mode with FastAPI
|
|
|
53
53
|
[](https://pypi.org/project/lockbot/)
|
|
54
54
|
[](https://github.com/DynamicHeart/lockbot/pkgs/container/lockbot)
|
|
55
55
|
|
|
56
|
+
<img src="./docs/demo.gif" width="600" />
|
|
57
|
+
|
|
56
58
|
## Features
|
|
57
59
|
|
|
58
60
|
- **Device Lock Bot** — Lock/unlock individual GPUs or devices on a cluster
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|