Pengantar Wayang

Boneka adalah sistem manajemen konfigurasi. Ini digunakan untuk membawa host ke kondisi yang diinginkan dan mempertahankan kondisi tersebut.

Saya telah bekerja dengan Wayang selama lebih dari lima tahun sekarang. Teks ini pada dasarnya adalah kompilasi poin-poin penting yang diterjemahkan dan disusun ulang dari dokumentasi resmi, yang akan memungkinkan pemula untuk dengan cepat memahami esensi Wayang.

Pengantar Wayang

Informasi dasar

Sistem operasi Wayang adalah klien-server, meskipun juga mendukung operasi tanpa server dengan fungsionalitas terbatas.

Model operasi tarik digunakan: secara default, setiap setengah jam sekali, klien menghubungi server untuk konfigurasi dan menerapkannya. Jika Anda pernah bekerja dengan Ansible, maka mereka menggunakan model push yang berbeda: administrator memulai proses penerapan konfigurasi, klien sendiri tidak akan menerapkan apa pun.

Selama komunikasi jaringan, enkripsi TLS dua arah digunakan: server dan klien memiliki kunci pribadi dan sertifikat yang sesuai. Biasanya server mengeluarkan sertifikat untuk klien, namun pada prinsipnya dimungkinkan untuk menggunakan CA eksternal.

Pengantar manifesto

Dalam terminologi Wayang ke server boneka Menghubung node (node). Konfigurasi untuk node telah ditulis dalam manifesto dalam bahasa pemrograman khusus - Puppet DSL.

DSL Boneka adalah bahasa deklaratif. Ini menggambarkan keadaan node yang diinginkan dalam bentuk deklarasi sumber daya individual, misalnya:

  • File tersebut ada dan memiliki konten tertentu.
  • Paket telah diinstal.
  • Layanan telah dimulai.

Sumber daya dapat saling berhubungan:

  • Ada ketergantungan, hal ini mempengaruhi urutan penggunaan sumber daya.
    Misalnya, “instal paketnya terlebih dahulu, lalu edit file konfigurasinya, lalu mulai layanannya.”
  • Ada pemberitahuan - jika sumber daya telah berubah, ia mengirimkan pemberitahuan ke sumber daya yang berlangganan sumber daya tersebut.
    Misalnya, jika file konfigurasi berubah, Anda dapat memulai ulang layanan secara otomatis.

Selain itu, DSL Boneka memiliki fungsi dan variabel, serta pernyataan kondisional dan penyeleksi. Berbagai mekanisme templating juga didukung - EPP dan ERB.

Wayang ditulis dalam bahasa Ruby, sehingga banyak konstruksi dan istilah yang diambil dari sana. Ruby memungkinkan Anda memperluas Wayang - menambahkan logika kompleks, jenis sumber daya baru, fungsi.

Saat Wayang berjalan, manifes untuk setiap node tertentu di server dikompilasi ke dalam direktori. Direktori adalah daftar sumber daya dan hubungannya setelah menghitung nilai fungsi, variabel, dan perluasan pernyataan kondisional.

Sintaks dan gaya kode

Berikut adalah bagian dari dokumentasi resmi yang akan membantu Anda memahami sintaks jika contoh yang diberikan tidak cukup:

Berikut ini contoh tampilan manifesnya:

# Комментарии пишутся, как и много где, после решётки.
#
# Описание конфигурации ноды начинается с ключевого слова node,
# за которым следует селектор ноды — хостнейм (с доменом или без)
# или регулярное выражение для хостнеймов, или ключевое слово default.
#
# После этого в фигурных скобках описывается собственно конфигурация ноды.
#
# Одна и та же нода может попасть под несколько селекторов. Про приоритет
# селекторов написано в статье про синтаксис описания нод.
node 'hostname', 'f.q.d.n', /regexp/ {
  # Конфигурация по сути является перечислением ресурсов и их параметров.
  #
  # У каждого ресурса есть тип и название.
  #
  # Внимание: не может быть двух ресурсов одного типа с одинаковыми названиями!
  #
  # Описание ресурса начинается с его типа. Тип пишется в нижнем регистре.
  # Про разные типы ресурсов написано ниже.
  #
  # После типа в фигурных скобках пишется название ресурса, потом двоеточие,
  # дальше идёт опциональное перечисление параметров ресурса и их значений.
  # Значения параметров указываются через т.н. hash rocket (=>).
  resource { 'title':
    param1 => value1,
    param2 => value2,
    param3 => value3,
  }
}

Indentasi dan jeda baris bukan merupakan bagian wajib dari manifes, namun ada yang direkomendasikan panduan gaya. Ringkasan:

  • Indentasi dua spasi, tab tidak digunakan.
  • Kurung kurawal dipisahkan dengan spasi, titik dua tidak dipisahkan dengan spasi.
  • Koma setelah setiap parameter, termasuk yang terakhir. Setiap parameter berada pada baris terpisah. Pengecualian dibuat untuk kasus tanpa parameter dan satu parameter: Anda dapat menulis dalam satu baris dan tanpa koma (mis. resource { 'title': } и resource { 'title': param => value }).
  • Panah pada parameter harus berada pada level yang sama.
  • Panah hubungan sumber daya tertulis di depannya.

Lokasi file di pappetserver

Untuk penjelasan lebih lanjut, saya akan memperkenalkan konsep “direktori root”. Direktori root adalah direktori yang berisi konfigurasi Wayang untuk node tertentu.

Direktori root bervariasi tergantung pada versi Wayang dan lingkungan yang digunakan. Lingkungan adalah kumpulan konfigurasi independen yang disimpan dalam direktori terpisah. Biasanya digunakan dalam kombinasi dengan git, dalam hal ini lingkungan dibuat dari cabang git. Oleh karena itu, setiap node terletak di lingkungan tertentu. Ini dapat dikonfigurasi pada node itu sendiri, atau di ENC, yang akan saya bahas di artikel berikutnya.

  • Dalam versi ketiga ("Boneka lama") direktori dasarnya adalah /etc/puppet. Penggunaan lingkungan bersifat opsional - misalnya, kami tidak menggunakannya dengan Wayang lama. Jika lingkungan digunakan, biasanya disimpan di dalamnya /etc/puppet/environments, direktori root akan menjadi direktori lingkungan. Jika lingkungan tidak digunakan, direktori root akan menjadi direktori dasar.
  • Mulai dari versi keempat (“Boneka baru”), penggunaan lingkungan menjadi wajib, dan direktori dasar dipindahkan ke sana /etc/puppetlabs/code. Oleh karena itu, lingkungan disimpan di /etc/puppetlabs/code/environments, direktori root adalah direktori lingkungan.

Harus ada subdirektori di direktori root manifests, yang berisi satu atau lebih manifes yang mendeskripsikan node. Selain itu, harus ada subdirektori modules, yang berisi modul. Saya akan memberi tahu Anda apa itu modul nanti. Selain itu, Wayang lama mungkin juga memiliki subdirektori files, yang berisi berbagai file yang kami salin ke node. Di Wayang baru, semua file ditempatkan dalam modul.

File manifes memiliki ekstensi .pp.

Beberapa contoh pertempuran

Deskripsi node dan sumber daya di dalamnya

Di simpul server1.testdomain sebuah file harus dibuat /etc/issue dengan konten Debian GNU/Linux n l. File harus dimiliki oleh pengguna dan grup root, hak akses harus 644.

Kami menulis manifesto:

node 'server1.testdomain' {   # блок конфигурации, относящийся к ноде server1.testdomain
    file { '/etc/issue':   # описываем файл /etc/issue
        ensure  => present,   # этот файл должен существовать
        content => 'Debian GNU/Linux n l',   # у него должно быть такое содержимое
        owner   => root,   # пользователь-владелец
        group   => root,   # группа-владелец
        mode    => '0644',   # права на файл. Они заданы в виде строки (в кавычках), потому что иначе число с 0 в начале будет воспринято как записанное в восьмеричной системе, и всё пойдёт не так, как задумано
    }
}

Hubungan antar sumber daya pada sebuah node

Di simpul server2.testdomain nginx harus berjalan, bekerja dengan konfigurasi yang telah disiapkan sebelumnya.

Mari kita uraikan masalahnya:

  • Paket perlu diinstal nginx.
  • File konfigurasi perlu disalin dari server.
  • Layanan harus berjalan nginx.
  • Jika konfigurasi diperbarui, layanan harus dimulai ulang.

Kami menulis manifesto:

node 'server2.testdomain' {   # блок конфигурации, относящийся к ноде server2.testdomain
    package { 'nginx':   # описываем пакет nginx
        ensure => installed,   # он должен быть установлен
    }
  # Прямая стрелка (->) говорит о том, что ресурс ниже должен
  # создаваться после ресурса, описанного выше.
  # Такие зависимости транзитивны.
    -> file { '/etc/nginx':   # описываем файл /etc/nginx
        ensure  => directory,   # это должна быть директория
        source  => 'puppet:///modules/example/nginx-conf',   # её содержимое нужно брать с паппет-сервера по указанному адресу
        recurse => true,   # копировать файлы рекурсивно
        purge   => true,   # нужно удалять лишние файлы (те, которых нет в источнике)
        force   => true,   # удалять лишние директории
    }
  # Волнистая стрелка (~>) говорит о том, что ресурс ниже должен
  # подписаться на изменения ресурса, описанного выше.
  # Волнистая стрелка включает в себя прямую (->).
    ~> service { 'nginx':   # описываем сервис nginx
        ensure => running,   # он должен быть запущен
        enable => true,   # его нужно запускать автоматически при старте системы
    }
  # Когда ресурс типа service получает уведомление,
  # соответствующий сервис перезапускается.
}

Agar ini berfungsi, Anda memerlukan kira-kira lokasi file berikut di server boneka:

/etc/puppetlabs/code/environments/production/ # (это для нового Паппета, для старого корневой директорией будет /etc/puppet)
├── manifests/
│   └── site.pp
└── modules/
    └── example/
        └── files/
            └── nginx-conf/
                ├── nginx.conf
                ├── mime.types
                └── conf.d/
                    └── some.conf

Jenis Sumber Daya

Daftar lengkap jenis sumber daya yang didukung dapat ditemukan di sini dalam dokumentasi, di sini saya akan menjelaskan lima tipe dasar, yang dalam praktik saya cukup untuk menyelesaikan sebagian besar masalah.

fillet

Mengelola file, direktori, symlink, kontennya, dan hak akses.

Parameter:

  • nama Sumberdaya — jalur ke file (opsional)
  • path — jalur ke file (jika tidak ditentukan dalam nama)
  • memastikan - jenis file:
    • absent - menghapus file
    • present — harus ada file jenis apa pun (jika tidak ada file, akan dibuat file biasa)
    • file - file biasa
    • directory - direktori
    • link - tautan simbolik
  • Konten — isi file (hanya cocok untuk file biasa, tidak dapat digunakan bersama-sama sumber или target)
  • sumber — tautan ke jalur tempat Anda ingin menyalin konten file (tidak dapat digunakan bersamaan dengan Konten или target). Dapat ditentukan sebagai URI dengan skema puppet: (kemudian file dari server boneka akan digunakan), dan dengan skemanya http: (Saya harap jelas apa yang akan terjadi dalam kasus ini), dan bahkan dengan diagramnya file: atau sebagai jalur absolut tanpa skema (maka file dari FS lokal pada node akan digunakan)
  • target — di mana symlink seharusnya mengarah (tidak dapat digunakan bersamaan dengan Konten или sumber)
  • pemilik — pengguna yang seharusnya memiliki file tersebut
  • kelompok — grup tempat file tersebut seharusnya berada
  • mode — izin file (sebagai string)
  • kambuh - memungkinkan pemrosesan direktori rekursif
  • pembersihan - memungkinkan penghapusan file yang tidak dijelaskan dalam Wayang
  • kekuatan - memungkinkan penghapusan direktori yang tidak dijelaskan dalam Wayang

paket

Menginstal dan menghapus paket. Mampu menangani notifikasi - menginstal ulang paket jika parameter ditentukan instal ulang_on_refresh.

Parameter:

  • nama Sumberdaya — nama paket (opsional)
  • nama — nama paket (jika tidak ditentukan dalam nama)
  • pemberi — manajer paket yang akan digunakan
  • memastikan — keadaan paket yang diinginkan:
    • present, installed - versi apa pun diinstal
    • latest - versi terbaru diinstal
    • absent - dihapus (apt-get remove)
    • purged — dihapus bersama dengan file konfigurasi (apt-get purge)
    • held - versi paket terkunci (apt-mark hold)
    • любая другая строка — versi yang ditentukan telah diinstal
  • instal ulang_on_refresh - jika true, kemudian setelah menerima pemberitahuan, paket akan diinstal ulang. Berguna untuk distribusi berbasis sumber, di mana pembangunan kembali paket mungkin diperlukan saat mengubah parameter build. Bawaan false.

layanan

Mengelola layanan. Mampu memproses notifikasi - memulai ulang layanan.

Parameter:

  • nama Sumberdaya — layanan yang akan dikelola (opsional)
  • nama — layanan yang perlu dikelola (jika tidak disebutkan dalam namanya)
  • memastikan — status layanan yang diinginkan:
    • running - diluncurkan
    • stopped - berhenti
  • aktif — mengontrol kemampuan untuk memulai layanan:
    • true — jalankan otomatis diaktifkan (systemctl enable)
    • mask - disamarkan (systemctl mask)
    • false — jalan otomatis dinonaktifkan (systemctl disable)
  • Restart - perintah untuk memulai kembali layanan
  • status — perintah untuk memeriksa status layanan
  • telah dimulai ulang — menunjukkan apakah skrip init layanan mendukung restart. Jika false dan parameternya ditentukan Restart — nilai parameter ini digunakan. Jika false dan parameter Restart tidak ditentukan - layanan dihentikan dan mulai dimulai ulang (tetapi systemd menggunakan perintah systemctl restart).
  • status has — menunjukkan apakah skrip init layanan mendukung perintah tersebut status. Jika false, maka nilai parameter digunakan status. Bawaan true.

eksekutif

Menjalankan perintah eksternal. Jika Anda tidak menentukan parameter menciptakan, hanya jika, kecuali kalau или hanya menyegarkan, perintah akan dijalankan setiap kali Wayang dijalankan. Mampu memproses notifikasi - menjalankan perintah.

Parameter:

  • nama Sumberdaya — perintah yang akan dieksekusi (opsional)
  • Command — perintah yang akan dijalankan (jika tidak ditentukan dalam nama)
  • path — jalur untuk mencari file yang dapat dieksekusi
  • hanya jika — jika perintah yang ditentukan dalam parameter ini dilengkapi dengan kode pengembalian nol, perintah utama akan dijalankan
  • kecuali kalau — jika perintah yang ditentukan dalam parameter ini dilengkapi dengan kode pengembalian bukan nol, perintah utama akan dijalankan
  • menciptakan — jika file yang ditentukan dalam parameter ini tidak ada, perintah utama akan dijalankan
  • hanya menyegarkan - jika true, maka perintah hanya akan dijalankan ketika eksekutif ini menerima pemberitahuan dari sumber lain
  • cwd — direktori tempat menjalankan perintah
  • pemakai — pengguna yang akan menjalankan perintah
  • pemberi - cara menjalankan perintah:
    • POSIX — proses anak baru saja dibuat, pastikan untuk menentukannya path
    • tempurung - perintah diluncurkan di shell /bin/sh, mungkin tidak ditentukan path, Anda dapat menggunakan globbing, pipa, dan fitur shell lainnya. Biasanya terdeteksi secara otomatis jika ada karakter khusus (|, ;, &&, || dll).

cron

Mengontrol pekerjaan cron.

Parameter:

  • nama Sumberdaya - hanya semacam pengenal
  • memastikan — status pekerjaan mahkota:
    • present - buat jika tidak ada
    • absent - hapus jika ada
  • Command - perintah apa yang harus dijalankan
  • lingkungan Hidup — di lingkungan mana perintah dijalankan (daftar variabel lingkungan dan nilainya melalui =)
  • pemakai — dari pengguna mana perintah akan dijalankan
  • menit, jam, hari kerja, bulan tersebut., bulan hari — kapan harus menjalankan cron. Jika salah satu atribut ini tidak ditentukan, nilainya di crontab akan menjadi *.

Dalam Wayang 6.0 cron seolah-olah dikeluarkan dari kotak di server boneka, jadi tidak ada dokumentasi di situs umum. Tapi dia ada di dalam kotak di agen wayang, jadi tidak perlu menginstalnya secara terpisah. Anda dapat melihat dokumentasinya dalam dokumentasi untuk Wayang versi kelimaAtau di GitHub.

Tentang sumber daya secara umum

Persyaratan keunikan sumber daya

Kesalahan paling umum yang kita temui adalah Deklarasi duplikat. Kesalahan ini terjadi ketika dua atau lebih sumber daya berjenis sama dengan nama yang sama muncul di direktori.

Oleh karena itu, saya akan menulis lagi: manifes untuk node yang sama tidak boleh berisi sumber daya dengan tipe yang sama dengan judul yang sama!

Terkadang ada kebutuhan untuk menginstal paket dengan nama yang sama, tetapi dengan pengelola paket yang berbeda. Dalam hal ini, Anda perlu menggunakan parameter nameuntuk menghindari kesalahan:

package { 'ruby-mysql':
  ensure   => installed,
  name     => 'mysql',
  provider => 'gem',
}
package { 'python-mysql':
  ensure   => installed,
  name     => 'mysql',
  provider => 'pip',
}

Jenis sumber daya lain memiliki opsi serupa untuk membantu menghindari duplikasi - name у layanan, command у eksekutif, dan seterusnya.

Metaparameter

Setiap jenis sumber daya memiliki beberapa parameter khusus, apa pun sifatnya.

Daftar lengkap parameter meta dalam dokumentasi Boneka.

Daftar pendek:

  • membutuhkan — parameter ini menunjukkan sumber daya mana yang bergantung pada sumber daya ini.
  • sebelum - Parameter ini menentukan sumber daya mana yang bergantung pada sumber daya ini.
  • berlangganan — parameter ini menentukan dari sumber mana sumber daya ini menerima pemberitahuan.
  • memberitahukan — Parameter ini menentukan sumber daya mana yang menerima pemberitahuan dari sumber daya ini.

Semua metaparameter yang terdaftar menerima satu tautan sumber daya atau serangkaian tautan dalam tanda kurung siku.

Tautan ke sumber daya

Tautan sumber daya hanyalah penyebutan sumber daya. Mereka terutama digunakan untuk menunjukkan ketergantungan. Merujuk sumber daya yang tidak ada akan menyebabkan kesalahan kompilasi.

Sintaks tautannya adalah sebagai berikut: jenis sumber daya dengan huruf kapital (jika nama jenis mengandung titik dua, maka setiap bagian nama di antara titik dua ditulis dengan huruf kapital), kemudian nama sumber daya dalam tanda kurung siku (huruf besar huruf nama tidak berubah!). Tidak boleh ada spasi, tanda kurung siku ditulis tepat setelah nama tipe.

Contoh:

file { '/file1': ensure => present }
file { '/file2':
  ensure => directory,
  before => File['/file1'],
}
file { '/file3': ensure => absent }
File['/file1'] -> File['/file3']

Ketergantungan dan pemberitahuan

Dokumentasi di sini.

Seperti yang dinyatakan sebelumnya, ketergantungan sederhana antar sumber daya bersifat transitif. Omong-omong, berhati-hatilah saat menambahkan dependensi - Anda dapat membuat dependensi siklik, yang akan menyebabkan kesalahan kompilasi.

Berbeda dengan ketergantungan, notifikasi tidak bersifat transitif. Aturan berikut berlaku untuk notifikasi:

  • Jika sumber daya menerima pemberitahuan, sumber daya tersebut diperbarui. Tindakan pembaruan bergantung pada jenis sumber daya - eksekutif menjalankan perintah, layanan memulai ulang layanan, paket menginstal ulang paket. Jika tindakan pembaruan tidak ditentukan pada sumber daya, maka tidak ada yang terjadi.
  • Selama satu kali Wayang dijalankan, sumber daya diperbarui tidak lebih dari sekali. Hal ini dimungkinkan karena notifikasi menyertakan dependensi dan grafik dependensi tidak berisi siklus.
  • Jika Wayang mengubah status sumber daya, sumber daya tersebut mengirimkan pemberitahuan ke semua sumber daya yang berlangganannya.
  • Jika suatu sumber daya diperbarui, ia akan mengirimkan pemberitahuan ke semua sumber daya yang berlangganan sumber daya tersebut.

Menangani parameter yang tidak ditentukan

Sebagai aturan, jika beberapa parameter sumber daya tidak memiliki nilai default dan parameter ini tidak ditentukan dalam manifes, maka Wayang tidak akan mengubah properti ini untuk sumber daya terkait di node. Misalnya, jika sumber daya bertipe fillet parameter tidak ditentukan owner, maka Wayang tidak akan mengubah pemilik file terkait.

Pengenalan kelas, variabel dan definisi

Misalkan kita memiliki beberapa node yang memiliki bagian konfigurasi yang sama, tetapi terdapat juga perbedaan - jika tidak, kita dapat menjelaskan semuanya dalam satu blok node {}. Tentu saja, Anda cukup menyalin bagian konfigurasi yang identik, tetapi secara umum ini adalah solusi yang buruk - konfigurasi bertambah, dan jika Anda mengubah bagian umum konfigurasi, Anda harus mengedit hal yang sama di banyak tempat. Pada saat yang sama, sangat mudah untuk membuat kesalahan, dan secara umum, prinsip KERING (jangan ulangi sendiri) diciptakan karena suatu alasan.

Untuk mengatasi masalah ini ada desain seperti kelas.

Kelas

Kelas adalah blok kode si kecil yang diberi nama. Kelas diperlukan untuk menggunakan kembali kode.

Pertama kelas perlu dijelaskan. Deskripsi itu sendiri tidak menambahkan sumber daya apa pun di mana pun. Kelas dijelaskan dalam manifes:

# Описание класса начинается с ключевого слова class и его названия.
# Дальше идёт тело класса в фигурных скобках.
class example_class {
    ...
}

Setelah ini kelas dapat digunakan:

# первый вариант использования — в стиле ресурса с типом class
class { 'example_class': }
# второй вариант использования — с помощью функции include
include example_class
# про отличие этих двух вариантов будет рассказано дальше

Contoh dari tugas sebelumnya - mari kita pindahkan instalasi dan konfigurasi nginx ke dalam sebuah kelas:

class nginx_example {
    package { 'nginx':
        ensure => installed,
    }
    -> file { '/etc/nginx':
        ensure => directory,
        source => 'puppet:///modules/example/nginx-conf',
        recure => true,
        purge  => true,
        force  => true,
    }
    ~> service { 'nginx':
        ensure => running,
        enable => true,
    }
}

node 'server2.testdomain' {
    include nginx_example
}

Variabel

Kelas dari contoh sebelumnya tidak fleksibel sama sekali karena selalu membawa konfigurasi nginx yang sama. Mari kita buat path ke variabel konfigurasi, lalu kelas ini dapat digunakan untuk menginstal nginx dengan konfigurasi apa pun.

Itu bisa dilakukan menggunakan variabel.

Perhatian: variabel dalam Wayang tidak dapat diubah!

Selain itu, suatu variabel hanya dapat diakses setelah dideklarasikan, jika tidak maka nilai variabel tersebut akan berubah undef.

Contoh bekerja dengan variabel:

# создание переменных
$variable = 'value'
$var2 = 1
$var3 = true
$var4 = undef
# использование переменных
$var5 = $var6
file { '/tmp/text': content => $variable }
# интерполяция переменных — раскрытие значения переменных в строках. Работает только в двойных кавычках!
$var6 = "Variable with name variable has value ${variable}"

Boneka punya ruang nama, dan variabelnya, karenanya, miliki area visibilitas: Variabel dengan nama yang sama dapat didefinisikan dalam namespace yang berbeda. Saat menyelesaikan nilai suatu variabel, variabel dicari di namespace saat ini, lalu di namespace terlampir, dan seterusnya.

Contoh ruang nama:

  • global - variabel di luar deskripsi kelas atau simpul pergi ke sana;
  • namespace simpul dalam deskripsi simpul;
  • namespace kelas dalam deskripsi kelas.

Untuk menghindari ambiguitas saat mengakses suatu variabel, Anda dapat menentukan namespace dalam nama variabel:

# переменная без пространства имён
$var
# переменная в глобальном пространстве имён
$::var
# переменная в пространстве имён класса
$classname::var
$::classname::var

Mari kita sepakat bahwa jalur menuju konfigurasi nginx terletak pada variabel $nginx_conf_source. Maka kelasnya akan terlihat seperti ini:

class nginx_example {
    package { 'nginx':
        ensure => installed,
    }
    -> file { '/etc/nginx':
        ensure => directory,
        source => $nginx_conf_source,   # здесь используем переменную вместо фиксированной строки
        recure => true,
        purge  => true,
        force  => true,
    }
    ~> service { 'nginx':
        ensure => running,
        enable => true,
    }
}

node 'server2.testdomain' {
    $nginx_conf_source = 'puppet:///modules/example/nginx-conf'
    include nginx_example
}

Namun, contoh yang diberikan buruk karena ada "pengetahuan rahasia" bahwa suatu variabel dengan nama ini dan itu digunakan di suatu tempat di dalam kelas. Jauh lebih tepat untuk menjadikan pengetahuan ini umum - kelas dapat memiliki parameter.

Parameter kelas adalah variabel di namespace kelas, ditentukan di header kelas dan dapat digunakan seperti variabel biasa di badan kelas. Nilai parameter ditentukan saat menggunakan kelas dalam manifes.

Parameter dapat diatur ke nilai default. Jika suatu parameter tidak memiliki nilai default dan nilainya tidak disetel saat digunakan, maka akan menyebabkan kesalahan kompilasi.

Mari kita membuat parameter kelas dari contoh di atas dan menambahkan dua parameter: yang pertama, wajib, adalah jalur ke konfigurasi, dan yang kedua, opsional, adalah nama paket dengan nginx (di Debian, misalnya, ada paket nginx, nginx-light, nginx-full).

# переменные описываются сразу после имени класса в круглых скобках
class nginx_example (
  $conf_source,
  $package_name = 'nginx-light', # параметр со значением по умолчанию
) {
  package { $package_name:
    ensure => installed,
  }
  -> file { '/etc/nginx':
    ensure  => directory,
    source  => $conf_source,
    recurse => true,
    purge   => true,
    force   => true,
  }
  ~> service { 'nginx':
    ensure => running,
    enable => true,
  }
}

node 'server2.testdomain' {
  # если мы хотим задать параметры класса, функция include не подойдёт* — нужно использовать resource-style declaration
  # *на самом деле подойдёт, но про это расскажу в следующей серии. Ключевое слово "Hiera".
  class { 'nginx_example':
    conf_source => 'puppet:///modules/example/nginx-conf',   # задаём параметры класса точно так же, как параметры для других ресурсов
  }
}

Dalam Wayang, variabel diketik. Makan banyak tipe data. Tipe data biasanya digunakan untuk memvalidasi nilai parameter yang diteruskan ke kelas dan definisi. Jika parameter yang diteruskan tidak cocok dengan tipe yang ditentukan, kesalahan kompilasi akan terjadi.

Tipenya ditulis tepat sebelum nama parameter:

class example (
  String $param1,
  Integer $param2,
  Array $param3,
  Hash $param4,
  Hash[String, String] $param5,
) {
  ...
}

Kelas: sertakan nama kelas vs kelas{'namakelas':}

Setiap kelas adalah sumber daya bertipe kelas. Seperti halnya jenis sumber daya lainnya, tidak boleh ada dua instance dari kelas yang sama pada node yang sama.

Jika Anda mencoba menambahkan kelas ke node yang sama dua kali menggunakan class { 'classname':} (tidak ada perbedaan, dengan parameter berbeda atau sama), akan terjadi kesalahan kompilasi. Namun jika Anda menggunakan kelas dalam gaya sumber daya, Anda dapat langsung menyetel semua parameternya secara eksplisit di manifes.

Namun, jika Anda menggunakan include, maka kelas dapat ditambahkan sebanyak yang diinginkan. Faktanya adalah itu include adalah fungsi idempoten yang memeriksa apakah suatu kelas telah ditambahkan ke direktori. Jika kelas tidak ada dalam direktori, ia menambahkannya, dan jika sudah ada, ia tidak melakukan apa pun. Namun jika digunakan include Anda tidak dapat menyetel parameter kelas selama deklarasi kelas - semua parameter yang diperlukan harus disetel di sumber data eksternal - Hiera atau ENC. Kami akan membicarakannya di artikel berikutnya.

Mendefinisikan

Seperti yang telah dikatakan di blok sebelumnya, kelas yang sama tidak dapat hadir pada sebuah node lebih dari satu kali. Namun, dalam beberapa kasus, Anda harus dapat menggunakan blok kode yang sama dengan parameter berbeda pada node yang sama. Dengan kata lain, diperlukan jenis sumber daya tersendiri.

Misalnya, untuk menginstal modul PHP, kita melakukan hal berikut di Avito:

  1. Instal paket dengan modul ini.
  2. Mari buat file konfigurasi untuk modul ini.
  3. Kami membuat symlink ke konfigurasi untuk php-fpm.
  4. Kami membuat symlink ke konfigurasi untuk php cli.

Dalam kasus seperti itu, desain seperti mendefinisikan (tentukan, tipe yang ditentukan, tipe sumber daya yang ditentukan). Define mirip dengan kelas, namun terdapat perbedaan: pertama, setiap Define adalah tipe sumber daya, bukan sumber daya; kedua, setiap definisi memiliki parameter implisit $title, tempat nama sumber daya saat dideklarasikan. Seperti halnya kelas, definisinya harus dideskripsikan terlebih dahulu, baru kemudian dapat digunakan.

Contoh sederhana dengan modul untuk PHP:

define php74::module (
  $php_module_name = $title,
  $php_package_name = "php7.4-${title}",
  $version = 'installed',
  $priority = '20',
  $data = "extension=${title}.son",
  $php_module_path = '/etc/php/7.4/mods-available',
) {
  package { $php_package_name:
    ensure          => $version,
    install_options => ['-o', 'DPkg::NoTriggers=true'],  # триггеры дебиановских php-пакетов сами создают симлинки и перезапускают сервис php-fpm - нам это не нужно, так как и симлинками, и сервисом мы управляем с помощью Puppet
  }
  -> file { "${php_module_path}/${php_module_name}.ini":
    ensure  => $ensure,
    content => $data,
  }
  file { "/etc/php/7.4/cli/conf.d/${priority}-${php_module_name}.ini":
    ensure  => link,
    target  => "${php_module_path}/${php_module_name}.ini",
  }
  file { "/etc/php/7.4/fpm/conf.d/${priority}-${php_module_name}.ini":
    ensure  => link,
    target  => "${php_module_path}/${php_module_name}.ini",
  }
}

node server3.testdomain {
  php74::module { 'sqlite3': }
  php74::module { 'amqp': php_package_name => 'php-amqp' }
  php74::module { 'msgpack': priority => '10' }
}

Cara termudah untuk menangkap kesalahan deklarasi Duplikat adalah di Define. Hal ini terjadi jika suatu definisi memiliki sumber daya dengan nama konstan, dan terdapat dua atau lebih contoh definisi ini pada beberapa node.

Sangat mudah untuk melindungi diri Anda dari hal ini: semua sumber daya di dalam definisi harus memiliki nama tergantung pada $title. Alternatifnya adalah penambahan sumber daya secara idempoten; dalam kasus yang paling sederhana, cukup dengan memindahkan sumber daya yang umum untuk semua contoh definisi ke dalam kelas terpisah dan memasukkan kelas ini ke dalam fungsi definisi include idempoten.

Ada cara lain untuk mencapai idempotensi saat menambahkan sumber daya, yaitu menggunakan fungsi defined и ensure_resources, tapi aku akan menceritakannya padamu di episode berikutnya.

Ketergantungan dan pemberitahuan untuk kelas dan definisi

Kelas dan definisi menambahkan aturan berikut untuk menangani dependensi dan notifikasi:

  • ketergantungan pada kelas/definisi menambah ketergantungan pada semua sumber daya kelas/definisi;
  • ketergantungan kelas/definisi menambah ketergantungan pada semua sumber daya kelas/definisi;
  • pemberitahuan kelas/definisi memberi tahu semua sumber daya kelas/definisi;
  • langganan class/define berlangganan semua sumber daya class/define.

Pernyataan bersyarat dan penyeleksi

Dokumentasi di sini.

if

Semuanya sederhana di sini:

if ВЫРАЖЕНИЕ1 {
  ...
} elsif ВЫРАЖЕНИЕ2 {
  ...
} else {
  ...
}

kecuali kalau

kecuali jika merupakan kebalikannya: blok kode akan dieksekusi jika ekspresi salah.

unless ВЫРАЖЕНИЕ {
  ...
}

kasus

Tidak ada yang rumit juga di sini. Anda dapat menggunakan nilai reguler (string, angka, dll.), ekspresi reguler, dan tipe data sebagai nilai.

case ВЫРАЖЕНИЕ {
  ЗНАЧЕНИЕ1: { ... }
  ЗНАЧЕНИЕ2, ЗНАЧЕНИЕ3: { ... }
  default: { ... }
}

Penyeleksi

Selector adalah konstruksi bahasa yang mirip dengan case, namun alih-alih mengeksekusi satu blok kode, ia mengembalikan sebuah nilai.

$var = $othervar ? { 'val1' => 1, 'val2' => 2, default => 3 }

Modul

Jika konfigurasinya kecil, maka dapat dengan mudah disimpan dalam satu manifes. Namun semakin banyak konfigurasi yang kami jelaskan, semakin banyak kelas dan node yang ada di manifes, semakin bertambah, dan menjadi tidak nyaman untuk digunakan.

Selain itu, ada masalah penggunaan kembali kode - ketika semua kode ada dalam satu manifes, sulit untuk membagikan kode ini dengan orang lain. Untuk mengatasi kedua masalah ini, Wayang memiliki entitas yang disebut modul.

Modul - ini adalah kumpulan kelas, definisi, dan entitas Wayang lainnya yang ditempatkan di direktori terpisah. Dengan kata lain, modul adalah bagian independen dari logika Wayang. Misalnya, mungkin ada modul untuk bekerja dengan nginx, dan itu akan berisi apa dan hanya apa yang diperlukan untuk bekerja dengan nginx, atau mungkin ada modul untuk bekerja dengan PHP, dan seterusnya.

Modul memiliki versi, dan ketergantungan modul satu sama lain juga didukung. Ada repositori modul yang terbuka - Bengkel Boneka.

Di server boneka, modul terletak di subdirektori modul dari direktori root. Di dalam setiap modul terdapat skema direktori standar - manifes, file, templat, lib, dan sebagainya.

Struktur file dalam sebuah modul

Akar modul mungkin berisi direktori berikut dengan nama deskriptif:

  • manifests - itu berisi manifesto
  • files - itu berisi file
  • templates - berisi template
  • lib — itu berisi kode Ruby

Ini bukanlah daftar lengkap direktori dan file, tetapi artikel ini cukup untuk saat ini.

Nama sumber daya dan nama file dalam modul

Dokumentasi di sini.

Sumber daya (kelas, definisi) dalam modul tidak dapat diberi nama apa pun yang Anda suka. Selain itu, terdapat korespondensi langsung antara nama sumber daya dan nama file tempat Wayang akan mencari deskripsi sumber daya tersebut. Jika Anda melanggar aturan penamaan, Wayang tidak akan menemukan deskripsi sumber daya, dan Anda akan mendapatkan kesalahan kompilasi.

Aturannya sederhana:

  • Semua sumber daya dalam modul harus berada dalam namespace modul. Jika modul dipanggil foo, maka semua sumber daya di dalamnya harus diberi nama foo::<anything>, atau hanya foo.
  • Sumber daya dengan nama modul harus ada dalam file init.pp.
  • Untuk sumber daya lainnya, skema penamaan file adalah sebagai berikut:
    • awalan dengan nama modul dibuang
    • semua titik dua, jika ada, diganti dengan garis miring
    • ekstensi ditambahkan .pp

Saya akan mendemonstrasikannya dengan sebuah contoh. Katakanlah saya sedang menulis modul nginx. Ini berisi sumber daya berikut:

  • kelas nginx dijelaskan dalam manifes init.pp;
  • kelas nginx::service dijelaskan dalam manifes service.pp;
  • mendefinisikan nginx::server dijelaskan dalam manifes server.pp;
  • mendefinisikan nginx::server::location dijelaskan dalam manifes server/location.pp.

Template

Pasti Anda sendiri sudah mengetahui apa itu template, saya tidak akan menjelaskannya secara detail disini. Tapi aku akan meninggalkannya untuk berjaga-jaga tautan ke Wikipedia.

Cara menggunakan template: Arti dari template dapat diperluas menggunakan suatu fungsi template, yang meneruskan jalur ke templat. Untuk sumber daya bertipe fillet digunakan bersama dengan parameternya content. Misalnya seperti ini:

file { '/tmp/example': content => template('modulename/templatename.erb')

Lihat jalur <modulename>/<filename> menyiratkan file <rootdir>/modules/<modulename>/templates/<filename>.

Selain itu, ada fungsi inline_template — ia menerima teks templat sebagai masukan, bukan nama file.

Di dalam templat, Anda dapat menggunakan semua variabel Wayang dalam cakupan saat ini.

Wayang mendukung templat dalam format ERB dan EPP:

Secara singkat tentang ERB

Struktur kendali:

  • <%= ВЫРАЖЕНИЕ %> — masukkan nilai ekspresi
  • <% ВЫРАЖЕНИЕ %> — menghitung nilai suatu ekspresi (tanpa memasukkannya). Pernyataan bersyarat (jika) dan perulangan (masing-masing) biasanya ada di sini.
  • <%# КОММЕНТАРИЙ %>

Ekspresi dalam ERB ditulis dalam Ruby (ERB sebenarnya adalah Ruby Tertanam).

Untuk mengakses variabel dari manifes, Anda perlu menambahkan @ ke nama variabel. Untuk menghapus jeda baris yang muncul setelah konstruksi kontrol, Anda perlu menggunakan tag penutup -%>.

Contoh penggunaan template

Katakanlah saya sedang menulis modul untuk mengontrol ZooKeeper. Kelas yang bertanggung jawab untuk membuat konfigurasi terlihat seperti ini:

class zookeeper::configure (
  Array[String] $nodes,
  Integer $port_client,
  Integer $port_quorum,
  Integer $port_leader,
  Hash[String, Any] $properties,
  String $datadir,
) {
  file { '/etc/zookeeper/conf/zoo.cfg':
    ensure  => present,
    content => template('zookeeper/zoo.cfg.erb'),
  }
}

Dan templat yang sesuai zoo.cfg.erb - Jadi:

<% if @nodes.length > 0 -%>
<% @nodes.each do |node, id| -%>
server.<%= id %>=<%= node %>:<%= @port_leader %>:<%= @port_quorum %>;<%= @port_client %>
<% end -%>
<% end -%>

dataDir=<%= @datadir %>

<% @properties.each do |k, v| -%>
<%= k %>=<%= v %>
<% end -%>

Fakta dan Variabel Bawaan

Seringkali bagian spesifik dari konfigurasi bergantung pada apa yang sedang terjadi pada node. Misalnya, bergantung pada rilis Debian, Anda perlu menginstal satu atau beberapa versi paket. Anda dapat memantau semua ini secara manual, menulis ulang manifes jika node berubah. Namun ini bukanlah pendekatan yang serius; otomatisasi jauh lebih baik.

Untuk memperoleh informasi tentang node, Wayang memiliki mekanisme yang disebut fakta. Fakta - ini adalah informasi tentang node, tersedia dalam manifes sebagai variabel biasa di namespace global. Misalnya, nama host, versi sistem operasi, arsitektur prosesor, daftar pengguna, daftar antarmuka jaringan dan alamatnya, dan masih banyak lagi. Fakta tersedia dalam manifes dan templat sebagai variabel reguler.

Contoh bekerja dengan fakta:

notify { "Running OS ${facts['os']['name']} version ${facts['os']['release']['full']}": }
# ресурс типа notify просто выводит сообщение в лог

Secara formal, fakta memiliki nama (string) dan nilai (tersedia berbagai tipe: string, array, kamus). Makan serangkaian fakta bawaan. Anda juga bisa menulis sendiri. Pengumpul fakta dijelaskan seperti fungsi di Rubybaik sebagai file yang dapat dieksekusi. Fakta juga bisa disajikan dalam bentuk file teks dengan data pada node.

Selama operasi, agen boneka pertama-tama menyalin semua pengumpul fakta yang tersedia dari server pappet ke node, setelah itu meluncurkannya dan mengirimkan fakta yang dikumpulkan ke server; Setelah ini, server mulai menyusun katalog.

Fakta dalam bentuk file executable

Fakta-fakta tersebut ditempatkan dalam modul di direktori facts.d. Tentu saja, file tersebut harus dapat dieksekusi. Saat dijalankan, mereka harus mengeluarkan informasi ke keluaran standar dalam format YAML atau key=value.

Jangan lupa bahwa fakta ini berlaku untuk semua node yang dikendalikan oleh server kecil tempat modul Anda disebarkan. Oleh karena itu, dalam skrip, berhati-hatilah untuk memeriksa apakah sistem memiliki semua program dan file yang diperlukan agar fakta Anda dapat berfungsi.

#!/bin/sh
echo "testfact=success"
#!/bin/sh
echo '{"testyamlfact":"success"}'

Fakta Ruby

Fakta-fakta tersebut ditempatkan dalam modul di direktori lib/facter.

# всё начинается с вызова функции Facter.add с именем факта и блоком кода
Facter.add('ladvd') do
# в блоках confine описываются условия применимости факта — код внутри блока должен вернуть true, иначе значение факта не вычисляется и не возвращается
  confine do
    Facter::Core::Execution.which('ladvdc') # проверим, что в PATH есть такой исполняемый файл
  end
  confine do
    File.socket?('/var/run/ladvd.sock') # проверим, что есть такой UNIX-domain socket
  end
# в блоке setcode происходит собственно вычисление значения факта
  setcode do
    hash = {}
    if (out = Facter::Core::Execution.execute('ladvdc -b'))
      out.split.each do |l|
        line = l.split('=')
        next if line.length != 2
        name, value = line
        hash[name.strip.downcase.tr(' ', '_')] = value.strip.chomp(''').reverse.chomp(''').reverse
      end
    end
    hash  # значение последнего выражения в блоке setcode является значением факта
  end
end

Fakta teks

Fakta-fakta tersebut ditempatkan pada node di direktori /etc/facter/facts.d di Boneka lama atau /etc/puppetlabs/facts.d di Boneka baru.

examplefact=examplevalue
---
examplefact2: examplevalue2
anotherfact: anothervalue

Mendapatkan Fakta

Ada dua cara untuk mendekati fakta:

  • melalui kamus $facts: $facts['fqdn'];
  • menggunakan nama fakta sebagai nama variabel: $fqdn.

Yang terbaik adalah menggunakan kamus $facts, atau lebih baik lagi, tunjukkan namespace global ($::facts).

Berikut adalah bagian dokumentasi yang relevan.

Variabel Bawaan

Selain fakta, ada juga beberapa variabel, tersedia di namespace global.

  • fakta yang dipercaya — variabel yang diambil dari sertifikat klien (karena sertifikat biasanya diterbitkan di server poppet, agen tidak bisa begitu saja mengambil dan mengubah sertifikatnya, sehingga variabelnya “tepercaya”): nama sertifikat, nama sertifikat host dan domain, ekstensi dari sertifikat.
  • fakta server —variabel yang terkait dengan informasi tentang server—versi, nama, alamat IP server, lingkungan.
  • fakta agen — variabel ditambahkan langsung oleh agen boneka, dan bukan oleh faktor — nama sertifikat, versi agen, versi boneka.
  • variabel utama - Variabel pappetmaster (sic!). Ini hampir sama dengan di fakta server, ditambah nilai parameter konfigurasi tersedia.
  • variabel kompiler — variabel kompiler yang berbeda di setiap cakupan: nama modul saat ini dan nama modul tempat objek saat ini diakses. Mereka dapat digunakan, misalnya, untuk memeriksa bahwa kelas privat Anda tidak digunakan langsung dari modul lain.

Tambahan 1: bagaimana menjalankan dan men-debug semua ini?

Artikel tersebut berisi banyak contoh kode boneka, tetapi tidak memberi tahu kami sama sekali cara menjalankan kode ini. Baiklah, aku sedang mengoreksi diriku sendiri.

Agen sudah cukup untuk menjalankan Wayang, tetapi untuk sebagian besar kasus, Anda juga memerlukan server.

Agen

Setidaknya sejak versi XNUMX, paket agen boneka dari repositori resmi Puppetlabs berisi semua dependensi (ruby dan permata terkait), sehingga tidak ada kesulitan instalasi (saya berbicara tentang distribusi berbasis Debian - kami tidak menggunakan distribusi berbasis RPM).

Dalam kasus paling sederhana, untuk menggunakan konfigurasi boneka, cukup meluncurkan agen dalam mode tanpa server: asalkan kode boneka disalin ke node, luncurkan puppet apply <путь к манифесту>:

atikhonov@atikhonov ~/puppet-test $ cat helloworld.pp 
node default {
    notify { 'Hello world!': }
}
atikhonov@atikhonov ~/puppet-test $ puppet apply helloworld.pp 
Notice: Compiled catalog for atikhonov.localdomain in environment production in 0.01 seconds
Notice: Hello world!
Notice: /Stage[main]/Main/Node[default]/Notify[Hello world!]/message: defined 'message' as 'Hello world!'
Notice: Applied catalog in 0.01 seconds

Tentu saja, lebih baik menyiapkan server dan menjalankan agen pada node dalam mode daemon - kemudian setiap setengah jam mereka akan menerapkan konfigurasi yang diunduh dari server.

Anda dapat meniru model kerja push - buka node yang Anda minati dan mulai sudo puppet agent -t. Kunci -t (--test) sebenarnya mencakup beberapa opsi yang dapat diaktifkan satu per satu. Opsi-opsi ini mencakup hal berikut:

  • jangan berjalan dalam mode daemon (secara default, agen memulai dalam mode daemon);
  • dimatikan setelah menerapkan katalog (secara default, agen akan terus bekerja dan menerapkan konfigurasi setiap setengah jam sekali);
  • tulis catatan kerja terperinci;
  • menampilkan perubahan pada file.

Agen memiliki mode operasi tanpa perubahan - Anda dapat menggunakannya ketika Anda tidak yakin telah menulis konfigurasi yang benar dan ingin memeriksa apa sebenarnya yang akan diubah agen selama operasi. Mode ini diaktifkan oleh parameter --noop pada baris perintah: sudo puppet agent -t --noop.

Selain itu, Anda dapat mengaktifkan log debug pekerjaan - di dalamnya, boneka menulis tentang semua tindakan yang dilakukannya: tentang sumber daya yang sedang diproses, tentang parameter sumber daya ini, tentang program apa yang diluncurkannya. Tentu saja ini menjadi parameternya --debug.

Server

Saya tidak akan mempertimbangkan pengaturan lengkap server pappet dan penerapan kode ke dalamnya dalam artikel ini; Saya hanya akan mengatakan bahwa di luar kotak ada versi server yang berfungsi penuh yang tidak memerlukan konfigurasi tambahan untuk bekerja dengan sejumlah kecil node (katakanlah, hingga seratus). Jumlah node yang lebih besar akan memerlukan penyetelan - secara default, server boneka meluncurkan tidak lebih dari empat pekerja, untuk kinerja yang lebih besar, Anda perlu menambah jumlah mereka dan jangan lupa untuk meningkatkan batas memori, jika tidak, server akan sering mengumpulkan sampah.

Penerapan kode - jika Anda membutuhkannya dengan cepat dan mudah, lihat (di r10k)[https://github.com/puppetlabs/r10k], untuk instalasi kecil seharusnya cukup.

Tambahan 2: Pedoman Pengkodean

  1. Tempatkan semua logika di kelas dan definisi.
  2. Pertahankan kelas dan definisi dalam modul, bukan dalam manifes yang menjelaskan node.
  3. Gunakan fakta.
  4. Jangan membuat if berdasarkan nama host.
  5. Jangan ragu untuk menambahkan parameter untuk kelas dan definisi - ini lebih baik daripada logika implisit yang disembunyikan di badan kelas/definisi.

Saya akan menjelaskan mengapa saya merekomendasikan melakukan ini di artikel berikutnya.

Kesimpulan

Mari kita akhiri dengan perkenalan. Pada artikel selanjutnya saya akan bercerita tentang Hiera, ENC dan PuppetDB.

Hanya pengguna terdaftar yang dapat berpartisipasi dalam survei. Masuk, silakan.

Sebenarnya, ada lebih banyak materi - saya dapat menulis artikel tentang topik berikut, memilih apa yang ingin Anda baca:

  • 59,1%Konstruksi boneka tingkat lanjut - beberapa hal tingkat berikutnya: loop, pemetaan, dan ekspresi lambda lainnya, pengumpul sumber daya, sumber daya yang diekspor, dan komunikasi antar-host melalui Boneka, tag, penyedia, tipe data abstrak.13
  • 31,8%“Saya admin ibu saya” atau bagaimana kami di Avito berteman dengan beberapa server kecil dengan versi berbeda, dan, pada prinsipnya, bagian tentang administrasi server kecil.7
  • 81,8%Cara kami menulis kode boneka: instrumentasi, dokumentasi, pengujian, CI/CD.18

22 pengguna memilih. 9 pengguna abstain.

Sumber: www.habr.com