Coverage for src/edwh_restic_plugin/tasks.py: 0%

56 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-11-10 20:53 +0100

1import json 

2 

3import invoke 

4from edwh.tasks import DOCKER_COMPOSE 

5from invoke import task 

6from print_color import print # fixme: replace with termcolor 

7 

8from .env import DOTENV, read_dotenv, set_env_value 

9from .repositories import Repository, registrations 

10 

11 

12def cli_repo(connection_choice: str = None, restichostname: str = None) -> Repository: 

13 """ 

14 Create a repository object and set up the connection to the backend. 

15 :param connection_choice: choose where you want to store the repo (local, SFTP, B2, swift) 

16 :param restichostname: which hostname to force for restic, or blank for default. 

17 :return: repository object 

18 """ 

19 env = read_dotenv(DOTENV) 

20 if restichostname: 

21 set_env_value(DOTENV, "RESTICHOSTNAME", restichostname) 

22 

23 options = registrations.to_ordered_dict() 

24 

25 connection_lowercase = "" 

26 if connection_choice is None: 

27 # search for the most important backup and use it as default 

28 for option in options: 

29 if f"{option.upper()}_NAME" in env: 

30 connection_lowercase = option.lower() 

31 break 

32 else: 

33 connection_lowercase = connection_choice.lower() 

34 

35 if connection_lowercase not in options: 

36 options = ", ".join(list(options)) 

37 raise ValueError(f"Invalid connection type {connection_choice}. Please use one of {options}!") 

38 

39 repoclass = options[connection_lowercase] 

40 print("Use connection: ", connection_lowercase) 

41 repo = repoclass() 

42 repo.setup() 

43 return repo 

44 

45 

46@task(aliases=("setup", "init")) 

47def configure(c, connection_choice=None, restichostname=None): 

48 """Setup or update the backup command for your environment. 

49 connection_choice: choose where you want to store the repo (local, SFTP, B2, swift) 

50 restichostname: which hostname to force for restic, or blank for default. 

51 """ 

52 

53 # It has been decided to create a main path called 'backups' for each repository. 

54 # This can be changed or removed if desired. 

55 # A password is only passed with a few functions. 

56 cli_repo(connection_choice, restichostname).configure(c) 

57 

58 

59@task 

60def backup(c, target: str = "", connection_choice: str = None, message: str = None, verbose: bool = True): 

61 """Performs a backup operation using restic on a local or remote/cloud file system. 

62 

63 Args: 

64 c (Context) 

65 target (str): The target of the backup (e.g. 'files', 'stream'; default is all types). 

66 connection_choice (str): The name of the connection to use for the backup. 

67 Defaults to None, which means the default connection will be used. 

68 message (str): A message to attach to the backup snapshot. 

69 Defaults to None, which means no message will be attached. 

70 verbose (bool): If True, outputs more information about the backup process. Defaults to False. 

71 

72 Raises: 

73 Exception: If an error occurs during the backup process. 

74 

75 """ 

76 # After 'backup', a file path can be specified.In this script, a test file is chosen at './test/testbestand'. 

77 # It can be replaced with the desired path over which restic should perform a backup. 

78 # The option --verbose provides more information about the backup that is made.It can be removed if desired. 

79 

80 # By using additions, it is possible to specify what should be included: 

81 # --exclude ,Specified one or more times to exclude one or more items. 

82 # --iexclude, Same as --exclude but ignores the case of paths. 

83 # --exclude-caches, Specified once to exclude folders containing this special file. 

84 # --exclude-file, Specified one or more times to exclude items listed in a given file. 

85 # --iexclude-file, Same as exclude-file but ignores cases like in --iexclude. 

86 # --exclude-if-present 'foo', Specified one or more times to exclude a folder's content if it contains. 

87 # a file called 'foo' (optionally having a given header, no wildcards for the file name supported). 

88 # --exclude-larger-than 'size', Specified once to excludes files larger than the given size. 

89 # Please see 'restic help backup' for more specific information about each exclude option. 

90 

91 cli_repo(connection_choice).backup(c, verbose, target, message) 

92 

93 

94@task 

95def restore(c, connection_choice: str = None, snapshot: str = "latest", target: str = "", verbose: bool = True): 

96 """ 

97 The restore function restores the latest backed-up files by default and puts them in a restore folder. 

98 

99 IMPORTANT: please provide -t for the path where the restore should go. Also remember to include -c for the service 

100 where the backup is stored. 

101 

102 :type c: Context 

103 :param connection_choice: the service where the files are backed up, e.g., 'local' or 'os' (= openstack). 

104 :param snapshot: the ID where the files are backed up, default value is 'latest'. 

105 :param target: The target of the backup (e.g. 'files', 'stream'; default is all types). 

106 :param verbose: display verbose logs (inv restore -v). 

107 :return: None 

108 """ 

109 # For restore, --target is the location where the restore should be placed, --path is the file/path that should be 

110 # retrieved from the repository. 

111 # 'which_restore' is a user input to enable restoring an earlier backup (default = latest). 

112 # Stop the postgres services. 

113 c.run(f"{DOCKER_COMPOSE} stop -t 1 pg-0 pg-1 pgpool", warn=True, hide=True) 

114 

115 # Get the volumes that are being used. 

116 docker_inspect: invoke.Result = c.run("docker inspect pg-0 pg-1", hide=True, warn=True) 

117 if docker_inspect.ok: 

118 # Only if ok, because if pg-0 and pg-1 do not exist, this does not exist either, and nothing needs to be removed 

119 inspected = json.loads(docker_inspect.stdout) 

120 volumes_to_remove = [] 

121 for service in inspected: 

122 volumes_to_remove.extend(mount["Name"] for mount in service["Mounts"] if mount["Type"] == "volume") 

123 # Remove the containers before a volume can be removed. 

124 c.run(f"{DOCKER_COMPOSE} rm -f pg-0 pg-1") 

125 # Remove the volumes. 

126 for volume_name in volumes_to_remove: 

127 c.run(f"docker volume rm {volume_name}") 

128 

129 cli_repo(connection_choice).restore(c, verbose, target, snapshot) 

130 # print("`inv up` to restart the services.") 

131 

132 

133@task(iterable=["tag"]) 

134def snapshots(c, connection_choice: str = None, tag: str = None, n: int = 1, verbose: bool = False): 

135 """ 

136 With this you can see per repo which repo is made when and where, \ 

137 the repo-id can be used at inv restore as an option 

138 

139 :type c: Context 

140 :param connection_choice: service 

141 :param tag: files, stream ect 

142 :param n: amount of snapshot to view, default=1(latest) 

143 :param verbose: show which commands are being executed? 

144 :return: None 

145 """ 

146 # if tags is None set tag to default tags 

147 if tag is None: 

148 tag = ["files", "stream"] 

149 

150 cli_repo(connection_choice).snapshot(c, tags=tag, n=n, verbose=verbose) 

151 

152 

153@task() 

154def run(c, connection_choice: str = None): 

155 """ 

156 This function prepares for restic and runs the input command until the user types "exit". 

157 

158 :type c: Context 

159 :param connection_choice: The connection name of the repository. 

160 """ 

161 

162 cli_repo(connection_choice).prepare_for_restic(c) 

163 while (command := input("> ")) != "exit": 

164 print(c.run(command, hide=True, warn=True, pty=True))