Guix — самая продвинутая операционная система

в 13:06, , рубрики: Arch Linux, Debian, Emacs-Guix, gentoo, Guile Scheme, Guix, nix, nixos, open source, Portage, UNIX, ад зависимостей, системное программирование

Операционные системы (ОС) — обширная тема. На протяжении десятилетий здесь доминировал один подход: Unix. Действительно, большинство современных систем, включая большинство дистрибутивов GNU/Linux, *BSD и macOS, придерживаются архитектуры Unix. (Windows нет, но там почти ничего интересного по этой теме).

В 2000 году Роб Пайк выступил с докладом о том, почему исследования системного ПО не релеванты. Из-за пессимизма или пренебрежения к сообществу он, кажется, полностью проигнорировал жалобы, собранные многими Unix-пользователями в книге The Unix-Haters Handbook (1994). Книга умышленно саркастична, однако указывает на некоторые критические проблемы систем Unix — и они не решены до сих пор.

В 2006 году Элко Доситра опубликовал диссертацию «Полностью функциональная модель развёртывания программного обеспечения», где описан функциональный менеджер пакетов Nix. В 2008 году автор опубликовал NixOS: полностью функциональный дистрибутив Linux. В то время как NixOS повторно использует много свободного ПО для Unix-систем, она настолько отходит от дизайна и философии Unix, что вряд ли её можно назвать «системой Unix».

Nix — гигантский шаг вперёд в системной разработке. Эта ОС не только решила множество проблем Unix (включая критику из вышеупомянутого сборника), но также открыла путь для многих других функций и исследований, которые могут сыграть очень важную роль в наше время, когда надёжность и безопасность стали главной темой многих научных, общественных и политических дебатов.

Пайк ошибался. И это доказывает ещё один более общий момент: вероятно, разумнее воздержаться от объявления нерелевантности любого исследования, если ты не можешь доказать невозможность дальнейшего развития. И упомянутый доклад вряд ли можно считать математическим доказательством. Он только укрепил идею, что Unix «достаточно хорош» и что следует смириться с его особенностями и проблемами.

К счастью, этот ненужный пессимизм оказался недальновидным и не сохранился надолго: всего через пару лет система Nix доказала его неправоту.

Появление Guix

Guix — это менеджер пакетов на Nix, а GuixSD —операционная система, эквивалент NixOS, которая стремится быть «полностью программируемой ОС». Действительно, здесь почти всё написано и настраивается в Guile Scheme: от управления пакетами Guix до системы инициализации GNU shepherd.

Guix значительно отличается от операционных систем Unix. Можете просмотреть документацию и оценить степень изменений:

Преимущества Guix

Преимущества Guix революционны до такой степени, что остальные ОС выглядят устаревшими системами по сравнению с ней.

Мои личные любимые функции:

  • Неуязвимость системы: Guix ведёт историю всех изменений как на системном, так и на пользовательском уровне. Если обновление что-то сломает, всегда можно откатить. Это делает систему фактически неуязвимой.
  • Целостность: поскольку конфигурация декларативна, это даёт пользователю или системному администратору полный контроль. На других вариантах Unix гораздо труднее сказать, когда изменяется какой-то случайный файл конфигурации.
  • Полностью программируемая ОС: программируйте системные конфигурации и используйте систему контроля версий. Многие системные службы можно настроить в Guile Scheme: от правил udev до Xorg, PAM и т. д. Благодаря Guile, конфигурация может быть обусловлена оборудованием или даже именем хоста!
  • Прямая замена другим (не таких хорошим) пакетным менеджерам: зачем отдельно управлять пакетами Emacs, Python или TeXlive, если есть единый интерфейс для всех (см. ниже)! Так проще писать и обслуживать декларации для профилей пользователей.
  • Определения пакетов с помощью Guile: гораздо эффективнее разрабатывать определения пакетов в массовом порядке. Он выгодно заменяет такие концепции, как флаги Portage USE (см. ниже).
  • Несколько путей выдачи пакетов: У пакета Guix может иметь несколько «путей выдачи», которые служат для разделения различных компонентов (библиотеки, дополнительные инструменты, документация и т. д.). В других операционных системах (обычно Debian) сложнее угадать, какие пакеты подходят друг другу.
  • Неразмножаемые входы: В терминологии Guix «входы» — это зависимости пакетов. Профиль пользователя и среда содержат только явно установленные пользователем пакеты и не обязательно их зависимости. Например, см. inxi — средство создания отчётов о системной информации: если меня интересуют только отчёты о системе/оборудовании inxi, необязательно добавлять в PATH два-три десятка дополнительных инструментов командной строки. Guix позволяет отображать в профиле пользователя только то, что ему действительно нужно.
  • Окружения Guix: при запуске guix environment SOME-PACKAGES Guix устанавливает временное окружение, где представлены все требования для SOME-PACKAGES. Это можно использовать для простой настройки среды сборки для проекта, а также в иных целях (см. ниже). Одно замечательное качество — они позволяют запускать программы, не устанавливая их в профиль пользователя.
  • Частичные обновления: поддерживаются на 100%. Это, возможно, основная причина поломок в плавающих релизах вроде Arch Linux и Gentoo: поскольку там одновременно поддерживается только несколько версий (обычно всего одна), то вся система должна обновляться целиком. Это означает больше трафика при каждом обновлении. С помощью Guix любой пакет обновляется по отдельности.
  • Непрерывная интеграция или почему Guix может работать без мейнтейнеров пакетов: благодаря воспроизводимым сборкам и поддержке частичных обновлений, если пакет работает в Guix, то он будет работать «всегда» и не сломается при следующем обновлении какой-то зависимости (точнее, если зависимость ломает пакет, то это тривиально исправляется, чтобы использовать правильную версию библиотеки). Таким образом, работу с пакетами можно перенести на «фермы сборки» (одна на Hydra из проекта Nix, другая на Cuirass). Сравните это с большинством других сообществ GNU/Linux, которым для обновления тысяч пакетов требуются десятки мейнтейнеров. Такой подход не масштабируется: в итоге эти дистрибутивы стагнируют на паре тысяч пакетов. В Guix количество пакетов может спокойно расти, не опасаясь краха. В то же время контрибуторов можно использовать более эффективно.

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

  • guix import и guix refresh: автоматическое и рекурсивное создание или обновление определений пакета. Одновременно обрабатываются сотни определений. Такие функции подчёркивают преимущества реального языка программирования в ОС. Что является трудной задачей в большинстве операционных систем, относительно легко реализуется в Guix.
  • Каналы Guix: одна из моих любимых функций! В Arch Linux или Gentoo требуется создавать локальный репозиторий. Поскольку они не поддерживают частичные обновления, пользователь должен время от времени заниматься некоторым обслуживанием (т. е. убедиться, что обновления зависимостей не нарушают пакеты). Каналы Guix выгодно заменяют оверлеи AUR из Arch Linux и Gentoo, позволяя любому распространять свои определения пакетов, например, из репозиториев Git. Опять же, это гарантирует полную прозрачность (откаты, история и т. д.).
  • Emacs-Guix: насколько я знаю, Guix — единственный дистрибутив, который поставляется с самым мощным пользовательским интерфейсом Emacs!
  • Guix packs: реальная альтернатива контейнерам, таким как Docker. Большинство контейнерных систем страдают от критических проблем: они не воспроизводятся и в реальности представляют собой непрозрачные двоичные файлы, что категорически неприемлемо для пользователей, которые заботятся о доверии, безопасности и конфиденциальности. Напротив, Guix packs абсолютно ясны, воспроизводимы и прозрачны.
  • guix system vm и guix system disk-image: Guix делает тривиальным воспроизведение всей текущей системы в виде live USB, внутри VM или на удалённой машине.

Guix по сравнению с конкурентами

Debian, Arch Linux и большинство других дистрибутивов GNU/Linux

В дистрибутивах GNU/Linux обычно отсутствуют вышеупомянутые преимущества Guix. Самые критичные недостатки:

  • Отсутствие поддержки нескольких версий пакетов или «ад зависимостей». Скажем, последний mpv требует нового ffmpeg, но обновление ffmpeg ломает большинство других программ. Мы застряли перед дилеммой: либо ломаем некоторые пакеты, либо сохраняем старые версии. Хуже того, может вообще не оказаться подходящего пакета или поддержка ОС отсутствует. Эта проблема присуща большинству дистрибутивов, которые не могут гарантировать выполнение своей основной задачи: пакет для любой программы.
  • Критичная зависимость от мейнтейнеров. Нефункциональное управление пакетами означает, что все пакеты нужно постоянно тестировать на совместимость. Это много тяжёлой работы для тех, на чьи плечи возложили эту задачу. На практике это означает, что качество управления пакетами в значительной степени зависит от людей. Дистрибутив без достаточного количества мейнтейнеров неизбежно пострадает и, возможно, умрёт. Это требование к рабочей силе нормально не масштабируется и по мере увеличения количества пакетов ведёт к увеличению сложности (и кодовой базы, и управления).

Gentoo, *BSD

У Gentoo и других дистрибутивов с менеджером пакетов Portage есть знаменитая функция: флаги USE для активации функций во всей системе (например, отключение звука, включение поддержки GUI и т. д.).

Флаги USE делают тривиальным включение и отключение функций от автора пакета (и преимущество в том, что они протестированы). С другой стороны, Portage не позволяет настраивать функции, не продуманные заранее. Например, если у пакета есть дополнительный звук, но автор не выставил соответствующий флаг, пользователь ничего не сможет с этим поделать (кроме создания нового определения пакета).

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

(loop-over (TARGET-PACKAGES)
  (package
    (inherit TARGET)
    (changes-here... including patches, build options, etc.))

Такой код пакетно устанавливает определения для TARGET-PACKAGES с вашими изменениями. Ни одно из изменений не нужно вносить в определение пакета. В любое время пользователь сохраняет полный контроль над изменениями, которые могут быть внесены в пакеты.

Я любил Gentoo, но после перехода на Guix ограниченность Portage стала очевидной.

  • Система флагов USE не позволяет настраивать незапланированные произвольные функции.
  • Использование флагов добавляет целый класс сложности (см. довольно сложную семантику atom) для описания и управления взаимоотношениями функций между пакетами. Guix полностью удаляет этот уровень сложности, используя Guile Scheme для программирования отношений.

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

На практике Guix предоставляет предварительно скомпилированные пакеты — огромная экономия времени по сравнению с Gentoo (хотя Portage поддерживает распространение двоичных пакетов).

Системы *BSD (например, FreeBSD) страдают от аналогичных проблем в make config.

Nix

Nix стала историческим прорывом в исследовании операционных систем, а Guix почти все свои идеи позаимствовала оттуда. Сегодня Nix по-прежнему остаётся одной из лучших активных ОС. Вероятно, Guix вообще бы не существовала, если бы не один недостаток.

На мой взгляд, Guix решает основную проблему Nix: вместо собственного предметно-ориентированного языка (DSL) тут применяется полноценный язык программирования Guile Scheme на основе Lisp.

«Внедрение собственного языка программирования» — очень распространённое заблуждение в разработке программного обеспечения. Это сильно ударило по многим проектам, где конфигурация или язык программирования страдали от следующих недостатков:

  • ограниченная выразительность и возможности;
  • ещё один язык для изучения (но не что-то очень полезное и универсальное), который требует от пользователя некоторых усилий и, таким образом, создаёт барьер входа;
  • менее читабельный код (по крайней мере, поначалу);
  • часто низкая производительность.

Есть огромное множество проектов на доморощенных или слишком ограниченных языках:

  • XML, HTML (ещё лучше: S-XML)
  • Make, Autoconf, Automake, Cmake и др.
  • Bash, Zsh, Fish (ещё лучше: Eshell или scsh)
  • JSON, TOML, YAML
  • Ebuild из Portage в Nix и многие другие синтаксические правила определений пакетов ОС
  • Firefox, когда использовал XUL (с тех пор Mozilla отказалась от него) и большинство других доморощенных языков для расширений
  • SQL
  • Octave, R, PARI/GP, большинство научных программ (например, Common Lisp, Racket и другая Scheme)
  • Регулярные выражения (rx в Emacs, PEG в Racket и др.)
  • sed, AWK и др.
  • Большинство конфигураций для init, включая systemd (ещё лучше: GNU Shepherd)
  • cron (ещё лучше: mcron)
  • conky (не полностью программируемый, хотя это должна быть самая ожидаемая функция подобной программы)
  • TeX, LaTeX (и все деривативы), Asymptote (ещё лучше: scribble, skribilo — пока в разработке; по состоянию на январь 2019 года TeX/LaTeX по-прежнему используется как промежуточный шаг в подготовке PDF)
  • Большинство программ с конфигурациями, которые не используют язык программирования общего назначения.

Повторное изобретение колеса, обычно, не самая хорошая идея. Когда дело доходит до таких важных инструментов, как языки программирования, у этого весьма драматические последствия. Требуются ненужные дополнительные усилия, возникают ошибки. Cообщество рассеивается. Более консолидированные сообщества более эффективны и лучше используют своё время, если улучшают существующие, хорошо разработанные языки программирования.

Не только для десктопа

Guix поддерживает несколько архитектур (i686, x86_64, ARMv7 и AArch64 по состоянию на январь 2019 года), и планирует поддержку большего количества ядер за пределами экосистемы Linux (скажем, варианты *BSD, GNU Hurd или, может, ваша собственная система!).

Это делает Guix отличным инструментом для развёртывания (воспроизводимых) серверов и других специализированных систем. Думаю, во встроенных системах Guix может очень хорошо конкурировать с OpenWRT (хотя потребуется некоторая работа по портированию на встроенные системы).

Самовоспроидимые live USB

Выше я упомянул о guix system disk-image: например, он позволяет воссоздать текущую систему на USB-флэшке.

Таким образом клон текущей системы легко подключить в любом месте и реплицировать точную актуальную среду (за вычетом аппаратного обеспечения). Туда можно включить пользовательские данные: ключи PGP, электронную почту. Всё доступно сразу после загрузки.

Очевидно, что клонирование работает и дальше с той машины, на которой установлен клон: вместо «голой» Guix развёртывается полноценная ОС, готовая для работы.

Замена других пакетных менеджеров

Emacs, Python, Ruby… и мощь guix environment

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

  • Повсеместная воспроизводимость.
  • Повсеместные откаты.
  • Не нужно изучать ещё один пакетный менеджер.

В этом месте нужно упомянуть guix environment. Эта команда настраивает временную среду только с определённым набором пакетов, как virtualenv. Киллер-фича в том, что она универсальна для всех языков и их комбинаций.

TeXlive

(Дисклеймер: по состоянию на январь 2019 года система сборки TeXlive для Guix переделывается).

TeXlive удостоился отдельного упоминания, потому что он особенно ужасен :), что ещё раз подтверждает спасительную роль Guix!

Большинство операционных систем на базе Unix обычно распространяют TeXlive в составе наборов пакетов. Например, в Arch Linux есть десяток таких. Если вам нужны некоторые пакеты TeX из разных наборов, то Arch Linux не оставляет выбора, кроме как установить тысячи (возможно, ненужных) пакетов, а TeXlive занимает много места: сотни мегабайт.

В качестве альтернативы можно установить TeXlive вручную, но давайте посмотрим правде в глаза: tlmgr — просто плохой пакетный менеджер, и он требует утомительной дополнительной работы.

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

Ядро

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

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

В Guix ядро — полностью настраиваемый обычный пакет, как и любой другой. Можно всё настроить и передать файл конфигурации ядра в определение пакета.

Например, ниже приведены определения несвободного ядра Linux с драйвером iwlwifi (предупреждение: настоятельно не рекомендую использовать проприетарные драйверы, так как они представляют серьёзную угрозу вашей приватности и свободе):

(define-module (ambrevar linux-custom)
  #:use-module (guix gexp)
  #:use-module (guix packages)
  #:use-module (guix download)
  #:use-module (guix git-download)
  #:use-module (guix build-system trivial)
  #:use-module ((guix licenses) #:prefix license:)
  #:use-module (gnu packages linux)
  #:use-module (srfi srfi-1))

(define-public linux-nonfree
  (package
    (inherit linux-libre)
    (name "linux-nonfree")
    (version (package-version linux-libre))
    (source
     (origin
      (method url-fetch)
      (uri
       (string-append
	"https://www.kernel.org/pub/linux/kernel/v4.x/"
	"linux-" version ".tar.xz"))
      (sha256
       (base32
	"1lm2s9yhzyqra1f16jrjwd66m3jl43n5k7av2r9hns8hdr1smmw4"))))
    (native-inputs
     `(("kconfig" ,(local-file "./linux-custom.conf"))
       ,@(alist-delete "kconfig" (package-native-inputs linux-libre))))))

(define (linux-firmware-version) "9d40a17beaf271e6ad47a5e714a296100eef4692")
(define (linux-firmware-source version)
  (origin
    (method git-fetch)
    (uri (git-reference
	  (url (string-append "https://git.kernel.org/pub/scm/linux/kernel"
			      "/git/firmware/linux-firmware.git"))
	  (commit version)))
    (file-name (string-append "linux-firmware-" version "-checkout"))
    (sha256
     (base32
      "099kll2n1zvps5qawnbm6c75khgn81j8ns0widiw0lnwm8s9q6ch"))))

(define-public linux-firmware-iwlwifi
  (package
    (name "linux-firmware-iwlwifi")
    (version (linux-firmware-version))
    (source (linux-firmware-source version))
    (build-system trivial-build-system)
    (arguments
     `(#:modules ((guix build utils))
       #:builder (begin
		   (use-modules (guix build utils))
		   (let ((source (assoc-ref %build-inputs "source"))
			 (fw-dir (string-append %output "/lib/firmware/")))
		     (mkdir-p fw-dir)
		     (for-each (lambda (file)
				 (copy-file file
					    (string-append fw-dir (basename file))))
			       (find-files source
					   "iwlwifi-.*\.ucode$|LICENSE\.iwlwifi_firmware$"))
		     #t))))
    (home-page "https://wireless.wiki.kernel.org/en/users/drivers/iwlwifi")
    (synopsis "Non-free firmware for Intel wifi chips")
    (description "Non-free iwlwifi firmware")
    (license (license:non-copyleft
	      "https://git.kernel.org/cgit/linux/kernel/git/firmware/linux-firmware.git/tree/LICENCE.iwlwifi_firmware?id=HEAD"))))

Кастомное ядро и встроенное ПО можно условно включить в текущую конфигурацию системы (какой-нибудь файл config.scm):

(define *lspci*
  (let* ((port (open-pipe* OPEN_READ "lspci"))
	 (str (get-string-all port)))
    (close-pipe port)
    str))

(operating-system
 (host-name "...")
 ;;...

 (kernel (cond
	   ((string-match "Network controller: Intel Corporation Wireless 8888"
			  *lspci*)
	    linux-nonfree)
	   (#t linux-libre)))
 (firmware (append (list linux-firmware-iwlwifi)
		    %base-firmware))

Затем выполните следующие действия для установки новой конфигурации системы:

sudo -E guix system reconfigure config.scm

Даже не устанавливая новое ядро, вы можете напрямую создать образ, готовый к загрузке с USB-накопителя.

Игры

Поскольку пакеты Guix используют передовые технологии (например, последние версии Mesa) и допускают полную настройку ядра, это идеальная платформа для игр и, в частности, для упаковки игр!

К сожалению, игровая индустрия пока далека от философии свободного программного обеспечения, и очень немногие игры упакованы в рамках официального проекта Guix.

Хотя Guix выступает за свободные программы и не приемлет в своём репозитории никакой проприетарщины, по иронии судьбы, многие продвинутые функции делают Guix идеальным менеджером пакетов для несвободных программ.

Некоторые из преимуществ:

  • guix environment позволяет запускать любое приложение в изолированном контейнере, который ограничивает доступ к сети, скрывает файловую систему (нет риска, что проприетарная программа украдёт какой-то из ваших файлов, скажем, биткоин-кошелек или ключи PGP) и даже информацию системного уровня, такую как имя пользователя. Это необходимо для запуска любой ненадёжной программы с закрытым исходным кодом.
  • Функциональное управление пакетами: программы с закрытым исходным кодом обычно не выдерживают проверку временем и ломаются, когда зависимость библиотеки изменяет свой API. Поскольку Guix определяет пакеты поверх любой версии любой зависимости (без конфликтов с текущей системой), Guix позволяет создавать пакеты для игр с закрытым исходным кодом, которые будут работать вечно.
  • Воспроизводимая среда: программы с закрытым исходным кодом обычно плохо переносятся и могут вести себя по-разному в системах с немного разными зависимостями. Свойство воспроизводимости Guix подразумевает, что если мы заставим пакет Guix работать один раз, то он будет работать всегда (если не считать поломку железа или изменение аппаратной конфигурации).

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

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

Хитрости и советы

Emacs-Guix

Одно из удивительных преимуществ Guix — интерфейс Emacs-Guix, который позволяет устанавливать и удалять пакеты, выборочно обновлять, искать, переходить к определению пакета, управлять поколениями, печатать «различия» между ними и многое другое.

У него есть режимы разработки для сборки и программирования, а также специальная интерактивная среда Scheme REPL. Это уникальный пользовательский интерфейс для операционной системы.

Есть ещё интерфейс Helm System Packages, который частично перекрывается с Emacs-Guix, но мне он кажется более приятным для быстрого поиска пакетов и быстрых операций.

Хранение данных

Поскольку Guix сохраняет несколько поколений системных конфигураций (включая всю историю пакетов), то требует больше места на диске, чем другие операционные системы.

По моему опыту, в 2018 году раздел 25 ГБ приходилось чистить примерно раз в месяц (с учётом, что у меня большие запросы про количеству пакетов), а раздел 50 ГБ можно не трогать целый год.

Для очистки хранилища удобно использовать команду guix gc, но она может удалить «слишком много пакетов», то есть пакеты, которые понадобятся сразу при следующем обновлении.

В Emacs-Guix есть команда m-x guix-store-dead-item, которая сортирует мёртвые пакеты по размеру и позволяет удалять их по отдельности.

Если нужно проанализировать зависимости, посмотрите на guix gc --references и guix gc --requisites. Это можно комбинировать с результатом выдачи guix build ..., чтобы увидеть разные элементы графа зависимостей.

Например, чтобы просмотреть код одного из скриптов сборки, откройте файл, возвращаемый следующей командой:

$ guix gc --references $(guix build -d coreutils) | grep builder
/gnu/store/v02xky6f5rvjywd7ficzi5pyibbmk6cq-coreutils-8.29-guile-builder

Генерация манифеста

Часто бывает полезно сгенерировать манифест всех пакетов, установленных в каком-то профиле.

Это можно сделать с помощью следующего скрипта Guile:

(use-modules (guix profiles)
	     (ice-9 match)
	     (ice-9 pretty-print))

(match (command-line)
  ((_ where)
   (pretty-print
    `(specifications->manifest
      ',(map manifest-entry-name (manifest-entries (profile-manifest where))))))
  (_ (error "Please provide the path to a Guix profile.")))

Например, запускаете его на профиле ~/.guix-profile:

$ guile -s manifest-to-manifest.scm ~/.guix-profile

В моих dotfiles отслеживается история установленных пакетов. Поскольку я также сохраняю версию Guix, то могу вернуться к точному состоянию своей системы в любой момент в прошлом.

Ссылки

Некоторые веб-интерфейсы:

Документы:

Неофициальные пакеты:

Автор: m1rko

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js