Skip to content

Commit 8e6deb1

Browse files
authored
Saimon/backup to s3 (#354)
1 parent 99746a1 commit 8e6deb1

6 files changed

Lines changed: 168 additions & 9 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 run(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.run(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: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import logging
22
import asyncio
3-
import urllib.parse
43

54
from ..lib.retries import execute_with_retries
65
from .types import Metadata, Block
76
from .crypto import decrypt_key, decrypt_block
87
from .decompressor import decompress
9-
from ..exceptions.transport import BadRequest, Unauthorized, Forbidden, Unprocessable, InternalServerError, HTTPError
8+
from ..exceptions.transport import BadRequest, Unauthorized, Unprocessable, InternalServerError, HTTPError
109
from ..exceptions.direct import (
1110
AuthorizationError, BlockListConnectionError, BlockListTimeout, BlockValidationException, BlocksNotFoundError,
1211
DecompressBlockError, DecryptBlockError, DecryptKeyError, DirectIOError, DownloadConnectionError,
@@ -73,10 +72,6 @@ async def get_object(client, file_id, chunk):
7372
raise exception
7473

7574

76-
def is_azure_object_storage(chunk):
77-
return urllib.parse.urlparse(chunk.url).netloc.endswith('core.windows.net')
78-
79-
8075
async def decrypt_object(file_id, encrypted_object, encryption_key, chunk):
8176
"""
8277
Decrypt Encrypted Object.
@@ -88,7 +83,7 @@ async def decrypt_object(file_id, encrypted_object, encryption_key, chunk):
8883
:rtype: bytes
8984
"""
9085
try:
91-
return decrypt_block(encrypted_object[16:] if is_azure_object_storage(chunk) else encrypted_object, encryption_key)
86+
return decrypt_block(encrypted_object, encryption_key)
9287
except DirectIOError:
9388
logger.error('Failed to decrypt block.')
9489
raise DecryptBlockError(file_id, chunk)
@@ -207,7 +202,7 @@ async def get_chunks(api, bearer, file_id):
207202
return Metadata(file_id, response)
208203
except BadRequest as error:
209204
raise ObjectNotFoundError(file_id) from error
210-
except (Unauthorized, Forbidden) as error:
205+
except Unauthorized as error:
211206
raise AuthorizationError(file_id) from error
212207
except Unprocessable as error:
213208
raise UnsupportedStorageError(file_id) from error

docs/source/UserGuides/Portal/Administration.rst

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,39 @@ Server Tasks
605605
for task in admin.servers.tasks.scheduled('database'):
606606
print(task.name)
607607
608+
609+
Main Database Backup to S3
610+
--------------------------
611+
612+
.. automethod:: cterasdk.core.servers.Backup.enable
613+
:noindex:
614+
615+
.. code-block:: python
616+
617+
name = 'target-s3-bucket-name'
618+
access, secret = 'access-key', 'secret-access-key'
619+
endpoint = 's3.eu-west-1.amazonaws.com'
620+
https = True
621+
622+
bucket = core_types.AmazonS3(name, access, secret, endpoint, https) # use verify_ssl=False to trust all certificates
623+
user.servers.backup.enable(bucket, 60) # backup every 60 minutes
624+
625+
626+
.. automethod:: cterasdk.core.servers.Backup.disable
627+
:noindex:
628+
629+
.. code-block:: python
630+
631+
user.servers.backup.disable()
632+
633+
.. automethod:: cterasdk.core.servers.Backup.status
634+
:noindex:
635+
636+
.. code-block:: python
637+
638+
is_connected = user.servers.backup.connected()
639+
640+
608641
Messaging Service
609642
=================
610643

tests/ut/core/admin/test_servers.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import munch
55
from cterasdk.common import Object
66
from cterasdk.core import servers
7+
from cterasdk.core.types import AmazonS3
78
from cterasdk import exceptions
89
from tests.ut.core.admin import base_admin
910

@@ -110,6 +111,72 @@ def test_modify_success(self):
110111
self._assert_equal_objects(actual_param, expected_param)
111112
self.assertEqual(ret, put_response)
112113

114+
def test_system_database(self):
115+
with mock.patch("cterasdk.core.servers.query.run") as query_mock:
116+
query_mock.return_value = munch.Munch({
117+
'objects': [munch.Munch({'mainDB': True})]
118+
})
119+
server = servers.Servers(self._global_admin).system_database
120+
expected_query_params = base_admin.BaseCoreTest._create_query_params(start_from=0, count_limit=50, filters=[
121+
munch.Munch({
122+
'field': 'mainDB',
123+
'restriction': 'eq',
124+
'_classname': 'BooleanFilter',
125+
'value': True
126+
})
127+
])
128+
actual_query_params = query_mock.call_args[0][2]
129+
self._assert_equal_objects(actual_query_params, expected_query_params)
130+
self.assertEqual(server.mainDB, True)
131+
132+
def test_enable_server_backup(self):
133+
self._init_global_admin()
134+
server_name = 'server'
135+
with mock.patch("cterasdk.core.servers.Servers.system_database", new_callable=mock.PropertyMock) as query_mock:
136+
query_mock.return_value = munch.Munch({'name': server_name, 'backupToBucket': None})
137+
bucket, access, secret, endpoint = 'bucket-name', 'access', 'secret', 'www.endpoint.com'
138+
bucket = AmazonS3(bucket, access, secret, endpoint, True, verify_ssl=False)
139+
servers.Servers(self._global_admin).backup.enable(bucket, 60)
140+
self._global_admin.api.put.assert_called_once_with(f'/servers/{server_name}', mock.ANY)
141+
actual_param = self._global_admin.api.put.call_args[0][1]
142+
expected_param = munch.Munch({
143+
'enabled': True,
144+
'exportSchedulePeriod': 60,
145+
'details': TestCoreServers._create_database_backup_server_object(bucket)
146+
})
147+
self._assert_equal_objects(actual_param.backupToBucket, expected_param)
148+
149+
@staticmethod
150+
def _create_database_backup_server_object(bucket):
151+
return munch.Munch({
152+
'storage': bucket.driver,
153+
'bucket': bucket.bucket,
154+
'accessKey': bucket.access_key,
155+
'secretKey': bucket.secret_key,
156+
'endPoint': bucket.endpoint,
157+
'useHttps': bucket.https,
158+
'trustAllCertificates': bucket.trust_all_certificates,
159+
'masterHost': None,
160+
'usePathStyleAddressing': False
161+
})
162+
163+
def test_disable_server_backup(self):
164+
self._init_global_admin()
165+
server_name = 'server'
166+
with mock.patch("cterasdk.core.servers.Servers.system_database", new_callable=mock.PropertyMock) as query_mock:
167+
query_mock.return_value = munch.Munch({'name': server_name, 'backupToBucket': munch.Munch({'enabled': True})})
168+
servers.Servers(self._global_admin).backup.disable()
169+
self._global_admin.api.put.assert_called_once_with(f'/servers/{server_name}', mock.ANY)
170+
actual_param = self._global_admin.api.put.call_args[0][1]
171+
self._assert_equal_objects(actual_param.backupToBucket.enabled, False)
172+
173+
def test_server_backup_status(self):
174+
self._init_global_admin()
175+
with mock.patch("cterasdk.core.servers.Servers.system_database", new_callable=mock.PropertyMock) as query_mock:
176+
query_mock.return_value = munch.Munch({'backupToBucket': munch.Munch({'status': 'Connected'})})
177+
ret = servers.Servers(self._global_admin).backup.connected()
178+
self.assertEqual(ret, True)
179+
113180
@staticmethod
114181
def _create_server_object(name=None, app=None, preview=None, enable_public_ip=None,
115182
public_ip=None, allow_user_login=None, enable_replication=None, replica_of=None):

0 commit comments

Comments
 (0)