skilleter-thingy 0.0.22__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of skilleter-thingy might be problematic. Click here for more details.
- skilleter_thingy/__init__.py +0 -0
- skilleter_thingy/addpath.py +107 -0
- skilleter_thingy/aws.py +548 -0
- skilleter_thingy/borger.py +269 -0
- skilleter_thingy/colour.py +213 -0
- skilleter_thingy/console_colours.py +63 -0
- skilleter_thingy/dc_curses.py +278 -0
- skilleter_thingy/dc_defaults.py +221 -0
- skilleter_thingy/dc_util.py +50 -0
- skilleter_thingy/dircolors.py +308 -0
- skilleter_thingy/diskspacecheck.py +67 -0
- skilleter_thingy/docker.py +95 -0
- skilleter_thingy/docker_purge.py +113 -0
- skilleter_thingy/ffind.py +536 -0
- skilleter_thingy/files.py +142 -0
- skilleter_thingy/ggit.py +90 -0
- skilleter_thingy/ggrep.py +154 -0
- skilleter_thingy/git.py +1368 -0
- skilleter_thingy/git2.py +1307 -0
- skilleter_thingy/git_br.py +180 -0
- skilleter_thingy/git_ca.py +142 -0
- skilleter_thingy/git_cleanup.py +287 -0
- skilleter_thingy/git_co.py +220 -0
- skilleter_thingy/git_common.py +61 -0
- skilleter_thingy/git_hold.py +154 -0
- skilleter_thingy/git_mr.py +92 -0
- skilleter_thingy/git_parent.py +77 -0
- skilleter_thingy/git_review.py +1416 -0
- skilleter_thingy/git_update.py +385 -0
- skilleter_thingy/git_wt.py +96 -0
- skilleter_thingy/gitcmp_helper.py +322 -0
- skilleter_thingy/gitlab.py +193 -0
- skilleter_thingy/gitprompt.py +274 -0
- skilleter_thingy/gl.py +174 -0
- skilleter_thingy/gphotosync.py +610 -0
- skilleter_thingy/linecount.py +155 -0
- skilleter_thingy/logger.py +112 -0
- skilleter_thingy/moviemover.py +133 -0
- skilleter_thingy/path.py +156 -0
- skilleter_thingy/photodupe.py +110 -0
- skilleter_thingy/phototidier.py +248 -0
- skilleter_thingy/popup.py +87 -0
- skilleter_thingy/process.py +112 -0
- skilleter_thingy/py_audit.py +131 -0
- skilleter_thingy/readable.py +270 -0
- skilleter_thingy/remdir.py +126 -0
- skilleter_thingy/rmdupe.py +550 -0
- skilleter_thingy/rpylint.py +91 -0
- skilleter_thingy/run.py +334 -0
- skilleter_thingy/s3_sync.py +383 -0
- skilleter_thingy/splitpics.py +99 -0
- skilleter_thingy/strreplace.py +82 -0
- skilleter_thingy/sysmon.py +435 -0
- skilleter_thingy/tfm.py +920 -0
- skilleter_thingy/tfm_pane.py +595 -0
- skilleter_thingy/tfparse.py +101 -0
- skilleter_thingy/tidy.py +160 -0
- skilleter_thingy/trimpath.py +84 -0
- skilleter_thingy/window_rename.py +92 -0
- skilleter_thingy/xchmod.py +125 -0
- skilleter_thingy/yamlcheck.py +89 -0
- skilleter_thingy-0.0.22.dist-info/LICENSE +619 -0
- skilleter_thingy-0.0.22.dist-info/METADATA +22 -0
- skilleter_thingy-0.0.22.dist-info/RECORD +67 -0
- skilleter_thingy-0.0.22.dist-info/WHEEL +5 -0
- skilleter_thingy-0.0.22.dist-info/entry_points.txt +43 -0
- skilleter_thingy-0.0.22.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
# Dircolors, a Python library for colorizing and formatting filenames like GNU Coreutils'
|
|
2
|
+
# ls and dircolors programs.
|
|
3
|
+
# Requires python 3.3 or later
|
|
4
|
+
#
|
|
5
|
+
# Copyright 2019 Allen Wild <allenwild93@gmail.com>
|
|
6
|
+
# Modifications copyright 2020 John Skilleter <john@skilleter.org.uk>
|
|
7
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
8
|
+
|
|
9
|
+
""" dircolors, a Python library to colorize filenames based on their type
|
|
10
|
+
for terminal use, like GNU ls and dircolors. """
|
|
11
|
+
|
|
12
|
+
from collections import OrderedDict
|
|
13
|
+
from io import StringIO, TextIOBase
|
|
14
|
+
import os
|
|
15
|
+
import stat
|
|
16
|
+
|
|
17
|
+
from skilleter_thingy import dc_defaults
|
|
18
|
+
from skilleter_thingy import dc_util
|
|
19
|
+
|
|
20
|
+
__all__ = ['Dircolors']
|
|
21
|
+
|
|
22
|
+
_CODE_MAP = OrderedDict()
|
|
23
|
+
def _init_code_map():
|
|
24
|
+
""" mapping between the key name in the .dircolors file and the two letter
|
|
25
|
+
code found in the LS_COLORS environment variable.
|
|
26
|
+
Used for parsing .dircolors files. """
|
|
27
|
+
# This code is wrapped in a function so we can disable pylint's whitespace check
|
|
28
|
+
# on a limited scope.
|
|
29
|
+
# pylint: disable=bad-whitespace
|
|
30
|
+
_CODE_MAP['RESET'] = 'rs'
|
|
31
|
+
_CODE_MAP['DIR'] = 'di'
|
|
32
|
+
_CODE_MAP['LINK'] = 'ln'
|
|
33
|
+
_CODE_MAP['MULTIHARDLINK'] = 'mh'
|
|
34
|
+
_CODE_MAP['FIFO'] = 'pi'
|
|
35
|
+
_CODE_MAP['SOCK'] = 'so'
|
|
36
|
+
_CODE_MAP['DOOR'] = 'do'
|
|
37
|
+
_CODE_MAP['BLK'] = 'bd'
|
|
38
|
+
_CODE_MAP['CHR'] = 'cd'
|
|
39
|
+
_CODE_MAP['ORPHAN'] = 'or'
|
|
40
|
+
_CODE_MAP['MISSING'] = 'mi'
|
|
41
|
+
_CODE_MAP['SETUID'] = 'su'
|
|
42
|
+
_CODE_MAP['SETGID'] = 'sg'
|
|
43
|
+
_CODE_MAP['CAPABILITY'] = 'ca'
|
|
44
|
+
_CODE_MAP['STICKY_OTHER_WRITABLE'] = 'tw'
|
|
45
|
+
_CODE_MAP['OTHER_WRITABLE'] = 'ow'
|
|
46
|
+
_CODE_MAP['STICKY'] = 'st'
|
|
47
|
+
_CODE_MAP['EXEC'] = 'ex'
|
|
48
|
+
|
|
49
|
+
_init_code_map()
|
|
50
|
+
del _init_code_map
|
|
51
|
+
|
|
52
|
+
class Dircolors:
|
|
53
|
+
""" Main dircolors class. Contains a database of formats corresponding to file types,
|
|
54
|
+
modes, and extensions. Use the format() method to check a file and color it appropriately.
|
|
55
|
+
"""
|
|
56
|
+
def __init__(self, load=True):
|
|
57
|
+
""" Initialize a Dircolors object. If load=True (the default), then try
|
|
58
|
+
to load dircolors info from the LS_COLORS environment variable.
|
|
59
|
+
If no data is obtained from LS_COLORS, load the defaults.
|
|
60
|
+
If load=False, don't even load defaults. """
|
|
61
|
+
self._loaded = False
|
|
62
|
+
self._codes = OrderedDict()
|
|
63
|
+
self._extensions = OrderedDict()
|
|
64
|
+
if load:
|
|
65
|
+
if not self.load_from_environ():
|
|
66
|
+
self.load_defaults()
|
|
67
|
+
|
|
68
|
+
def __bool__(self):
|
|
69
|
+
""" convenience method for checking whether this Dircolors object has loaded a database.
|
|
70
|
+
Can be used like
|
|
71
|
+
d = Dircolors()
|
|
72
|
+
if d:
|
|
73
|
+
d.format(somefile)
|
|
74
|
+
"""
|
|
75
|
+
return self._loaded
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def loaded(self):
|
|
79
|
+
""" return a boolean indicating whether some valid dircolors data has been loaded """
|
|
80
|
+
return self._loaded
|
|
81
|
+
|
|
82
|
+
def clear(self):
|
|
83
|
+
""" Clear the loaded data """
|
|
84
|
+
self._loaded = False
|
|
85
|
+
self._codes.clear()
|
|
86
|
+
self._extensions.clear()
|
|
87
|
+
|
|
88
|
+
def load_from_lscolors(self, lscolors):
|
|
89
|
+
""" Load the dircolors database from a string in the same format as the LS_COLORS
|
|
90
|
+
environment variable.
|
|
91
|
+
Returns True if data was successfully loaded, False otherwise (e.g. if
|
|
92
|
+
envvar is unset). Regardless, the current database will be cleared """
|
|
93
|
+
self.clear()
|
|
94
|
+
if not lscolors:
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
for item in lscolors.split(':'):
|
|
98
|
+
try:
|
|
99
|
+
code, color = item.split('=', 1)
|
|
100
|
+
except ValueError:
|
|
101
|
+
continue # no key=value, just ignore
|
|
102
|
+
if code.startswith('*.'):
|
|
103
|
+
self._extensions[code[1:]] = color
|
|
104
|
+
else:
|
|
105
|
+
self._codes[code] = color
|
|
106
|
+
|
|
107
|
+
if self._codes or self._extensions:
|
|
108
|
+
self._loaded = True
|
|
109
|
+
return self._loaded
|
|
110
|
+
|
|
111
|
+
def load_from_environ(self, envvar='LS_COLORS'):
|
|
112
|
+
""" Load the dircolors database from an environment variable. By default,
|
|
113
|
+
use LS_COLORS like the GNU Coreutils `ls` program.
|
|
114
|
+
Returns True if data was successfully loaded, False otherwise (e.g. if
|
|
115
|
+
envvar is unset). Regardless, the current database will be cleared. """
|
|
116
|
+
return self.load_from_lscolors(os.environ.get(envvar))
|
|
117
|
+
|
|
118
|
+
def load_from_dircolors(self, database, strict=False):
|
|
119
|
+
""" Load the dircolors database from a GNU-compatible .dircolors file.
|
|
120
|
+
May raise any of the usual OSError exceptions if filename doesn't exist
|
|
121
|
+
or otherwise can't be read.
|
|
122
|
+
|
|
123
|
+
database can be a string representing a filename, or a file-like object
|
|
124
|
+
opened in text mode (i.e. a subclass of io.TextIOBase). To load from the
|
|
125
|
+
contents of a .dircolors file, wrap it in an io.StringIO object.
|
|
126
|
+
|
|
127
|
+
If strict is True, raise ValueError on the first unparsed line
|
|
128
|
+
|
|
129
|
+
Returns a boolean indicating whether any data was loaded.
|
|
130
|
+
The current database will always be cleared. """
|
|
131
|
+
self.clear()
|
|
132
|
+
if isinstance(database, str):
|
|
133
|
+
file = open(database, 'r')
|
|
134
|
+
elif isinstance(database, TextIOBase):
|
|
135
|
+
file = database
|
|
136
|
+
else:
|
|
137
|
+
raise ValueError('database must be str or io.TextIOBase, not %s'%type(database))
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
for line in file:
|
|
141
|
+
# remove comments and skip empty lines
|
|
142
|
+
line = line.split('#')[0].strip()
|
|
143
|
+
if not line:
|
|
144
|
+
continue
|
|
145
|
+
|
|
146
|
+
# make sure there's two space-separated fields
|
|
147
|
+
split = line.split()
|
|
148
|
+
if len(split) != 2:
|
|
149
|
+
if strict:
|
|
150
|
+
raise ValueError('Warning: unable to parse dircolors line "%s"'%line)
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
key, val = split
|
|
154
|
+
if key == 'TERM':
|
|
155
|
+
continue # ignore TERM directives
|
|
156
|
+
elif key in _CODE_MAP:
|
|
157
|
+
self._codes[_CODE_MAP[key]] = val
|
|
158
|
+
elif key.startswith('.'):
|
|
159
|
+
self._extensions[key] = val
|
|
160
|
+
elif strict:
|
|
161
|
+
raise ValueError('Warning: unable to parse dircolors line "%s"'%line)
|
|
162
|
+
# elif not strict, skip
|
|
163
|
+
|
|
164
|
+
if self._codes or self._extensions:
|
|
165
|
+
self._loaded = True
|
|
166
|
+
return self._loaded
|
|
167
|
+
finally:
|
|
168
|
+
file.close()
|
|
169
|
+
|
|
170
|
+
def load_defaults(self):
|
|
171
|
+
""" Load the default database. """
|
|
172
|
+
self.clear()
|
|
173
|
+
return self.load_from_dircolors(StringIO(dc_defaults.DEFAULT_DIRCOLORS), True)
|
|
174
|
+
|
|
175
|
+
def generate_lscolors(self):
|
|
176
|
+
""" Output the database in the format used by the LS_COLORS environment variable. """
|
|
177
|
+
if not self._loaded:
|
|
178
|
+
return ''
|
|
179
|
+
|
|
180
|
+
def gen_pairs():
|
|
181
|
+
for pair in self._codes.items():
|
|
182
|
+
yield pair
|
|
183
|
+
for pair in self._extensions.items():
|
|
184
|
+
# change .xyz to *.xyz
|
|
185
|
+
yield '*' + pair[0], pair[1]
|
|
186
|
+
|
|
187
|
+
return ':'.join('%s=%s'%pair for pair in gen_pairs())
|
|
188
|
+
|
|
189
|
+
def _format_code(self, text, code):
|
|
190
|
+
""" format text with an lscolors code. Return text unmodified if code
|
|
191
|
+
isn't found in the database """
|
|
192
|
+
val = self._codes.get(code, None)
|
|
193
|
+
if val:
|
|
194
|
+
return '\033[%sm%s\033[%sm'%(val, text, self._codes.get('rs', '0'))
|
|
195
|
+
return text
|
|
196
|
+
|
|
197
|
+
def _format_ext(self, text, ext):
|
|
198
|
+
""" Format text according to the given file extension.
|
|
199
|
+
ext must have a leading '.'
|
|
200
|
+
text need not actually end in '.ext' """
|
|
201
|
+
val = self._extensions.get(ext, '0')
|
|
202
|
+
if val:
|
|
203
|
+
return '\033[%sm%s\033[%sm'%(val, text, self._codes.get('rs', '0'))
|
|
204
|
+
return text
|
|
205
|
+
|
|
206
|
+
def format_mode(self, text, mode):
|
|
207
|
+
""" Format and color the given text based on the given file mode.
|
|
208
|
+
|
|
209
|
+
`mode` can be an integer, usually the st_mode field of an os.stat_result
|
|
210
|
+
object obtained from os.stat() or similar function. It can also be an os.stat_result
|
|
211
|
+
object, and the st_mode field will be extracted automatically.
|
|
212
|
+
|
|
213
|
+
If mode is zero then the formatting is performed only according to the filename.
|
|
214
|
+
|
|
215
|
+
`text` is an arbitrary string which will be colored according to the bits
|
|
216
|
+
set in `mode` and the colors database loaded in this Dircolors object.
|
|
217
|
+
|
|
218
|
+
If `mode` represents a symlink, it will be formatted as such with no dereferencing
|
|
219
|
+
(since this function doesn't know the file name) """
|
|
220
|
+
if not self._loaded:
|
|
221
|
+
return text
|
|
222
|
+
|
|
223
|
+
if isinstance(mode, int):
|
|
224
|
+
pass
|
|
225
|
+
elif isinstance(mode, os.stat_result):
|
|
226
|
+
mode = mode.st_mode
|
|
227
|
+
else:
|
|
228
|
+
raise ValueError('mode must be int or os.stat_result, not %s'%type(mode))
|
|
229
|
+
|
|
230
|
+
if mode:
|
|
231
|
+
if stat.S_ISDIR(mode):
|
|
232
|
+
if (mode & (stat.S_ISVTX | stat.S_IWOTH)) == (stat.S_ISVTX | stat.S_IWOTH):
|
|
233
|
+
# sticky and world-writable
|
|
234
|
+
return self._format_code(text, 'tw')
|
|
235
|
+
if mode & stat.S_ISVTX:
|
|
236
|
+
# sticky but not world-writable
|
|
237
|
+
return self._format_code(text, 'st')
|
|
238
|
+
if mode & stat.S_IWOTH:
|
|
239
|
+
# world-writable but not sticky
|
|
240
|
+
return self._format_code(text, 'ow')
|
|
241
|
+
# normal directory
|
|
242
|
+
return self._format_code(text, 'di')
|
|
243
|
+
|
|
244
|
+
# special file?
|
|
245
|
+
# pylint: disable=bad-whitespace
|
|
246
|
+
special_types = (
|
|
247
|
+
(stat.S_IFLNK, 'ln'), # symlink
|
|
248
|
+
(stat.S_IFIFO, 'pi'), # pipe (FIFO)
|
|
249
|
+
(stat.S_IFSOCK, 'so'), # socket
|
|
250
|
+
(stat.S_IFBLK, 'bd'), # block device
|
|
251
|
+
(stat.S_IFCHR, 'cd'), # character device
|
|
252
|
+
(stat.S_ISUID, 'su'), # setuid
|
|
253
|
+
(stat.S_ISGID, 'sg'), # setgid
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
for mask, code in special_types:
|
|
257
|
+
if (mode & mask) == mask:
|
|
258
|
+
return self._format_code(text, code)
|
|
259
|
+
|
|
260
|
+
# executable file?
|
|
261
|
+
if mode & (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH):
|
|
262
|
+
return self._format_code(text, 'ex')
|
|
263
|
+
|
|
264
|
+
# regular file, format according to its extension
|
|
265
|
+
_, ext = os.path.splitext(text)
|
|
266
|
+
if ext:
|
|
267
|
+
return self._format_ext(text, ext)
|
|
268
|
+
return text
|
|
269
|
+
|
|
270
|
+
def format(self, file, cwd=None, follow_symlinks=False, show_target=False):
|
|
271
|
+
""" Format and color the file given by the name `file`.
|
|
272
|
+
|
|
273
|
+
If `cwd` is not None, it should be a string for the directory relative
|
|
274
|
+
to which `file` is looked up, or an integer representing a directory
|
|
275
|
+
descriptor (usually from `os.open()`).
|
|
276
|
+
|
|
277
|
+
If the file does not exist, it is formatted according to the filename only.
|
|
278
|
+
|
|
279
|
+
Use follow_symlinks to dereference symlinks entirely.
|
|
280
|
+
Use show_target=True with follow_symlinks=False to format both the link name
|
|
281
|
+
and its target in the format:
|
|
282
|
+
linkname -> target
|
|
283
|
+
With linkname formatted as a link color, and the link target formatted as its respective
|
|
284
|
+
type. If the link target is another link, it will not be recursively dereferenced. """
|
|
285
|
+
|
|
286
|
+
if not self.loaded:
|
|
287
|
+
return file
|
|
288
|
+
|
|
289
|
+
if os.path.exists(file):
|
|
290
|
+
try:
|
|
291
|
+
statbuf = dc_util.stat_at(file, cwd, follow_symlinks)
|
|
292
|
+
except OSError as e:
|
|
293
|
+
return '%s [Error stat-ing: %s]'%(file, e.strerror)
|
|
294
|
+
|
|
295
|
+
mode = statbuf.st_mode
|
|
296
|
+
if (not follow_symlinks) and show_target and stat.S_ISLNK(mode):
|
|
297
|
+
target_path = dc_util.readlink_at(file, cwd)
|
|
298
|
+
try:
|
|
299
|
+
dc_util.stat_at(target_path, cwd) # check for broken link
|
|
300
|
+
target = self.format(target_path, cwd, False, False)
|
|
301
|
+
except OSError:
|
|
302
|
+
# format as "orphan"
|
|
303
|
+
target = self._format_code(target_path, 'or') + ' [broken link]'
|
|
304
|
+
return self._format_code(file, 'ln') + ' -> ' + target
|
|
305
|
+
else:
|
|
306
|
+
mode = 0
|
|
307
|
+
|
|
308
|
+
return self.format_mode(file, mode)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#! /usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
################################################################################
|
|
4
|
+
""" Check how much free space is available on all filesystems, ignoring
|
|
5
|
+
read-only filesystems, /dev and tmpfs.
|
|
6
|
+
|
|
7
|
+
Issue a warning if any are above 90% used.
|
|
8
|
+
"""
|
|
9
|
+
################################################################################
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
import argparse
|
|
13
|
+
import psutil
|
|
14
|
+
|
|
15
|
+
################################################################################
|
|
16
|
+
|
|
17
|
+
WARNING_LEVEL = 15
|
|
18
|
+
|
|
19
|
+
################################################################################
|
|
20
|
+
|
|
21
|
+
def main():
|
|
22
|
+
""" Do everything """
|
|
23
|
+
|
|
24
|
+
parser = argparse.ArgumentParser(description='Check for filesystems that are running low on space')
|
|
25
|
+
parser.add_argument('--level', action='store', type=int, default=WARNING_LEVEL,
|
|
26
|
+
help='Warning if less than this amount of space is available on any writeable, mounted filesystem (default=%d)' % WARNING_LEVEL)
|
|
27
|
+
args = parser.parse_args()
|
|
28
|
+
|
|
29
|
+
if args.level < 0 or args.level > 100:
|
|
30
|
+
print('Invalid value: %d' % args.level)
|
|
31
|
+
sys.exit(3)
|
|
32
|
+
|
|
33
|
+
disks = psutil.disk_partitions()
|
|
34
|
+
devices = []
|
|
35
|
+
warning = []
|
|
36
|
+
|
|
37
|
+
for disk in disks:
|
|
38
|
+
if 'ro' not in disk.opts.split(',') and disk.device not in devices:
|
|
39
|
+
devices.append(disk.device)
|
|
40
|
+
usage = psutil.disk_usage(disk.mountpoint)
|
|
41
|
+
|
|
42
|
+
disk_space = 100 - usage.percent
|
|
43
|
+
|
|
44
|
+
if disk_space < args.level:
|
|
45
|
+
warning.append('%s has only %2.1f%% space available' % (disk.mountpoint, disk_space))
|
|
46
|
+
|
|
47
|
+
if warning:
|
|
48
|
+
print('Filesystems with less than %d%% available space:' % args.level)
|
|
49
|
+
print('\n'.join(warning))
|
|
50
|
+
|
|
51
|
+
################################################################################
|
|
52
|
+
|
|
53
|
+
def diskspacecheck():
|
|
54
|
+
"""Entry point"""
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
main()
|
|
58
|
+
|
|
59
|
+
except KeyboardInterrupt:
|
|
60
|
+
sys.exit(1)
|
|
61
|
+
except BrokenPipeError:
|
|
62
|
+
sys.exit(2)
|
|
63
|
+
|
|
64
|
+
################################################################################
|
|
65
|
+
|
|
66
|
+
if __name__ == '__main__':
|
|
67
|
+
diskspacecheck()
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#! /usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
################################################################################
|
|
4
|
+
""" Docker interface for Thingy
|
|
5
|
+
|
|
6
|
+
Copyright (C) 2017 John Skilleter
|
|
7
|
+
|
|
8
|
+
Note that this:
|
|
9
|
+
* Only implements functions required by docker-purge
|
|
10
|
+
* Only has basic error checking, in that it raises DockerError
|
|
11
|
+
for any error returned by the external docker command.
|
|
12
|
+
"""
|
|
13
|
+
################################################################################
|
|
14
|
+
|
|
15
|
+
# TODO: Convert to use thingy.proc
|
|
16
|
+
from skilleter_thingy import process
|
|
17
|
+
|
|
18
|
+
################################################################################
|
|
19
|
+
|
|
20
|
+
class DockerError(Exception):
|
|
21
|
+
""" Exception for dockery things """
|
|
22
|
+
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
################################################################################
|
|
26
|
+
|
|
27
|
+
def instances(all=False):
|
|
28
|
+
""" Return a list of all current Docker instances """
|
|
29
|
+
|
|
30
|
+
cmd = ['docker', 'ps', '-q']
|
|
31
|
+
|
|
32
|
+
if all:
|
|
33
|
+
cmd.append('-a')
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
for result in process.run(cmd):
|
|
37
|
+
yield result
|
|
38
|
+
except process.RunError as exc:
|
|
39
|
+
raise DockerError(exc)
|
|
40
|
+
|
|
41
|
+
################################################################################
|
|
42
|
+
|
|
43
|
+
def stop(instance, force=False):
|
|
44
|
+
""" Stop the specified Docker instance """
|
|
45
|
+
|
|
46
|
+
# TODO: force option not implemented
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
process.run(['docker', 'stop', instance])
|
|
50
|
+
except process.RunError as exc:
|
|
51
|
+
raise DockerError(exc)
|
|
52
|
+
|
|
53
|
+
################################################################################
|
|
54
|
+
|
|
55
|
+
def rm(instance, force=False):
|
|
56
|
+
""" Remove the specified instance """
|
|
57
|
+
|
|
58
|
+
cmd = ['docker', 'rm']
|
|
59
|
+
|
|
60
|
+
if force:
|
|
61
|
+
cmd.append('--force')
|
|
62
|
+
|
|
63
|
+
cmd.append(instance)
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
process.run(cmd)
|
|
67
|
+
except process.RunError as exc:
|
|
68
|
+
raise DockerError(exc)
|
|
69
|
+
|
|
70
|
+
################################################################################
|
|
71
|
+
|
|
72
|
+
def images():
|
|
73
|
+
""" Return a list of all current Docker images """
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
for result in process.run(['docker', 'images', '-q']):
|
|
77
|
+
yield result
|
|
78
|
+
except process.RunError as exc:
|
|
79
|
+
raise DockerError(exc)
|
|
80
|
+
|
|
81
|
+
################################################################################
|
|
82
|
+
|
|
83
|
+
def rmi(image, force=False):
|
|
84
|
+
""" Remove the specified image """
|
|
85
|
+
|
|
86
|
+
cmd = ['docker', 'rmi']
|
|
87
|
+
if force:
|
|
88
|
+
cmd.append('--force')
|
|
89
|
+
|
|
90
|
+
cmd.append(image)
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
process.run(cmd)
|
|
94
|
+
except process.RunError as exc:
|
|
95
|
+
raise DockerError(exc)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#! /usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
################################################################################
|
|
4
|
+
""" Thingy docker-purge command
|
|
5
|
+
|
|
6
|
+
Copyright (C) 2017 John Skilleter
|
|
7
|
+
|
|
8
|
+
Initial version - contains only basic error checking and limited debug output.
|
|
9
|
+
"""
|
|
10
|
+
################################################################################
|
|
11
|
+
|
|
12
|
+
import sys
|
|
13
|
+
import re
|
|
14
|
+
import argparse
|
|
15
|
+
|
|
16
|
+
from skilleter_thingy import logger
|
|
17
|
+
from skilleter_thingy import docker
|
|
18
|
+
|
|
19
|
+
################################################################################
|
|
20
|
+
|
|
21
|
+
def initialise():
|
|
22
|
+
""" Parse the command line """
|
|
23
|
+
|
|
24
|
+
parser = argparse.ArgumentParser(description='Purge docker instances and images')
|
|
25
|
+
|
|
26
|
+
parser.add_argument('-s', '--stop', action='store_true', help='Stop Docker instances')
|
|
27
|
+
parser.add_argument('-k', '--kill', action='store_true', help='Kill Docker instances')
|
|
28
|
+
parser.add_argument('-r', '--remove', action='store_true', help='Remove Docker images')
|
|
29
|
+
parser.add_argument('-l', '--list', action='store_true', help='List what would be done without doing it')
|
|
30
|
+
parser.add_argument('-f', '--force', action='store_true', help='Forcibly kill/remove instances')
|
|
31
|
+
parser.add_argument('--debug', action='store_true', help='Enable debug output')
|
|
32
|
+
parser.add_argument('images', nargs='*', help='List of Docker containers (regular expression)')
|
|
33
|
+
|
|
34
|
+
args = parser.parse_args()
|
|
35
|
+
|
|
36
|
+
# Configure logging
|
|
37
|
+
|
|
38
|
+
log = logger.init('git-prompt')
|
|
39
|
+
|
|
40
|
+
if args.debug:
|
|
41
|
+
log.setLevel(logger.DEBUG)
|
|
42
|
+
log.info('Debug logging enabled')
|
|
43
|
+
|
|
44
|
+
# Default is to stop matching images
|
|
45
|
+
|
|
46
|
+
if not args.stop and not args.kill and not args.remove:
|
|
47
|
+
args.stop = True
|
|
48
|
+
|
|
49
|
+
# Default is to match all containers
|
|
50
|
+
|
|
51
|
+
if not args.images:
|
|
52
|
+
args.images = '.*'
|
|
53
|
+
else:
|
|
54
|
+
args.images = '|'.join(args.images)
|
|
55
|
+
|
|
56
|
+
log.info('Arguments: %s', args)
|
|
57
|
+
return args
|
|
58
|
+
|
|
59
|
+
################################################################################
|
|
60
|
+
|
|
61
|
+
def main(args):
|
|
62
|
+
""" Main code """
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
if args.stop or args.kill:
|
|
66
|
+
for instance in docker.instances():
|
|
67
|
+
if re.match(args.images, instance):
|
|
68
|
+
|
|
69
|
+
print('Stopping instance: %s' % instance)
|
|
70
|
+
|
|
71
|
+
if not args.list:
|
|
72
|
+
docker.stop(instance, force=args.force)
|
|
73
|
+
|
|
74
|
+
if args.kill:
|
|
75
|
+
for instance in docker.instances(all=True):
|
|
76
|
+
if re.match(args.images, instance):
|
|
77
|
+
|
|
78
|
+
print('Removing instance: %s' % instance)
|
|
79
|
+
|
|
80
|
+
if not args.list:
|
|
81
|
+
docker.rm(instance)
|
|
82
|
+
|
|
83
|
+
if args.remove:
|
|
84
|
+
for image in docker.images():
|
|
85
|
+
if re.match(args.images, image):
|
|
86
|
+
|
|
87
|
+
print('Removing image: %s' % image)
|
|
88
|
+
|
|
89
|
+
if not args.list:
|
|
90
|
+
docker.rmi(image, force=args.force)
|
|
91
|
+
|
|
92
|
+
except docker.DockerError as exc:
|
|
93
|
+
sys.stderr.write(str(exc))
|
|
94
|
+
sys.exit(1)
|
|
95
|
+
|
|
96
|
+
################################################################################
|
|
97
|
+
|
|
98
|
+
def docker_purge():
|
|
99
|
+
"""Entry point"""
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
config = initialise()
|
|
103
|
+
main(config)
|
|
104
|
+
|
|
105
|
+
except KeyboardInterrupt:
|
|
106
|
+
sys.exit(1)
|
|
107
|
+
except BrokenPipeError:
|
|
108
|
+
sys.exit(2)
|
|
109
|
+
|
|
110
|
+
################################################################################
|
|
111
|
+
|
|
112
|
+
if __name__ == '__main__':
|
|
113
|
+
docker_purge()
|