Issues (23)

backuppc_clone/command/AutoCommand.py (1 issue)

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
This code seems to be duplicated in your project.
Loading history...
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