Fast HDD Eraser – Быстрое стирание дисков

Мне часто приходится затирать жёсткие диски пачками, например при модернизации стораджей, и мне был очень необходим скоростной источник потока данных которыми затирается целевой диск. Чтобы более менее надёжно затереть данные на диске неразрушив сам диск, недостаточно затереть его нулями из /dev/zero. В тоже время поток данных из /dev/urandom слишком медленный чтобы быстро затирать современные жёсткие диски.
Поскольку необходимый мне уровень стирания не претендовал на уровень военной разведки то я решил выкрутиться через большой массив случайных (псевдослучайных) чисел. А чтобы массив этот быстро работал я решил закинуть его в память. И /dev/shm – идеальное место.
Для стирания я использовал raid контроллер чтобы затирать сразу множество дисков. А для автоматизации процесса написал bash-скрипт. Скрипт имеет свой счётчик продвижения и его можно прервать Ctrl+C и затем продожить с той позиции где он был прерван.

Предупреждение! Скриптом можно уничтожить важные данные, поэтому используйте его на свой страх и риск.

#!/bin/bash
#
# Script to high speed destoy all data on HDD with random data
#

  # randomdatafile in memory fs
  randomdatafile=/dev/shm/randombaloon.raw
  # file to continue writing if script was aborted
  cfgfile=$0.continue

  # one block will be 4Mb
  blocksize="$((4*1024*1024))"

  if [[ -z $1 ]]; then
    DISKLIST=`fdisk -l 2>/dev/null | grep '^Disk[ \t]\+/dev/' | grep -v mapper`
    DISKLIST1=`echo -e "${DISKLIST}" | sed -e 's/\://g' -e 's/\,//g' | awk '{print $2" "$3" "$4" "$5" "$6}'`

    PS3='Please enter block device to destroy data: '
    IFS=$'\n' read -d '' -r -a options <<< "${DISKLIST1}"
    select opt in "${options[@]}" "Quit"
    do
      if (( REPLY == 1 + ${#options[@]} )) ; then
        echo "Exit"
        exit 1
      elif (( REPLY > 0 && REPLY <= ${#options[@]} )) ; then
        target=`echo ${opt} | awk '{print $1}'`
        break
      else
        echo "Incorrect choice. Try again."
      fi
    done
  else
    target="$1"
  fi

  if [[ ! -b $target ]]; then
    echo "$target: is not a block device"
    exit 1
  elif [[ ! -w $target ]]; then
    echo "$target: has no write permission"
    exit 1
  elif grep -q $target /etc/mtab; then
    echo "$target: is mounted! Script is aborted!"
    exit 1
  fi

  if [[ -f ${cfgfile} ]] ; then
    # read previous counter
    start="$(< ${cfgfile})"
    read -p "Found prevous conter. Continue from [${start}]?" input
    if [[ ! -z ${input} ]] ; then
      if [[ -n ${input//[0-9]/} ]]; then
        echo "Value must be an integer!"
        echo "Aborting."
        exit 1
      else
        start=${input}
      fi
    fi
  else
    start=0
  fi

  if [[ ! -e ${randomdatafile} ]] ; then
    echo "Generate initial random data array and write 16x4M blocks to /dev/shm/"
    # with /dev/urandom pseudorandom generator
    # for i in `seq 0 15` ; do echo -en "Write block:\t$i\t-\t"; head -c ${blocksize} < /dev/urandom | 2> /dev/null | dd of=${randomdatafile} bs=4M seek=$i conv=notrunc 2>&1 | grep copied | awk '{print $8" "$9}' ; done
    # with openssl pseudorandom generator
    for i in `seq 0 15` ; do echo -en "Write block:\t$i\t-\t"; openssl rand -rand /dev/urandom ${blocksize} 2> /dev/null | dd of=${randomdatafile} bs=4M seek=$i conv=notrunc 2>&1 | grep copied | awk '{print $8" "$9}' ; done
  else
    echo "${randomdatafile} file exists. Ok."
  fi

  filesize=`stat -c%s ${randomdatafile}`
  if [[ ${filesize} -ne $(($blocksize*16)) ]] ; then
    echo "File size ${filesize} is not equal $(($blocksize*16))"
    exit 1
  fi
  echo "${randomdatafile} file size is Ok."

  echo "Ready to write data to ${target}"

  fdisk_str=`fdisk -l ${target} 2>/dev/null | grep ${target}`
  echo -e "${fdisk_str}"

  echo
  echo "WARNING: IT WILL DESTROY ALL DATA ON THE DISK"
  echo "Are you sure you want to proceed?"

  read -p 'Type YES if you really want to proceed: ' input
  echo
  if [[ ${input} != YES  ]]; then
    echo "Ok. Aborting."
    exit 1
  fi

  # calculate disk size
  disksize=`echo -e "${fdisk_str}" | grep '^Disk' | grep 'bytes$' | awk '{print $(NF-1)}'`
  disksizeinblocks=$(($disksize / $blocksize))

  # write data
  for ((i=${start}; 1; i++)); do 
    position=$((${i}*16))
    size=$((${i}*64))
    echo -en "Writed from ${size}M\tpos:${position} of ${disksizeinblocks}(BLKs)\ti:${i}\t"
    dd if=${randomdatafile} of=${target} bs=4M seek=${position} oflag=direct 2>&1 | grep copied | awk '{print $8" "$9}'
    A=("${PIPESTATUS[@]}")
    if [[ "${A[0]}" -ne "0" ]] ; then
      echo -e "dd has returned an error ${A[0]}."
      exit ${A[0]}
    fi
    if [[ "${position}" -ge "${disksizeinblocks}" ]] ; then
      echo -e "Something wrond. dd sould return an error because the end is reached on destenation device."
      exit 1
    fi
    echo "$i" > "${cfgfile}.new"
    mv "${cfgfile}.new" "${cfgfile}"
  done

  echo "Done..."

UPDATE
Первая версия скрипта была с pipefail от которых я решил избавиться так как меня интересует не любая ошибка в цепочке команд, а только ошибка первой команды dd. Я сделал это через массив ${PIPESTATUS[@] который содержит результат последего выполнения цепочки команд.

Примечание. Поскольку в современных условиях 64 мегабайта, которые используются в скрипте как быстрый буфер “случайных” данных, не такой большой объём то этот скрипт не контролирует объём свободной памяти на /dev/shm и использование swap. Поэтому если её (реальной RAM памяти) будет недостаточно работа может сильно замедлиться изза процесса свапирования. Также при острой нехватке памяти ядро может начать “расстрел” процессов по OOM (Out-Of-Memory). Поэтому запуская скрипт проверьте есть ли память для буфера нужного объёма.