Написал поисковик для файлопомоек — pyFileSearcher

По работе нужно было внести ясность в постоянно убывающее место на файловых серверах, на которые пользователи любят сгружать всякий мусор, не относящийся к работе. Столкнувшись с такой необходимостью, полез искать софт и выяснил, что хотя индексаторов в целом довольно много, но они или перегружены функционалом и нацелены на домашнего пользователя — такие просто не справляются с большим объемом файлов; или являются какими-то безумными корпоративными системами, которые выглядят страшно шописец, ставятся как отдельный сервер с разворачиваемыми агентами и настраиваются так, что без поллитры не разобраться.

В общем, я решил что было бы прикольно написать что-то свое. Эту мысль я вынашивал наверное больше года, так как яжнепрограммист ну абсолютно, но вот прошел курс по гуям в питоне и подумал что надо бы попробовать че получится.

Получилось портабельное приложение, которое не обладает горой функционала, но во-первых умеет сожрать в себя 20 миллионов файлов, во-вторых — умеет искать по нужным в быту параметрам, таким как размер файла, тип, и — это важно — дате добавления в индекс. Строго говоря я не видел тулзов, которые бы сами запоминали время, когда файл был обнаружен. Да, у файла есть время создания и время модификации — и казалось бы их должно хватать для отфильтровывания новых файлов, когда мы хотим их найти. Но хрен там был, эти атрибуты ведут себя черт знает как — например файл притащенный с плеера может показать какой-нить 1700й год до нашей эры.

В общем ладно, это все лирика, вот что вышло:

Аскетично, но работает. Прожка может запускаться с ключом, который стартует сканирование, так что можно запускать его раз в сутки по шедулеру и потом смотреть — что же пользователи накидали за прошедшую неделю, нет ли свежих релизов с рутрекера ))

При скролле результатов отсутствующие файлы подсвечиваются красным, список можно сохранить в csv чтобы предъявить владельцу каталога на сервере или его начальнику =) Фильтры поиска можно сохранять (обычно мне нужны медиафайлы размером не менее, список расширений прилагается, в индексе появились за неделю)

Вся эта балалайка разумеется умеет работать под виндой, но и линукс не забыл (была бы макось под рукой, тестил бы код еще и под маком). В качестве базы данных можно использовать или sqlite, базы которого можно рожать прямо из проги, или подключиться к MySQL — мой случай, 20 лямов записей sqlite тупо не вывозит (собственно это проблема большинства несложных индексаторов, какие можно найти в инете)

В общем получилось такое промежуточное решение — простенькое и на скорую руку, не требующее воскуривания мануалов перед использованием, но и не умирающее от большого файлсервера. Да, поиск внутри файлов не умеет, как и кучу других плюшек, так что для домашнего использования скорее всего не пригодится, но мою задачу решает лучше чем что-то похожее, что я использовал (Locate32 — от его интерфейса и возможностей я отталкивался, но он с некоторой периодичностью терял конфиг, жрал под гиг оперативки из-за использования локальных баз, и был виндуз-онли. Хотя в целом прога более чем годная). Так что вот он, первый релиз: https://github.com/qiwichupa/pyFileSearcher/releases также залил на сурсфордж.

Думаю еще пару фишек потом добавить, типа поиска по файлам которые были удалены, потому что оно бывает нужно. Но это как будет время и желание — базовые мои хотелки оно уже удовлетворяет, может кому пригодится тоже =)

 

Настройка хранилища с iSCSI для VMware

Не нашел нормальной статьи как из говна и палок собрать СХД для ESXI от вмвари, так что распишу сам как же это делается.

Для начала нужно СХД.

Подготовка iSCSI-хранилища

Назовем это сервер-хранилище, чтобы не путать с серверами вмвари.

Это может быть любой комп, желательно сервер, желательно с рейдом. Я поставил на него Debian, потому что я люблю дебиан.

Сразу хинт: в рабочей обстановке на сервере скорее всего не будет интернета, поэтому для установки софта лучше развернуть  отдельный дебиан на сервере (виртуалке) с доступом и в инет и в локалку, поставить на него apt-cacher-ng, а на нашей разворачиваемой хранилке создать файл «/etc/apt/apt.conf.d/66proxy» с содержимым

 
 
  1. Acquire::http { Proxy "http://IP:3142"; }

 Тогда конкретно apt будет качать все через этот «прокси» и не придется настраивать настоящую проксю, которая скорее всего не нужна а может и противоречить безопасности.

Однако про iSCSI. Ставим единственный необходимый пакет:

apt update && apt install tgt

После этого добавляем конфиг

/etc/tgt/conf.d/storage_target.conf:
 
  1. <target iqn.2019-10.server1.domain.local:lun1>
  2. scsi_id IET 00010001
  3. controller_tid 1
  4. lun 1
  5. backing-store /dev/mapper/server1-storage
  6. </target>

 

Что тут почем:

iqn.2019-10.server1.domain.local:lun1 — тут iqn.2019-10 — это стандартное начало с указанием даты создания луна  например; storage1.server1.local — адрес нашего сервера; lun1 — первый (и единственный) лун. Фактически во всей этой строке можно писать значения от балды, они несут скорее идентификационное значение для нас.

«scsi_id IET 00010001» , «controller_tid 1» и «lun 1» —  айдишник луна,  айдишник контроллера луна и номер луна, все идут в связке и имеют важное значение, если в планах подключение нескольких импровизированных серверов-хранилищ к серверу с вмварью. Чтобы вмварь не подумала что ваши два (три, пять, двадцать два) сервера-хранилища содержат один и тот же лун,  все эти номера должны быть уникальны. Если у вас по одному луну на хранилище — просто нумеруйте их последовательно: 00010001, 1, 1 — для первого сервера-хранилища; 00020001, 2, 2 — для второго; 00030001, 3, 3 — для третьего и т.д.

backing-store — неотформатированное  блочное устройство, в моем случае том LVM. Имя должно быть уникально, разумеется, в пределах сервера-хранилища.

В моем случае я настраиваю два хранилища, так что вот конфиг второго

/etc/tgt/conf.d/storage_target.conf:
 
  1. <target iqn.2019-10.server2.domain.local:lun1>
  2. scsi_id IET 00020001
  3. controller_tid 2
  4. lun 2
  5. backing-store /dev/mapper/server2-storage
  6. </target>

 

Собственно, всё. Стоит только уточнить — сервер-хранилище и esxi-сервер должны быть в одной подсети, по крайней мере если у вас ESXI не одной из последних версий, которые научились в машрутизируемый iSCSI. Я буду использовать старенький уже ESXI 5, так что для моего случая это замечание справедливо.

Настройка сервера ESXI

Итак, у меня отдельно стоящий сервер ESXI без — это важно — железного iSCSI-адаптера. Если у вас они есть и вмварь их видит (как на картинке ниже) — стоит использовать их, если нет — нажимайте синюю ссылку Add над списком:

После нажатия, вам должны предложить добавить софтварный iSCSI-адаптер

Окей, добавили

Заходим в свойства и добавляем интерфейс из виртуального свича. Замечу, что эта штука не поддерживает аггрегированные физические интерфейсы и откажется добавлять виртуальный, если у него в конфиге прописано более одного физического.

Дальше переходим на вкладку Dynamic Discovery и добавляем айпишники серверов-хранилищ (порт по умолчанию)

Закрываем окна, соглашаемся обновить список лунов и вуаля — видим по два имени на одно однолунное хранилище (имя луна имя контроллера). Обратите внимание — айдишники луна и контроллера входят в имя (к id контроллера дописывается «0000»), а номер луна — показывается в графе LUN.

 

Теперь остается через Storage — Add storage превратить луны в хранилища VMware

Debian Installer: Detect network hardware, missing firmware BNX2

При установке дебиана на сервера подобные IBM x3650 можно столкнуться с ошибкой определения сетевых драйверов: «Some of your hardware needs non-free firmware files to operate», что означает необходимость подсунуть инсталлятору несвободные драйвера, указанные ниже.

В данном случае это «bnx2/bnx2-mips-09-6.2.1b.fw», но может быть и другой в зависимости от железа. Необходимые драйвера можно скачать вот тут: https://github.com/cernekee/linux-firmware/tree/master/bnx2. Их нужно положить на съемный носитель как предписывает инсталлятор (то есть, в данном случае, в папку bnx2 в корне флешки) и нажать YES. Вполне возможно, что операцию придется провести несколько раз, если в сервере несколько сетевых карт, требующих разных драйверов — в таком случае можно скачать их все сразу, или подсовывать по мере необходимости. Отмонтировать или монтировать флешку не нужно — можно смело втыкать и вытыкать из порта сколько понадобится.

Из моих наблюдений: драйвера не подцеплялись со съемного диска Zalman VE200, эмулирующего CD-привод с ISO-образом инсталлятора. Может особенность конкретного устройства, но скорее потому что сам диск был отформатирован в NTFS, так что флешку под дрова лучше сразу форматнуть в FAT32.

CyrTranscoder — свой велосипед для борьбы с кракозябрами

В качестве лишней практики написал перекодировщик кракозябр, наподобие старой-доброй утилки «Штирлиц».

Утилка жрет текст построчно и пытается угадать какая пара кодировок была закосячена. Существует в двух вариантах: с графическим интерфейсом и консольный вариант. Оба варианта — просто скрипты на питоне, но для удобства пользования для винды собран гуевый бинарник.

Скачать можно на гитхабе: https://github.com/qiwichupa/cyrtranscode/releases

LVM: изменение размера, снапшоты, миграция

LVM — старая и добрая технология, представляющая дополнительный слой абстракции между физическими дисками или разделами на них, и томами, отформатированными в вашу любимую файловую систему. Так как статей на тему настройки LVM в инете валом, я ограничусь парой интересных фич, ради которых, собственно, LVM и стоит затевать.

Перво-наперво

У нас должно быть свободное место в Volume Group.  Нет смысла без особой необходимости забивать все свободное место логическими томами, когда их свободное место будет использовано не до конца. Если у нас один хард на 500 гиг, мы делаем группу на весь этот объем, а дальше, например, делаем один том в 1G под swap, другой — в 9G под /, и, например, третий — на 50G под /home. У нас остается  440G свободного места, которое мы в любой момент можем докинуть в любой раздел, а также использовать под снапшоты. Соу…

 

Как добавить места на Logical Volume

Увеличение можно проделывать на смонтированной файловой системе.

Допустим, у нас есть LVM-том с линуксовым корнем /dev/mypc/root, содержащий на себе линуксовый корень. Докинуть 100 мегабайт можно командой:

 
 
  1. lvextend -L +100m /dev/mypc/root
  2. resize2fs /dev/mypc/root

 

Первая команда увеличит том, вторая — распространит на него файловую систему.

 

 

 

Как сделать и откатить снапшот

Допустим, у нас все еще есть LVM-том /dev/mypc/root, содержащий на себе линуксовый корень. Если нам нужно, например, накатить новые дрова для видюхи и мы боимся все сломать, делаем снапшот:

 
 
  1. lvcreate -L2G -s -n rootsnapshot /dev/mypc/root

 

Эта команда сделает снапшот /dev/mypc/rootsnapshot, который будет содержать состояние на момент создания снапшота. При этом есть ограничение в 2 гига на изменения в нашем корне, если превысим — снапшот перестанет быть валидным и его можно будет только удалить:

 
 
  1. lvremove /dev/mypc/rootsnapshot

 

Так как хотелось бы снапшот уметь откатить в случае если у нас все поломается, то вариант с переполнением зарезервированного места — не вариант для нас.

 

Автоматическое увеличение размера снапшота LVM

Тут все просто, но не так просто, как часто пишут в интернетах. Нужно:

  1. Изменить файлик /etc/lvm/lvm.conf:
    snapshot_autoextend_threshold = 80
    snapshot_autoextend_percent = 20
    monitoring = 1

    Эти опции включают мониторинг, а так же увеличивают размер снапшота на 20% (от текущего размера) каждый раз, когда он оказывается занят изменениями на 80%.

  2. Установить dmeventd, который может не стоять, но который нужен чтобы все работало!

Теперь, когда наш снапшот умеет расти в размере и ему почти ничего не страшно, можно поиздеваться над корнем и откатить все изменения командой:

 
 
  1. lvconvert --merge  /dev/mypc/rootsnapshot

 

Вообще эта команда должна производиться при отмонтированном разделе, но так как в нашем случае это корень, то отмонтировать его не получится, и lvconvert скажет, что сделает все в лучшем виде при следующей активации тома — то есть при перезагрузке.

 

Как смигрировать на другой физический диск

Очень вкусная возможность лайв-миграции позволяет перетащить свои разделы с одного диска на второй, не отрываясь от работы.

Допустим у нас есть диск /dev/sda. На его разделе /dev/sda1 у нас покоится Volume Group «mypc» с нашими логическими томами:

  1. root — с корнем линукса (/),
  2. home — с домашними  каталогами (/home),
  3. swap — с разделом подкачки.

И нам нужно все это добро перенести на новый хард — /dev/sdb, который гол как сокол и ничего ж там нет.

Готовим новый диск

 
 
  1. # Создаем раздел на новом диске:
  2. parted -a opt /dev/sdb mkpart primary 0% 100%
  3. # Создаем физический том LVM:
  4. pvcreate /dev/sdb1
  5. # Расширяем Volume Group на новый физический том:
  6. vgextend mypc /dev/sdb1

 

Сдвигаем наши три логических тома со старого физического тома на новый:

 
 
  1. pvmove -n /dev/mypc/root /dev/sda1 /dev/sdb1
  2. pvmove -n /dev/mypc/home /dev/sda1 /dev/sdb1
  3. pvmove -n /dev/mypc/swap /dev/sda1 /dev/sdb1

 

Избавляемся от старого харда

 
 
  1. # Удаляем физический том LVM из группы "mypc":
  2. vgreduce mypc /dev/sda1
  3. # Удаляем физический том LVM:
  4. pvremove /dev/sda1

 

Устанавливаем загрузчик на новый диск и обновляем его конфиг:

 
 
  1. grub-install /dev/sdb
  2. update-grub

 

После перезагрузки останется вытащить старый диск и при необходимости выставить в биосе загрузку с нового.

Мой первый большой и серьезный кусок кода

Сегодня у меня появился небольшой повод для гордости. Я, конечно, не программист, но админ-  периодически небольшие скрипты писать приходится. Но сегодня я дописал (до юзабельного состояния, не идеального, не суперкрасивого, но юзабельного) свой первый большой скрипт-гуй для бэкапа LVM-разделов и, собственно, восстановления из бэкапа. Процентов на 70 это была тренировка питона, ибо написано все на нем, и процентов на 30 — решение своей хотелки управления бэкапами более-менее наглядно и без переписывания неинтерактивных скриптов по каждому чиху.

Итак, что и как оно выглядит (май ингриш из вери перфект, так что возможны смешные косяки).
Скрипт запускается под обычным юзером и в этом случае умеет вызывать судо:

Стартовый экран выглядит вот так:

При первом запуске он запоминает текущую директорию, в которой был запущен, и делает ее рабочей при последующих запусках. Если потом его запустить в другой дире (как на текущем скрине) — будет предлагать сохранить ее как рабочую.
Тут же, не отходя от кассы можно набить список исключений.

Допустим мы хотим забэкапиться, выбираем бэкап!

Скрипт сам находит все смонтированные LVM-разделы и позволяет пометить те, которые хочется забэкапить. В результате бэкапа образуются файлы — первый всегда полный бэкап, последующие автоматом инкрементируются.

Если инкрементов накопилось дофига и больше, и какие-то промежуточные хочется убить — это можно сделать опцией мерджинга. Выделенный файл бэкапа поглотит тот, что перед ним (таким образом можно слить два файла разных разделов — я пока не понял это скорее баг или фича ))).

Ну и наконец — восстановление.
Выбираем файл, который хотим восстановить:

Видим список файлов, которые распакуются один за другим, если мы выбрали инкремент. Выбираем одну из смонтированных директорий (список формируется автоматом), ставим галочку, если перед восстановлением хотим очистить эту директорию от содержимого.

В общем вышла такая себе управлялка бэкапами, для наглядного процесса копирования и восстановления.

Код на гитхабе: https://github.com/qiwichupa/lvmdarbackuper/blob/master/lvmdarbackuper.py

Папка windows\temp забивается файлами cab_xxxx

Данная проблема вызвана сбоем службы автоматического обновления Windows, в частности при работе с серверами обновлений WSUS.

Пошаговое решение проблемы выглядит так:

  1. Остановка службы обновлений (wuauserv)
  2. Остановка службы trustedinstaller
  3. Удаление содержимого папки c:\windows\temp
  4. Удаление cab-файлов из папки c:\windows\logs\CBS
  5. Удаление папки  C:\windows\softwaredistribution
  6. Запуск сервиса trustedinstaller
  7. Запуск службы обновления

Для удаленного автоматического решения проблемы можно воспользоваться скриптом:

fix_winupdate_tmp_cab.ps1
 
  1. $Machine = read-host "Type in the Computer Name"
  2. $windowsUpdateService = 'wuauserv'
  3. $trustedInstallerService = 'trustedinstaller'
  4. function Set-ServiceState
  5. {
  6.     [CmdletBinding()]
  7.     param(
  8.         [string]$ComputerName,
  9.         [string]$ServiceName
  10.     )
  11.     Write-Verbose "Evaluating $ServiceName on $ComputerName."
  12.     [string]$WaitForIt = ""
  13.     [string]$Verb = ""
  14.     [string]$Result = "FAILED"
  15.     $svc = Get-Service -computername $ComputerName -name $ServiceName
  16.     Switch ($svc.status) {
  17.         'Stopped' {
  18.             Write-Verbose "[$ServiceName] is currently Stopped. Starting."
  19.             $Verb = "start"
  20.             $WaitForIt = 'Running'
  21.             $svc.Start()
  22.         }
  23.         'Running' {
  24.             Write-Verbose "[$ServiceName] is Running. Stopping."
  25.             $Verb = "stop"
  26.             $WaitForIt = 'Stopped'
  27.             $svc.Stop()
  28.         }
  29.         default {
  30.             Write-Verbose "$ServiceName is $($svc.status). Taking no action."
  31.         }
  32.     }
  33.     if ($WaitForIt -ne "") {
  34.         Try { # For some reason, we cannot use -ErrorAction after the next statement:
  35.             $svc.WaitForStatus($WaitForIt,'00:02:00')
  36.         } Catch {
  37.             Write-Warning "After waiting for 2 minutes, $ServiceName failed to $Verb."
  38.         }
  39.         $svc = (get-service -computername $ComputerName -name $ServiceName)
  40.         if ($svc.status -eq $WaitForIt) {
  41.             $Result = 'SUCCESS'
  42.         }
  43.         Write-Verbose "$Result - $ServiceName on $ComputerName is $($svc.status)"
  44.         Write-Verbose ("{0} - {1} on {2} is {4}" -f $Result, $ServiceName, $ComputerName, $svc.status)
  45.     }
  46. }
  47. # stop update service
  48. Write-Host "stop update service"
  49. Set-ServiceState -ComputerName $Machine -ServiceName $windowsUpdateService -Verbose
  50. #removes temp files and renames software distribution folder
  51. Write-Host "removes temp files and renames software distribution folder"
  52. Remove-Item \\$Machine\c$\windows\temp\* -recurse
  53. Rename-Item \\$Machine\c$\windows\SoftwareDistribution SoftwareDistribution.old
  54. #restarts update service
  55. Write-Host "restarts update service"
  56. Set-ServiceState -ComputerName $Machine -ServiceName $windowsUpdateService
  57. #removes software distribution.old
  58. Write-Host "removes software distribution.old"
  59. Remove-Item \\$Machine\c$\windows\SoftwareDistribution.old -recurse
  60. #stops trustedinstaller service
  61. Write-Host "stops trustedinstaller service"
  62. Set-ServiceState -ComputerName $Machine -ServiceName $trustedInstallerService
  63. #removes cab files from trustedinstaller
  64. Write-Host "removes cab files from trustedinstaller"
  65. remove-item \\$Machine\c$\windows\Logs\CBS\* -recurse
  66. #restarts trustedinstaller service
  67. Write-Host "restarts trustedinstaller service"
  68. Set-ServiceState -ComputerName $Machine -ServiceName $trustedInstallerService
  69. #rebuilds cab files from WSUS
  70. Write-Host "rebuilds cab files from WSUS"
  71. invoke-command -ComputerName $Machine -ScriptBlock { & cmd.exe "c:\windows\system32\wuauclt.exe /detectnow" }

Источник: https://community.spiceworks.com/topic/495234-windows-temp-file-is-full-of-cab_xxxx-files-on-windows-server-2008-r2

В chrome под linux не работает встроенный чат twitch

Столкнулся с фантастической проблемой на одном из своих компов. Встроенные в сайты чаты твича перестали работать, выдавая или стандартную заглушку:

Или показывая чат, иногда с его содержимым, но с «колесиком» загрузки поверх и без возможности что-либо писать:Такая проблема проявлялась только в хроме (вернее в хромиуме, но не суть) и только на одном из двух аналогичных в плане софта компов. Также проблема возникала только если чат был встроен по ссылке вида «http://www.twitch.tv/channelname/chat» и не проявлялась при встраивании по ссылке вида «https://www.twitch.tv/embed/channelname/chat».

Так как на втором компе с той же версией линукса (Linux Mint 17.3) и Chromium ( 64.0.3282.140) проблемы не наблюдалось, я заглянул в консоль (CTRL   Shift I) и сравнил ошибки. И действительно, на «нерабочем» компе наблюдалась ошибка «Uncaught RangeError: Unsupported time zone specified undefined» Оказалось, что на этом компе у меня выставлена таймзона «-3», соответствующая Москве, а не зона с названием «Москва». Не знаю какого рожна хром решил к этому придраться, но выставление зоны на «именную» решило данную дикую проблему.

Напомню, что в этих наших debian-like дистрах зона меняется командой:

 
 
  1. dpkg-reconfigure tzdata

 

Второй вариант где может не работать

Идем в настройки хрома:
«Настройки контента» -> «Файлы Cookie», убеждаемся что снят флаг «Блокировать сторонние файлы cookie»

Как давно выключена эта виртуалка?

Набросал небольшой скриптец для VMware ESXi, чтобы проверять как давно менялось состояние виртуальных машин. При запуске он заполняет кастомное поле «Power» состоянием и датой, когда это состояние было обнаружено. Кинув в шедулер можно с некоторой точностью понимать, что вот эту виртуалку выключили только вчера, а вот та — выключена уже пару лет (конечно после того как этот скрипт пару лет проверял ее состояние =). Скрипт использует VMware PowerCLI

check-vm-powerstate.ps1
 
  1. Add-PSSnapin VMware.VimAutomation.Core
  2. Connect-VIServer -Server localhost
  3. Get-VM  | ForEach-Object {
  4.                         $vm = $_
  5.                         $date = (Get-Date -UFormat "%Y-%m-%d  %R").ToString()
  6.                         $AnnotationStatus =  (Get-Annotation $_ | Where-Object { $_.Name -eq "Power"}).Value
  7.                        
  8.                         if ( ($_.PowerState -eq 'PoweredOn') -and ($AnnotationStatus -like 'Down*') ) {
  9.                             $vm | Set-Annotation -CustomAttribute "Power" -Value "Up since $date"
  10.                         }
  11.                         if ( ($_.PowerState -eq 'PoweredOn') -and ($AnnotationStatus -eq '') ) {
  12.                             $vm | Set-Annotation -CustomAttribute "Power" -Value "Up since  < $date"
  13.                         }
  14.                         if ( ($_.PowerState -eq 'PoweredOff') -and ($AnnotationStatus -like 'Up*') ) {
  15.                             $vm | Set-Annotation -CustomAttribute "Power" -Value "Down since $date"
  16.                         }
  17.                         if ( ($_.PowerState -eq 'PoweredOff') -and ($AnnotationStatus -eq '') ) {
  18.                             $vm | Set-Annotation -CustomAttribute "Power" -Value "Down since < $date"
  19.                         }
  20.                     }

Поправить пакет *.deb на скорую руку

 

Столкнулся с банальной задачей — после быстрого формирования пакета (агента системы мониторинга) при помощи checkinstall, захотелось дополнить его правильным конфигом и стартовыми скриптами. На скорую руку это делается примерно так:

  1. Распаковываем пакет:
    dpkg-deb -R zabbix-agent.deb extracted/
  2. Внутри каталога extracted/DEBIAN создаем исполняемый файл postinst с требуемым скриптом:
     
     
    1. #!/bin/bash
    2. #
    3. #тело скрипта
    4. #
    5. exit 0
  3. Докладываем в extracted/ все необходимые файлы, размещая по нужным путям. Не забываем о правах доступа при создании каталогов, если софт будет работать не от рута.
  4. Собираем пакет обратно:
    dpkg-deb -b extracted
    mv extracted.deb  zabbix-agent-new.deb

Источники гуглинга:
https://askubuntu.com/questions/825321/
https://askubuntu.com/questions/62534/