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.
- calagoapi/__init__.py +190 -0
- calagoapi/account/__init__.py +14 -0
- calagoapi/account/account.py +292 -0
- calagoapi/account/auth.py +72 -0
- calagoapi/account/models.py +221 -0
- calagoapi/admin/__init__.py +48 -0
- calagoapi/admin/admin.py +341 -0
- calagoapi/admin/managers.py +490 -0
- calagoapi/admin/resources.py +970 -0
- calagoapi/aio/__init__.py +9 -0
- calagoapi/aio/console.py +289 -0
- calagoapi/aio/panel.py +176 -0
- calagoapi/api/__init__.py +63 -0
- calagoapi/api/account.py +195 -0
- calagoapi/api/admin.py +402 -0
- calagoapi/api/attachments.py +27 -0
- calagoapi/api/auth.py +52 -0
- calagoapi/api/backups.py +99 -0
- calagoapi/api/client.py +198 -0
- calagoapi/api/databases.py +217 -0
- calagoapi/api/files.py +210 -0
- calagoapi/api/network.py +84 -0
- calagoapi/api/public.py +13 -0
- calagoapi/api/schedules.py +79 -0
- calagoapi/api/servers.py +114 -0
- calagoapi/api/users.py +27 -0
- calagoapi/api/versions.py +29 -0
- calagoapi/backups/__init__.py +8 -0
- calagoapi/backups/backup.py +292 -0
- calagoapi/backups/manager.py +177 -0
- calagoapi/console/__init__.py +6 -0
- calagoapi/console/console.py +392 -0
- calagoapi/databases/__init__.py +12 -0
- calagoapi/databases/database.py +445 -0
- calagoapi/databases/explorer.py +159 -0
- calagoapi/databases/manager.py +122 -0
- calagoapi/files/__init__.py +9 -0
- calagoapi/files/file.py +196 -0
- calagoapi/files/manager.py +437 -0
- calagoapi/network/__init__.py +8 -0
- calagoapi/network/allocation.py +99 -0
- calagoapi/network/manager.py +132 -0
- calagoapi/nodes/__init__.py +6 -0
- calagoapi/nodes/node.py +477 -0
- calagoapi/panel.py +162 -0
- calagoapi/schedules/__init__.py +14 -0
- calagoapi/schedules/builders.py +461 -0
- calagoapi/schedules/manager.py +107 -0
- calagoapi/schedules/schedule.py +214 -0
- calagoapi/servers/__init__.py +13 -0
- calagoapi/servers/admin.py +430 -0
- calagoapi/servers/generic.py +785 -0
- calagoapi/servers/manager.py +167 -0
- calagoapi/servers/minecraft.py +207 -0
- calagoapi/tunnels/__init__.py +5 -0
- calagoapi/tunnels/tunnel.py +112 -0
- calagoapi/users/__init__.py +11 -0
- calagoapi/users/activity.py +45 -0
- calagoapi/users/subuser.py +127 -0
- calagoapi/users/user.py +267 -0
- calagoapi/utils/__init__.py +43 -0
- calagoapi/utils/convert.py +113 -0
- calagoapi/utils/exceptions.py +72 -0
- calagoapi/utils/model.py +142 -0
- calagoapi/utils/resource.py +100 -0
- calagoapi/utils/time.py +68 -0
- calagoapi-1.0.dev1.dist-info/METADATA +266 -0
- calagoapi-1.0.dev1.dist-info/RECORD +71 -0
- calagoapi-1.0.dev1.dist-info/WHEEL +5 -0
- calagoapi-1.0.dev1.dist-info/licenses/LICENSE +674 -0
- calagoapi-1.0.dev1.dist-info/top_level.txt +1 -0
calagoapi/nodes/node.py
ADDED
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
from calagoapi.utils.resource import AdminResource, AdminManager
|
|
2
|
+
from calagoapi.servers.admin import AdminServer
|
|
3
|
+
from calagoapi.utils.model import idOf
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Node(AdminResource):
|
|
7
|
+
'''
|
|
8
|
+
A node (machine running the Calagopus daemon). Requires an admin API key.
|
|
9
|
+
|
|
10
|
+
Example:
|
|
11
|
+
node = panel.admin.nodes.getNode('...')
|
|
12
|
+
print(node.getName(), node.getSystemOverview()['version'])
|
|
13
|
+
node.createAllocations('0.0.0.0', range(25565, 25576))
|
|
14
|
+
node.enableMaintenance()
|
|
15
|
+
node.restartAllServers()
|
|
16
|
+
'''
|
|
17
|
+
collection = 'nodes'
|
|
18
|
+
|
|
19
|
+
def _admin(self):
|
|
20
|
+
return self.panel.api.admin
|
|
21
|
+
|
|
22
|
+
# getters
|
|
23
|
+
def getURL(self) -> str:
|
|
24
|
+
'''Returns the internal daemon URL.'''
|
|
25
|
+
return self.data.get('url')
|
|
26
|
+
|
|
27
|
+
def getPublicURL(self) -> str:
|
|
28
|
+
'''Returns the public daemon URL browsers use, if different.'''
|
|
29
|
+
return self.data.get('public_url')
|
|
30
|
+
|
|
31
|
+
def getLocation(self) -> dict:
|
|
32
|
+
'''Returns the node's location.'''
|
|
33
|
+
return self.data.get('location', {})
|
|
34
|
+
|
|
35
|
+
def getLocationId(self) -> str:
|
|
36
|
+
'''Returns the node's location UUID.'''
|
|
37
|
+
return self.getLocation().get('uuid')
|
|
38
|
+
|
|
39
|
+
def getMemory(self) -> int:
|
|
40
|
+
'''Returns the total memory available for servers, in MiB.'''
|
|
41
|
+
return self.data.get('memory')
|
|
42
|
+
|
|
43
|
+
def getDisk(self) -> int:
|
|
44
|
+
'''Returns the total disk available for servers, in MiB.'''
|
|
45
|
+
return self.data.get('disk')
|
|
46
|
+
|
|
47
|
+
def getSftpHost(self) -> str:
|
|
48
|
+
'''Returns the SFTP host shown to users.'''
|
|
49
|
+
return self.data.get('sftp_host')
|
|
50
|
+
|
|
51
|
+
def getSftpPort(self) -> int:
|
|
52
|
+
'''Returns the SFTP port.'''
|
|
53
|
+
return self.data.get('sftp_port')
|
|
54
|
+
|
|
55
|
+
def getBackupConfiguration(self) -> dict:
|
|
56
|
+
'''Returns the backup configuration used by the node, if any.'''
|
|
57
|
+
return self.data.get('backup_configuration')
|
|
58
|
+
|
|
59
|
+
def isDeploymentEnabled(self) -> bool:
|
|
60
|
+
'''Returns True if new servers may be deployed on the node.'''
|
|
61
|
+
return bool(self.data.get('deployment_enabled'))
|
|
62
|
+
|
|
63
|
+
def isInMaintenance(self) -> bool:
|
|
64
|
+
'''Returns True if the node is in maintenance mode.'''
|
|
65
|
+
return bool(self.data.get('maintenance_enabled'))
|
|
66
|
+
|
|
67
|
+
# settings
|
|
68
|
+
def duplicate(self, name: str) -> 'Node':
|
|
69
|
+
'''Creates a copy of the node configuration under a new name.'''
|
|
70
|
+
return self._duplicate(name=name)
|
|
71
|
+
|
|
72
|
+
def enableMaintenance(self) -> 'Node':
|
|
73
|
+
'''Puts the node into maintenance mode.'''
|
|
74
|
+
return self.update(maintenance_enabled=True)
|
|
75
|
+
|
|
76
|
+
def disableMaintenance(self) -> 'Node':
|
|
77
|
+
'''Ends maintenance mode.'''
|
|
78
|
+
return self.update(maintenance_enabled=False)
|
|
79
|
+
|
|
80
|
+
def enableDeployment(self) -> 'Node':
|
|
81
|
+
'''Allows automatic deployment of new servers.'''
|
|
82
|
+
return self.update(deployment_enabled=True)
|
|
83
|
+
|
|
84
|
+
def disableDeployment(self) -> 'Node':
|
|
85
|
+
'''Prevents automatic deployment of new servers.'''
|
|
86
|
+
return self.update(deployment_enabled=False)
|
|
87
|
+
|
|
88
|
+
def setLocation(self, location) -> 'Node':
|
|
89
|
+
'''Moves the node to another location.'''
|
|
90
|
+
return self.update(location_uuid=idOf(location))
|
|
91
|
+
|
|
92
|
+
def getConfig(self) -> dict:
|
|
93
|
+
'''Returns the daemon configuration file and which paths are locked.'''
|
|
94
|
+
return self._admin().nodeConfig(self.getUUID())
|
|
95
|
+
|
|
96
|
+
def updateConfig(self, config: dict) -> bool:
|
|
97
|
+
'''Applies changes to the daemon configuration. Returns True if applied.'''
|
|
98
|
+
return self._admin().updateNodeConfig(self.getUUID(), config)
|
|
99
|
+
|
|
100
|
+
# system
|
|
101
|
+
def getSystemOverview(self) -> dict:
|
|
102
|
+
'''Returns daemon version, CPU, memory, kernel and server counts.'''
|
|
103
|
+
return self._admin().nodeGet(self.getUUID(), 'system/overview')
|
|
104
|
+
|
|
105
|
+
def getSystemStats(self) -> dict:
|
|
106
|
+
'''Returns live CPU, memory, disk and network statistics.'''
|
|
107
|
+
return self._admin().nodeGet(self.getUUID(), 'system/stats')['stats']
|
|
108
|
+
|
|
109
|
+
def getSystemIPs(self) -> list:
|
|
110
|
+
'''Returns the IP addresses of the machine.'''
|
|
111
|
+
return self._admin().nodeGet(self.getUUID(), 'system/ips')['ips']
|
|
112
|
+
|
|
113
|
+
def getSystemLogs(self) -> list:
|
|
114
|
+
'''Returns the daemon log files.'''
|
|
115
|
+
return self._admin().nodeGet(self.getUUID(), 'system/logs')['log_files']
|
|
116
|
+
|
|
117
|
+
def getSystemLog(self, file: str, lines: int = None) -> str:
|
|
118
|
+
'''Returns the contents of a daemon log file.'''
|
|
119
|
+
return self._admin().nodeSystemLog(self.getUUID(), file, lines)
|
|
120
|
+
|
|
121
|
+
def getCapacity(self) -> dict:
|
|
122
|
+
'''Returns the node's limits and how much is allocated to servers.'''
|
|
123
|
+
return self._admin().nodeGet(self.getUUID(), 'capacity')
|
|
124
|
+
|
|
125
|
+
def getBandwidth(self) -> dict:
|
|
126
|
+
'''Returns whether bandwidth limiting is available on the node.'''
|
|
127
|
+
return self._admin().nodeGet(self.getUUID(), 'bandwidth')
|
|
128
|
+
|
|
129
|
+
def getTransfers(self) -> dict:
|
|
130
|
+
'''Returns running server transfers.'''
|
|
131
|
+
return self._admin().nodeGet(self.getUUID(), 'transfers')['transfers']
|
|
132
|
+
|
|
133
|
+
def getTransferringServers(self, search: str = None, page: int = None, per_page: int = None):
|
|
134
|
+
'''Returns servers currently being transferred to or from this node.'''
|
|
135
|
+
return self._api().related(self.getUUID(), 'transfers/servers', 'servers', search, page, per_page).map(
|
|
136
|
+
lambda data: AdminServer(self.panel, data)
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# tokens and pairing
|
|
140
|
+
def getToken(self) -> dict:
|
|
141
|
+
'''Returns the daemon token (token_id, token).'''
|
|
142
|
+
return self._admin().nodeGet(self.getUUID(), 'token')
|
|
143
|
+
|
|
144
|
+
def resetToken(self) -> dict:
|
|
145
|
+
'''Generates a new daemon token. The daemon must be reconfigured.'''
|
|
146
|
+
return self._admin().nodePost(self.getUUID(), 'reset-token')
|
|
147
|
+
|
|
148
|
+
def createEnrollmentCode(self, remote: str = None) -> dict:
|
|
149
|
+
'''Creates a one-time code the daemon can use to enroll itself (code, expires).'''
|
|
150
|
+
return self._admin().nodePost(self.getUUID(), 'enrollment', {'remote': remote})
|
|
151
|
+
|
|
152
|
+
def pair(self, pairing_code: str, remote: str = None) -> None:
|
|
153
|
+
'''Pairs the node with a daemon showing `pairing_code`.'''
|
|
154
|
+
return self._admin().nodePost(self.getUUID(), 'pair', {'pairing_code': pairing_code, 'remote': remote})
|
|
155
|
+
|
|
156
|
+
# servers
|
|
157
|
+
def getServers(self, search: str = None, page: int = None, per_page: int = None):
|
|
158
|
+
'''Returns the servers on this node as AdminServer objects.'''
|
|
159
|
+
return self._api().related(self.getUUID(), 'servers', 'servers', search, page, per_page).map(
|
|
160
|
+
lambda data: AdminServer(self.panel, data)
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def getServerResources(self) -> dict:
|
|
164
|
+
'''Returns live resource usage of every server on the node.'''
|
|
165
|
+
return self._admin().nodeGet(self.getUUID(), 'servers/resources')['resources']
|
|
166
|
+
|
|
167
|
+
def powerServers(self, action: str, servers: list = None) -> int:
|
|
168
|
+
'''
|
|
169
|
+
Sends a power action to servers on the node.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
action: start, stop, restart or kill.
|
|
173
|
+
servers: Servers (objects or ids). Every server on the node when omitted.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Number of affected servers.
|
|
177
|
+
'''
|
|
178
|
+
return self._admin().nodeServerPower(self.getUUID(), action, [idOf(s) for s in servers] if servers else None)
|
|
179
|
+
|
|
180
|
+
def startAllServers(self) -> int:
|
|
181
|
+
'''Starts every server on the node.'''
|
|
182
|
+
return self.powerServers('start')
|
|
183
|
+
|
|
184
|
+
def stopAllServers(self) -> int:
|
|
185
|
+
'''Stops every server on the node.'''
|
|
186
|
+
return self.powerServers('stop')
|
|
187
|
+
|
|
188
|
+
def restartAllServers(self) -> int:
|
|
189
|
+
'''Restarts every server on the node.'''
|
|
190
|
+
return self.powerServers('restart')
|
|
191
|
+
|
|
192
|
+
def transferServers(self, node, servers: list = None, allocation_mode: str = 'preserve_ports',
|
|
193
|
+
transfer_backups: bool = True, delete_source_backups: bool = False,
|
|
194
|
+
archive_format: str = 'tar_zstd', compression_level: str = None, multiplex_channels: int = 0) -> None:
|
|
195
|
+
'''
|
|
196
|
+
Moves servers from this node to another node.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
node: Target node.
|
|
200
|
+
servers: Servers to move. Every server when omitted.
|
|
201
|
+
allocation_mode: none, random_primary, random_all, preserve_ports,
|
|
202
|
+
egg_config_deployment or egg_config_self_assign_range.
|
|
203
|
+
transfer_backups: Move backups along.
|
|
204
|
+
delete_source_backups: Delete moved backups from this node.
|
|
205
|
+
'''
|
|
206
|
+
selector = {'type': 'uuids', 'uuids': [idOf(s) for s in servers]} if servers else {'type': 'all'}
|
|
207
|
+
return self._admin().nodeServerTransfer(
|
|
208
|
+
self.getUUID(),
|
|
209
|
+
servers=selector,
|
|
210
|
+
node_uuid=idOf(node),
|
|
211
|
+
allocation_mode=allocation_mode,
|
|
212
|
+
transfer_backups=transfer_backups,
|
|
213
|
+
delete_source_backups=delete_source_backups,
|
|
214
|
+
archive_format=archive_format,
|
|
215
|
+
compression_level=compression_level,
|
|
216
|
+
multiplex_channels=multiplex_channels,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
# allocations
|
|
220
|
+
def getAllocations(self, search: str = None, ip: str = None, port_from: int = None, port_to: int = None,
|
|
221
|
+
assigned: bool = None, page: int = None, per_page: int = None):
|
|
222
|
+
'''
|
|
223
|
+
Returns the node's allocations.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
ip: Only this IP.
|
|
227
|
+
port_from: Lowest port.
|
|
228
|
+
port_to: Highest port.
|
|
229
|
+
assigned: True for allocations used by servers, False for free ones.
|
|
230
|
+
'''
|
|
231
|
+
return self._admin().nodeAllocations(self.getUUID(), search, ip, port_from, port_to, assigned, page, per_page)
|
|
232
|
+
|
|
233
|
+
def getAvailableAllocations(self, search: str = None, page: int = None, per_page: int = None):
|
|
234
|
+
'''Returns allocations not assigned to any server.'''
|
|
235
|
+
return self._admin().nodeAvailableAllocations(self.getUUID(), search, page, per_page)
|
|
236
|
+
|
|
237
|
+
def getAllocationIPs(self) -> list:
|
|
238
|
+
'''Returns the IPs that have allocations.'''
|
|
239
|
+
return self._admin().nodeAllocationIps(self.getUUID())
|
|
240
|
+
|
|
241
|
+
def createAllocations(self, ip: str, ports, ip_alias: str = None) -> int:
|
|
242
|
+
'''
|
|
243
|
+
Creates allocations.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
ip: IP address.
|
|
247
|
+
ports: Iterable of ports, e.g. range(25565, 25600).
|
|
248
|
+
ip_alias: Hostname shown to users.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
Number of allocations created.
|
|
252
|
+
'''
|
|
253
|
+
return self._admin().createNodeAllocations(self.getUUID(), ip, list(ports), ip_alias)
|
|
254
|
+
|
|
255
|
+
def setAllocationAlias(self, ip: str, ip_alias: str = None, allocations: list = None) -> dict:
|
|
256
|
+
'''Changes the IP and/or alias of allocations (all when `allocations` is omitted).'''
|
|
257
|
+
return self._admin().updateNodeAllocations(self.getUUID(), _allocationSelector(allocations), ip, ip_alias)
|
|
258
|
+
|
|
259
|
+
def deleteAllocations(self, allocations: list = None, force: bool = False) -> dict:
|
|
260
|
+
'''
|
|
261
|
+
Deletes allocations.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
allocations: Allocations (dicts or UUIDs). Every allocation when omitted.
|
|
265
|
+
force: Also delete allocations that are assigned to servers.
|
|
266
|
+
'''
|
|
267
|
+
return self._admin().deleteNodeAllocations(self.getUUID(), _allocationSelector(allocations), force)
|
|
268
|
+
|
|
269
|
+
# mounts, devices and database hosts
|
|
270
|
+
def getMounts(self, search: str = None, page: int = None, per_page: int = None):
|
|
271
|
+
'''Returns the mounts attached to the node.'''
|
|
272
|
+
return self._api().related(self.getUUID(), 'mounts', 'mounts', search, page, per_page)
|
|
273
|
+
|
|
274
|
+
def attachMount(self, mount) -> None:
|
|
275
|
+
'''Attaches a mount to the node.'''
|
|
276
|
+
return self._api().attach(self.getUUID(), 'mounts', 'mount_uuid', idOf(mount))
|
|
277
|
+
|
|
278
|
+
def detachMount(self, mount) -> None:
|
|
279
|
+
'''Detaches a mount from the node.'''
|
|
280
|
+
return self._api().detach(self.getUUID(), 'mounts', idOf(mount))
|
|
281
|
+
|
|
282
|
+
def isMountAllowed(self, mount) -> bool:
|
|
283
|
+
'''Returns True if the daemon allows the mount's source path.'''
|
|
284
|
+
return self._admin().nodeGet(self.getUUID(), f'mounts/{idOf(mount)}/allowed')['allowed']
|
|
285
|
+
|
|
286
|
+
def getDevices(self, search: str = None, page: int = None, per_page: int = None):
|
|
287
|
+
'''Returns the devices attached to the node.'''
|
|
288
|
+
return self._api().related(self.getUUID(), 'devices', 'devices', search, page, per_page)
|
|
289
|
+
|
|
290
|
+
def attachDevice(self, device) -> None:
|
|
291
|
+
'''Attaches a device to the node.'''
|
|
292
|
+
return self._api().attach(self.getUUID(), 'devices', 'device_uuid', idOf(device))
|
|
293
|
+
|
|
294
|
+
def detachDevice(self, device) -> None:
|
|
295
|
+
'''Detaches a device from the node.'''
|
|
296
|
+
return self._api().detach(self.getUUID(), 'devices', idOf(device))
|
|
297
|
+
|
|
298
|
+
def isDeviceAllowed(self, device) -> bool:
|
|
299
|
+
'''Returns True if the daemon allows the device.'''
|
|
300
|
+
return self._admin().nodeGet(self.getUUID(), f'devices/{idOf(device)}/allowed')['allowed']
|
|
301
|
+
|
|
302
|
+
def getDatabaseHosts(self, search: str = None, page: int = None, per_page: int = None):
|
|
303
|
+
'''Returns database hosts linked to the node.'''
|
|
304
|
+
return self._api().related(self.getUUID(), 'database-hosts', 'database_hosts', search, page, per_page)
|
|
305
|
+
|
|
306
|
+
def attachDatabaseHost(self, database_host) -> None:
|
|
307
|
+
'''Links a database host to the node.'''
|
|
308
|
+
return self._api().attach(self.getUUID(), 'database-hosts', 'database_host_uuid', idOf(database_host))
|
|
309
|
+
|
|
310
|
+
def detachDatabaseHost(self, database_host) -> None:
|
|
311
|
+
'''Unlinks a database host.'''
|
|
312
|
+
return self._api().detach(self.getUUID(), 'database-hosts', idOf(database_host))
|
|
313
|
+
|
|
314
|
+
def getDatabaseAgentHosts(self, search: str = None, page: int = None, per_page: int = None):
|
|
315
|
+
'''Returns database agent hosts linked to the node.'''
|
|
316
|
+
return self._api().related(self.getUUID(), 'database-agent-hosts', 'database_agent_hosts', search, page, per_page)
|
|
317
|
+
|
|
318
|
+
def attachDatabaseAgentHost(self, host) -> None:
|
|
319
|
+
'''Links a database agent host to the node.'''
|
|
320
|
+
return self._api().attach(self.getUUID(), 'database-agent-hosts', 'database_agent_host_uuid', idOf(host))
|
|
321
|
+
|
|
322
|
+
def detachDatabaseAgentHost(self, host) -> None:
|
|
323
|
+
'''Unlinks a database agent host.'''
|
|
324
|
+
return self._api().detach(self.getUUID(), 'database-agent-hosts', idOf(host))
|
|
325
|
+
|
|
326
|
+
# backups
|
|
327
|
+
def getBackups(self, detached: bool = None, search: str = None, page: int = None, per_page: int = None):
|
|
328
|
+
'''Returns backups stored on the node. `detached=True` for backups whose server is gone.'''
|
|
329
|
+
return self._admin().nodeBackups(self.getUUID(), detached, search, page, per_page)
|
|
330
|
+
|
|
331
|
+
def getBackup(self, backup_id: str) -> dict:
|
|
332
|
+
'''Returns one backup stored on the node.'''
|
|
333
|
+
return self._admin().nodeBackup(self.getUUID(), backup_id)
|
|
334
|
+
|
|
335
|
+
def getBackupArchiveInfo(self, backup_id: str) -> dict:
|
|
336
|
+
'''Returns the archive format and size a backup download would have.'''
|
|
337
|
+
return self._admin().nodeBackupAction(self.getUUID(), backup_id, 'query', 'GET')
|
|
338
|
+
|
|
339
|
+
def getBackupDownloadUrl(self, backup_id: str) -> str:
|
|
340
|
+
'''Returns a signed download URL for a backup.'''
|
|
341
|
+
return self._admin().nodeBackupAction(self.getUUID(), backup_id, 'download', 'GET')['url']
|
|
342
|
+
|
|
343
|
+
def restoreBackup(self, backup_id: str, server, truncate_directory: bool = False, restore_startup: bool = None) -> None:
|
|
344
|
+
'''Restores a node backup onto a server.'''
|
|
345
|
+
return self._admin().nodeBackupAction(self.getUUID(), backup_id, 'restore', json={
|
|
346
|
+
'server_uuid': idOf(server),
|
|
347
|
+
'truncate_directory': truncate_directory,
|
|
348
|
+
'restore_startup': restore_startup,
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
def exportBackup(self, backup_id: str, server, path: str, archive_format: str = None, foreground: bool = None) -> dict:
|
|
352
|
+
'''Writes a node backup as an archive into a server's files.'''
|
|
353
|
+
return self._admin().nodeBackupAction(self.getUUID(), backup_id, 'export', json={
|
|
354
|
+
'server_uuid': idOf(server),
|
|
355
|
+
'path': path,
|
|
356
|
+
'archive_format': archive_format,
|
|
357
|
+
'foreground': foreground,
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
def detachBackup(self, backup_id: str) -> None:
|
|
361
|
+
'''Detaches a backup from its server, keeping the backup.'''
|
|
362
|
+
return self._admin().nodeBackupAction(self.getUUID(), backup_id, 'detach')
|
|
363
|
+
|
|
364
|
+
def reattachBackup(self, backup_id: str, server) -> None:
|
|
365
|
+
'''Attaches a detached backup to a server.'''
|
|
366
|
+
return self._admin().nodeBackupAction(self.getUUID(), backup_id, 'reattach', json={'server_uuid': idOf(server)})
|
|
367
|
+
|
|
368
|
+
def reassignBackup(self, backup_id: str, database_instance) -> None:
|
|
369
|
+
'''Assigns a database backup to another database instance.'''
|
|
370
|
+
return self._admin().nodeBackupAction(self.getUUID(), backup_id, 'reassign', json={'database_instance_uuid': idOf(database_instance)})
|
|
371
|
+
|
|
372
|
+
def deleteBackup(self, backup_id: str, force: bool = False) -> None:
|
|
373
|
+
'''Deletes a backup stored on the node.'''
|
|
374
|
+
return self._admin().nodeBackupAction(self.getUUID(), backup_id, None, 'DELETE', json={'force': force})
|
|
375
|
+
|
|
376
|
+
def deleteFailedBackups(self, force: bool = False, detached: bool = None) -> int:
|
|
377
|
+
'''Deletes failed backups on the node. Returns how many were queued.'''
|
|
378
|
+
return self._admin().nodeDeleteFailedBackups(self.getUUID(), force, detached)
|
|
379
|
+
|
|
380
|
+
# tunnel
|
|
381
|
+
def getTunnel(self) -> dict:
|
|
382
|
+
'''Returns the node tunnel and its status.'''
|
|
383
|
+
return self._admin().nodeTunnel(self.getUUID())
|
|
384
|
+
|
|
385
|
+
def createTunnel(self, host: str, port: int) -> dict:
|
|
386
|
+
'''Creates the node tunnel endpoint.'''
|
|
387
|
+
return self._admin().nodeTunnel(self.getUUID(), 'POST', {'host': host, 'port': port})['tunnel']
|
|
388
|
+
|
|
389
|
+
def updateTunnel(self, host: str = None, port: int = None) -> dict:
|
|
390
|
+
'''Changes the tunnel endpoint.'''
|
|
391
|
+
return self._admin().nodeTunnel(self.getUUID(), 'PATCH', {'host': host, 'port': port})['tunnel']
|
|
392
|
+
|
|
393
|
+
def deleteTunnel(self) -> None:
|
|
394
|
+
'''Removes the node tunnel.'''
|
|
395
|
+
return self._admin().nodeTunnel(self.getUUID(), 'DELETE')
|
|
396
|
+
|
|
397
|
+
def getTunnelMetrics(self) -> dict:
|
|
398
|
+
'''Returns tunnel traffic metrics for the node and its peers.'''
|
|
399
|
+
return self._admin().nodeTunnel(self.getUUID(), suffix='/metrics')
|
|
400
|
+
|
|
401
|
+
def rotateTunnel(self) -> None:
|
|
402
|
+
'''Rotates the tunnel credentials.'''
|
|
403
|
+
return self._admin().nodeTunnel(self.getUUID(), 'POST', suffix='/rotate')
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
class NodeManager(AdminManager):
|
|
407
|
+
'''
|
|
408
|
+
Node management, available as `panel.admin.nodes`.
|
|
409
|
+
|
|
410
|
+
Example:
|
|
411
|
+
node = panel.admin.nodes.createNode('node-1', location, 'https://node1.example.com:8080', memory=65536, disk=1048576)
|
|
412
|
+
print(node.createEnrollmentCode()['code'])
|
|
413
|
+
'''
|
|
414
|
+
model = Node
|
|
415
|
+
|
|
416
|
+
def getNodes(self, search: str = None, page: int = None, per_page: int = None):
|
|
417
|
+
'''Lists nodes as a Page of Node objects.'''
|
|
418
|
+
return self._list(search, page, per_page)
|
|
419
|
+
|
|
420
|
+
def getNode(self, node_id: str) -> Node:
|
|
421
|
+
'''Returns a node by UUID.'''
|
|
422
|
+
return self._get(node_id)
|
|
423
|
+
|
|
424
|
+
def getNodeByName(self, name: str) -> Node:
|
|
425
|
+
'''Returns a node by exact name, or None.'''
|
|
426
|
+
return self._find(name)
|
|
427
|
+
|
|
428
|
+
def createNode(self, name: str, location, url: str, memory: int, disk: int, sftp_port: int = 2022,
|
|
429
|
+
deployment_enabled: bool = True, maintenance_enabled: bool = False, description: str = None,
|
|
430
|
+
public_url: str = None, sftp_host: str = None, backup_configuration=None) -> Node:
|
|
431
|
+
'''
|
|
432
|
+
Creates a node.
|
|
433
|
+
|
|
434
|
+
Args:
|
|
435
|
+
name: Node name.
|
|
436
|
+
location: Location object or UUID.
|
|
437
|
+
url: Daemon URL, e.g. "https://node1.example.com:8080".
|
|
438
|
+
memory: Memory available for servers, in MiB.
|
|
439
|
+
disk: Disk available for servers, in MiB.
|
|
440
|
+
sftp_port: SFTP port.
|
|
441
|
+
'''
|
|
442
|
+
return self._create(
|
|
443
|
+
name=name,
|
|
444
|
+
location_uuid=idOf(location),
|
|
445
|
+
url=url,
|
|
446
|
+
memory=memory,
|
|
447
|
+
disk=disk,
|
|
448
|
+
sftp_port=sftp_port,
|
|
449
|
+
deployment_enabled=deployment_enabled,
|
|
450
|
+
maintenance_enabled=maintenance_enabled,
|
|
451
|
+
description=description,
|
|
452
|
+
public_url=public_url,
|
|
453
|
+
sftp_host=sftp_host,
|
|
454
|
+
backup_configuration_uuid=idOf(backup_configuration),
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
def deleteNode(self, node) -> None:
|
|
458
|
+
'''Deletes a node. It must not have servers.'''
|
|
459
|
+
return self._delete(node)
|
|
460
|
+
|
|
461
|
+
def getCapacities(self) -> dict:
|
|
462
|
+
'''Returns allocated resources of every node.'''
|
|
463
|
+
return self.admin.nodeCapacities()
|
|
464
|
+
|
|
465
|
+
def probe(self, url: str, pairing_code: str) -> dict:
|
|
466
|
+
'''Checks whether a daemon at `url` is reachable and shows `pairing_code`.'''
|
|
467
|
+
return self.admin.probeNode(url, pairing_code)
|
|
468
|
+
|
|
469
|
+
def updateConfigs(self, nodes: list, config: dict) -> dict:
|
|
470
|
+
'''Applies the same daemon configuration change to several nodes.'''
|
|
471
|
+
return self.admin.updateNodesConfig([idOf(n) for n in nodes], config)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _allocationSelector(allocations):
|
|
475
|
+
if not allocations:
|
|
476
|
+
return {'type': 'all'}
|
|
477
|
+
return {'type': 'uuids', 'uuids': [a.get('uuid') if isinstance(a, dict) else idOf(a) for a in allocations]}
|
calagoapi/panel.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
|
|
3
|
+
from calagoapi.api import API, Client
|
|
4
|
+
from calagoapi.servers import ServerManager
|
|
5
|
+
from calagoapi.account import Account, Auth
|
|
6
|
+
from calagoapi.admin import Admin
|
|
7
|
+
from calagoapi.utils.exceptions import CalagoAPIError, PanelConnectionError, ServerError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Panel:
|
|
11
|
+
'''
|
|
12
|
+
A connection to a Calagopus panel. This is the entry point of CalagoAPI.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
url: Panel URL, e.g. "https://panel.example.com".
|
|
16
|
+
api_key: API key from Account -> API Keys.
|
|
17
|
+
name: Optional label, handy when managing several panels.
|
|
18
|
+
timeout: Request timeout in seconds.
|
|
19
|
+
verify: Verify TLS certificates. Set to False for self-signed panels.
|
|
20
|
+
max_retries: How often rate limited (429) requests, and GET requests that hit
|
|
21
|
+
a connection error or 502/503/504, are retried. 0 disables retries.
|
|
22
|
+
|
|
23
|
+
Attributes:
|
|
24
|
+
servers: ServerManager, the servers the API key can access.
|
|
25
|
+
account: Account, the user that owns the API key.
|
|
26
|
+
admin: Admin, panel administration (requires admin permissions).
|
|
27
|
+
auth: Auth, public login/registration endpoints.
|
|
28
|
+
api: Internal API modules, for calls not wrapped by CalagoAPI.
|
|
29
|
+
|
|
30
|
+
Example:
|
|
31
|
+
from calagoapi import Panel
|
|
32
|
+
|
|
33
|
+
panel = Panel(
|
|
34
|
+
url='https://panel.example.com',
|
|
35
|
+
api_key='...'
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
server = panel.servers.getServer('abc123')
|
|
39
|
+
server.sendCommand('say Hello')
|
|
40
|
+
server.restart()
|
|
41
|
+
|
|
42
|
+
# or close the connections automatically
|
|
43
|
+
with Panel(url='https://panel.example.com', api_key='...') as panel:
|
|
44
|
+
print(panel.servers.getServers())
|
|
45
|
+
'''
|
|
46
|
+
def __init__(self, url: str, api_key: str, name: str = None, timeout: int = 10, verify: bool = True,
|
|
47
|
+
max_retries: int = 3):
|
|
48
|
+
self.name = name or url
|
|
49
|
+
self.url = url.rstrip('/')
|
|
50
|
+
self.api_key = api_key
|
|
51
|
+
|
|
52
|
+
# internal API layer
|
|
53
|
+
self.client = Client(self.url, api_key, timeout=timeout, verify=verify, user_agent='CalagoAPI', max_retries=max_retries)
|
|
54
|
+
self.api = API(self.client)
|
|
55
|
+
|
|
56
|
+
# open live consoles, closed by close()
|
|
57
|
+
self.consoles = set()
|
|
58
|
+
|
|
59
|
+
# high-level managers
|
|
60
|
+
self.servers = ServerManager(self)
|
|
61
|
+
self.account = Account(self)
|
|
62
|
+
self.admin = Admin(self)
|
|
63
|
+
self.auth = Auth(self)
|
|
64
|
+
|
|
65
|
+
def __repr__(self):
|
|
66
|
+
return f'<Panel {self.name!r}>'
|
|
67
|
+
|
|
68
|
+
def __enter__(self):
|
|
69
|
+
return self
|
|
70
|
+
|
|
71
|
+
def __exit__(self, *exc):
|
|
72
|
+
self.close()
|
|
73
|
+
|
|
74
|
+
def close(self) -> None:
|
|
75
|
+
'''
|
|
76
|
+
Closes the pooled HTTP connections and any open console connection.
|
|
77
|
+
|
|
78
|
+
Called automatically when the Panel is used in a `with` block.
|
|
79
|
+
'''
|
|
80
|
+
for console in list(self.consoles):
|
|
81
|
+
# async consoles are closed by AsyncPanel.close()
|
|
82
|
+
if not inspect.iscoroutinefunction(console.close):
|
|
83
|
+
console.close()
|
|
84
|
+
self.client.close()
|
|
85
|
+
|
|
86
|
+
def __str__(self):
|
|
87
|
+
hidden = f'{self.api_key[:6]}...' if self.api_key else None
|
|
88
|
+
return f'Panel name: {self.name}\nURL: {self.url}\nAPI key: {hidden}'
|
|
89
|
+
|
|
90
|
+
# http core
|
|
91
|
+
def get(self, path: str, params: dict = None):
|
|
92
|
+
'''
|
|
93
|
+
Sends a raw GET request and returns the decoded response.
|
|
94
|
+
|
|
95
|
+
Example:
|
|
96
|
+
panel.get('/api/client/permissions')
|
|
97
|
+
'''
|
|
98
|
+
return self.client.get(path, params=params)
|
|
99
|
+
|
|
100
|
+
def post(self, path: str, json: dict = None, params: dict = None):
|
|
101
|
+
'''Sends a raw POST request and returns the decoded response.'''
|
|
102
|
+
return self.client.post(path, json=json, params=params)
|
|
103
|
+
|
|
104
|
+
def put(self, path: str, json: dict = None, params: dict = None):
|
|
105
|
+
'''Sends a raw PUT request and returns the decoded response.'''
|
|
106
|
+
return self.client.put(path, json=json, params=params)
|
|
107
|
+
|
|
108
|
+
def patch(self, path: str, json: dict = None, params: dict = None):
|
|
109
|
+
'''Sends a raw PATCH request and returns the decoded response.'''
|
|
110
|
+
return self.client.patch(path, json=json, params=params)
|
|
111
|
+
|
|
112
|
+
def delete(self, path: str, json: dict = None, params: dict = None):
|
|
113
|
+
'''Sends a raw DELETE request and returns the decoded response.'''
|
|
114
|
+
return self.client.delete(path, json=json, params=params)
|
|
115
|
+
|
|
116
|
+
# panel health
|
|
117
|
+
def ping(self) -> bool:
|
|
118
|
+
'''
|
|
119
|
+
Returns True if the panel answers, without checking the API key.
|
|
120
|
+
|
|
121
|
+
Example:
|
|
122
|
+
if not panel.ping():
|
|
123
|
+
print('Panel is down')
|
|
124
|
+
'''
|
|
125
|
+
try:
|
|
126
|
+
self.client.request('GET', '/api/auth/oauth', raw=True)
|
|
127
|
+
return True
|
|
128
|
+
except (PanelConnectionError, ServerError):
|
|
129
|
+
return False
|
|
130
|
+
except CalagoAPIError:
|
|
131
|
+
# any other HTTP error still means the panel answered
|
|
132
|
+
return True
|
|
133
|
+
|
|
134
|
+
def validateAPI(self) -> bool:
|
|
135
|
+
'''Returns True if the API key is valid.'''
|
|
136
|
+
try:
|
|
137
|
+
self.api.account.get()
|
|
138
|
+
return True
|
|
139
|
+
except CalagoAPIError:
|
|
140
|
+
return False
|
|
141
|
+
|
|
142
|
+
def isAdmin(self) -> bool:
|
|
143
|
+
'''Returns True if the API key belongs to a root administrator.'''
|
|
144
|
+
return self.account.isAdmin()
|
|
145
|
+
|
|
146
|
+
# public panel information
|
|
147
|
+
def getSettings(self) -> dict:
|
|
148
|
+
'''
|
|
149
|
+
Returns the public panel settings (app name, captcha, enabled features, server time, ...).
|
|
150
|
+
|
|
151
|
+
Example:
|
|
152
|
+
print(panel.getSettings()['app']['name'])
|
|
153
|
+
'''
|
|
154
|
+
return self.api.public.settings()
|
|
155
|
+
|
|
156
|
+
def getLanguages(self) -> list:
|
|
157
|
+
'''Returns the languages the panel is translated into.'''
|
|
158
|
+
return self.api.public.languages()
|
|
159
|
+
|
|
160
|
+
def getAnnouncements(self) -> list:
|
|
161
|
+
'''Returns the announcements currently shown to everyone.'''
|
|
162
|
+
return self.api.public.announcements()
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from .schedule import Schedule, ScheduleStep
|
|
2
|
+
from .manager import ScheduleManager
|
|
3
|
+
from .builders import triggers, conditions, actions, backups, variable
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
'Schedule',
|
|
7
|
+
'ScheduleStep',
|
|
8
|
+
'ScheduleManager',
|
|
9
|
+
'triggers',
|
|
10
|
+
'conditions',
|
|
11
|
+
'actions',
|
|
12
|
+
'backups',
|
|
13
|
+
'variable',
|
|
14
|
+
]
|