1 | import os |
||
2 | import json |
||
3 | from pathlib import Path |
||
4 | from typing import Dict, Optional |
||
5 | |||
6 | from cleo import Output |
||
7 | |||
8 | from backuppc_clone.Config import Config |
||
9 | from backuppc_clone.DataLayer import DataLayer |
||
10 | from backuppc_clone.command.BaseCommand import BaseCommand |
||
11 | from backuppc_clone.helper.AuxiliaryFiles import AuxiliaryFiles |
||
12 | from backuppc_clone.helper.BackupClone import BackupClone |
||
13 | from backuppc_clone.helper.BackupDelete import BackupDelete |
||
14 | from backuppc_clone.helper.BackupInfoScanner import BackupInfoScanner |
||
15 | from backuppc_clone.helper.HostDelete import HostDelete |
||
16 | from backuppc_clone.helper.PoolSync import PoolSync |
||
17 | |||
18 | |||
19 | View Code Duplication | class AutoCommand(BaseCommand): |
|
0 ignored issues
–
show
Duplication
introduced
by
![]() |
|||
20 | """ |
||
21 | Clones the original in automatic mode |
||
22 | |||
23 | auto |
||
24 | {clone.cfg : The configuration file of the clone} |
||
25 | """ |
||
26 | |||
27 | # ------------------------------------------------------------------------------------------------------------------ |
||
28 | def __scan_original_backups(self) -> None: |
||
29 | """ |
||
30 | Scans the original hosts backups. |
||
31 | """ |
||
32 | self._io.title('Inventorying Original Backups') |
||
33 | |||
34 | helper = BackupInfoScanner(self._io) |
||
35 | helper.scan() |
||
36 | DataLayer.instance.commit() |
||
37 | self.__write_stats() |
||
38 | |||
39 | # ------------------------------------------------------------------------------------------------------------------ |
||
40 | def __sync_auxiliary_files(self) -> None: |
||
41 | """ |
||
42 | Synchronises auxiliary files (i.e. files directly under a host directory but not part of a backup). |
||
43 | """ |
||
44 | self._io.title('Synchronizing Auxiliary Files') |
||
45 | |||
46 | helper = AuxiliaryFiles(self._io) |
||
47 | helper.synchronize() |
||
48 | |||
49 | # ------------------------------------------------------------------------------------------------------------------ |
||
50 | def __show_overview_stats(self) -> None: |
||
51 | """ |
||
52 | Shows the number of backups, cloned backups, backups to clone, and number of obsolete cloned backups. |
||
53 | """ |
||
54 | stats = DataLayer.instance.overview_get_stats() |
||
55 | |||
56 | self._io.writeln(' # backups : {}'.format(stats['n_backups'])) |
||
57 | self._io.writeln(' # cloned backups : {}'.format(stats['n_cloned_backups'])) |
||
58 | self._io.writeln(' # backups still to clone : {}'.format(stats['n_not_cloned_backups'])) |
||
59 | self._io.writeln(' # obsolete cloned backups: {}'.format(stats['n_obsolete_cloned_backups'])) |
||
60 | self._io.writeln('') |
||
61 | |||
62 | # ------------------------------------------------------------------------------------------------------------------ |
||
63 | def __write_stats(self) -> None: |
||
64 | """ |
||
65 | Writes the stats to the stats file. |
||
66 | """ |
||
67 | stats = DataLayer.instance.overview_get_stats() |
||
68 | text = json.dumps(stats, indent=4) |
||
69 | Path(Config.instance.stats_file).write_text(text) |
||
70 | |||
71 | # ------------------------------------------------------------------------------------------------------------------ |
||
72 | def __remove_obsolete_hosts(self) -> None: |
||
73 | """ |
||
74 | Removes obsolete hosts. |
||
75 | """ |
||
76 | hosts = DataLayer.instance.host_get_obsolete() |
||
77 | if hosts: |
||
78 | self._io.title('Removing Obsolete Hosts') |
||
79 | |||
80 | for host in hosts: |
||
81 | self._io.section('Removing host {}'.format(host['hst_name'])) |
||
82 | |||
83 | helper = HostDelete(self._io) |
||
84 | helper.delete_host(host['hst_name']) |
||
85 | |||
86 | DataLayer.instance.commit() |
||
87 | self.__write_stats() |
||
88 | |||
89 | self._io.writeln('') |
||
90 | |||
91 | # ------------------------------------------------------------------------------------------------------------------ |
||
92 | def __remove_obsolete_backups(self) -> None: |
||
93 | """ |
||
94 | Removes obsolete host backups. |
||
95 | """ |
||
96 | backups = DataLayer.instance.backup_get_obsolete() |
||
97 | if backups: |
||
98 | self._io.title('Removing Obsolete Host Backups') |
||
99 | |||
100 | for backup in backups: |
||
101 | self._io.section('Removing backup {}/{}'.format(backup['hst_name'], backup['bck_number'])) |
||
102 | |||
103 | helper = BackupDelete(self._io) |
||
104 | helper.delete_backup(backup['hst_name'], backup['bck_number']) |
||
105 | |||
106 | self.__write_stats() |
||
107 | |||
108 | self._io.writeln('') |
||
109 | |||
110 | # ------------------------------------------------------------------------------------------------------------------ |
||
111 | def __remove_partially_cloned_backups(self) -> None: |
||
112 | """ |
||
113 | Removes backups that are still marked "in progress" (and hence cloned partially). |
||
114 | """ |
||
115 | backups = DataLayer.instance.backup_partially_cloned() |
||
116 | if backups: |
||
117 | self._io.title('Removing Partially Cloned Host Backups') |
||
118 | |||
119 | for backup in backups: |
||
120 | self._io.section('Removing backup {}/{}'.format(backup['hst_name'], backup['bck_number'])) |
||
121 | |||
122 | helper = BackupDelete(self._io) |
||
123 | helper.delete_backup(backup['hst_name'], backup['bck_number']) |
||
124 | |||
125 | self.__write_stats() |
||
126 | |||
127 | self._io.writeln('') |
||
128 | |||
129 | # ------------------------------------------------------------------------------------------------------------------ |
||
130 | @staticmethod |
||
131 | def __get_next_clone_target() -> Optional[Dict]: |
||
132 | """ |
||
133 | Returns the metadata of the host backup that needs to be cloned. |
||
134 | |||
135 | :dict|None: |
||
136 | """ |
||
137 | backup = DataLayer.instance.backup_get_next(Config.instance.last_pool_scan) |
||
138 | if not backup: |
||
139 | backup = DataLayer.instance.backup_get_next(-1) |
||
140 | |||
141 | return backup |
||
142 | |||
143 | # ------------------------------------------------------------------------------------------------------------------ |
||
144 | def __resync_pool(self, backup: Dict) -> None: |
||
145 | """ |
||
146 | Re-syncs the pool if required for cloning a backup. |
||
147 | |||
148 | @param dict backup: The metadata of the backup. |
||
149 | """ |
||
150 | if Config.instance.last_pool_scan < backup['bob_end_time']: |
||
151 | self._io.title('Maintaining Clone Pool and Pool Metadata') |
||
152 | |||
153 | helper = PoolSync(self._io) |
||
154 | helper.synchronize() |
||
155 | |||
156 | DataLayer.instance.commit() |
||
157 | |||
158 | # ------------------------------------------------------------------------------------------------------------------ |
||
159 | def __clone_backup(self, backup: Dict) -> None: |
||
160 | """ |
||
161 | Clones a backup. |
||
162 | |||
163 | @param dict backup: The metadata of the backup. |
||
164 | """ |
||
165 | self._io.title('Cloning Backup {}/{}'.format(backup['bob_host'], backup['bob_number'])) |
||
166 | |||
167 | helper = BackupClone(self._io) |
||
168 | helper.clone_backup(backup['bob_host'], backup['bob_number']) |
||
169 | |||
170 | DataLayer.instance.commit() |
||
171 | self.__write_stats() |
||
172 | |||
173 | # ------------------------------------------------------------------------------------------------------------------ |
||
174 | def __handle_file_not_found(self, backup: Dict, error: FileNotFoundError) -> None: |
||
175 | """ |
||
176 | Handles a FileNotFoundError exception. |
||
177 | |||
178 | @param dict backup: The metadata of the backup. |
||
179 | @param FileNotFoundError error: The exception. |
||
180 | """ |
||
181 | if self._io.get_verbosity() >= Output.VERBOSITY_VERBOSE: |
||
182 | self._io.warning(str(error)) |
||
183 | |||
184 | self._io.block('Resynchronization of the pool is required') |
||
185 | |||
186 | # The host backup might have been partially cloned. |
||
187 | helper = BackupDelete(self._io) |
||
188 | helper.delete_backup(backup['bob_host'], backup['bob_number']) |
||
189 | |||
190 | # Force resynchronization of pool. |
||
191 | Config.instance.last_pool_scan = -1 |
||
192 | |||
193 | # Commit the transaction. |
||
194 | DataLayer.instance.commit() |
||
195 | |||
196 | # ------------------------------------------------------------------------------------------------------------------ |
||
197 | def _handle_command(self) -> None: |
||
198 | """ |
||
199 | Executes the command. |
||
200 | """ |
||
201 | DataLayer.instance.disconnect() |
||
202 | |||
203 | while True: |
||
204 | pid = os.fork() |
||
205 | |||
206 | if pid == 0: |
||
207 | DataLayer.instance.connect() |
||
208 | |||
209 | self.__remove_partially_cloned_backups() |
||
210 | self.__scan_original_backups() |
||
211 | self.__show_overview_stats() |
||
212 | self.__remove_obsolete_hosts() |
||
213 | self.__remove_obsolete_backups() |
||
214 | |||
215 | backup = self.__get_next_clone_target() |
||
216 | if backup is None: |
||
217 | exit(1) |
||
218 | |||
219 | try: |
||
220 | self.__resync_pool(backup) |
||
221 | self.__clone_backup(backup) |
||
222 | except FileNotFoundError as error: |
||
223 | self.__handle_file_not_found(backup, error) |
||
224 | |||
225 | exit(0) |
||
226 | |||
227 | pid, status = os.wait() |
||
228 | if status != 0: |
||
229 | break |
||
230 | |||
231 | self.__sync_auxiliary_files() |
||
232 | |||
233 | # ---------------------------------------------------------------------------------------------------------------------- |
||
234 |