.NET Core di Linux, DevOps dengan menunggang kuda

Kami mengembangkan DevOps sebaik mungkin. Kami ada 8 orang, dan Vasya adalah yang paling keren di Windows. Tiba-tiba Vasya pergi, dan saya mendapat tugas meluncurkan proyek baru yang disediakan oleh pengembangan Windows. Ketika saya membuang seluruh tumpukan pengembangan Windows ke atas meja, saya menyadari bahwa situasinya sangat menyusahkan...

Beginilah ceritanya dimulai Alexandra Sinchinova pada DevOpsConf. Ketika spesialis Windows terkemuka meninggalkan perusahaan, Alexander bertanya-tanya apa yang harus dilakukan sekarang. Beralih ke Linux, tentu saja! Alexander akan memberi tahu Anda bagaimana dia berhasil membuat preseden dan mentransfer sebagian pengembangan Windows ke Linux menggunakan contoh proyek yang telah selesai untuk 100 pengguna akhir.

.NET Core di Linux, DevOps dengan menunggang kuda

Bagaimana cara mengirimkan proyek ke RPM dengan mudah dan mudah menggunakan TFS, Puppet, Linux .NET core? Bagaimana cara mendukung pembuatan versi database proyek jika tim pengembangan pertama kali mendengar kata Postgres dan Flyway, dan tenggat waktunya adalah lusa? Bagaimana cara berintegrasi dengan Docker? Bagaimana cara memotivasi pengembang .NET untuk meninggalkan Windows dan smoothie demi Puppet dan Linux? Bagaimana cara menyelesaikan konflik ideologis jika tidak ada kekuatan, keinginan, atau sumber daya untuk mempertahankan Windows dalam produksi? Tentang hal ini, serta tentang Web Deploy, pengujian, CI, tentang praktik penggunaan TFS dalam proyek yang ada, dan, tentu saja, tentang kruk yang rusak dan solusi yang berfungsi, dalam transkrip laporan Alexander.


Jadi, Vasya pergi, tugasnya ada di tangan saya, para pengembang menunggu dengan tidak sabar dengan garpu rumput. Ketika saya akhirnya menyadari bahwa Vasya tidak dapat dikembalikan, saya mulai berbisnis. Untuk memulainya, saya menilai persentase Win VM di armada kami. Skornya tidak mendukung Windows.

.NET Core di Linux, DevOps dengan menunggang kuda

Karena kami secara aktif mengembangkan DevOps, saya menyadari bahwa ada sesuatu yang perlu diubah dalam pendekatan penyampaian aplikasi baru. Hanya ada satu solusi - jika memungkinkan, transfer semuanya ke Linux. Google membantu saya - saat itu .Net sudah di-porting ke Linux, dan saya menyadari bahwa inilah solusinya!

Mengapa .NET core digabungkan dengan Linux?

Ada beberapa alasan untuk hal ini. Antara β€œmembayar uang” dan β€œtidak membayar”, mayoritas akan memilih yang kedua - seperti saya. Lisensi untuk MSDB berharga sekitar $1; memelihara armada mesin virtual Windows membutuhkan biaya ratusan dolar. Bagi perusahaan besar, hal ini merupakan pengeluaran yang besar. Itu sebabnya tabungan - alasan pertama. Bukan yang paling penting, tapi salah satu yang signifikan.

Mesin virtual Windows menggunakan lebih banyak sumber daya dibandingkan mesin virtual Linux - mereka berat. Mengingat skala perusahaannya yang besar, kami memilih Linux.

Sistem ini hanya diintegrasikan ke dalam CI yang sudah ada. Kami menganggap diri kami DevOps progresif, kami menggunakan Bamboo, Jenkins, dan GitLab CI, sehingga sebagian besar pekerjaan kami berjalan di Linux.

Alasan terakhir adalah iringan yang nyaman. Kami perlu menurunkan hambatan masuk bagi β€œpengawal”—orang-orang yang memahami bagian teknis, memastikan layanan tidak terputus, dan menjaga layanan dari jalur kedua. Mereka sudah terbiasa dengan tumpukan Linux, sehingga lebih mudah bagi mereka untuk memahami, mendukung, dan memelihara produk baru daripada menghabiskan sumber daya tambahan untuk memahami fungsionalitas perangkat lunak yang sama untuk platform Windows.

Persyaratan

Pertama dan terutama - kenyamanan solusi baru bagi pengembang. Tidak semuanya siap dengan perubahan, terutama setelah kata Linux diucapkan. Pengembang menginginkan Visual Studio favorit mereka, TFS dengan tes otomatis untuk rakitan dan smoothie. Bagaimana pengiriman ke produksi terjadi tidak penting bagi mereka. Oleh karena itu, kami memutuskan untuk tidak mengubah proses biasa dan membiarkan semuanya tidak berubah untuk pengembangan Windows.

Dibutuhkan proyek baru diintegrasikan ke dalam CI yang ada. Relnya sudah ada dan semua pekerjaan harus dilakukan dengan mempertimbangkan parameter sistem manajemen konfigurasi, standar pengiriman yang diterima, dan sistem pemantauan.

Kemudahan dukungan dan pengoperasian, sebagai syarat batas minimum masuk bagi seluruh peserta baru dari berbagai divisi dan departemen pendukung.

Batas waktu - kemarin.

Menangkan Grup Pengembangan

Apa yang sedang dikerjakan tim Windows saat itu?

.NET Core di Linux, DevOps dengan menunggang kuda

Sekarang saya dapat dengan yakin mengatakan itu Server Identitas4 adalah alternatif gratis yang keren untuk ADFS dengan kemampuan serupa, atau apa Inti Kerangka Entitas - surga bagi seorang pengembang, di mana Anda tidak perlu repot menulis skrip SQL, tetapi mendeskripsikan kueri dalam database dalam istilah OOP. Namun kemudian, saat mendiskusikan rencana tindakan, saya melihat tumpukan ini seolah-olah itu adalah tulisan paku Sumeria, hanya mengenali PostgreSQL dan Git.

Saat itu kami aktif menggunakan Wayang sebagai sistem manajemen konfigurasi. Di sebagian besar proyek kami, kami menggunakan GitLab CI, Elastis, menggunakan layanan beban tinggi yang seimbang HAProksi memantau semuanya dengan Zabbix, ligamen grafana ΠΈ Prometheus, Kain triko vol, dan semua ini berputar pada potongan-potongan besi HPESXi pada VMware. Semua orang tahu itu - genre klasik.

.NET Core di Linux, DevOps dengan menunggang kuda

Mari kita lihat dan coba pahami apa yang terjadi sebelum kita memulai semua intervensi ini.

Apa yang terjadi

TFS adalah sistem yang cukup kuat yang tidak hanya mengirimkan kode dari pengembang ke mesin produksi akhir, tetapi juga memiliki serangkaian integrasi yang sangat fleksibel dengan berbagai layanan - untuk menyediakan CI di tingkat lintas platform.

.NET Core di Linux, DevOps dengan menunggang kuda
Sebelumnya, ini adalah jendela padat. TFS menggunakan beberapa agen Build, yang digunakan untuk merakit banyak proyek. Setiap agen memiliki 3-4 pekerja untuk memparalelkan tugas dan mengoptimalkan proses. Kemudian, menurut rencana rilis, TFS mengirimkan Build yang baru dibuat ke server aplikasi Windows.

Apa yang ingin kami capai?

Kami menggunakan TFS untuk pengiriman dan pengembangan, dan menjalankan aplikasi di server Aplikasi Linux, dan ada semacam keajaiban di antara keduanya. Ini kotak ajaib dan inilah tantangan pekerjaan yang akan datang. Sebelum saya membongkarnya, saya akan minggir dan menyampaikan beberapa patah kata tentang aplikasi tersebut.

Proyek

Aplikasi ini menyediakan fungsionalitas untuk menangani kartu prabayar.

.NET Core di Linux, DevOps dengan menunggang kuda

Pelanggan

Ada dua jenis pengguna. Pertama memperoleh akses dengan masuk menggunakan sertifikat SSL SHA-2. kamu kedua ada akses menggunakan login dan kata sandi.

HAProxy

Kemudian permintaan klien masuk ke HAProxy, yang memecahkan masalah berikut:

  • otorisasi utama;
  • penghentian SSL;
  • menyetel permintaan HTTP;
  • permintaan siaran.

Sertifikat klien diverifikasi di sepanjang rantai. Kami - kewenangan dan kami mampu membelinya, karena kami sendiri yang menerbitkan sertifikat untuk melayani klien.

Perhatikan poin ketiga, kita akan kembali lagi nanti.

Backend

Mereka berencana membuat backend di Linux. Backend berinteraksi dengan database, memuat daftar hak istimewa yang diperlukan dan kemudian, bergantung pada hak istimewa yang dimiliki pengguna yang berwenang, menyediakan akses untuk menandatangani dokumen keuangan dan mengirimkannya untuk dieksekusi, atau menghasilkan semacam laporan.

Menghemat dengan HAProxy

Selain dua konteks yang dinavigasi setiap klien, ada juga konteks identitas. Server Identitas4 hanya memungkinkan Anda untuk masuk, ini adalah analog gratis dan kuat untuk ADFS - Layanan Federasi Direktori Aktif.

Permintaan identitas diproses dalam beberapa langkah. Langkah pertama - pelanggan masuk ke bagian belakang, yang berkomunikasi dengan server ini dan memeriksa keberadaan token untuk klien. Jika tidak ditemukan, permintaan dikembalikan ke konteks asalnya, tetapi dengan pengalihan, dan dengan pengalihan tersebut menuju ke identitas.

Langkah kedua - permintaan telah diterima ke halaman otorisasi di IdentityServer, tempat klien mendaftar, dan token yang telah lama ditunggu-tunggu itu muncul di database IdentityServer.

Langkah ketiga - klien dialihkan kembali dengan konteks dari mana hal itu berasal.

.NET Core di Linux, DevOps dengan menunggang kuda

IdentityServer4 memiliki fitur: itu mengembalikan respons terhadap permintaan pengembalian melalui HTTP. Tidak peduli seberapa keras kami berjuang dalam menyiapkan server, tidak peduli seberapa banyak kami mencerahkan diri dengan dokumentasi, setiap kali kami menerima permintaan klien awal dengan URL yang datang melalui HTTPS, dan IdentityServer mengembalikan konteks yang sama, tetapi dengan HTTP. Kami terkejut! Dan kami mentransfer semua ini melalui konteks identitas ke HAProxy, dan di header kami harus mengubah protokol HTTP ke HTTPS.

Apa peningkatannya dan di mana Anda menabung?

Kami menghemat uang dengan menggunakan solusi gratis untuk mengotorisasi sekelompok pengguna, sumber daya, karena kami tidak menempatkan IdentityServer4 sebagai node terpisah di segmen terpisah, tetapi menggunakannya bersama dengan backend di server yang sama tempat backend aplikasi berjalan .

Bagaimana cara kerjanya

Jadi, seperti yang saya janjikan - Kotak Ajaib. Kami sudah memahami bahwa kami dijamin akan beralih ke Linux. Mari kita merumuskan tugas-tugas spesifik yang memerlukan solusi.

.NET Core di Linux, DevOps dengan menunggang kuda

Pertunjukan boneka. Untuk menyampaikan dan mengelola layanan dan konfigurasi aplikasi, resep keren harus ditulis. Gulungan pensil dengan fasih menunjukkan betapa cepat dan efisiennya hal itu dilakukan.

Metode Pengiriman. Standarnya adalah RPM. Semua orang memahami bahwa di Linux Anda tidak dapat melakukannya tanpanya, tetapi proyek itu sendiri, setelah perakitan, adalah sekumpulan file DLL yang dapat dieksekusi. Jumlahnya sekitar 150, proyeknya cukup sulit. Satu-satunya solusi yang harmonis adalah mengemas biner ini ke dalam RPM dan menerapkan aplikasi darinya.

Pembuatan versi. Kami harus sering merilisnya, dan kami harus memutuskan bagaimana membentuk nama paketnya. Ini adalah pertanyaan mengenai tingkat integrasi dengan TFS. Kami memiliki agen pembangunan di Linux. Saat TFS mengirimkan tugas ke penangan - pekerja - ke agen Build, TFS juga meneruskan sekumpulan variabel yang berakhir di lingkungan proses penangan. Variabel lingkungan ini berisi nama Build, nama versi, dan variabel lainnya. Baca selengkapnya tentang ini di bagian β€œMembangun paket RPM”.

Menyiapkan TFS turun untuk menyiapkan Pipeline. Sebelumnya, kami mengumpulkan semua proyek Windows di agen Windows, tetapi sekarang agen Linux muncul - agen Build, yang perlu disertakan dalam grup build, diperkaya dengan beberapa artefak, dan diberi tahu jenis proyek apa yang akan dibangun di agen Build ini , dan entah bagaimana memodifikasi Pipeline.

Server Identitas. ADFS bukan pilihan kami, kami memilih Open Source.

Mari kita lihat komponennya.

kotak ajaib

Terdiri dari empat bagian.

.NET Core di Linux, DevOps dengan menunggang kuda

Agen Pembuatan Linux. Linux, karena kami membangunnya - ini logis. Bagian ini dilakukan dalam tiga langkah.

  • Konfigurasikan pekerja dan tidak sendirian, karena pekerjaan yang terdistribusi pada proyek tersebut diharapkan terjadi.
  • Instal .NET Core 1.x. Mengapa 1.x padahal 2.0 sudah tersedia di repositori standar? Karena ketika kami memulai pengembangan, versi stabilnya adalah 1.09, dan diputuskan untuk membuat proyek berdasarkan versi tersebut.
  • Git 2.x.

Repositori RPM. Paket RPM perlu disimpan di suatu tempat. Diasumsikan bahwa kami akan menggunakan repositori RPM perusahaan yang sama yang tersedia untuk semua host Linux. Itulah yang mereka lakukan. Server repositori dikonfigurasi kait web yang mengunduh paket RPM yang diperlukan dari lokasi yang ditentukan. Versi paket dilaporkan ke webhook oleh agen Build.

GitLab. Perhatian! GitLab di sini digunakan bukan oleh pengembang, tetapi oleh departemen operasi untuk mengontrol versi aplikasi, versi paket, memantau status semua mesin Linux, dan menyimpan resep - semua manifes Wayang.

Wayang β€” menyelesaikan semua masalah kontroversial dan memberikan konfigurasi yang kami inginkan dari Gitlab.

Kami mulai menyelam. Bagaimana cara kerja pengiriman DLL ke RPM?

Pengiriman DDL ke RPM

Katakanlah kita memiliki bintang rock pengembangan .NET. Ia menggunakan Visual Studio dan membuat cabang rilis. Setelah itu, ia mengunggahnya ke Git, dan Git di sini adalah entitas TFS, yaitu repositori aplikasi tempat pengembang bekerja.

.NET Core di Linux, DevOps dengan menunggang kuda

Setelah itu TFS melihat bahwa komit baru telah tiba. Aplikasi yang mana? Dalam pengaturan TFS terdapat label yang menunjukkan sumber daya apa yang dimiliki agen Build tertentu. Dalam hal ini, dia melihat bahwa kami sedang membangun proyek .NET Core dan memilih agen Linux Build dari kumpulan.

Agen Build menerima sumber dan mengunduh yang diperlukan ketergantungan dari repositori .NET, npm, dll. dan setelah membangun aplikasi itu sendiri dan pengemasan selanjutnya, mengirimkan paket RPM ke repositori RPM.

Di sisi lain, hal berikut terjadi. Insinyur departemen operasi terlibat langsung dalam peluncuran proyek: dia mengubah versi paket Hiera di repositori tempat resep aplikasi disimpan, setelah itu Wayang terpicu Yum, mengambil paket baru dari repositori, dan versi baru aplikasi siap digunakan.

.NET Core di Linux, DevOps dengan menunggang kuda

Semuanya sederhana dalam kata-kata, tetapi apa yang terjadi di dalam agen Build itu sendiri?

Kemasan DLL RPM

Menerima sumber proyek dan tugas pembangunan dari TFS. Agen pembangun mulai membangun proyek itu sendiri dari sumber. Proyek rakitan tersedia sebagai satu set file DLL, yang dikemas dalam arsip zip untuk mengurangi beban pada sistem file.

Arsip ZIP dibuang ke direktori pembuatan paket RPM. Selanjutnya, skrip Bash menginisialisasi variabel lingkungan, menemukan versi Build, versi proyek, jalur ke direktori build, dan menjalankan RPM-build. Setelah build selesai, paket dipublikasikan ke repositori lokal, yang terletak di agen Build.

Selanjutnya dari Build agent ke server di repositori RPM Permintaan JSON dikirim menunjukkan nama versi dan build. Webhook, yang saya bicarakan sebelumnya, mengunduh paket ini dari repositori lokal di agen Build dan membuat rakitan baru tersedia untuk instalasi.

.NET Core di Linux, DevOps dengan menunggang kuda

Mengapa skema pengiriman paket khusus ini ke repositori RPM? Mengapa saya tidak bisa segera mengirim paket yang sudah dirakit ke repositori? Faktanya adalah bahwa ini adalah syarat untuk menjamin keamanan. Skenario ini membatasi kemungkinan orang yang tidak berwenang mengunggah paket RPM ke server yang dapat diakses oleh semua mesin Linux.

Pembuatan versi basis data

Saat berkonsultasi dengan tim pengembangan, ternyata orang-orang tersebut lebih dekat dengan MS SQL, tetapi di sebagian besar proyek non-Windows kami sudah menggunakan PostgreSQL dengan sekuat tenaga. Karena kami telah memutuskan untuk meninggalkan semua yang berbayar, kami mulai menggunakan PostgreSQL di sini juga.

.NET Core di Linux, DevOps dengan menunggang kuda

Pada bagian ini saya ingin memberi tahu Anda bagaimana kami membuat versi database dan bagaimana kami memilih antara Flyway dan Entity Framework Core. Mari kita lihat pro dan kontra mereka.

Kontra

Jalur Terbang hanya berjalan satu arah, kita kita tidak bisa memutar kembali - ini adalah kerugian yang signifikan. Anda dapat membandingkannya dengan Entity Framework Core dengan cara lain - dalam hal kenyamanan pengembang. Anda ingat bahwa kami mengedepankan hal ini, dan kriteria utamanya adalah tidak mengubah apa pun untuk pengembangan Windows.

Untuk Jalur Terbang kami diperlukan semacam pembungkusagar orang-orang tidak menulis Kueri SQL. Mereka lebih dekat untuk beroperasi dalam istilah OOP. Kami menulis instruksi untuk bekerja dengan objek database, membuat kueri SQL dan menjalankannya. Versi baru database sudah siap, diuji - semuanya baik-baik saja, semuanya berfungsi.

Entity Framework Core memiliki kekurangan - di bawah beban berat membangun kueri SQL suboptimal, dan penurunan database bisa sangat signifikan. Namun karena kami tidak memiliki layanan dengan beban tinggi, kami tidak menghitung beban dalam ratusan RPS, kami menerima risiko ini dan melimpahkan masalahnya ke masa depan kami.

Kelebihan:

Inti Kerangka Entitas bekerja di luar kotak dan mudah dikembangkan, dan Jalur Terbang Mudah diintegrasikan ke dalam CI yang ada. Tapi kami membuatnya nyaman bagi pengembang :)

Prosedur penggulungan

Wayang melihat bahwa ada perubahan dalam versi paket, termasuk versi yang bertanggung jawab untuk migrasi. Pertama, ia menginstal paket yang berisi skrip migrasi dan fungsionalitas terkait database. Setelah ini, aplikasi yang bekerja dengan database di-restart. Berikutnya adalah instalasi komponen lainnya. Urutan instalasi paket dan peluncuran aplikasi dijelaskan dalam manifes boneka.

Aplikasi menggunakan data sensitif, seperti token, kata sandi basis data, semua ini ditarik ke dalam konfigurasi dari master Wayang, di mana data tersebut disimpan dalam bentuk terenkripsi.

Masalah TFS

Setelah kami memutuskan dan menyadari bahwa semuanya benar-benar berfungsi untuk kami, saya memutuskan untuk melihat apa yang terjadi dengan rakitan di TFS secara keseluruhan untuk departemen pengembangan Win pada proyek lain - apakah kami sedang membangun/meluncurkan dengan cepat atau tidak, dan menemukan masalah signifikan dengan kecepatan.

Salah satu proyek utama membutuhkan waktu 12-15 menit untuk dirakit - itu waktu yang lama, Anda tidak bisa hidup seperti itu. Analisis cepat menunjukkan penurunan yang parah pada I/O, dan ini terjadi pada array.

Setelah menganalisanya komponen demi komponen, saya mengidentifikasi tiga fokus. Pertama - "Antivirus Kaspersky", yang memindai sumber di semua agen Windows Build. Kedua - Windows Pengindeks. Itu tidak dinonaktifkan, dan semuanya diindeks secara real time di agen Build selama proses penerapan.

Ketiga - Instal Npm. Ternyata di sebagian besar Pipeline kami menggunakan skenario persis seperti ini. Kenapa dia jahat? Prosedur instalasi Npm dijalankan ketika pohon ketergantungan terbentuk package-lock.json, tempat versi paket yang akan digunakan untuk membangun proyek dicatat. Kelemahannya adalah Npm install menarik paket versi terbaru dari Internet setiap saat, dan ini memakan banyak waktu untuk proyek besar.

Pengembang terkadang bereksperimen pada mesin lokal untuk menguji cara kerja sebagian atau keseluruhan proyek. Kadang-kadang ternyata semuanya keren secara lokal, tetapi mereka merakitnya, meluncurkannya, dan tidak ada yang berhasil. Kami mulai mencari tahu apa masalahnya - ya, versi paket yang berbeda dengan dependensi.

keputusan

  • Sumber dalam pengecualian AV.
  • Nonaktifkan pengindeksan.
  • Pergi ke npm ci.

Kelebihan npm ci adalah kita Kami mengumpulkan pohon ketergantungan satu kali, dan kami mendapat kesempatan untuk menyediakannya kepada pengembang daftar paket saat ini, yang dengannya dia dapat bereksperimen secara lokal sebanyak yang dia suka. Ini menghemat waktu pengembang yang menulis kode.

Konfigurasi

Sekarang sedikit tentang konfigurasi repositori. Secara historis kami menggunakan Nexus untuk mengelola repositori, termasuk REPO internal. Repositori internal ini berisi semua komponen yang kami gunakan untuk keperluan internal, misalnya pemantauan yang ditulis sendiri.

.NET Core di Linux, DevOps dengan menunggang kuda

Kami juga menggunakan NuGet, karena memiliki caching yang lebih baik dibandingkan dengan pengelola paket lainnya.

Hasil

Setelah kami mengoptimalkan Agen Pembuat, waktu pembuatan rata-rata berkurang dari 12 menit menjadi 7 menit.

Jika kami menghitung semua mesin yang dapat kami gunakan untuk Windows, namun beralih ke Linux dalam proyek ini, kami menghemat sekitar $10. Dan itu hanya untuk lisensi, dan lebih banyak lagi jika kami memperhitungkan kontennya.

Rencana

Untuk kuartal berikutnya, kami berencana berupaya mengoptimalkan pengiriman kode.

Beralih ke image Docker prebuild. TFS adalah hal yang keren dengan banyak plugin yang memungkinkan Anda berintegrasi ke dalam Pipeline, termasuk perakitan berbasis pemicu, misalnya, image Docker. Kami ingin menjadikan pemicu ini untuk pemicu yang sama package-lock.json. Jika komposisi komponen yang digunakan untuk membangun proyek berubah, kami membuat image Docker baru. Ini kemudian digunakan untuk menyebarkan wadah dengan aplikasi yang telah dirakit. Hal ini tidak terjadi saat ini, namun kami berencana untuk beralih ke arsitektur layanan mikro di Kubernetes, yang secara aktif berkembang di perusahaan kami dan telah melayani solusi produksi sejak lama.

Ringkasan

Saya mendorong semua orang untuk membuang Windows, tapi itu bukan karena saya tidak tahu cara memasaknya. Alasannya adalah sebagian besar solusi Opensource demikian tumpukan Linux. Apakah kamu baik-baik saja menghemat sumber daya. Menurut pendapat saya, masa depan adalah milik solusi Open Source di Linux dengan komunitas yang kuat.

Profil pembicara Alexander Sinchinov di GitHub.

Konferensi DevOps adalah konferensi tentang integrasi proses pengembangan, pengujian dan operasi untuk para profesional oleh para profesional. Itu sebabnya proyek yang dibicarakan Alexander? diimplementasikan dan dikerjakan, dan pada hari pertunjukan ada dua rilis yang berhasil. Pada Konferensi DevOps di RIT++ Pada tanggal 27 dan 28 Mei akan ada lebih banyak lagi kasus serupa dari praktisi. Anda masih bisa melompat ke gerbong terakhir dan menyampaikan laporan atau luangkan waktumu untuk memesan tiket. Temui kami di Skolkovo!

Sumber: www.habr.com

Tambah komentar