calagoapi 1.0.dev1__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.
Files changed (71) hide show
  1. calagoapi/__init__.py +190 -0
  2. calagoapi/account/__init__.py +14 -0
  3. calagoapi/account/account.py +292 -0
  4. calagoapi/account/auth.py +72 -0
  5. calagoapi/account/models.py +221 -0
  6. calagoapi/admin/__init__.py +48 -0
  7. calagoapi/admin/admin.py +341 -0
  8. calagoapi/admin/managers.py +490 -0
  9. calagoapi/admin/resources.py +970 -0
  10. calagoapi/aio/__init__.py +9 -0
  11. calagoapi/aio/console.py +289 -0
  12. calagoapi/aio/panel.py +176 -0
  13. calagoapi/api/__init__.py +63 -0
  14. calagoapi/api/account.py +195 -0
  15. calagoapi/api/admin.py +402 -0
  16. calagoapi/api/attachments.py +27 -0
  17. calagoapi/api/auth.py +52 -0
  18. calagoapi/api/backups.py +99 -0
  19. calagoapi/api/client.py +198 -0
  20. calagoapi/api/databases.py +217 -0
  21. calagoapi/api/files.py +210 -0
  22. calagoapi/api/network.py +84 -0
  23. calagoapi/api/public.py +13 -0
  24. calagoapi/api/schedules.py +79 -0
  25. calagoapi/api/servers.py +114 -0
  26. calagoapi/api/users.py +27 -0
  27. calagoapi/api/versions.py +29 -0
  28. calagoapi/backups/__init__.py +8 -0
  29. calagoapi/backups/backup.py +292 -0
  30. calagoapi/backups/manager.py +177 -0
  31. calagoapi/console/__init__.py +6 -0
  32. calagoapi/console/console.py +392 -0
  33. calagoapi/databases/__init__.py +12 -0
  34. calagoapi/databases/database.py +445 -0
  35. calagoapi/databases/explorer.py +159 -0
  36. calagoapi/databases/manager.py +122 -0
  37. calagoapi/files/__init__.py +9 -0
  38. calagoapi/files/file.py +196 -0
  39. calagoapi/files/manager.py +437 -0
  40. calagoapi/network/__init__.py +8 -0
  41. calagoapi/network/allocation.py +99 -0
  42. calagoapi/network/manager.py +132 -0
  43. calagoapi/nodes/__init__.py +6 -0
  44. calagoapi/nodes/node.py +477 -0
  45. calagoapi/panel.py +162 -0
  46. calagoapi/schedules/__init__.py +14 -0
  47. calagoapi/schedules/builders.py +461 -0
  48. calagoapi/schedules/manager.py +107 -0
  49. calagoapi/schedules/schedule.py +214 -0
  50. calagoapi/servers/__init__.py +13 -0
  51. calagoapi/servers/admin.py +430 -0
  52. calagoapi/servers/generic.py +785 -0
  53. calagoapi/servers/manager.py +167 -0
  54. calagoapi/servers/minecraft.py +207 -0
  55. calagoapi/tunnels/__init__.py +5 -0
  56. calagoapi/tunnels/tunnel.py +112 -0
  57. calagoapi/users/__init__.py +11 -0
  58. calagoapi/users/activity.py +45 -0
  59. calagoapi/users/subuser.py +127 -0
  60. calagoapi/users/user.py +267 -0
  61. calagoapi/utils/__init__.py +43 -0
  62. calagoapi/utils/convert.py +113 -0
  63. calagoapi/utils/exceptions.py +72 -0
  64. calagoapi/utils/model.py +142 -0
  65. calagoapi/utils/resource.py +100 -0
  66. calagoapi/utils/time.py +68 -0
  67. calagoapi-1.0.dev1.dist-info/METADATA +266 -0
  68. calagoapi-1.0.dev1.dist-info/RECORD +71 -0
  69. calagoapi-1.0.dev1.dist-info/WHEEL +5 -0
  70. calagoapi-1.0.dev1.dist-info/licenses/LICENSE +674 -0
  71. calagoapi-1.0.dev1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,214 @@
1
+ from calagoapi.utils.model import Model, idOf
2
+
3
+
4
+ class Schedule(Model):
5
+ '''
6
+ A server schedule.
7
+
8
+ Args:
9
+ server: The owning server.
10
+ data: Raw schedule data from Calagopus.
11
+
12
+ Example:
13
+ schedule = server.schedules.getSchedule('Nightly restart')
14
+ if not schedule.isEnabled():
15
+ schedule.enable()
16
+ schedule.run()
17
+ print(schedule.getStatus())
18
+ '''
19
+ def __init__(self, server, data: dict):
20
+ super().__init__(server.panel, data)
21
+ self.server = server
22
+
23
+ @property
24
+ def schedule_id(self):
25
+ return self.getUUID()
26
+
27
+ def _api(self):
28
+ return self.panel.api.schedules
29
+
30
+ # getters
31
+ def getName(self) -> str:
32
+ '''Returns the schedule name.'''
33
+ return self.data.get('name')
34
+
35
+ def getTriggers(self) -> list:
36
+ '''Returns the triggers that start this schedule.'''
37
+ return self.data.get('triggers', [])
38
+
39
+ def getCondition(self) -> dict:
40
+ '''Returns the condition checked before running.'''
41
+ return self.data.get('condition')
42
+
43
+ def getLastRun(self) -> str:
44
+ '''Returns when the schedule last ran, or None.'''
45
+ return self.data.get('last_run')
46
+
47
+ def getLastFailure(self) -> str:
48
+ '''Returns when the schedule last failed, or None.'''
49
+ return self.data.get('last_failure')
50
+
51
+ def getStatus(self) -> dict:
52
+ '''
53
+ Returns the live run status.
54
+
55
+ Returns:
56
+ dict with `running` (bool), `step` (current step UUID) and `errors` (step UUID -> message).
57
+ '''
58
+ return self._api().status(self.server.server_id, self.schedule_id)
59
+
60
+ # state checks
61
+ def isEnabled(self) -> bool:
62
+ '''Returns True if the schedule is enabled.'''
63
+ return bool(self.data.get('enabled'))
64
+
65
+ def isRunning(self) -> bool:
66
+ '''Returns True if the schedule is currently executing.'''
67
+ return bool(self.getStatus().get('running'))
68
+
69
+ # actions
70
+ def refresh(self) -> 'Schedule':
71
+ '''Re-fetches the schedule from the panel.'''
72
+ self.data = self._api().get(self.server.server_id, self.schedule_id)
73
+ return self
74
+
75
+ def update(self, name: str = None, enabled: bool = None, triggers: list = None, condition: dict = None) -> 'Schedule':
76
+ '''
77
+ Updates the schedule. Only the given fields change.
78
+
79
+ Example:
80
+ schedule.update(triggers=[triggers.cron('0 0 6 * * *')])
81
+ '''
82
+ self._api().update(self.server.server_id, self.schedule_id, name, enabled, triggers, condition)
83
+ return self._update({'name': name, 'enabled': enabled, 'triggers': triggers, 'condition': condition})
84
+
85
+ def rename(self, name: str) -> 'Schedule':
86
+ '''Renames the schedule.'''
87
+ return self.update(name=name)
88
+
89
+ def setName(self, name: str) -> 'Schedule':
90
+ '''Alias of rename().'''
91
+ return self.update(name=name)
92
+
93
+ def enable(self) -> 'Schedule':
94
+ '''Enables the schedule.'''
95
+ return self.update(enabled=True)
96
+
97
+ def disable(self) -> 'Schedule':
98
+ '''Disables the schedule.'''
99
+ return self.update(enabled=False)
100
+
101
+ def run(self, skip_condition: bool = False) -> None:
102
+ '''
103
+ Runs the schedule now.
104
+
105
+ Args:
106
+ skip_condition: Run even if the schedule's condition does not match.
107
+ '''
108
+ return self._api().trigger(self.server.server_id, self.schedule_id, skip_condition)
109
+
110
+ def abort(self) -> None:
111
+ '''Stops the schedule if it is currently running.'''
112
+ return self._api().abort(self.server.server_id, self.schedule_id)
113
+
114
+ def duplicate(self, name: str) -> 'Schedule':
115
+ '''Copies the schedule, including its steps, under a new name.'''
116
+ return Schedule(self.server, self._api().duplicate(self.server.server_id, self.schedule_id, name))
117
+
118
+ def export(self) -> dict:
119
+ '''
120
+ Exports the schedule as a dict that can be imported on any server.
121
+
122
+ Example:
123
+ data = schedule.export()
124
+ other_server.schedules.importSchedule(data)
125
+ '''
126
+ return self._api().export(self.server.server_id, self.schedule_id)
127
+
128
+ def delete(self) -> None:
129
+ '''Deletes the schedule.'''
130
+ return self._api().delete(self.server.server_id, self.schedule_id)
131
+
132
+ # steps
133
+ def getSteps(self) -> list:
134
+ '''Returns the steps in execution order.'''
135
+ steps = self._api().steps(self.server.server_id, self.schedule_id)
136
+ return sorted([ScheduleStep(self, data) for data in steps], key=lambda s: s.getOrder())
137
+
138
+ def addStep(self, action: dict, order: int = None) -> 'ScheduleStep':
139
+ '''
140
+ Adds a step.
141
+
142
+ Args:
143
+ action: An action from `calagoapi.actions`.
144
+ order: Position. Defaults to the end.
145
+
146
+ Example:
147
+ schedule.addStep(actions.sendCommand('say Hello'))
148
+ '''
149
+ if order is None:
150
+ order = max([s.getOrder() for s in self.getSteps()], default=0) + 1
151
+ return ScheduleStep(self, self._api().createStep(self.server.server_id, self.schedule_id, action, order))
152
+
153
+ def setSteps(self, actions: list) -> list:
154
+ '''Replaces every step with the given actions, in order.'''
155
+ for step in self.getSteps():
156
+ step.delete()
157
+ return [self.addStep(action, order) for order, action in enumerate(actions, start=1)]
158
+
159
+ def orderSteps(self, steps: list) -> None:
160
+ '''Reorders steps. `steps` is a list of ScheduleStep objects or UUIDs.'''
161
+ return self._api().orderSteps(self.server.server_id, self.schedule_id, [idOf(s) for s in steps])
162
+
163
+
164
+ class ScheduleStep(Model):
165
+ '''
166
+ One step (action) of a schedule.
167
+
168
+ Example:
169
+ for step in schedule.getSteps():
170
+ print(step.getOrder(), step.getActionType())
171
+ '''
172
+ def __init__(self, schedule: Schedule, data: dict):
173
+ super().__init__(schedule.panel, data)
174
+ self.schedule = schedule
175
+ self.server = schedule.server
176
+
177
+ def _args(self):
178
+ return self.server.server_id, self.schedule.schedule_id, self.getUUID()
179
+
180
+ def __repr__(self):
181
+ return f'<ScheduleStep {self.getOrder()} {self.getActionType()!r}>'
182
+
183
+ def getAction(self) -> dict:
184
+ '''Returns the action dict.'''
185
+ return self.data.get('action', {})
186
+
187
+ def getActionType(self) -> str:
188
+ '''Returns the action type, e.g. "send_command".'''
189
+ return self.getAction().get('type')
190
+
191
+ def getOrder(self) -> int:
192
+ '''Returns the position of the step.'''
193
+ return self.data.get('order', 0)
194
+
195
+ def getError(self) -> str:
196
+ '''Returns the error from the last run of this step, or None.'''
197
+ return self.data.get('error')
198
+
199
+ def hasError(self) -> bool:
200
+ '''Returns True if the step failed on its last run.'''
201
+ return bool(self.data.get('error'))
202
+
203
+ def update(self, action: dict = None, order: int = None) -> 'ScheduleStep':
204
+ '''Changes the action and/or position of the step.'''
205
+ self.panel.api.schedules.updateStep(*self._args(), action, order)
206
+ return self._update({'action': action, 'order': order})
207
+
208
+ def duplicate(self) -> 'ScheduleStep':
209
+ '''Copies the step.'''
210
+ return ScheduleStep(self.schedule, self.panel.api.schedules.duplicateStep(*self._args()))
211
+
212
+ def delete(self) -> None:
213
+ '''Deletes the step.'''
214
+ return self.panel.api.schedules.deleteStep(*self._args())
@@ -0,0 +1,13 @@
1
+ from .generic import GenericServer
2
+ from .minecraft import MinecraftServer
3
+ from .manager import ServerManager, ServerGroup
4
+ from .admin import AdminServer, AdminServerManager
5
+
6
+ __all__ = [
7
+ 'GenericServer',
8
+ 'MinecraftServer',
9
+ 'ServerManager',
10
+ 'ServerGroup',
11
+ 'AdminServer',
12
+ 'AdminServerManager',
13
+ ]
@@ -0,0 +1,430 @@
1
+ from calagoapi.utils.resource import AdminResource, AdminManager
2
+ from calagoapi.servers.generic import GenericServer
3
+ from calagoapi.utils.model import idOf
4
+ from calagoapi.utils.exceptions import NotFoundError
5
+
6
+
7
+ class AdminServer(AdminResource):
8
+ '''
9
+ A server as seen by an administrator.
10
+
11
+ Gives access to everything a server owner cannot change: limits, owner,
12
+ egg, node, suspension, transfers, allocations and more. Use
13
+ `asServer()` to get the regular GenericServer for power, files, etc.
14
+
15
+ Example:
16
+ server = panel.admin.servers.getServer('abc123')
17
+ server.setLimits(memory=8192)
18
+ server.suspend()
19
+ server.transfer(other_node)
20
+ '''
21
+ collection = 'servers'
22
+
23
+ def __repr__(self):
24
+ return f'<AdminServer {self.getName()!r} ({self.getUUID()})>'
25
+
26
+ # getters
27
+ def getShortId(self) -> str:
28
+ '''Returns the short id.'''
29
+ return self.data.get('uuid_short')
30
+
31
+ def getExternalId(self) -> str:
32
+ '''Returns the external id used by billing systems.'''
33
+ return self.data.get('external_id')
34
+
35
+ def getOwner(self) -> dict:
36
+ '''Returns the owner user.'''
37
+ return self.data.get('owner', {})
38
+
39
+ def getOwnerId(self) -> str:
40
+ '''Returns the owner's UUID.'''
41
+ return self.getOwner().get('uuid')
42
+
43
+ def getNode(self) -> dict:
44
+ '''Returns the node the server runs on.'''
45
+ return self.data.get('node', {})
46
+
47
+ def getNodeId(self) -> str:
48
+ '''Returns the node UUID.'''
49
+ return self.getNode().get('uuid')
50
+
51
+ def getEgg(self) -> dict:
52
+ '''Returns the egg.'''
53
+ return self.data.get('egg', {})
54
+
55
+ def getNest(self) -> dict:
56
+ '''Returns the nest the egg belongs to.'''
57
+ return self.data.get('nest', {})
58
+
59
+ def getLimits(self) -> dict:
60
+ '''Returns resource limits: cpu, memory, memory_overhead, swap, disk, io_weight, bandwidth.'''
61
+ return self.data.get('limits', {})
62
+
63
+ def getFeatureLimits(self) -> dict:
64
+ '''Returns feature limits: allocations, databases, backups, schedules, subdomains.'''
65
+ return self.data.get('feature_limits', {})
66
+
67
+ def getStartupCommand(self) -> str:
68
+ '''Returns the startup command.'''
69
+ return self.data.get('startup')
70
+
71
+ def getDockerImage(self) -> str:
72
+ '''Returns the docker image.'''
73
+ return self.data.get('image')
74
+
75
+ def getLabels(self) -> dict:
76
+ '''Returns the server labels.'''
77
+ return self.data.get('labels', {})
78
+
79
+ def getPinnedCPUs(self) -> list:
80
+ '''Returns the CPU cores the server is pinned to.'''
81
+ return self.data.get('pinned_cpus', [])
82
+
83
+ def getInstallStatus(self) -> str:
84
+ '''Returns None when installed, otherwise e.g. 'installing' or 'install_failed'.'''
85
+ return self.data.get('status')
86
+
87
+ # state checks
88
+ def isSuspended(self) -> bool:
89
+ '''Returns True if the server is suspended.'''
90
+ return bool(self.data.get('is_suspended'))
91
+
92
+ def isTransferring(self) -> bool:
93
+ '''Returns True while the server is being transferred.'''
94
+ return bool(self.data.get('is_transferring'))
95
+
96
+ def isInstalled(self) -> bool:
97
+ '''Returns True when the server is installed.'''
98
+ return self.getInstallStatus() is None
99
+
100
+ # settings
101
+ def asServer(self):
102
+ '''
103
+ Returns the regular GenericServer for this server.
104
+
105
+ Example:
106
+ panel.admin.servers.getServer(server_id).asServer().restart()
107
+ '''
108
+ return GenericServer(self.panel, self.getUUID())
109
+
110
+ def suspend(self) -> 'AdminServer':
111
+ '''Suspends the server.'''
112
+ return self.update(suspended=True)
113
+
114
+ def unsuspend(self) -> 'AdminServer':
115
+ '''Lifts a suspension.'''
116
+ return self.update(suspended=False)
117
+
118
+ def setLimits(self, **limits) -> 'AdminServer':
119
+ '''
120
+ Changes resource limits. Unspecified limits keep their value.
121
+
122
+ Example:
123
+ server.setLimits(memory=4096, cpu=200, disk=20480)
124
+ '''
125
+ return self.update(limits={**self.getLimits(), **limits})
126
+
127
+ def setFeatureLimits(self, **limits) -> 'AdminServer':
128
+ '''
129
+ Changes feature limits. Unspecified limits keep their value.
130
+
131
+ Example:
132
+ server.setFeatureLimits(backups=5, databases=2)
133
+ '''
134
+ return self.update(feature_limits={**self.getFeatureLimits(), **limits})
135
+
136
+ def setOwner(self, user) -> 'AdminServer':
137
+ '''Transfers ownership to another user (User object or UUID).'''
138
+ return self.update(owner_uuid=idOf(user))
139
+
140
+ def setEgg(self, egg, startup: str = None, image: str = None) -> 'AdminServer':
141
+ '''Changes the egg, optionally with a new startup command and image.'''
142
+ return self.update(egg_uuid=idOf(egg), startup=startup, image=image)
143
+
144
+ def setStartupCommand(self, startup: str) -> 'AdminServer':
145
+ '''Changes the startup command.'''
146
+ return self.update(startup=startup)
147
+
148
+ def setDockerImage(self, image: str) -> 'AdminServer':
149
+ '''Changes the docker image.'''
150
+ return self.update(image=image)
151
+
152
+ def setExternalId(self, external_id: str) -> 'AdminServer':
153
+ '''Sets the external id.'''
154
+ return self.update(external_id=external_id)
155
+
156
+ def setLabels(self, labels: dict) -> 'AdminServer':
157
+ '''Replaces the server labels.'''
158
+ return self.update(labels=labels)
159
+
160
+ def setBackupConfiguration(self, backup_configuration) -> 'AdminServer':
161
+ '''Changes where backups are stored.'''
162
+ return self.update(backup_configuration_uuid=idOf(backup_configuration))
163
+
164
+ def clearState(self) -> None:
165
+ '''Resets a stuck install/restore/transfer state.'''
166
+ return self.panel.api.admin.clearServerState(self.getUUID())
167
+
168
+ def delete(self, force: bool = False, delete_backups: bool = False) -> None:
169
+ '''
170
+ Deletes the server.
171
+
172
+ Args:
173
+ force: Delete from the panel even if the node cannot be reached.
174
+ delete_backups: Also delete the server's backups.
175
+ '''
176
+ return self._api().delete(self.getUUID(), force=force, delete_backups=delete_backups)
177
+
178
+ # transfers
179
+ def transfer(self, node, allocation=None, allocations: list = None, backups: list = None,
180
+ delete_source_backups: bool = False, archive_format: str = 'tar_zstd',
181
+ compression_level: str = None, multiplex_channels: int = 0) -> None:
182
+ '''
183
+ Moves the server to another node.
184
+
185
+ Args:
186
+ node: Target node (object or UUID).
187
+ allocation: Primary allocation on the target node.
188
+ allocations: Extra allocations on the target node.
189
+ backups: Backups (objects or UUIDs) to move along.
190
+ delete_source_backups: Delete moved backups from the old node.
191
+ archive_format: Archive format used for the transfer.
192
+ compression_level: best_speed, good_speed, good_compression or best_compression.
193
+ multiplex_channels: Parallel transfer streams.
194
+ '''
195
+ return self.panel.api.admin.transferServer(
196
+ self.getUUID(),
197
+ node_uuid=idOf(node),
198
+ allocation_uuid=idOf(allocation),
199
+ allocation_uuids=[idOf(a) for a in allocations or []],
200
+ backups=[idOf(b) for b in backups or []],
201
+ delete_source_backups=delete_source_backups,
202
+ archive_format=archive_format,
203
+ compression_level=compression_level,
204
+ multiplex_channels=multiplex_channels,
205
+ )
206
+
207
+ def cancelTransfer(self) -> None:
208
+ '''Cancels a running transfer.'''
209
+ return self.panel.api.admin.cancelServerTransfer(self.getUUID())
210
+
211
+ # allocations
212
+ def getAllocations(self, search: str = None, page: int = None, per_page: int = None):
213
+ '''Returns the server's allocations.'''
214
+ return self.panel.api.admin.serverAllocations(self.getUUID(), search, page, per_page)
215
+
216
+ def addAllocation(self, allocation) -> dict:
217
+ '''Assigns a node allocation to the server.'''
218
+ return self.panel.api.admin.addServerAllocation(self.getUUID(), idOf(allocation))
219
+
220
+ def removeAllocation(self, allocation) -> None:
221
+ '''Removes an allocation from the server.'''
222
+ return self.panel.api.admin.removeServerAllocation(self.getUUID(), idOf(allocation))
223
+
224
+ def setPrimaryAllocation(self, allocation) -> None:
225
+ '''Makes an allocation the primary one.'''
226
+ return self.panel.api.admin.updateServerAllocation(self.getUUID(), idOf(allocation), primary=True)
227
+
228
+ # variables
229
+ def getVariables(self) -> list:
230
+ '''Returns every startup variable, including hidden ones.'''
231
+ return self.panel.api.admin.serverVariables(self.getUUID())
232
+
233
+ def setVariables(self, variables: dict) -> None:
234
+ '''Sets startup variables, given as {env_variable: value}.'''
235
+ return self.panel.api.admin.setServerVariables(
236
+ self.getUUID(), [{'env_variable': k, 'value': str(v)} for k, v in variables.items()]
237
+ )
238
+
239
+ def setVariable(self, key: str, value) -> None:
240
+ '''Sets one startup variable.'''
241
+ return self.setVariables({key: value})
242
+
243
+ # logs, backups, databases
244
+ def getLogs(self, lines: int = None) -> str:
245
+ '''Returns console output.'''
246
+ return self.panel.api.admin.serverLogs(self.getUUID(), lines)
247
+
248
+ def getInstallLogs(self, lines: int = None) -> str:
249
+ '''Returns the output of the last install script run.'''
250
+ return self.panel.api.admin.serverLogs(self.getUUID(), lines, install=True)
251
+
252
+ def getBackups(self, partially_detached: bool = None, search: str = None, page: int = None, per_page: int = None):
253
+ '''Returns the server's backups.'''
254
+ return self.panel.api.admin.serverBackups(self.getUUID(), partially_detached, search, page, per_page)
255
+
256
+ def deleteFailedBackups(self, force: bool = False) -> int:
257
+ '''Deletes every failed backup. Returns how many were queued.'''
258
+ return self.panel.api.admin.serverDeleteFailedBackups(self.getUUID(), force)
259
+
260
+ def getDatabases(self, search: str = None, page: int = None, per_page: int = None):
261
+ '''Returns the server's hosted databases.'''
262
+ return self.panel.api.admin.serverDatabases(self.getUUID(), search, page, per_page)
263
+
264
+ def getDatabaseInstances(self, search: str = None, page: int = None, per_page: int = None):
265
+ '''Returns the server's database instances.'''
266
+ return self.panel.api.admin.serverDatabaseInstances(self.getUUID(), search, page, per_page)
267
+
268
+ # mounts and devices
269
+ def getMounts(self, available: bool = False, search: str = None, page: int = None, per_page: int = None):
270
+ '''Returns attached mounts, or mounts that could be attached with available=True.'''
271
+ return self.panel.api.admin.serverMounts(self.getUUID(), available, search, page, per_page)
272
+
273
+ def attachMount(self, mount) -> None:
274
+ '''Attaches a mount.'''
275
+ return self._api().attach(self.getUUID(), 'mounts', 'mount_uuid', idOf(mount))
276
+
277
+ def detachMount(self, mount) -> None:
278
+ '''Detaches a mount.'''
279
+ return self._api().detach(self.getUUID(), 'mounts', idOf(mount))
280
+
281
+ def getDevices(self, available: bool = False, search: str = None, page: int = None, per_page: int = None):
282
+ '''Returns attached devices, or devices that could be attached with available=True.'''
283
+ return self.panel.api.admin.serverDevices(self.getUUID(), available, search, page, per_page)
284
+
285
+ def attachDevice(self, device) -> None:
286
+ '''Attaches a device.'''
287
+ return self._api().attach(self.getUUID(), 'devices', 'device_uuid', idOf(device))
288
+
289
+ def detachDevice(self, device) -> None:
290
+ '''Detaches a device.'''
291
+ return self._api().detach(self.getUUID(), 'devices', idOf(device))
292
+
293
+
294
+ class AdminServerManager(AdminManager):
295
+ '''
296
+ Admin server management, available as `panel.admin.servers`.
297
+
298
+ Example:
299
+ server = panel.admin.servers.createServer(
300
+ name='Survival',
301
+ owner=user,
302
+ egg=paper_egg,
303
+ node=node,
304
+ allocation=allocation,
305
+ memory=4096,
306
+ disk=20480,
307
+ )
308
+ '''
309
+ model = AdminServer
310
+
311
+ def getServers(self, search: str = None, page: int = None, per_page: int = None):
312
+ '''Lists every server on the panel as a Page of AdminServer objects.'''
313
+ return self._list(search, page, per_page)
314
+
315
+ def getServer(self, server_id: str) -> AdminServer:
316
+ '''Returns a server by UUID.'''
317
+ return self._get(server_id)
318
+
319
+ def getServerByExternalId(self, external_id: str) -> AdminServer:
320
+ '''Returns a server by external id.'''
321
+ return self._wrap(self.admin.serverByExternalId(external_id))
322
+
323
+ def _eggDefaults(self, egg_id):
324
+ for entry in self.admin.allEggs():
325
+ for egg in entry.get('eggs', []):
326
+ if egg.get('uuid') == egg_id:
327
+ return entry['nest']['uuid'], egg
328
+ raise NotFoundError(f'Egg {egg_id} not found')
329
+
330
+ def _build(self, name, owner, egg, memory, disk, cpu, swap, io_weight, memory_overhead, startup, image,
331
+ variables, allocations_limit, databases, backups, schedules, extra):
332
+ egg_id = idOf(egg)
333
+ if startup is None or image is None or variables is None:
334
+ nest_id, egg_data = self._eggDefaults(egg_id)
335
+ if startup is None:
336
+ startup = next(iter(egg_data.get('startup_commands', {}).values()), '')
337
+ if image is None:
338
+ image = next(iter(egg_data.get('docker_images', {}).values()), '')
339
+ defaults = {
340
+ v['env_variable']: v.get('default_value') or ''
341
+ for v in self.admin.eggVariables(nest_id, egg_id).listAll()
342
+ }
343
+ variables = {**defaults, **(variables or {})}
344
+
345
+ fields = {
346
+ 'name': name,
347
+ 'owner_uuid': idOf(owner),
348
+ 'egg_uuid': egg_id,
349
+ 'limits': {'cpu': cpu, 'memory': memory, 'memory_overhead': memory_overhead, 'swap': swap, 'disk': disk, 'io_weight': io_weight},
350
+ 'feature_limits': {'allocations': allocations_limit, 'databases': databases, 'backups': backups, 'schedules': schedules},
351
+ 'startup': startup,
352
+ 'image': image,
353
+ 'variables': [{'env_variable': k, 'value': str(v)} for k, v in variables.items()],
354
+ 'pinned_cpus': [],
355
+ 'hugepages_passthrough_enabled': False,
356
+ 'kvm_passthrough_enabled': False,
357
+ 'start_on_completion': True,
358
+ 'skip_installer': False,
359
+ }
360
+ fields.update(extra)
361
+ return fields
362
+
363
+ def createServer(self, name: str, owner, egg, node, allocation=None, allocations: list = None,
364
+ memory: int = 1024, disk: int = 10240, cpu: int = 100, swap: int = 0, io_weight: int = None,
365
+ memory_overhead: int = 0, startup: str = None, image: str = None, variables: dict = None,
366
+ allocations_limit: int = 0, databases: int = 0, backups: int = 0, schedules: int = 0,
367
+ **extra) -> AdminServer:
368
+ '''
369
+ Creates a server on a specific node.
370
+
371
+ Startup command, docker image and variables default to the egg's defaults.
372
+
373
+ Args:
374
+ name: Server name.
375
+ owner: User object or UUID.
376
+ egg: Egg object or UUID.
377
+ node: Node object or UUID.
378
+ allocation: Primary allocation (object or UUID).
379
+ allocations: Extra allocations.
380
+ memory: Memory limit in MiB (0 = unlimited).
381
+ disk: Disk limit in MiB (0 = unlimited).
382
+ cpu: CPU limit in percent, 100 = one core (0 = unlimited).
383
+ swap: Swap in MiB (-1 = unlimited).
384
+ io_weight: Block IO weight (10-1000).
385
+ memory_overhead: Extra memory for the container in MiB.
386
+ startup: Startup command.
387
+ image: Docker image.
388
+ variables: Startup variables as {env_variable: value}.
389
+ allocations_limit: How many allocations the owner may add.
390
+ databases: How many databases the owner may create.
391
+ backups: How many backups the owner may keep.
392
+ schedules: How many schedules the owner may create.
393
+ **extra: Any other Calagopus field, e.g. description, external_id,
394
+ start_on_completion, skip_installer, timezone, labels.
395
+ '''
396
+ fields = self._build(name, owner, egg, memory, disk, cpu, swap, io_weight, memory_overhead, startup, image,
397
+ variables, allocations_limit, databases, backups, schedules, extra)
398
+ fields['node_uuid'] = idOf(node)
399
+ fields['allocation_uuid'] = idOf(allocation)
400
+ fields['allocation_uuids'] = [idOf(a) for a in allocations or []]
401
+ return self._create(**fields)
402
+
403
+ def deployServer(self, name: str, owner, egg, locations: list, allow_overallocation: bool = False,
404
+ memory: int = 1024, disk: int = 10240, cpu: int = 100, swap: int = 0, io_weight: int = None,
405
+ memory_overhead: int = 0, startup: str = None, image: str = None, variables: dict = None,
406
+ allocations_limit: int = 0, databases: int = 0, backups: int = 0, schedules: int = 0,
407
+ **extra) -> AdminServer:
408
+ '''
409
+ Creates a server and lets Calagopus pick the node and allocation.
410
+
411
+ Args:
412
+ locations: Locations (objects or UUIDs) the server may be placed in.
413
+ allow_overallocation: Allow nodes that are already full.
414
+
415
+ All other arguments are the same as createServer().
416
+
417
+ Example:
418
+ panel.admin.servers.deployServer('Lobby', user, egg, locations=[eu_location], memory=2048)
419
+ '''
420
+ fields = self._build(name, owner, egg, memory, disk, cpu, swap, io_weight, memory_overhead, startup, image,
421
+ variables, allocations_limit, databases, backups, schedules, extra)
422
+ fields['deployment'] = {
423
+ 'location_uuids': [idOf(l) for l in locations],
424
+ 'allow_overallocation': allow_overallocation,
425
+ }
426
+ return self._wrap(self.admin.deployServer(**fields))
427
+
428
+ def deleteServer(self, server, force: bool = False, delete_backups: bool = False) -> None:
429
+ '''Deletes a server.'''
430
+ return self._delete(server, force=force, delete_backups=delete_backups)