forked from cockpit-project/bots
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathimage-prune
executable file
·223 lines (183 loc) · 8.44 KB
/
image-prune
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
#!/usr/bin/env python3
# This file is part of Cockpit.
#
# Copyright (C) 2013 Red Hat, Inc.
#
# Cockpit is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
#
# Cockpit is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
# Days after which images expire if not in use
IMAGE_EXPIRE = 14
import argparse
import os
import subprocess
import sys
import time
from task import github
from machine.machine_core.constants import IMAGES_DIR
from machine.machine_core.directories import get_images_data_dir
# threshold in G below which unreferenced qcow2 images will be pruned, even if they aren't old
PRUNE_THRESHOLD_G = float(os.environ.get("PRUNE_THRESHOLD_G", 15))
def enough_disk_space():
"""Check if available disk space in our data store is sufficient
"""
st = os.statvfs(get_images_data_dir())
free = st.f_bavail * st.f_frsize / (1024 * 1024 * 1024)
return free >= PRUNE_THRESHOLD_G
def get_refs(open_pull_requests=True, offline=False):
"""Return dictionary for available refs of the format {'rhel-7.4': 'ad50328990e44c22501bd5e454746d4b5e561b7c'}
Expects to be called from the top level of the git checkout
If offline is true, git show-ref is used instead of listing the remote
"""
# get all remote heads and filter empty lines
# output of ls-remote has the format
#
# d864d3792db442e3de3d1811fa4bc371793a8f4f refs/heads/master
# ad50328990e44c22501bd5e454746d4b5e561b7c refs/heads/rhel-7.4
refs = {}
considerable = {}
g = github.GitHub()
if open_pull_requests:
if offline:
raise Exception("Unable to consider open pull requests when in offline mode")
for p in g.pulls():
files = g.get("pulls/{0}/files".format(p["number"]))
images = []
for fl in files:
fl_name = fl['filename']
if fl_name.startswith("images/"):
fl_name_split = fl_name.split("/", 2)
if "/" not in fl_name_split[1]:
images.append(fl_name_split[1])
if images:
sha = p["head"]["sha"]
considerable[sha] = images
subprocess.call(["git", "fetch", "origin", "pull/{0}/head".format(p["number"])])
refs["pull request #{} ({})".format(p["number"], p["title"])] = sha
git_cmd = "show-ref" if offline else "ls-remote"
ref_output = subprocess.check_output(["git", git_cmd], universal_newlines=True).splitlines()
# filter out the "refs/heads/" prefix and generate a dictionary
prefix = "refs/heads"
for ln in ref_output:
[ref, name] = ln.split()
if name.startswith(prefix):
refs[name[len(prefix):]] = ref
return (refs, considerable)
def get_image_links(ref, git_path):
"""Return all image links for the given git ref
Expects to be called from the top level of the git checkout
"""
# get all the links we have first
# trailing slash on path is important
if not git_path.endswith("/"):
git_path = "{0}/".format(git_path)
try:
entries = subprocess.check_output(["git", "ls-tree", "--name-only", ref, git_path],
universal_newlines=True).splitlines()
except subprocess.CalledProcessError as e:
if e.returncode == 128:
sys.stderr.write("Skipping {0} due to tree error.\n".format(ref))
return []
raise
links = [subprocess.check_output(["git", "show", "{0}:{1}".format(ref, entry)], universal_newlines=True)
for entry in entries]
return [link for link in links if link.endswith(".qcow2")]
def get_image_names(quiet=False, open_pull_requests=True, offline=False):
"""Return all image names used by all branches and optionally in open pull requests
"""
images = set()
# iterate over visible refs (mostly branches)
# this hinges on being in the top level directory of the the git checkout
(refs, considerable) = get_refs(open_pull_requests, offline)
# list images present in each branch / pull request
for name, ref in refs.items():
if not quiet:
sys.stderr.write("Considering images from {0} ({1})\n".format(name, ref))
for link in get_image_links(ref, "images"):
if ref in considerable:
for consider in considerable[ref]:
if link.startswith(consider):
images.add(link)
else:
images.add(link)
return images
def prune_images(force, dryrun, quiet=False, open_pull_requests=True, offline=False, checkout_only=False):
"""Prune images
"""
now = time.time()
# everything we want to keep
if checkout_only:
targets = set()
else:
targets = get_image_names(quiet, open_pull_requests, offline)
# what we have in the current checkout might already have been added by its branch, but check anyway
for filename in os.listdir(IMAGES_DIR):
path = os.path.join(IMAGES_DIR, filename)
# only consider original image entries as trustworthy sources and ignore non-links
if path.endswith(".qcow2") or path.endswith(".partial") or not os.path.islink(path):
continue
target = os.readlink(path)
targets.add(target)
expiry_threshold = now - IMAGE_EXPIRE * 86400
for filename in os.listdir(get_images_data_dir()):
path = os.path.join(get_images_data_dir(), filename)
if not force and (enough_disk_space() and os.lstat(path).st_mtime > expiry_threshold):
continue
if os.path.isfile(path) and \
(path.endswith(".xz") or path.endswith(".qcow2") or path.endswith(".partial")) and \
filename not in targets:
if not quiet or dryrun:
sys.stderr.write("Pruning {0}\n".format(filename))
if not dryrun:
os.unlink(path)
# now prune broken links
for filename in os.listdir(IMAGES_DIR):
path = os.path.join(IMAGES_DIR, filename)
# don't prune original image entries and ignore non-links
if not path.endswith(".qcow2") or not os.path.islink(path):
continue
# if the link isn't valid, prune
if not os.path.isfile(path):
if not quiet or dryrun:
sys.stderr.write("Pruning link {0}\n".format(path))
if not dryrun:
os.unlink(path)
def every_image():
result = []
for filename in os.listdir(IMAGES_DIR):
link = os.path.join(IMAGES_DIR, filename)
if os.path.islink(link):
result.append(filename)
return result
def main():
parser = argparse.ArgumentParser(description='Prune downloaded images')
parser.add_argument("--force", action="store_true", help="Delete images even if they aren't old")
parser.add_argument("--quiet", action="store_true", help="Make downloading quieter")
parser.add_argument("-d", "--dry-run-prune", dest="dryrun", action="store_true",
help="Don't actually delete images and links")
parser.add_argument("-b", "--branches-only", dest="branches_only", action="store_true",
help="Don't consider pull requests on GitHub, only look at branches")
parser.add_argument("-c", "--checkout-only", dest="checkout_only", action="store_true",
help="Consider neither pull requests on GitHub nor branches, only look at the current checkout")
parser.add_argument("-o", "--offline", dest="offline", action="store_true",
help="Don't access external sources such as GitHub")
args = parser.parse_args()
try:
prune_images(args.force, args.dryrun, quiet=args.quiet,
open_pull_requests=(not args.branches_only),
offline=args.offline, checkout_only=args.checkout_only)
except RuntimeError as ex:
sys.stderr.write("image-prune: {0}\n".format(str(ex)))
return 1
return 0
if __name__ == '__main__':
sys.exit(main())