|
3 | 3 | import shutil |
4 | 4 | import tarfile |
5 | 5 | import subprocess |
| 6 | +import datetime |
| 7 | +import tempfile |
| 8 | +import yaml |
6 | 9 | from glob import glob |
7 | 10 | from ckan_cloud_operator import kubectl |
8 | 11 | from ckan_cloud_operator.gitlab import CkanGitlab |
| 12 | +from ckan_cloud_operator.config import manager as config_manager |
| 13 | +from ckan_cloud_operator.drivers.gcloud import driver as gcloud_driver |
| 14 | +from ckan_cloud_operator.providers.cluster import manager as cluster_manager |
9 | 15 |
|
10 | 16 |
|
11 | 17 | INSTANCE_NAME = os.environ.get('INSTANCE_NAME') |
@@ -64,21 +70,113 @@ def export_storage(instance_id): |
64 | 70 | shell=True) |
65 | 71 |
|
66 | 72 |
|
| 73 | +def export_db(instance_id): |
| 74 | + instance_spec = kubectl.get(f'ckancloudckaninstance {instance_id}')['spec'] |
| 75 | + db_name = instance_spec['db']['name'] |
| 76 | + db_prefix = instance_spec['db'].get('dbPrefix') |
| 77 | + datastore_name = instance_spec['datastore']['name'] |
| 78 | + datastore_prefix = instance_spec['datastore'].get('dbPrefix') |
| 79 | + gs_base_url = config_manager.get(secret_name='ckan-cloud-provider-db-gcloudsql-credentials', |
| 80 | + key='backups-gs-base-url') |
| 81 | + db_prefix_path = f'{db_prefix}/' if db_prefix else '' |
| 82 | + datastore_prefix_path = f'{datastore_prefix}/' if datastore_prefix else '' |
| 83 | + latest_gs_urls = { |
| 84 | + 'db': None, |
| 85 | + 'datastore': None |
| 86 | + } |
| 87 | + latest_gs_urls_datetimes = { |
| 88 | + 'db': None, |
| 89 | + 'datastore': None |
| 90 | + } |
| 91 | + for dbtype in ['db', 'datastore']: |
| 92 | + for minus_days in (0, 1, 2): |
| 93 | + dt = (datetime.datetime.now() - datetime.timedelta(days=minus_days)) |
| 94 | + datepath = dt.strftime('%Y/%m/%d') |
| 95 | + datesuffix = dt.strftime('%Y%m%d') |
| 96 | + if dbtype == 'datastore': |
| 97 | + ls_arg = f'{gs_base_url}{datastore_prefix_path}/{datepath}/*/{datastore_name}_{datesuffix}*.gz' |
| 98 | + else: |
| 99 | + ls_arg = f'{gs_base_url}{db_prefix_path}/{datepath}/*/{db_name}_{datesuffix}*.gz' |
| 100 | + output = gcloud_driver.check_output( |
| 101 | + *cluster_manager.get_provider().get_project_zone(), f'ls -l "{ls_arg}"', |
| 102 | + gsutil=True |
| 103 | + ) |
| 104 | + for line in output.decode().splitlines(): |
| 105 | + gsurl = line.strip().split(' ')[-1].strip() |
| 106 | + if gsurl.startswith('gs://'): |
| 107 | + gs_url_datetime = datetime.datetime.strptime(gsurl.split('/')[-1].split('.')[-2].split('_')[-1], '%Y%m%d%H%M') |
| 108 | + if not latest_gs_urls[dbtype] or latest_gs_urls_datetimes[dbtype] < gs_url_datetime: |
| 109 | + latest_gs_urls[dbtype], latest_gs_urls_datetimes[dbtype] = gsurl, gs_url_datetime |
| 110 | + if latest_gs_urls[dbtype]: |
| 111 | + break |
| 112 | + return latest_gs_urls['db'], latest_gs_urls['datastore'] |
| 113 | + # for dbtype in ['db', 'datastore']: |
| 114 | + # gsurl, gsurl_datetime = latest_gs_urls[dbtype], latest_gs_urls_datetimes[dbtype] |
| 115 | + # filename = '{}-{}'.format(dbtype, gsurl.split('/')[-1]) |
| 116 | + # print(f'Downloading {gsurl} ({gsurl_datetime}) --> {filename}') |
| 117 | + # gcloud_driver.check_call( |
| 118 | + # *cluster_manager.get_provider().get_project_zone(), |
| 119 | + # f'cp {gsurl} ./{filename}', |
| 120 | + # gsutil=True |
| 121 | + # ) |
| 122 | + |
| 123 | + |
| 124 | +def gsutil_publish(filename, gsurl, duration='7d'): |
| 125 | + if gsurl: |
| 126 | + if filename: |
| 127 | + subprocess.check_call(f'ls -lah {filename}', shell=True) |
| 128 | + gcloud_driver.check_call( |
| 129 | + *cluster_manager.get_provider().get_project_zone(), |
| 130 | + f'cp ./{filename} {gsurl}', |
| 131 | + gsutil=True |
| 132 | + ) |
| 133 | + with tempfile.NamedTemporaryFile('w') as f: |
| 134 | + f.write(config_manager.get(key='service-account-json', secret_name='ckan-cloud-provider-cluster-gcloud')) |
| 135 | + f.flush() |
| 136 | + output = gcloud_driver.check_output( |
| 137 | + *cluster_manager.get_provider().get_project_zone(), |
| 138 | + f'signurl -d {duration} {f.name} {gsurl}', |
| 139 | + gsutil=True |
| 140 | + ) |
| 141 | + signed_gsurls = [line for line in [line.strip().split('\t')[-1].strip() for line in output.decode().splitlines()] if len(line) > 20] |
| 142 | + assert len(signed_gsurls) == 1 |
| 143 | + return signed_gsurls[0] |
| 144 | + else: |
| 145 | + return None |
| 146 | + |
| 147 | + |
67 | 148 | def export_instance(instance_name, allowed_instance_names): |
68 | 149 | instance_id = get_instance_id(instance_name, allowed_instance_names) |
| 150 | + gs_base_url = config_manager.get(secret_name='ckan-cloud-provider-db-gcloudsql-credentials', |
| 151 | + key='backups-gs-base-url') |
| 152 | + backup_prefix = 'export/{}/{}'.format(instance_name, datetime.datetime.now().strftime('%Y%m%d%H%M')) |
69 | 153 | export_code(instance_id) |
70 | | - export_storage(instance_id) |
71 | 154 | with tarfile.open('code.tar.gz', 'w:gz') as tar: |
72 | 155 | for filename in glob('code_with_internal_secrets/**/*'): |
73 | 156 | fileparts = filename.split('/') |
74 | 157 | assert fileparts.pop(0) == 'code_with_internal_secrets' |
75 | 158 | tar.add(filename, '/'.join(fileparts)) |
| 159 | + code_signed_gsurl = gsutil_publish('code.tar.gz', f'{gs_base_url}/{backup_prefix}/code.tar.gz') |
| 160 | + code_with_secrets_signed_gsurl = gsutil_publish('code_with_internal_secrets.tar.gz', f'{gs_base_url}/{backup_prefix}/code_with_internal_secrets.tar.gz') |
| 161 | + export_storage(instance_id) |
76 | 162 | with tarfile.open('storage.tar.gz', 'w:gz') as tar: |
77 | 163 | for filename in glob('storage/**/*'): |
78 | 164 | fileparts = filename.split('/') |
79 | 165 | assert fileparts.pop(0) == 'storage' |
80 | 166 | tar.add(filename, '/'.join(fileparts)) |
| 167 | + storage_signed_gsurl = gsutil_publish('storage.tar.gz', f'{gs_base_url}/{backup_prefix}/storage.tar.gz') |
| 168 | + db_gs_url, datastore_gs_url = export_db(instance_id) |
| 169 | + db_signed_gsurl = gsutil_publish(None, db_gs_url) |
| 170 | + datastore_signed_gsurl = gsutil_publish(None, datastore_gs_url) |
| 171 | + return { |
| 172 | + 'code': code_signed_gsurl, |
| 173 | + 'code_with_secrets': code_with_secrets_signed_gsurl, |
| 174 | + 'storage': storage_signed_gsurl, |
| 175 | + 'db': db_signed_gsurl, |
| 176 | + 'datastore': datastore_signed_gsurl |
| 177 | + } |
81 | 178 |
|
82 | 179 |
|
83 | 180 | if __name__ == '__main__': |
84 | | - export_instance(INSTANCE_NAME, ALLOWED_INSTANCE_NAMES) |
| 181 | + print(yaml.dump(export_instance(INSTANCE_NAME, ALLOWED_INSTANCE_NAMES), default_flow_style=False)) |
| 182 | + exit(0) |
0 commit comments