├── shreddy2-partition.sh ├── shreddy2.service ├── LICENSE ├── README.rst └── shreddy2.py /shreddy2-partition.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | DISK=$1 4 | 5 | if case "${DISK}" in /dev/sd*) ;; *) false;; esac; then 6 | 7 | /sbin/parted -a optimal "${DISK}" --script -- mklabel msdos mkpart primary fat32 0% 100% 8 | exit $? 9 | fi 10 | 11 | exit 1 12 | -------------------------------------------------------------------------------- /shreddy2.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=The Shreddy2 service. 3 | After=syslog.target 4 | 5 | [Service] 6 | Type=simple 7 | User=shreddy2 8 | Group=shreddy2 9 | ExecStart=/usr/local/bin/shreddy2.py 10 | StandardOutput=syslog 11 | StandardError=syslog 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Martin Schobert, Pentagrid AG 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 18 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 21 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | The views and conclusions contained in the software and documentation are those 26 | of the authors and should not be interpreted as representing official policies, 27 | either expressed or implied, of the project. 28 | 29 | NON-MILITARY-USAGE CLAUSE 30 | Redistribution and use in source and binary form for military use and 31 | military research is not permitted. Infringement of these clauses may 32 | result in publishing the source code of the utilizing applications and 33 | libraries to the public. As this software is developed, tested and 34 | reviewed by *international* volunteers, this clause shall not be refused 35 | due to the matter of *national* security concerns. 36 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ``shreddy2.py`` -- The Raspberry Pi storage scrub station for USB thumb drives. 2 | 3 | .. contents:: 4 | :local: 5 | 6 | About 7 | ====== 8 | 9 | Operate your own USB memory stick cleaning station by running this piece of 10 | software on a Raspberry PI. USB storage media attached to it is overwritten 11 | according to NIST 800-88. The overwriting cycle is 0x00, 0xff and then random 12 | data as final pass. 13 | 14 | Read the full story at https://pentagrid.ch/en/blog/shreddy2-the-raspberry-pi-storage-scrub-station-for-usb-thumb-drives/ 15 | 16 | 17 | Setup 18 | ====== 19 | 20 | Set up the Raspberry Pi 21 | ------------------------- 22 | 23 | Set up a Raspberry Pi and install the Raspberry Pi OS according to the 24 | documentation on https://www.raspberrypi.org/software/. Do this in a 25 | safe environment. 26 | 27 | Log in to the Raspberry PI. Ensure the default password of the Raspberry 28 | Pi is changed. Otherwise, this is a risk for the data attached as USB sticks. 29 | 30 | Install dependencies 31 | --------------------- 32 | 33 | Install dependencies: 34 | 35 | :: 36 | 37 | sudo apt install python3-pyudev coreutils parted dosfstools 38 | 39 | 40 | If you like to use a busylight, run the following commands to install software: 41 | 42 | :: 43 | 44 | git clone https://github.com/nitram2342/pyBusylight.git 45 | cd pyBusylight/ 46 | sudo python3 setup.py install 47 | 48 | Shreddy will probe for the module, and if the module is not there, the 49 | feature is not used. 50 | 51 | Create a user that runs the software and adjust Udev rules 52 | ----------------------------------------------------------- 53 | 54 | Create a user that will later run the software: 55 | 56 | :: 57 | 58 | sudo useradd -d /var/shreddy2 -m -s /usr/sbin/nologin shreddy2 59 | 60 | 61 | Edit ``/etc/udev/rules.d/23-usb-storage-permissions.rules``: 62 | 63 | :: 64 | 65 | ACTION=="add", SUBSYSTEMS=="usb", SUBSYSTEM=="block", MODE="0660", OWNER="shreddy2" 66 | 67 | Edit ``/etc/udev/rules.d/42-usb-busylight.rules`` if you like to use a 68 | Kuando Busylight: 69 | 70 | :: 71 | 72 | SUBSYSTEM=="usb", ATTRS{idVendor}=="27bb", ATTRS{idProduct}=="3bc0", OWNER="shreddy2", MODE="0660" 73 | SUBSYSTEM=="usb", ATTRS{idVendor}=="27bb", ATTRS{idProduct}=="3bca", OWNER="shreddy2", MODE="0660" 74 | SUBSYSTEM=="usb", ATTRS{idVendor}=="27bb", ATTRS{idProduct}=="3bcc", OWNER="shreddy2", MODE="0660" 75 | SUBSYSTEM=="usb", ATTRS{idVendor}=="27bb", ATTRS{idProduct}=="3bcd", OWNER="shreddy2", MODE="0660" 76 | SUBSYSTEM=="usb", ATTRS{idVendor}=="27bb", ATTRS{idProduct}=="f848", OWNER="shreddy2", MODE="0660" 77 | 78 | Reload udev rules: 79 | 80 | :: 81 | 82 | sudo udevadm control --reload-rules 83 | 84 | Install Shreddy2 85 | ----------------- 86 | 87 | Get Shreddy2 source code: 88 | 89 | :: 90 | 91 | git clone https://github.com/pentagridsec/shreddy2 92 | cd shreddy2 93 | 94 | Install program files: 95 | 96 | :: 97 | 98 | sudo cp shreddy2.service /etc/systemd/system/ 99 | sudo cp shreddy2.py shreddy2-partition.sh /usr/local/bin 100 | sudo chown root.root /usr/local/bin/shreddy2.py /usr/local/bin/shreddy2-partition.sh /etc/systemd/system/shreddy2.service 101 | 102 | 103 | Allow Shreddy2 user to run ``/usr/local/bin/shreddy2-partition.sh`` in privileged mode. Therefore, edit ``/etc/sudoers.conf`` by running: 104 | 105 | :: 106 | 107 | sudo visudo 108 | 109 | ... and add the following line to the ``/etc/sudoers.conf`` configuration: 110 | 111 | :: 112 | 113 | shreddy2 ALL=(root) NOPASSWD:/usr/local/bin/shreddy2-partition.sh 114 | 115 | Enable and run server: 116 | 117 | :: 118 | 119 | sudo systemctl daemon-reload 120 | sudo systemctl enable shreddy2 121 | sudo systemctl start shreddy2 122 | sudo systemctl status shreddy2 123 | 124 | 125 | Security and operational notes 126 | ============================== 127 | 128 | * Overwriting Flash memory does not guarantee that there are no data residues 129 | left. It only reduces the probability. It is best effort. 130 | * If the shredding station is compromised and a storage medium is attached, 131 | information is exposed to the compromised station. You may want 132 | to run the shredding station in an air-gapped mode and using a Busylight for 133 | status signalling. 134 | * If you operate the erasing station in a less trustworthy environment, the 135 | station could be compromised. If you leave USB sticks, they may be removed 136 | by other people, either before or after the clean up operation. Furthermore, 137 | USB sticks could be replaced by malicious hardware that has got implants. 138 | * The erasing station should be labelled with a sufficiently noticeable warning. 139 | Otherwise, people think they could charge their phones. 140 | 141 | Copyright and Licence 142 | ====================== 143 | 144 | This software is developed by Martin Schobert . 145 | It is published under BSD license with a non-military clause. Please read 146 | ``LICENSE`` for license details. 147 | -------------------------------------------------------------------------------- /shreddy2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Shreddy2 - The Raspberry Pi storage scrub station for USB thumb drives. 4 | # 5 | # ----------------------------------------------------------------------------- 6 | # Copyright (c) 2021 Martin Schobert, Pentagrid AG 7 | # 8 | # All rights reserved. 9 | # 10 | # Redistribution and use in source and binary forms, with or without 11 | # modification, are permitted provided that the following conditions are met: 12 | # 13 | # 1. Redistributions of source code must retain the above copyright notice, this 14 | # list of conditions and the following disclaimer. 15 | # 2. Redistributions in binary form must reproduce the above copyright notice, 16 | # this list of conditions and the following disclaimer in the documentation 17 | # and/or other materials provided with the distribution. 18 | # 19 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 23 | # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 26 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | # 30 | # The views and conclusions contained in the software and documentation are those 31 | # of the authors and should not be interpreted as representing official policies, 32 | # either expressed or implied, of the project. 33 | # 34 | # NON-MILITARY-USAGE CLAUSE 35 | # Redistribution and use in source and binary form for military use and 36 | # military research is not permitted. Infringement of these clauses may 37 | # result in publishing the source code of the utilizing applications and 38 | # libraries to the public. As this software is developed, tested and 39 | # reviewed by *international* volunteers, this clause shall not be refused 40 | # due to the matter of *national* security concerns. 41 | # ----------------------------------------------------------------------------- 42 | 43 | import pyudev 44 | import subprocess 45 | import threading 46 | import socketserver 47 | import time 48 | import os 49 | import string 50 | from timeit import default_timer as timer 51 | 52 | from enum import Enum 53 | 54 | 55 | # TCP server port to use (using None disables the server) 56 | port = 2342 57 | # The host/IP address to bind the server 58 | host = "" 59 | 60 | # log 61 | last_devices = [] 62 | 63 | # busylight handler 64 | bl_handler = None 65 | 66 | # ------------------------------------------------------------------------- 67 | # Device 68 | # ------------------------------------------------------------------------- 69 | 70 | 71 | class DeviceStatus(Enum): 72 | NONE = 0 # off 73 | REMOVED = 1 # off 74 | DONE = 2 # green 75 | INSERTED = 3 # yellow 76 | RUNNING = 4 # red 77 | ERROR = 5 # red - blink 78 | 79 | def __lt__(self, other): 80 | # https://stackoverflow.com/questions/39268052/how-to-compare-enums-in-python/39269589 81 | if self.__class__ is other.__class__: 82 | return self.value < other.value 83 | return NotImplemented 84 | 85 | 86 | class Device: 87 | """ 88 | We keep some information about the device under shred in the Device class. 89 | """ 90 | 91 | def __init__(self, device_path, model): 92 | self.device_path = device_path 93 | self.model = "".join(filter(lambda x: x in string.printable, model)) 94 | self.status = DeviceStatus.NONE 95 | self.error_msg = None 96 | self.start = timer() 97 | 98 | def get_path(self): 99 | return self.device_path 100 | 101 | def get_model(self): 102 | return self.model 103 | 104 | def get_model(self): 105 | return self.model 106 | 107 | def set_status(self, status, message): 108 | self.status = status 109 | self.message = message 110 | 111 | def set_error(self, message): 112 | self.message = message 113 | self.status = DeviceStatus.ERROR 114 | 115 | def has_error(self): 116 | return self.status == DeviceStatus.ERROR 117 | 118 | def get_status(self): 119 | return self.status 120 | 121 | def get_status_as_str(self): 122 | if self.status == DeviceStatus.REMOVED: 123 | return "Removed" 124 | 125 | if self.status == DeviceStatus.DONE: 126 | return "Done" 127 | 128 | if self.status == DeviceStatus.INSERTED: 129 | return "Inserted" 130 | 131 | if self.status == DeviceStatus.RUNNING: 132 | return self.message 133 | 134 | if self.status == DeviceStatus.ERROR: 135 | return f"Error ({self.message})" 136 | 137 | return "None" 138 | 139 | 140 | # ------------------------------------------------------------------------- 141 | # Output part 142 | # ------------------------------------------------------------------------- 143 | 144 | 145 | class BusylightHandler(threading.Thread): 146 | def __init__(self, busylight): 147 | self.bl = busylight 148 | self.states = {} 149 | self.event = threading.Event() 150 | 151 | if self.bl: 152 | self.bl.keep_alive() 153 | 154 | threading.Thread.__init__(self) 155 | self.start() 156 | 157 | def set_status(self, medium, state): 158 | self.states[medium] = state 159 | self.event.set() 160 | 161 | def run(self): 162 | 163 | max_level = DeviceStatus.NONE 164 | 165 | while True: 166 | 167 | self.event.wait(None if max_level != DeviceStatus.ERROR else 2) 168 | self.event.clear() 169 | 170 | max_level = DeviceStatus.NONE 171 | for m in self.states: 172 | if self.states[m] > max_level: 173 | max_level = self.states[m] 174 | 175 | print(max_level) 176 | if self.bl: 177 | if max_level == DeviceStatus.NONE: 178 | color = (0, 0, 0) 179 | elif max_level == DeviceStatus.REMOVED: 180 | color = (0, 0, 0) 181 | elif max_level == DeviceStatus.INSERTED: 182 | color = (100, 100, 0) 183 | elif max_level == DeviceStatus.RUNNING: 184 | color = (100, 0, 0) 185 | elif max_level == DeviceStatus.DONE: 186 | color = (0, 100, 0) 187 | 188 | if max_level == DeviceStatus.ERROR: 189 | self.bl.blink(rgb=(255, 0, 0), interval=0.5, count=10) 190 | else: 191 | self.bl.set_rgb(color) 192 | self.bl.send() 193 | 194 | 195 | class ConnectionHandler(socketserver.StreamRequestHandler): 196 | 197 | """ 198 | The ConnectionHandler class is our interface to show the shredding status of current and past attached devices. 199 | """ 200 | 201 | # A few color definitions 202 | clear_screen = "\x1b[2J\x1b[1;1H" 203 | off = "\x1b[0m" 204 | red = "\x1b[31m" 205 | green = "\x1b[32m" 206 | cyan = "\x1b[36m" 207 | white = "\x1b[37m" 208 | yellow = "\x1b[33m" 209 | mangenta = "\x1b[35m" 210 | 211 | version_col = red 212 | logo_col = yellow 213 | 214 | def _render_page(self, enable_colors=True): 215 | """ 216 | Function to render the info screen. 217 | """ 218 | 219 | response = ( 220 | self.clear_screen 221 | + self.logo_col 222 | + " _______ __ __ ______ _______ ______ ______ __ __ " 223 | + self.version_col 224 | + " _______ \n" 225 | + self.logo_col 226 | + " | || | | || _ | | || | | | | | | |" 227 | + self.version_col 228 | + " | |\n" 229 | + self.logo_col 230 | + " | _____|| |_| || | || | ___|| _ || _ || |_| |" 231 | + self.version_col 232 | + " |____ |\n" 233 | + self.logo_col 234 | + " | |_____ | || |_||_ | |___ | | | || | | || |" 235 | + self.version_col 236 | + " ____| |\n" 237 | + self.logo_col 238 | + " |_____ || || __ || ___|| |_| || |_| ||_ _|" 239 | + self.version_col 240 | + " | ______|\n" 241 | + self.logo_col 242 | + " _____| || _ || | | || |___ | || | | | " 243 | + self.version_col 244 | + " | |_____ \n" 245 | + self.logo_col 246 | + " |_______||__| |__||___| |_||_______||______| |______| |___| " 247 | + self.version_col 248 | + " |_______|\n" 249 | + f"{self.cyan} +++ Shreddy, ready, go! +++ {self.white} Pentagrid AG - https://pentagrid.ch\n\n" 250 | + f"{self.red} Disclaimer: There is no guarantee that all data is completely deleted.{self.off}" 251 | + f"\n\n" 252 | ) 253 | 254 | if last_devices: 255 | response += "{:16} {:30} {:30}\n".format("Device", "Model", "Status") 256 | response += "{:16} {:30} {:30}\n\n".format("-" * 16, "-" * 30, "-" * 30) 257 | 258 | for dev in reversed(last_devices[-10:]): 259 | if dev: 260 | color = self.off 261 | 262 | if dev.get_status() == DeviceStatus.DONE: 263 | color = self.green 264 | elif dev.get_status() == DeviceStatus.REMOVED: 265 | color = self.off 266 | elif dev.get_status() == DeviceStatus.INSERTED: 267 | color = self.yellow 268 | elif dev.has_error(): 269 | color = self.red 270 | else: 271 | color = self.white 272 | 273 | response += "{:16} {:30} {}{:30}{}\n".format( 274 | dev.get_path(), 275 | dev.get_model(), 276 | color, 277 | dev.get_status_as_str(), 278 | self.off, 279 | ) 280 | else: 281 | response += "No device(s).\n" 282 | 283 | response += "\n\n" 284 | 285 | self.request.sendall(bytearray(response, "utf-8")) 286 | 287 | def handle(self): 288 | while True: 289 | self._render_page() 290 | time.sleep(5) 291 | 292 | 293 | def create_tcp_server(host, port): 294 | 295 | socketserver.TCPServer.allow_reuse_address = True 296 | server = socketserver.ThreadingTCPServer((host, port), ConnectionHandler) 297 | 298 | t = threading.Thread(target=server.serve_forever) 299 | t.start() 300 | 301 | 302 | # ------------------------------------------------------------------------- 303 | # Shredding part 304 | # ------------------------------------------------------------------------- 305 | 306 | 307 | def run_command(command): 308 | ret = subprocess.run(command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) 309 | if ret.returncode != 0: 310 | print(f"+ Command {command} failed with return code {ret.returncode}.") 311 | return False 312 | else: 313 | return True 314 | 315 | 316 | def check_commands_available(commands): 317 | for cmd in commands: 318 | if not run_command(cmd): 319 | print(f"+ Error: command {cmd[0]} not available.") 320 | return False 321 | return True 322 | 323 | 324 | def wait_for_device(path): 325 | for retry in range(1, 20): 326 | print(f"+ Check device availability {path}") 327 | if os.path.exists(path): 328 | # Check if we can open the device file 329 | try: 330 | os.close(os.open(path, os.O_RDONLY)) 331 | return True 332 | except IOError: 333 | pass 334 | print("+ Wait ...") 335 | time.sleep(1) 336 | print("+ Device not found") 337 | return False 338 | 339 | 340 | def erase_medium(device): 341 | 342 | path = device.get_path() 343 | 344 | bl_handler.set_status(path, DeviceStatus.RUNNING) 345 | 346 | num_passes = 3 347 | patterns = ["0", "255"] 348 | 349 | for i in range(1, 2): 350 | start = timer() 351 | print(f"+ Run pass {i} on {path}") 352 | device.set_status(DeviceStatus.RUNNING, f"overwriting pass {i}/{num_passes}") 353 | if not run_command(["badblocks", "-w", "-p", "1", "-t", patterns[i], path]): 354 | device.set_error("Erasing failed.") 355 | bl_handler.set_status(path, DeviceStatus.ERROR) 356 | return False 357 | print( 358 | "%s - Time for overwrite pass %d was: %.2f s" % (path, i, timer() - start) 359 | ) 360 | 361 | start = timer() 362 | print(f"+ Run pass 3 on {path}") 363 | device.set_status(DeviceStatus.RUNNING, f"overwriting pass 3/{num_passes}") 364 | if not run_command(["shred", "-vn", "1", path]): 365 | device.set_error("Erasing failed.") 366 | bl_handler.set_status(path, DeviceStatus.ERROR) 367 | return False 368 | print("%s - Time for overwrite pass %d was: %.2f s" % (path, 3, timer() - start)) 369 | 370 | time.sleep(3) 371 | 372 | start = timer() 373 | print(f"+ Partition disk {path}") 374 | # While 'parted' could be run for the device, it is not able 375 | # to inform the kernel about the change. 376 | device.set_status(DeviceStatus.RUNNING, "partitioning disk") 377 | if not run_command(["sudo", "shreddy2-partition.sh", path]): 378 | device.set_error("Partitioning disk failed") 379 | bl_handler.set_status(path, DeviceStatus.ERROR) 380 | return False 381 | print( 382 | "%s - Time for %s was: %.2f s" 383 | % (path, "shreddy2-partition.sh", timer() - start) 384 | ) 385 | 386 | start = timer() 387 | print(f"+ Wait for file system {path} to appear ...") 388 | fs_device = f"{path}1" 389 | 390 | # Sometimes the device is not immeditly available and 391 | # we need to wait. 392 | if not wait_for_device(fs_device): 393 | device.set_error(f"Device {fs_device} does not appear") 394 | bl_handler.set_status(path, DeviceStatus.ERROR) 395 | return False 396 | print("%s - Time for %s was: %.2f s" % (path, "waiting", timer() - start)) 397 | 398 | print(f"+ Create file system {path}") 399 | start = timer() 400 | device.set_status(DeviceStatus.RUNNING, "Creating file system") 401 | if not run_command(["mkfs.vfat", fs_device]): 402 | device.set_error("Creating file system failed") 403 | bl_handler.set_status(path, DeviceStatus.ERROR) 404 | return False 405 | print("%s - Time for %s was: %.2f s" % (path, "mkfs.vfat", timer() - start)) 406 | 407 | print("+ Completed") 408 | device.set_status(DeviceStatus.DONE, "done") 409 | bl_handler.set_status(path, DeviceStatus.DONE) 410 | 411 | return True 412 | 413 | 414 | # ------------------------------------------------------------------------- 415 | # Monitoring part 416 | # ------------------------------------------------------------------------- 417 | 418 | 419 | def monitor_events(): 420 | monitor = pyudev.Monitor.from_netlink(pyudev.Context()) 421 | monitor.filter_by(subsystem="block") 422 | 423 | for action, device in monitor: 424 | 425 | if device.device_type == "disk": 426 | 427 | if device.action == "add": 428 | 429 | print( 430 | "+ Device attached: {0} (dev-type: {1}, id-type: {2}, usb-driver: {3})".format( 431 | device.device_node, 432 | device.device_type, 433 | device.properties["ID_TYPE"], 434 | device.properties["ID_USB_DRIVER"], 435 | ) 436 | ) 437 | 438 | if ( 439 | device.properties["ID_TYPE"] == "disk" 440 | and device.properties["ID_USB_DRIVER"] == "usb-storage" 441 | ): 442 | print(f"+ Enqueue erase operation for {device.device_node}") 443 | 444 | dev = Device(device.device_node, device.properties["ID_MODEL"]) 445 | bl_handler.set_status(dev.get_path(), DeviceStatus.INSERTED) 446 | last_devices.append(dev) 447 | 448 | t = threading.Thread(target=erase_medium, args=(dev,)) 449 | t.start() 450 | 451 | elif device.action == "remove": 452 | print("+ Device detached: {}".format(device.device_node)) 453 | if last_devices: 454 | for d in reversed(last_devices): 455 | if d.get_path() == device.device_node: 456 | bl_handler.set_status(d.get_path(), DeviceStatus.REMOVED) 457 | d.set_status(DeviceStatus.REMOVED, "removed") 458 | 459 | 460 | # ------------------------------------------------------------------------- 461 | # Program start 462 | # ------------------------------------------------------------------------- 463 | 464 | 465 | def main(): 466 | global bl_handler 467 | 468 | # First check if required commands are available. Therefore, we 469 | # run them in a safe manor and check what the return code is. 470 | if not check_commands_available( 471 | [ 472 | ["shred", "--version"], 473 | ["parted", "-v"], 474 | # ['badblocks'], 475 | ["mkfs.vfat", "--help"], 476 | ] 477 | ): 478 | # When a command is not available, we stop 479 | return False 480 | else: 481 | 482 | try: 483 | from pybusylight import pybusylight 484 | 485 | bl = pybusylight.busylight() 486 | except ImportError: 487 | # Library is not present 488 | bl = None 489 | pass 490 | except ValueError: 491 | # Device is not present 492 | bl = None 493 | pass 494 | 495 | bl_handler = BusylightHandler(bl) 496 | 497 | if port is not None: 498 | print("+ Start TCP server.") 499 | create_tcp_server(host, port) 500 | print(f"+ Start device monitor. Waiting for devices.") 501 | monitor_events() 502 | 503 | 504 | if __name__ == "__main__": 505 | main() 506 | --------------------------------------------------------------------------------