Skip to content

Commit 2209200

Browse files
committed
support backup of portal to s3
1 parent 99746a1 commit 2209200

7 files changed

Lines changed: 294 additions & 57 deletions

File tree

cterasdk/core/query.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
from ..common import Object
55

66

7+
def database(core, path, param):
8+
return create_callback_function(core, path, callback_response=DefaultResponse)(param)
9+
10+
711
def create_callback_function(core, path, name=None, *, callback_response=None):
812
"""
913
Create a query callback function

cterasdk/core/servers.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class Servers(BaseCommand):
2020
def __init__(self, portal):
2121
super().__init__(portal)
2222
self.tasks = Tasks(self._core)
23+
self.backup = Backup(self._core)
2324

2425
def _get_entire_object(self, server):
2526
ref = f'/servers/{server}'
@@ -43,6 +44,19 @@ def get(self, name, include=None):
4344
raise ObjectNotFoundException(f'/servers/{name}')
4445
return server
4546

47+
@property
48+
def system_database(self):
49+
"""
50+
Retrieve the main database object
51+
52+
:returns: Main database object.
53+
:rtype: cterasdk.common.object.Object
54+
"""
55+
response = query.database(self._core, '/servers', query.QueryParamBuilder().addFilter(
56+
query.FilterBuilder('mainDB').eq(True)
57+
).build())
58+
return response.objects[0]
59+
4660
def list_servers(self, include=None):
4761
"""
4862
Retrieve the servers that comprise CTERA Portal.
@@ -70,7 +84,6 @@ def modify(self, name, server_name=None, app=None, preview=None, enable_public_i
7084
:param bool,optional enable_replication: Enable or disable database replication
7185
:param str,optional replica_of: Configure as a replicate of another Portal server. `enable_replication` must be set to `True`
7286
"""
73-
7487
server = self._get_entire_object(name)
7588
if enable_replication is True and replica_of is not None:
7689
server.replicationSettings = Object()
@@ -101,6 +114,40 @@ def modify(self, name, server_name=None, app=None, preview=None, enable_public_i
101114
raise CTERAException(f'Server modification failed: {ref}') from error
102115

103116

117+
class Backup(BaseCommand):
118+
119+
def connected(self):
120+
"""
121+
Verify connectivity to the backup S3 bucket.
122+
"""
123+
return self._core.servers.system_database.backupToBucket.status == 'Connected'
124+
125+
def enable(self, bucket, interval):
126+
"""
127+
Enable Main Database Backup to an S3 Bucket
128+
129+
:param cterasdk.core.types.Bucket bucket: Storage bucket
130+
:param int interval: Backup interval in minutes
131+
"""
132+
database = self._core.servers.system_database
133+
database.backupToBucket = Object(enabled=True, exportSchedulePeriod=interval, details=bucket.database_backup_server_object())
134+
logger.info("Enabling database backup. %s", {'server': database.name})
135+
response = self._core.api.put(f'/servers/{database.name}', database)
136+
logger.info("Database backup enabled. %s", {'server': database.name})
137+
return response
138+
139+
def disable(self):
140+
"""
141+
Disable Main Database Backup
142+
"""
143+
database = self._core.servers.system_database
144+
database.backupToBucket.enabled = False
145+
logger.info("Disabling database backup. %s", {'server': database.name})
146+
response = self._core.api.put(f'/servers/{database.name}', database)
147+
logger.info("Database backup disabled. %s", {'server': database.name})
148+
return response
149+
150+
104151
class Tasks(BaseCommand):
105152

106153
def background(self, name):

cterasdk/core/types.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,19 @@ def __init__(self, bucket, driver, access_key, secret_key, endpoint, https, dire
331331
def trust_all_certificates(self):
332332
return not self.verify_ssl
333333

334+
def database_backup_server_object(self):
335+
return Object(
336+
storage=self.driver,
337+
bucket=self.bucket,
338+
accessKey=self.access_key,
339+
secretKey=self.secret_key,
340+
endPoint=self.endpoint,
341+
useHttps=self.https,
342+
trustAllCertificates=self.trust_all_certificates,
343+
masterHost=None,
344+
usePathStyleAddressing=False
345+
)
346+
334347

335348
class AzureBlob(HTTPBucket):
336349

cterasdk/direct/lib.py

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,7 @@ async def get_object(client, file_id, chunk):
2727
:returns: Object
2828
:rtype: bytes
2929
"""
30-
message = (
31-
f"Downloading block #{chunk.number} "
32-
f"(offset={chunk.offset}, length={chunk.length})"
33-
)
30+
message = f"Downloading block (offset={chunk.offset}, length={chunk.length})"
3431

3532
if file_id:
3633
message += f" for file ID {file_id}"
@@ -61,10 +58,8 @@ async def get_object(client, file_id, chunk):
6158
"unknown": "Unknown error"
6259
}
6360

64-
message = (
65-
f"Failed to download block #{chunk.number} "
66-
f"(offset={chunk.offset}, length={chunk.length})"
67-
)
61+
message = f"Failed to download block (offset={chunk.offset}, length={chunk.length})"
62+
6863
if file_id:
6964
message = message + f" for file ID {file_id}"
7065

@@ -128,17 +123,14 @@ async def process_chunk(client, file_id, chunk, encryption_key, semaphore):
128123
:rtype: cterasdk.direct.types.Block
129124
"""
130125
async def process(client, chunk, encryption_key):
131-
message = (
132-
f"Processing block #{chunk.number} "
133-
f"(offset={chunk.offset}, length={chunk.length})"
134-
)
126+
message = f"Processing block (offset={chunk.offset}, length={chunk.length}) "
135127
if file_id:
136128
message = message + f" for file ID {file_id}"
137129
logger.debug(message)
138130
encrypted_object = await get_object(client, file_id, chunk)
139131
decrypted_object = await decrypt_object(file_id, encrypted_object, encryption_key, chunk)
140132
decompressed_object = await decompress_object(file_id, decrypted_object, chunk)
141-
return Block(file_id, chunk.number, chunk.offset, decompressed_object, chunk.length)
133+
return Block(file_id, chunk.offset, decompressed_object, chunk.length)
142134

143135
if semaphore is not None:
144136
async with semaphore:
@@ -188,7 +180,7 @@ def decrypt_encryption_key(file_id, wrapped_key, secret_access_key):
188180

189181

190182
@execute_with_retries(retries=3, backoff=1, max_backoff=10)
191-
async def get_chunks(api, bearer, file_id):
183+
async def get_chunks(api, bearer, file_id, start=None, end=None):
192184
"""
193185
Get Chunks.
194186
@@ -200,7 +192,8 @@ async def get_chunks(api, bearer, file_id):
200192
"""
201193
logger.debug('Listing blocks for file ID: %s', file_id)
202194
try:
203-
response = await api.get(f'{file_id}', headers={'Authorization': bearer})
195+
params = {k: v for k, v in [('rangeStart', start), ('rangeEnd', end)] if v is not None}
196+
response = await api.get(f'{file_id}', params=params, headers={'Authorization': bearer})
204197
if not response.chunks:
205198
logger.error('Could not find blocks for file ID: %s.', file_id)
206199
raise BlocksNotFoundError(file_id)

cterasdk/direct/types.py

Lines changed: 125 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -56,89 +56,177 @@ class CompressionLib:
5656

5757
class Chunk(Object):
5858

59-
def __init__(self, number, offset, url, length):
59+
def __init__(self, offset, url, length):
6060
"""
6161
Initialize a Chunk.
6262
63-
:param int number: Chunk number.
6463
:param int offset: Chunk offset.
6564
:param str url: Signed URL.
6665
:param int length: Object length.
6766
"""
6867
super().__init__(
69-
number=number,
7068
offset=offset,
7169
url=url,
7270
length=length
7371
)
7472

7573

76-
class Metadata(Object):
74+
class MetadataPart(Object):
7775
"""
78-
CTERA Direct IO File Metadata
76+
CTERA Direct I/O File Metadata Part
77+
78+
:ivar int start: Starting offset.
79+
:ivar int end: Ending offset.
80+
:ivar int length: Length of the range in bytes.
81+
:ivar bool encrypted: Indicates whether the range data is encrypted.
82+
:ivar str encryption_key: Encryption key used for the range data.
83+
:ivar bool compressed: Indicates whether the range data is compressed.
84+
:ivar str compression_alg: Compression algorithm used for the range data.
85+
:ivar list[cterasdk.common.object.Object] chunks: List of Chunk Objects.
7986
"""
80-
81-
def __init__(self, file_id, server_object):
87+
def __init__(self, offset, end, encrypted, encryption_key, compressed, compression_alg, chunks):
8288
"""
8389
Initialize a Direct IO metadata response object.
8490
85-
:param int file_id: File ID.
86-
:param cterasdk.common.object.Object server_object: Response Object.
91+
:param int offset: Starting offset.
92+
:param int end: Ending offset.
93+
:param bool encrypted: Indicates whether the file is encrypted.
94+
:param str encryption_key: Encryption key used for the file.
95+
:param bool compressed: Indicates whether the file is compressed.
96+
:param str compression_alg: Compression algorithm used for the file.
97+
:param list[cterasdk.common.object.Object] chunks: List of Chunk Objects.
8798
"""
8899
super().__init__(
89-
file_id=file_id,
90-
encrypted=server_object.encrypt_info.data_encrypted,
91-
compressed=server_object.compression_type != CompressionLib.Off,
92-
chunks=Metadata._format_chunks(server_object.chunks)
100+
start=offset,
101+
end=end,
102+
length=(end - offset) + 1,
103+
encrypted=encrypted,
104+
encryption_key=encryption_key,
105+
compressed=compressed,
106+
compression_alg=compression_alg
93107
)
94-
self.encryption_key = server_object.encrypt_info.wrapped_key if self.encrypted else None
95-
self.compression_library = server_object.compression_type if self.compressed else None
96-
last_chunk = self.chunks[-1]
97-
self.size = last_chunk.offset + last_chunk.length
108+
for chunk in chunks:
109+
self.chunks.append(Chunk(offset, chunk.url, chunk.len))
110+
offset = offset + chunk.len
98111

99112
@staticmethod
100-
def _format_chunks(server_object):
113+
def from_server_object(server_object):
114+
compressed = server_object.compression_type != CompressionLib.Off
115+
start, end = map(int, server_object.actual_blocks_range.range.split('-'))
116+
return MetadataPart(
117+
start,
118+
end,
119+
server_object.encrypt_info.data_encrypted,
120+
server_object.encrypt_info.wrapped_key if server_object.encrypt_info.data_encrypted else None,
121+
compressed,
122+
server_object.compression_type if compressed else None
123+
)
124+
125+
def __repr__(self):
126+
return str(self)
127+
128+
def __str__(self):
129+
return (
130+
f"{self.__class__.__name__}("
131+
f"{{'start': {self.start}, "
132+
f"'end': {self.end}, "
133+
f"'encrypted': {self.encrypted}, "
134+
f"'compressed': {self.compressed}, "
135+
f"'length': {self.length}, "
136+
f"'chunks': {len(self.chunks)}}})"
137+
)
138+
139+
140+
class Metadata(Object):
141+
"""
142+
CTERA Direct I/O File Metadata
143+
144+
:ivar int file_id: File ID.
145+
:ivar int file_size: File Size.
146+
:ivar list[cterasdk.direct.types.MetadataPart] parts: List of Metadata Parts.
147+
:ivar bool encrypted: Indicates whether the range data is encrypted.
148+
:ivar str encryption_key: Encryption key used for the range data.
149+
:ivar bool compressed: Indicates whether the range data is compressed.
150+
:ivar str compression_alg: Compression algorithm used for the range data.
151+
"""
152+
def __init__(self, file_id, *parts):
101153
"""
102-
Create Chunks.
154+
Initialize a Direct IO metadata response object.
103155
104156
:param int file_id: File ID.
105-
:param cterasdk.common.object.Object server_object: Server response.
106-
:param list[int] blocks: List of block numbers to retrieve.
107-
:returns: Chunk objects
108-
:rtype: list[cterasdk.direct.types.Chunk]
157+
:param list[cterasdk.direct.types.MetadataPart] parts: List of Metadata Parts.
109158
"""
110-
offset = 0
111-
chunks = []
112-
for number, chunk in enumerate(server_object, 1):
113-
chunks.append(Chunk(number, offset, chunk.url, chunk.len))
114-
offset = offset + chunk.len
115-
return chunks
159+
super().__init__(file_id=file_id, parts=parts)
160+
161+
@property
162+
def size(self):
163+
return self.parts[0].actual_blocks_range.file_size
164+
165+
@property
166+
def encrypted(self):
167+
return self.parts[0].encrypted
168+
169+
@property
170+
def encryption_key(self):
171+
return self.parts[0].encryption_key
172+
173+
@property
174+
def compressed(self):
175+
return self.parts[0].compressed
176+
177+
@property
178+
def compression_alg(self):
179+
return self.parts[0].compression_alg
180+
181+
@property
182+
def start(self):
183+
return self.parts[0].start
184+
185+
@property
186+
def end(self):
187+
return self.parts[-1].end
116188

117189
def serialize(self):
118190
"""
119-
Serialize Direct IO metadata to a dictionary.
191+
Serialize Direct I/O metadata to a dictionary.
120192
"""
121-
x = copy.deepcopy(self)
122-
if self.encrypted:
123-
x.encryption_key = utils.utf8_decode(base64.b64encode(self.encryption_key))
124-
return x
193+
chunks = []
194+
for part in self.parts:
195+
chunks.extend(part.chunks)
196+
197+
return {
198+
'file_id': self.file_id,
199+
'size': self.size,
200+
'encrypted': self.encrypted,
201+
'encryption_key': utils.utf8_decode(base64.b64encode(self.encryption_key)) if self.encrypted else None,
202+
'compressed': self.compressed,
203+
'compression_alg': self.compression_alg,
204+
'chunks': chunks
205+
}
206+
207+
def __repr__(self):
208+
return str(self)
209+
210+
def __str__(self):
211+
d = self.serialize()
212+
d.pop('encryption_key')
213+
d['chunks'] = len(d['chunks'])
214+
return d
125215

126216

127217
class Block:
128218
"""Block"""
129219

130-
def __init__(self, file_id, number, offset, data, length):
220+
def __init__(self, file_id, offset, data, length):
131221
"""
132222
Initialize a Block.
133223
134224
:param int file_id: File ID.
135-
:param int number: Block number.
136225
:param int offset: Block offset.
137226
:param bytes data: Bytes
138227
:param int length: Block length.
139228
"""
140229
self._file_id = file_id
141-
self._number = number
142230
self._offset = offset
143231
self._data = data
144232
self._length = length
@@ -147,10 +235,6 @@ def __init__(self, file_id, number, offset, data, length):
147235
def file_id(self):
148236
return self._file_id
149237

150-
@property
151-
def number(self):
152-
return self._number
153-
154238
@property
155239
def offset(self):
156240
return self._offset

0 commit comments

Comments
 (0)