Passed
Push — master ( ba6342...71f198 )
by P.R.
07:12
created

backuppc_clone.command.AutoCommand   A

Complexity

Total Complexity 25

Size/Duplication

Total Lines 221
Duplicated Lines 90.95 %

Importance

Changes 0
Metric Value
eloc 109
dl 201
loc 221
rs 10
c 0
b 0
f 0
wmc 25

11 Methods

Rating   Name   Duplication   Size   Complexity  
A AutoCommand.__remove_partially_cloned_backups() 17 17 3
A AutoCommand.__show_overview_stats() 11 11 1
A AutoCommand.__handle_file_not_found() 21 21 2
A AutoCommand.__sync_auxiliary_files() 8 8 1
A AutoCommand.__get_next_clone_target() 12 12 2
A AutoCommand.__scan_original_backups() 9 9 1
A AutoCommand.__remove_obsolete_hosts() 17 17 3
A AutoCommand.__remove_obsolete_backups() 17 17 3
B AutoCommand._handle_command() 35 35 6
A AutoCommand.__clone_backup() 12 12 1
A AutoCommand.__resync_pool() 13 13 2

How to fix   Duplicated Code   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

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