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,437 @@
1
+ import os
2
+
3
+ from calagoapi.files.file import File, splitPath
4
+ from calagoapi.utils.model import idOf
5
+ from calagoapi.utils.exceptions import NotFoundError
6
+
7
+
8
+ class FileManager:
9
+ '''
10
+ File management for one server, available as `server.files`.
11
+
12
+ Paths are always absolute paths inside the server, e.g. "/plugins/config.yml".
13
+
14
+ Args:
15
+ server: The server to manage files for.
16
+
17
+ Example:
18
+ files = server.files.listFiles('/')
19
+ props = server.files.readFile('/server.properties')
20
+ server.files.writeFile('/eula.txt', 'eula=true')
21
+ server.files.uploadFile('/plugins', 'MyPlugin.jar')
22
+ '''
23
+ def __init__(self, server):
24
+ self.server = server
25
+ self.api = server.panel.api.files
26
+
27
+ @property
28
+ def server_id(self):
29
+ return self.server.server_id
30
+
31
+ # browsing
32
+ def listFiles(self, directory: str = '/', sort: str = None, page: int = None, per_page: int = None):
33
+ '''
34
+ Lists the contents of a directory.
35
+
36
+ Args:
37
+ directory: Directory to list, defaults to the server root.
38
+ sort: Optional Calagopus sorting mode.
39
+ page: Only fetch this page. By default every entry is returned.
40
+ per_page: Page size.
41
+
42
+ Returns:
43
+ A Page (list) of File objects.
44
+
45
+ Example:
46
+ for file in server.files.listFiles('/world'):
47
+ print(file.getName(), file.isDirectory())
48
+ '''
49
+ _, entries = self.api.list(self.server_id, directory, sort, page, per_page)
50
+ return entries.map(lambda entry: File(self.server, entry, directory))
51
+
52
+ def getFile(self, path: str) -> File:
53
+ '''
54
+ Returns details about a single file or directory.
55
+
56
+ Example:
57
+ file = server.files.getFile('/server.properties')
58
+ print(file.getSize(formatted=True), file.getModified())
59
+ '''
60
+ directory, name = splitPath(path)
61
+ entries = self.api.stat(self.server_id, [name], directory)
62
+ return File(self.server, entries[0], directory) if entries else None
63
+
64
+ def exists(self, path: str) -> bool:
65
+ '''Returns True if the file or directory exists.'''
66
+ try:
67
+ return self.getFile(path) is not None
68
+ except NotFoundError:
69
+ return False
70
+
71
+ def getFilesystemInfo(self, directory: str = '/') -> dict:
72
+ '''
73
+ Returns information about the server filesystem.
74
+
75
+ Returns:
76
+ dict with is_filesystem_primary, is_filesystem_writable, is_filesystem_fast
77
+ and the list of in-progress uploads.
78
+ '''
79
+ result, _ = self.api.list(self.server_id, directory, page=1, per_page=1)
80
+ return {k: v for k, v in result.items() if k != 'entries'}
81
+
82
+ def searchFiles(self, query: str = None, root: str = '/', include: list = None, exclude: list = None,
83
+ min_size: int = None, max_size: int = None, case_insensitive: bool = True, context_lines: int = None):
84
+ '''
85
+ Searches files by path and/or contents.
86
+
87
+ Args:
88
+ query: Text to search for inside files.
89
+ root: Directory to search in.
90
+ include: Glob patterns of paths to include, e.g. ['*.yml'].
91
+ exclude: Glob patterns of paths to exclude.
92
+ min_size: Minimum file size in bytes.
93
+ max_size: Maximum file size in bytes.
94
+ case_insensitive: Ignore case for path patterns and contents.
95
+ context_lines: Lines of context to return around content matches.
96
+
97
+ Returns:
98
+ dict with `entries` (matching files) and `content_matches`.
99
+
100
+ Example:
101
+ result = server.files.searchFiles('max-players', include=['*.properties'])
102
+ '''
103
+ path_filter = None
104
+ if include or exclude:
105
+ path_filter = {'include': include or [], 'exclude': exclude or [], 'case_insensitive': case_insensitive}
106
+ size_filter = None
107
+ if min_size is not None or max_size is not None:
108
+ size_filter = {'min': min_size or 0, 'max': max_size if max_size is not None else 2**53}
109
+ content_filter = None
110
+ if query:
111
+ content_filter = {'query': query, 'max_search_size': 10 * 1024 * 1024, 'include_unmatched': False, 'case_insensitive': case_insensitive}
112
+ match_context = None
113
+ if context_lines is not None:
114
+ match_context = {'before': context_lines, 'after': context_lines, 'max_matches': 100}
115
+ result = self.api.search(self.server_id, root, path_filter, size_filter, content_filter, match_context)
116
+ result['entries'] = [File(self.server, entry, root) for entry in result.get('entries') or []]
117
+ return result
118
+
119
+ def getDirectorySizes(self, directory: str = '/', depth: int = None) -> dict:
120
+ '''
121
+ Returns a size breakdown of a directory tree.
122
+
123
+ Args:
124
+ directory: Directory to measure.
125
+ depth: How many levels of children to include.
126
+ '''
127
+ return self.api.directorySizes(self.server_id, directory, depth)
128
+
129
+ def getLargestDirectories(self, directory: str = '/') -> list:
130
+ '''Returns the largest directories under `directory`, biggest first.'''
131
+ return self.api.largestDirectories(self.server_id, directory)
132
+
133
+ def getFingerprint(self, path: str, algorithm: str = None) -> str:
134
+ '''
135
+ Returns a checksum of a file.
136
+
137
+ Args:
138
+ path: File to hash.
139
+ algorithm: Hash algorithm, e.g. 'sha256', 'md5'. Uses the panel default if omitted.
140
+ '''
141
+ return self.api.fingerprint(self.server_id, path, algorithm)
142
+
143
+ # reading and writing
144
+ def readFile(self, path: str) -> str:
145
+ '''
146
+ Returns the contents of a text file.
147
+
148
+ Example:
149
+ properties = server.files.readFile('/server.properties')
150
+ '''
151
+ return self.api.contents(self.server_id, path)
152
+
153
+ def readLines(self, path: str, start_line: int = None, end_line: int = None) -> dict:
154
+ '''
155
+ Reads a range of lines from a large file without downloading all of it.
156
+
157
+ Returns:
158
+ dict with content, start_line, end_line and eof.
159
+
160
+ Example:
161
+ chunk = server.files.readLines('/logs/latest.log', 1, 100)
162
+ print(chunk['content'])
163
+ '''
164
+ return self.api.lines(self.server_id, path, start_line, end_line)
165
+
166
+ def writeFile(self, path: str, content) -> None:
167
+ '''
168
+ Creates or replaces a file.
169
+
170
+ Args:
171
+ path: File to write.
172
+ content: Text (str) or bytes.
173
+
174
+ Example:
175
+ server.files.writeFile('/eula.txt', 'eula=true')
176
+ '''
177
+ return self.api.write(self.server_id, path, content)
178
+
179
+ def getRevisions(self, path: str) -> list:
180
+ '''Returns the saved revisions (edit history) of a file.'''
181
+ return self.api.revisions(self.server_id, path)
182
+
183
+ def getRevision(self, path: str, revision_id: str) -> str:
184
+ '''Returns the contents of a file at a previous revision.'''
185
+ return self.api.revision(self.server_id, path, revision_id)
186
+
187
+ # transfer
188
+ def getDownloadUrl(self, path: str, archive_format: str = None) -> str:
189
+ '''
190
+ Returns a signed, temporary URL to download a file or directory.
191
+
192
+ Args:
193
+ path: File or directory to download.
194
+ archive_format: Archive format used for directories, e.g. 'zip'.
195
+ '''
196
+ directory, name = splitPath(path)
197
+ return self.api.downloadUrl(self.server_id, [name], directory, archive_format=archive_format)
198
+
199
+ def downloadFile(self, path: str, destination: str = None):
200
+ '''
201
+ Downloads a file.
202
+
203
+ Args:
204
+ path: File on the server.
205
+ destination: Local path to save to. If omitted the bytes are returned.
206
+
207
+ Returns:
208
+ bytes, or the destination path.
209
+
210
+ Example:
211
+ server.files.downloadFile('/world.zip', 'world.zip')
212
+ '''
213
+ r = self.api.fetch(self.getDownloadUrl(path), stream=destination is not None)
214
+ if destination is None:
215
+ return r.content
216
+ with open(destination, 'wb') as f:
217
+ for chunk in r.iter_content(chunk_size=1024 * 1024):
218
+ f.write(chunk)
219
+ return destination
220
+
221
+ def getUploadUrl(self) -> str:
222
+ '''Returns a signed, temporary URL that accepts multipart uploads.'''
223
+ return self.api.uploadUrl(self.server_id)
224
+
225
+ def uploadFile(self, directory: str, file, name: str = None):
226
+ '''
227
+ Uploads a file into a directory.
228
+
229
+ Args:
230
+ directory: Target directory on the server.
231
+ file: Local file path, bytes, or an open binary file.
232
+ name: File name on the server. Defaults to the local file name.
233
+
234
+ Example:
235
+ server.files.uploadFile('/plugins', 'build/MyPlugin.jar')
236
+ server.files.uploadFile('/', b'eula=true', name='eula.txt')
237
+ '''
238
+ if isinstance(file, (str, os.PathLike)):
239
+ with open(file, 'rb') as f:
240
+ return self.api.upload(self.server_id, directory, [(name or os.path.basename(file), f.read())])
241
+ if name is None:
242
+ name = os.path.basename(getattr(file, 'name', '')) or 'upload'
243
+ data = file.read() if hasattr(file, 'read') else file
244
+ return self.api.upload(self.server_id, directory, [(name, data)])
245
+
246
+ def pullFile(self, url: str, directory: str = '/', name: str = None, use_header: bool = None, foreground: bool = None):
247
+ '''
248
+ Makes the server download a file from a URL.
249
+
250
+ Args:
251
+ url: Remote URL.
252
+ directory: Directory to save into.
253
+ name: File name, defaults to the name from the URL.
254
+ use_header: Use the Content-Disposition header for the name.
255
+ foreground: Wait for the download to finish before returning.
256
+
257
+ Example:
258
+ server.files.pullFile('https://example.com/plugin.jar', '/plugins')
259
+ '''
260
+ return self.api.pull(self.server_id, url, directory, name, use_header, foreground)
261
+
262
+ def queryPull(self, url: str) -> dict:
263
+ '''Inspects a remote URL before pulling it (file name, size, ...).'''
264
+ return self.api.queryPull(self.server_id, url)
265
+
266
+ # management
267
+ def deleteFiles(self, root: str, files: list) -> None:
268
+ '''
269
+ Deletes several files from one directory.
270
+
271
+ Example:
272
+ server.files.deleteFiles('/logs', ['old.log', 'older.log'])
273
+ '''
274
+ return self.api.delete(self.server_id, root, files)
275
+
276
+ def deleteFile(self, path: str) -> None:
277
+ '''
278
+ Deletes a single file or directory.
279
+
280
+ Example:
281
+ server.files.deleteFile('/world/session.lock')
282
+ '''
283
+ directory, name = splitPath(path)
284
+ return self.api.delete(self.server_id, directory, [name])
285
+
286
+ def renameFiles(self, root: str, files: list) -> None:
287
+ '''
288
+ Renames or moves several files.
289
+
290
+ Args:
291
+ root: Directory the paths are relative to.
292
+ files: List of {'from': ..., 'to': ...} dicts or (from, to) tuples.
293
+
294
+ Example:
295
+ server.files.renameFiles('/', [('world', 'world_old')])
296
+ '''
297
+ return self.api.rename(self.server_id, root, self._pairs(files))
298
+
299
+ def renameFile(self, path: str, new_path: str) -> None:
300
+ '''
301
+ Renames or moves a single file.
302
+
303
+ Example:
304
+ server.files.renameFile('/server.jar', '/server.old.jar')
305
+ '''
306
+ return self.api.rename(self.server_id, '/', [{'from': path.lstrip('/'), 'to': new_path.lstrip('/')}])
307
+
308
+ def moveFiles(self, root: str, files: list) -> None:
309
+ '''Moves files, same as renameFiles(). Kept for HungerLib compatibility.'''
310
+ return self.renameFiles(root, files)
311
+
312
+ def copyFile(self, path: str, destination: str = None, overwrite: bool = None, foreground: bool = None):
313
+ '''
314
+ Copies a single file.
315
+
316
+ Args:
317
+ path: File to copy.
318
+ destination: Target path. Calagopus picks a "copy" name when omitted.
319
+ overwrite: Replace the destination if it exists.
320
+ '''
321
+ result = self.api.copy(self.server_id, path, destination, overwrite, foreground)
322
+ return File(self.server, result['entry'], splitPath(destination or path)[0]) if result and result.get('entry') else result
323
+
324
+ def copyFiles(self, root: str, files: list, overwrite: bool = None, foreground: bool = None) -> dict:
325
+ '''
326
+ Copies several files.
327
+
328
+ Args:
329
+ root: Directory the paths are relative to.
330
+ files: List of {'from': ..., 'to': ...} dicts or (from, to) tuples.
331
+
332
+ Returns:
333
+ dict with the number copied and the files skipped.
334
+ '''
335
+ return self.api.copyMany(self.server_id, root, self._pairs(files), overwrite, foreground)
336
+
337
+ def copyToServer(self, files: list, destination_server, destination: str = '/', root: str = '/', foreground: bool = None):
338
+ '''
339
+ Copies files to another server.
340
+
341
+ Args:
342
+ files: List of {'from': ..., 'to': ...} dicts or (from, to) tuples.
343
+ destination_server: Server object or server id.
344
+ destination: Target directory on the other server.
345
+ root: Source directory the paths are relative to.
346
+ '''
347
+ return self.api.copyRemote(self.server_id, root, self._pairs(files), destination, idOf(destination_server), foreground)
348
+
349
+ def copyToServers(self, files: list, destination_servers: list, destination: str = '/', root: str = '/') -> list:
350
+ '''Copies files to several servers at once.'''
351
+ servers = [idOf(s) for s in destination_servers]
352
+ return self.api.copyRemoteMany(self.server_id, root, self._pairs(files), destination, servers)
353
+
354
+ def createFolder(self, directory: str, name: str) -> None:
355
+ '''
356
+ Creates a directory.
357
+
358
+ Example:
359
+ server.files.createFolder('/', 'backups')
360
+ '''
361
+ return self.api.createDirectory(self.server_id, directory, name)
362
+
363
+ def createDirectory(self, path: str) -> None:
364
+ '''Creates a directory from a full path, e.g. "/plugins/MyPlugin".'''
365
+ directory, name = splitPath(path)
366
+ return self.api.createDirectory(self.server_id, directory, name)
367
+
368
+ def createSymlink(self, link: str, target: str, root: str = '/') -> None:
369
+ '''Creates a symbolic link at `link` pointing to `target`.'''
370
+ return self.api.createSymlink(self.server_id, root, link, target)
371
+
372
+ def compress(self, root: str, files: list, format: str = 'tar_gz', name: str = None, foreground: bool = None):
373
+ '''
374
+ Compresses files into an archive.
375
+
376
+ Args:
377
+ root: Directory containing the files.
378
+ files: File names inside root.
379
+ format: tar, tar_gz, tar_xz, tar_bz2, tar_lz4, tar_zstd, zip or seven_zip.
380
+ name: Archive name. Generated when omitted.
381
+
382
+ Returns:
383
+ The created archive as a File, when the panel returns it.
384
+
385
+ Example:
386
+ archive = server.files.compress('/', ['world'], format='zip')
387
+ '''
388
+ result = self.api.compress(self.server_id, root, files, format, name, foreground)
389
+ return File(self.server, result['entry'], root) if result and result.get('entry') else result
390
+
391
+ def decompress(self, file_path: str, foreground: bool = None) -> None:
392
+ '''
393
+ Extracts an archive into the directory it lives in.
394
+
395
+ Example:
396
+ server.files.decompress('/uploads/world.zip')
397
+ '''
398
+ directory, name = splitPath(file_path)
399
+ return self.api.decompress(self.server_id, directory, name, foreground)
400
+
401
+ def chmod(self, root: str, files: list) -> None:
402
+ '''
403
+ Changes permissions of several files.
404
+
405
+ Args:
406
+ files: List of {'file': name, 'mode': '755', 'recursive': False} dicts.
407
+ '''
408
+ return self.api.chmod(self.server_id, root, files)
409
+
410
+ def chmodFile(self, path: str, mode: str, recursive: bool = False) -> None:
411
+ '''
412
+ Changes the permissions of one file.
413
+
414
+ Example:
415
+ server.files.chmodFile('/start.sh', '755')
416
+ '''
417
+ directory, name = splitPath(path)
418
+ return self.api.chmod(self.server_id, directory, [{'file': name, 'mode': str(mode), 'recursive': recursive}])
419
+
420
+ def querySQLite(self, path: str, query: str, rows: int = None, read_only: bool = True) -> list:
421
+ '''
422
+ Runs a SQL query against a SQLite database file on the server.
423
+
424
+ Example:
425
+ rows = server.files.querySQLite('/plugins/LuckPerms/luckperms-sqlite.db',
426
+ 'SELECT * FROM luckperms_players LIMIT 10')
427
+ '''
428
+ return self.api.sqliteQuery(self.server_id, path, query, rows, read_only)
429
+
430
+ def cancelOperation(self, operation_id: str) -> None:
431
+ '''Cancels a background file operation (compression, pull, copy...).'''
432
+ return self.api.cancelOperation(self.server_id, operation_id)
433
+
434
+ # helpers
435
+ @staticmethod
436
+ def _pairs(files):
437
+ return [f if isinstance(f, dict) else {'from': f[0], 'to': f[1]} for f in files]
@@ -0,0 +1,8 @@
1
+ from .allocation import Allocation, Subdomain
2
+ from .manager import NetworkManager
3
+
4
+ __all__ = [
5
+ 'Allocation',
6
+ 'Subdomain',
7
+ 'NetworkManager',
8
+ ]
@@ -0,0 +1,99 @@
1
+ from calagoapi.utils.model import Model, idOf
2
+
3
+
4
+ class Allocation(Model):
5
+ '''
6
+ An IP and port assigned to a server.
7
+
8
+ Example:
9
+ primary = server.network.getPrimaryAllocation()
10
+ print(primary.getAddress()) # 'play.example.com:25565'
11
+ extra = server.network.createAllocation()
12
+ extra.setNotes('Dynmap')
13
+ '''
14
+ def __init__(self, server, data: dict):
15
+ super().__init__(server.panel, data)
16
+ self.server = server
17
+
18
+ def __repr__(self):
19
+ return f'<Allocation {self.getAddress()!r}>'
20
+
21
+ def getIP(self) -> str:
22
+ '''Returns the IP address.'''
23
+ return self.data.get('ip')
24
+
25
+ def getAlias(self) -> str:
26
+ '''Returns the IP alias (usually a hostname), or None.'''
27
+ return self.data.get('ip_alias')
28
+
29
+ def getPort(self) -> int:
30
+ '''Returns the port.'''
31
+ return self.data.get('port')
32
+
33
+ def getAddress(self) -> str:
34
+ '''Returns "alias:port", or "ip:port" when there is no alias.'''
35
+ return f'{self.getAlias() or self.getIP()}:{self.getPort()}'
36
+
37
+ def getNotes(self) -> str:
38
+ '''Returns the allocation notes.'''
39
+ return self.data.get('notes')
40
+
41
+ def isPrimary(self) -> bool:
42
+ '''Returns True for the server's primary allocation.'''
43
+ return bool(self.data.get('is_primary'))
44
+
45
+ def setNotes(self, notes: str) -> 'Allocation':
46
+ '''Sets the allocation notes.'''
47
+ self.panel.api.network.updateAllocation(self.server.server_id, self.getUUID(), notes=notes)
48
+ return self._update({'notes': notes})
49
+
50
+ def makePrimary(self) -> 'Allocation':
51
+ '''Makes this the server's primary allocation.'''
52
+ self.panel.api.network.updateAllocation(self.server.server_id, self.getUUID(), primary=True)
53
+ return self._update({'is_primary': True})
54
+
55
+ def delete(self) -> None:
56
+ '''Removes the allocation from the server.'''
57
+ return self.panel.api.network.deleteAllocation(self.server.server_id, self.getUUID())
58
+
59
+
60
+ class Subdomain(Model):
61
+ '''
62
+ A subdomain pointing at one of the server's allocations (Subdomain Manager extension).
63
+
64
+ Example:
65
+ sub = server.network.createSubdomain('survival', domain='example.com')
66
+ print(sub.getAddress()) # 'survival.example.com'
67
+ '''
68
+ def __init__(self, server, data: dict):
69
+ super().__init__(server.panel, data)
70
+ self.server = server
71
+
72
+ def __repr__(self):
73
+ return f'<Subdomain {self.getAddress()!r}>'
74
+
75
+ def getSubdomain(self) -> str:
76
+ '''Returns the subdomain part, e.g. "survival".'''
77
+ return self.data.get('subdomain')
78
+
79
+ def getDomain(self) -> str:
80
+ '''Returns the parent domain, e.g. "example.com".'''
81
+ return (self.data.get('domain') or {}).get('domain')
82
+
83
+ def getAddress(self) -> str:
84
+ '''Returns the full hostname, e.g. "survival.example.com".'''
85
+ return f'{self.getSubdomain()}.{self.getDomain()}'
86
+
87
+ def getAllocation(self) -> Allocation:
88
+ '''Returns the allocation the subdomain points to, or None.'''
89
+ data = self.data.get('allocation')
90
+ return Allocation(self.server, data) if data else None
91
+
92
+ def setAllocation(self, allocation) -> 'Subdomain':
93
+ '''Points the subdomain at another allocation.'''
94
+ self.panel.api.network.updateSubdomain(self.server.server_id, self.getUUID(), idOf(allocation))
95
+ return self
96
+
97
+ def delete(self) -> None:
98
+ '''Deletes the subdomain.'''
99
+ return self.panel.api.network.deleteSubdomain(self.server.server_id, self.getUUID())
@@ -0,0 +1,132 @@
1
+ from calagoapi.network.allocation import Allocation, Subdomain
2
+ from calagoapi.utils.model import idOf
3
+ from calagoapi.utils.exceptions import InvalidActionError
4
+
5
+
6
+ class NetworkManager:
7
+ '''
8
+ Allocations, subdomains and firewall for one server, available as `server.network`.
9
+
10
+ Example:
11
+ print(server.network.getPrimaryAllocation().getAddress())
12
+ server.network.addFirewallRule('allow', ports=[25565], sources=['0.0.0.0/0'])
13
+ '''
14
+ def __init__(self, server):
15
+ self.server = server
16
+ self.api = server.panel.api.network
17
+
18
+ @property
19
+ def server_id(self):
20
+ return self.server.server_id
21
+
22
+ # allocations
23
+ def getAllocations(self, search: str = None, page: int = None, per_page: int = None):
24
+ '''Lists the server's allocations as a Page of Allocation objects.'''
25
+ return self.api.allocations(self.server_id, search, page, per_page).map(lambda data: Allocation(self.server, data))
26
+
27
+ def getPrimaryAllocation(self) -> Allocation:
28
+ '''Returns the primary allocation, or None.'''
29
+ return next((a for a in self.getAllocations() if a.isPrimary()), None)
30
+
31
+ def createAllocation(self) -> Allocation:
32
+ '''
33
+ Assigns a new allocation from the node's free pool.
34
+
35
+ Only works if the server's allocation limit allows it.
36
+ '''
37
+ return Allocation(self.server, self.api.createAllocation(self.server_id))
38
+
39
+ def setPrimaryAllocation(self, allocation) -> None:
40
+ '''Makes an allocation the primary one.'''
41
+ return self.api.updateAllocation(self.server_id, idOf(allocation), primary=True)
42
+
43
+ def deleteAllocation(self, allocation) -> None:
44
+ '''Removes an allocation from the server.'''
45
+ return self.api.deleteAllocation(self.server_id, idOf(allocation))
46
+
47
+ # subdomains
48
+ def getSubdomains(self, search: str = None, page: int = None, per_page: int = None):
49
+ '''Lists the server's subdomains as a Page of Subdomain objects.'''
50
+ return self.api.subdomains(self.server_id, search, page, per_page).map(lambda data: Subdomain(self.server, data))
51
+
52
+ def getAvailableDomains(self) -> list:
53
+ '''Returns the domains subdomains can be created on.'''
54
+ return self.api.subdomainDomains(self.server_id)
55
+
56
+ def createSubdomain(self, subdomain: str, domain=None, allocation=None) -> Subdomain:
57
+ '''
58
+ Creates a subdomain.
59
+
60
+ Args:
61
+ subdomain: Subdomain part, e.g. "survival".
62
+ domain: Domain name or UUID. Defaults to the first available domain.
63
+ allocation: Allocation to point to. Defaults to the primary allocation.
64
+
65
+ Example:
66
+ server.network.createSubdomain('survival', 'example.com')
67
+ '''
68
+ domains = self.getAvailableDomains()
69
+ if domain is None:
70
+ if not domains:
71
+ raise InvalidActionError('No subdomain domains are available for this server')
72
+ domain_id = domains[0]['uuid']
73
+ else:
74
+ match = next((d for d in domains if domain in (d.get('uuid'), d.get('domain'))), None)
75
+ domain_id = match['uuid'] if match else domain
76
+ if allocation is None:
77
+ allocation = self.getPrimaryAllocation()
78
+ return Subdomain(self.server, self.api.createSubdomain(self.server_id, domain_id, idOf(allocation), subdomain))
79
+
80
+ def deleteSubdomain(self, subdomain) -> None:
81
+ '''Deletes a subdomain.'''
82
+ return self.api.deleteSubdomain(self.server_id, idOf(subdomain))
83
+
84
+ # firewall
85
+ def getFirewall(self) -> dict:
86
+ '''
87
+ Returns the firewall state.
88
+
89
+ Returns:
90
+ dict with `supported` (bool), `rules` (list) and `allocation_ports`.
91
+ '''
92
+ return self.api.firewall(self.server_id)
93
+
94
+ def isFirewallSupported(self) -> bool:
95
+ '''Returns True if the node supports per-server firewalls.'''
96
+ return bool(self.getFirewall().get('supported'))
97
+
98
+ def getFirewallRules(self) -> list:
99
+ '''Returns the firewall rules.'''
100
+ return self.getFirewall().get('rules', [])
101
+
102
+ def setFirewallRules(self, rules: list) -> None:
103
+ '''
104
+ Replaces every firewall rule.
105
+
106
+ Args:
107
+ rules: List of {'action': 'allow'|'deny', 'protocols': ['tcp', 'udp'],
108
+ 'sources': ['1.2.3.4/32'], 'ports': [25565]} dicts.
109
+ '''
110
+ return self.api.setFirewallRules(self.server_id, rules)
111
+
112
+ def addFirewallRule(self, action: str, ports: list = None, sources: list = None, protocols: list = None) -> None:
113
+ '''
114
+ Appends a firewall rule.
115
+
116
+ Args:
117
+ action: 'allow' or 'deny'.
118
+ ports: Ports the rule applies to. All ports when omitted.
119
+ sources: Source IPs/CIDRs. Everyone when omitted.
120
+ protocols: ['tcp'], ['udp'] or both (default).
121
+
122
+ Example:
123
+ server.network.addFirewallRule('deny', sources=['203.0.113.7/32'])
124
+ '''
125
+ rule = {'action': action, 'protocols': protocols or ['tcp', 'udp'], 'sources': sources or []}
126
+ if ports:
127
+ rule['ports'] = ports
128
+ return self.setFirewallRules(self.getFirewallRules() + [rule])
129
+
130
+ def clearFirewallRules(self) -> None:
131
+ '''Removes every firewall rule.'''
132
+ return self.setFirewallRules([])
@@ -0,0 +1,6 @@
1
+ from .node import Node, NodeManager
2
+
3
+ __all__ = [
4
+ 'Node',
5
+ 'NodeManager',
6
+ ]