ورکشاپ RHEL 8 بیټا: د کاري ویب غوښتنلیکونو جوړول

RHEL 8 Beta پراختیا کونکو ته ډیری نوي ځانګړتیاوې وړاندې کوي، چې لیست یې کولی شي پاڼې واخلي، په هرصورت، د نوي شیانو زده کول تل په عمل کې ښه وي، نو لاندې موږ د ریډ Hat Enterprise Linux 8 Beta پر بنسټ د اپلیکیشن زیربنا جوړولو په اړه یو ورکشاپ وړاندې کوو.

ورکشاپ RHEL 8 بیټا: د کاري ویب غوښتنلیکونو جوړول

راځئ چې Python واخلو، د پراختیا کونکو ترمنځ د برنامه کولو مشهوره ژبه، د اساس په توګه، د Django او PostgreSQL ترکیب، د غوښتنلیکونو جوړولو لپاره خورا عام ترکیب، او د دوی سره کار کولو لپاره RHEL 8 Beta ترتیب کړئ. بیا به موږ یو څو نور (غیر طبقه بندي) اجزا اضافه کړو.

د ازموینې چاپیریال به بدل شي ، ځکه چې دا په زړه پوري ده چې د اتومات کولو امکانات وپلټئ ، د کانټینرونو سره کار کول او د ډیری سرورونو سره چاپیریال هڅه کول. د یوې نوې پروژې سره پیل کولو لپاره ، تاسو کولی شئ د لاس په واسطه د کوچني ، ساده پروټوټایپ رامینځته کولو سره پیل وکړئ نو تاسو واقعیا وګورئ چې څه پیښیږي او دا څنګه متقابل عمل کوي ، او بیا اتومات ته لاړشئ او ډیر پیچلي تشکیلات رامینځته کړئ. نن موږ د داسې یو پروټوټایپ د جوړولو په اړه خبرې کوو.

راځئ چې د RHEL 8 Beta VM عکس په ځای کولو سره پیل وکړو. تاسو کولی شئ له سکریچ څخه یو مجازی ماشین نصب کړئ، یا د KVM میلمانه عکس وکاروئ چې ستاسو د بیټا ګډون سره شتون لري. کله چې د میلمه عکس وکاروئ ، تاسو به اړتیا ولرئ یو مجازی CD تنظیم کړئ چې د کلاوډ ابتکار (Cloud-init) لپاره به میټاډاټا او د کارونکي ډیټا ولري. تاسو اړتیا نلرئ د ډیسک جوړښت یا موجود کڅوړو سره کوم ځانګړي کار وکړئ؛ هر ډول ترتیب به ترسره کړي.

راځئ چې ټول بهیر ته نږدې کتنه وکړو.

د جینګو نصب کول

د جینګو د نوي نسخې سره ، تاسو به د Python 3.5 یا وروسته سره مجازی چاپیریال (virtualenv) ته اړتیا ولرئ. د بیټا نوټونو کې تاسو لیدلی شئ چې Python 3.6 شتون لري، راځئ وګورو چې ایا دا واقعا قضیه ده:

[cloud-user@8beta1 ~]$ python
-bash: python: command not found
[cloud-user@8beta1 ~]$ python3
-bash: python3: command not found

Red Hat په فعاله توګه Python په RHEL کې د سیسټم اوزار کټ په توګه کاروي، نو ولې دا نتیجه ورکوي؟

حقیقت دا دی چې د پایتون ډیری پراختیا کونکي لاهم د Python 2 څخه Python 2 ته د لیږد په اړه فکر کوي ، پداسې حال کې چې Python 3 پخپله د فعال پرمختګ لاندې دی ، او ډیری او ډیرې نوې نسخې په دوامداره توګه څرګندیږي. له همدې امله، د ثابت سیسټم وسیلو اړتیا پوره کولو لپاره پداسې حال کې چې کاروونکو ته د Python مختلفو نوي نسخو ته د لاسرسي وړاندیز کوي، سیسټم Python په یوه نوي بسته کې لیږدول شوی او د Python 2.7 او 3.6 دواړه نصبولو وړتیا یې چمتو کړې. د بدلونونو په اړه نور معلومات او ولې دوی په خپرونه کې موندلی شئ د لینګډن وایټ بلاګ (Langdon White).

نو، د Python د کار کولو لپاره، تاسو یوازې دوه کڅوړې نصبولو ته اړتیا لرئ، د python3-pip سره د انحصار په توګه شامل دي.

sudo yum install python36 python3-virtualenv

ولې مستقیم ماډل زنګونه نه کاروئ لکه څنګه چې لینګډن وړاندیز کوي او pip3 نصب کړئ؟ د راتلونکي اتوماتیک په پام کې نیولو سره، دا معلومه ده چې ځواب ورکوونکي به د چلولو لپاره پایپ نصب ته اړتیا ولري، ځکه چې د پای ماډل د دودیز پایپ اجرا کولو سره د مجازیینو ملاتړ نه کوي.

ستاسو په اختیار کې د کار python3 ژباړونکي سره، تاسو کولی شئ د جینګو نصب کولو پروسې ته دوام ورکړئ او زموږ د نورو برخو سره یو کاري سیسټم ولرئ. په انټرنیټ کې د پلي کولو ډیری اختیارونه شتون لري. دلته یوه نسخه وړاندې شوې، مګر کاروونکي کولی شي خپل پروسې وکاروي.

موږ به د یوم په کارولو سره په ډیفالټ RHEL 8 کې د PostgreSQL او Nginx نسخې نصب کړو.

sudo yum install nginx postgresql-server

PostgreSQL به psycopg2 ته اړتیا ولري ، مګر دا اړتیا لري یوازې په مجازی چاپیریال کې شتون ولري ، نو موږ به دا د جینګو او ګنیکورن سره د pip3 په کارولو سره نصب کړو. مګر لومړی موږ اړتیا لرو چې virtualenv تنظیم کړو.

د جینګو پروژو نصبولو لپاره د سم ځای غوره کولو موضوع باندې تل ډیر بحثونه شتون لري ، مګر کله چې په شک کې وي ، تاسو تل کولی شئ د لینکس فایل سیسټم درجه بندي معیار ته مخه کړئ. په ځانګړې توګه، FHS وايي چې /srv د دې لپاره کارول کیږي: "د کوربه ځانګړي ډاټا ذخیره کول - هغه ډاټا چې سیسټم تولیدوي، لکه د ویب سرور ډیټا او سکریپټونه، په FTP سرورونو کې زیرمه شوي ډاټا، او د سیسټم ذخیره کولو کنټرول" نسخې (په FHS کې ښکاري. -2.3 په 2004 کې)."

دا په حقیقت کې زموږ قضیه ده، نو موږ هر هغه څه چې موږ ورته اړتیا لرو په / srv کې واچوو، کوم چې زموږ د غوښتنلیک کارونکي (کلاؤډ کارونکي) ملکیت دی.

sudo mkdir /srv/djangoapp
sudo chown cloud-user:cloud-user /srv/djangoapp
cd /srv/djangoapp
virtualenv django
source django/bin/activate
pip3 install django gunicorn psycopg2
./django-admin startproject djangoapp /srv/djangoapp

د PostgreSQL او Django تنظیم کول اسانه دي: ډیټابیس رامینځته کړئ ، یو کارن رامینځته کړئ ، اجازې تنظیم کړئ. یو شی په ذهن کې وساتئ کله چې په پیل کې د PostgreSQL نصب کول د postgresql-setup سکریپټ دی چې د postgresql-server کڅوړې سره نصب شوی. دا سکریپټ تاسو سره د ډیټابیس کلستر ادارې پورې اړوند لومړني دندې ترسره کولو کې مرسته کوي ، لکه د کلستر پیل کول یا د نوي کولو پروسه. په RHEL سیسټم کې د نوي PostgreSQL مثال تنظیم کولو لپاره، موږ باید کمانډ پرمخ یوسو:

sudo /usr/bin/postgresql-setup -initdb

بیا تاسو کولی شئ د Systemd په کارولو سره PostgreSQL پیل کړئ، ډیټابیس جوړ کړئ، او په جینګو کې یوه پروژه جوړه کړئ. په یاد ولرئ چې د پیرودونکي تصدیق کولو ترتیب فایل (معمولا pg_hba.conf) کې د بدلونونو کولو وروسته PostgreSQL بیا پیل کړئ ترڅو د غوښتنلیک کارونکي لپاره د پټنوم ذخیره تنظیم کړئ. که تاسو د نورو ستونزو سره مخ شئ، ډاډ ترلاسه کړئ چې د pg_hba.conf فایل کې د IPv4 او IPv6 ترتیبات بدل کړئ.

systemctl enable -now postgresql

sudo -u postgres psql
postgres=# create database djangoapp;
postgres=# create user djangouser with password 'qwer4321';
postgres=# alter role djangouser set client_encoding to 'utf8';
postgres=# alter role djangouser set default_transaction_isolation to 'read committed';
postgres=# alter role djangouser set timezone to 'utc';
postgres=# grant all on DATABASE djangoapp to djangouser;
postgres=# q

په فایل کې /var/lib/pgsql/data/pg_hba.conf:

# IPv4 local connections:
host    all        all 0.0.0.0/0                md5
# IPv6 local connections:
host    all        all ::1/128                 md5

په فایل کې /srv/djangoapp/settings.py:

# Database
DATABASES = {
   'default': {
       'ENGINE': 'django.db.backends.postgresql_psycopg2',
       'NAME': '{{ db_name }}',
       'USER': '{{ db_user }}',
       'PASSWORD': '{{ db_password }}',
       'HOST': '{{ db_host }}',
   }
}

په پروژه کې د settings.py فایل تنظیم کولو او د ډیټابیس ترتیب تنظیم کولو وروسته ، تاسو کولی شئ پرمختیایی سرور پیل کړئ ترڅو ډاډ ترلاسه کړئ چې هرڅه کار کوي. د پرمختیایي سرور پیل کولو وروسته، دا یو ښه نظر دی چې د ډیټابیس سره د پیوستون ازموینې لپاره د مدیر کاروونکي جوړ کړئ.

./manage.py runserver 0.0.0.0:8000
./manage.py createsuperuser

WSGI؟ وای؟

پرمختیایي سرور د ازموینې لپاره ګټور دی، مګر د غوښتنلیک چلولو لپاره تاسو باید د ویب سرور ګیټ وے انٹرفیس (WSGI) لپاره مناسب سرور او پراکسي ترتیب کړئ. ډیری عام ترکیبونه شتون لري، د بیلګې په توګه، د UWSGI سره Apache HTTPD یا Nginx د ګینیکورن سره.

د ویب سرور ګیټ وے انٹرفیس دنده د ویب سرور څخه د Python ویب چوکاټ ته غوښتنې لیږل دي. WSGI د هغه ناوړه تیر یوه نښه ده کله چې د CGI انجنونه شاوخوا وو، او نن ورځ WSGI د حقیقت معیار دی، پرته له دې چې د ویب سرور یا د Python چوکاټ کارول کیږي. مګر د دې پراخه کارونې سره سره ، لاهم د دې چوکاټونو سره کار کولو کې ډیری باریکي شتون لري ، او ډیری انتخابونه. په دې حالت کې، موږ به هڅه وکړو چې د ساکټ له لارې د ګینیکورن او نګینکس ترمنځ تعامل رامنځته کړو.

څرنګه چې دا دواړه برخې په ورته سرور کې نصب شوي، راځئ چې د شبکې ساکټ پرځای د UNIX ساکټ کارولو هڅه وکړو. څرنګه چې ارتباط په هر حالت کې ساکټ ته اړتیا لري، راځئ چې یو بل ګام پورته کړو او د سیسټمډ له لارې د ګنیکورن لپاره د ساکټ فعالول تنظیم کړو.

د ساکټ فعال شوي خدماتو رامینځته کولو پروسه خورا ساده ده. لومړی ، د یونټ فایل رامینځته کیږي چې د ListenStream لارښود لري هغه نقطې ته په ګوته کوي چیرې چې د UNIX ساکټ به رامینځته شي ، بیا د خدماتو لپاره د واحد فایل چې په هغه کې لارښود ته اړتیا لري د ساکټ واحد فایل ته به اشاره وکړي. بیا، د خدماتو واحد فایل کې، ټول هغه څه چې پاتې دي د مجازی چاپیریال څخه ګینیکورن ته زنګ ووهئ او د UNIX ساکټ او Django غوښتنلیک لپاره د WSGI پابند جوړ کړئ.

دلته د واحد فایلونو ځینې مثالونه دي چې تاسو یې د اساس په توګه کارولی شئ. لومړی موږ ساکټ ترتیب کړو.

[Unit]
Description=Gunicorn WSGI socket

[Socket]
ListenStream=/run/gunicorn.sock

[Install]
WantedBy=sockets.target

اوس تاسو اړتیا لرئ د ګینیکورن ډیمون تنظیم کړئ.

[Unit]
Description=Gunicorn daemon
Requires=gunicorn.socket
After=network.target

[Service]
User=cloud-user
Group=cloud-user
WorkingDirectory=/srv/djangoapp

ExecStart=/srv/djangoapp/django/bin/gunicorn 
         —access-logfile - 
         —workers 3 
         —bind unix:gunicorn.sock djangoapp.wsgi

[Install]
WantedBy=multi-user.target

د نګینکس لپاره ، دا د پراکسي تشکیلاتو فایلونو رامینځته کولو او د جامد مینځپانګې ذخیره کولو لپاره لارښود ترتیب کول یوه ساده مسله ده که تاسو یې کاروئ. په RHEL کې، د Nginx ترتیب کولو فایلونه په /etc/nginx/conf.d کې موقعیت لري. تاسو کولی شئ لاندې مثال په فایل کې کاپي کړئ /etc/nginx/conf.d/default.conf او خدمت پیل کړئ. ډاډ ترلاسه کړئ چې ستاسو د کوربه نوم سره سمون لپاره د سرور_نوم تنظیم کړئ.

server {
   listen 80;
   server_name 8beta1.example.com;

   location = /favicon.ico { access_log off; log_not_found off; }
   location /static/ {
       root /srv/djangoapp;
   }

   location / {
       proxy_set_header Host $http_host;
       proxy_set_header X-Real-IP $remote_addr;
       proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
       proxy_set_header X-Forwarded-Proto $scheme;
       proxy_pass http://unix:/run/gunicorn.sock;
   }
}

د سیسټمډ په کارولو سره د ګینیکورن ساکټ او نګینکس پیل کړئ او تاسو د ازموینې پیل کولو ته چمتو یاست.

د ګیټ وے خرابه تېروتنه؟

که تاسو خپل براوزر ته پته دننه کړئ، نو تاسو به ډیری احتمال د 502 خراب ګیټ وے تېروتنه ترلاسه کړئ. دا ممکن د غلط ترتیب شوي UNIX ساکټ اجازې له امله وي، یا دا ممکن په SELinux کې د لاسرسي کنټرول پورې اړوند د ډیرو پیچلو مسلو له امله وي.

د nginx تېروتنې لاګ کې تاسو کولی شئ داسې کرښه وګورئ:

2018/12/18 15:38:03 [crit] 12734#0: *3 connect() to unix:/run/gunicorn.sock failed (13: Permission denied) while connecting to upstream, client: 192.168.122.1, server: 8beta1.example.com, request: "GET / HTTP/1.1", upstream: "http://unix:/run/gunicorn.sock:/", host: "8beta1.example.com"

که موږ ګنیکورن مستقیم ازموینه وکړو، موږ به یو خالي ځواب ترلاسه کړو.

curl —unix-socket /run/gunicorn.sock 8beta1.example.com

راځئ چې معلومه کړو چې ولې دا پیښیږي. که تاسو لاګ خلاص کړئ، تاسو به ډیر احتمال وګورئ چې ستونزه د SELinux پورې اړه لري. له هغه ځایه چې موږ یو ډیمون چلوو چې د هغې لپاره هیڅ پالیسي نه ده جوړه شوې، دا د init_t په توګه نښه شوې. راځئ چې دا نظریه په عمل کې وڅیړو.

sudo setenforce 0

دا ټول ممکن د نیوکې او د وینې اوښکو لامل شي ، مګر دا یوازې د پروټوټایپ ډیبګ کوي. راځئ چې چیک غیر فعال کړو ترڅو ډاډ ترلاسه کړو چې دا ستونزه ده، له هغې وروسته به موږ هرڅه بیرته خپل ځای ته راستانه کړو.

په براوزر کې د پاڼې تازه کولو یا زموږ د curl کمانډ له سره په چلولو سره، تاسو کولی شئ د جینګو ازموینې پاڼه وګورئ.

نو، د دې ډاډ ترلاسه کولو سره چې هرڅه کار کوي او نور د اجازې ستونزې شتون نلري، موږ بیا SELinux فعال کړو.

sudo setenforce 1

زه به دلته د سیپولګین سره د audit2allow یا د خبرتیا پراساس پالیسیو رامینځته کولو په اړه وغږیږم ، ځکه چې دا مهال د جینګو هیڅ ریښتیني غوښتنلیک شتون نلري ، نو د دې بشپړ نقشه شتون نلري چې ګنیکورن ممکن څه ته لاسرسی ولري او څه ته باید لاسرسی رد کړي. له همدې امله، دا اړینه ده چې د سیسټم ساتلو لپاره SELinux روان وساتئ، پداسې حال کې چې غوښتنلیک ته اجازه ورکوي چې د پلټنې په لاګ کې پیغامونه پریږدي او بیا اصلي پالیسي له دوی څخه جوړه شي.

د جواز لرونکي ډومینونو مشخص کول

هرڅوک په SELinux کې د اجازه ورکړل شوي ډومینونو په اړه ندي اوریدلي، مګر دا نوي ندي. ډیری حتی پرته له دې چې پوه شي د دوی سره کار کاوه. کله چې پالیسي د پلټنې پیغامونو پراساس رامینځته کیږي ، رامینځته شوې پالیسي د حل شوي ډومین استازیتوب کوي. راځئ هڅه وکړو چې یو ساده اجازه پالیسي جوړه کړو.

د Gunicorn لپاره د ځانګړي اجازه شوي ډومین جوړولو لپاره، تاسو یو ډول پالیسۍ ته اړتیا لرئ، او تاسو اړتیا لرئ چې مناسب فایلونه په نښه کړئ. برسېره پردې، د نویو پالیسیو د راټولولو لپاره وسایلو ته اړتیا ده.

sudo yum install selinux-policy-devel

د اجازه ورکړل شوي ډومین میکانیزم د ستونزو پیژندلو لپاره عالي وسیله ده ، په ځانګړي توګه کله چې دا د ګمرک غوښتنلیک یا غوښتنلیکونو ته راځي چې دمخه رامینځته شوي پالیسۍ پرته لیږل کیږي. په دې حالت کې، د Gunicorn لپاره اجازه ورکړل شوې ډومین پالیسي به د امکان تر حده ساده وي - یو اصلي ډول اعلان کړئ (gunicorn_t)، یو ډول اعلان کړئ چې موږ به یې د څو اجرایوي وړونو نښه کولو لپاره وکاروو (gunicorn_exec_t)، او بیا د سیسټم لپاره لیږد تنظیم کړئ ترڅو په سمه توګه نښه شي. روانې پروسې وروستنۍ کرښه پالیسي ټاکي لکه څنګه چې د ډیفالټ لخوا د بارولو په وخت کې فعاله شوې.

gunicorn.te:

policy_module(gunicorn, 1.0)

type gunicorn_t;
type gunicorn_exec_t;
init_daemon_domain(gunicorn_t, gunicorn_exec_t)
permissive gunicorn_t;

تاسو کولی شئ د دې پالیسۍ فایل تالیف کړئ او خپل سیسټم ته یې اضافه کړئ.

make -f /usr/share/selinux/devel/Makefile
sudo semodule -i gunicorn.pp

sudo semanage permissive -a gunicorn_t
sudo semodule -l | grep permissive

راځئ چې وګورو چې ایا SELinux بل څه بندوي پرته له هغه څه چې زموږ نامعلوم ډیمون لاسرسی لري.

sudo ausearch -m AVC

type=AVC msg=audit(1545315977.237:1273): avc:  denied { write } for pid=19400 comm="nginx" name="gunicorn.sock" dev="tmpfs" ino=52977 scontext=system_u:system_r:httpd_t:s0 tcontext=system_u:object_r:var_run_t:s0 tclass=sock_file permissive=0

SELinux د ګینکورن لخوا کارول شوي UNIX ساکټ ته د معلوماتو لیکلو څخه Nginx مخه نیسي. عموما، په داسې حالتونو کې، پالیسۍ بدلون پیل کوي، مګر په وړاندې نورې ننګونې شتون لري. تاسو کولی شئ د ډومین تنظیمات د محدودیت ډومین څخه د اجازې ډومین ته بدل کړئ. اوس راځئ چې httpd_t د اجازې ډومین ته واړوو. دا به نګینکس ته اړین لاسرسی ورکړي او موږ کولی شو د نورو ډیبګ کولو کار ته دوام ورکړو.

sudo semanage permissive -a httpd_t

نو، یوځل چې تاسو د SELinux خوندي ساتلو اداره کړې (تاسو باید واقعیا د SELinux پروژه په محدود حالت کې نه پریږدئ) او د اجازې ډومینونه بار شوي ، تاسو اړتیا لرئ معلومه کړئ چې د gunicorn_exec_t په توګه څه نښه کولو ته اړتیا لري ترڅو هرڅه سم کار وکړي. بیا راځئ هڅه وکړو چې ویب پاڼې ته د لاسرسي محدودیتونو په اړه نوي پیغامونه وګورئ.

sudo ausearch -m AVC -c gunicorn

تاسو به ډیری پیغامونه وګورئ چې 'comm="gunicorn" لري چې په /srv/djangoapp کې په فایلونو کې مختلف شیان ترسره کوي، نو دا په څرګنده توګه د بیرغ کولو ارزښت یو له امرونو څخه دی.

مګر سربیره پردې ، د دې په څیر یو پیغام څرګندیږي:

type=AVC msg=audit(1545320700.070:1542): avc:  denied { execute } for pid=20704 comm="(gunicorn)" name="python3.6" dev="vda3" ino=8515706 scontext=system_u:system_r:init_t:s0 tcontext=unconfined_u:object_r:var_t:s0 tclass=file permissive=0

که تاسو د ګنیکورن خدمت حالت وګورئ یا د ps کمانډ پرمخ وړئ ، نو تاسو به هیڅ روان پروسې ونه ګورئ. داسې ښکاري چې ګونیکورن هڅه کوي زموږ په مجازی چاپیریال کې د Python ژباړونکي ته لاسرسی ومومي ، ممکن د کارګر سکریپټونو چلولو لپاره. نو اوس راځئ چې دا دوه د اجرا وړ فایلونه په نښه کړو او وګورو چې ایا موږ کولی شو زموږ د جینګو ازموینې پا pageه خلاص کړو.

chcon -t gunicorn_exec_t /srv/djangoapp/django/bin/gunicorn /srv/djangoapp/django/bin/python3.6

د ګونیکورن خدمت به د نوي ټګ غوره کولو دمخه بیا پیل کولو ته اړتیا ولري. تاسو کولی شئ دا سمدلاسه بیا پیل کړئ یا خدمت ودروئ او ساکټ ته اجازه ورکړئ کله چې تاسو سایټ په براوزر کې خلاص کړئ پیل کړئ. تایید کړئ چې پروسې د ps په کارولو سره سم لیبلونه ترلاسه کړي.

ps -efZ | grep gunicorn

وروسته د عادي SELinux پالیسي جوړول مه هیروئ!

که تاسو اوس د AVC پیغامونو ته ګورئ، وروستی پیغام د غوښتنلیک پورې اړوند هر څه لپاره اجازه = 1 لري، او د پاتې سیسټم لپاره اجازه = 0 لري. که تاسو پوهیږئ چې کوم ډول لاسرسي ریښتیني غوښتنلیک ته اړتیا لري ، تاسو کولی شئ ژر تر ژره د ورته ستونزو حل کولو غوره لاره ومومئ. مګر تر هغه وخته، دا غوره ده چې سیسټم خوندي وساتئ او د جینګو پروژې روښانه، د کار وړ پلټنې ترلاسه کړئ.

sudo ausearch -m AVC

پېښ شول!

د جینګو یوه کاري پروژه د Nginx او Gunicorn WSGI پراساس د فرنټ اینډ سره ښکاره شوې. موږ Python 3 او PostgreSQL 10 د RHEL 8 بیټا ذخیره کولو څخه تنظیم کړل. اوس تاسو کولی شئ مخکې لاړ شئ او د جینګو غوښتنلیکونه رامینځته کړئ (یا په ساده ډول ځای په ځای کړئ) یا په RHEL 8 بیټا کې نور موجود وسیلې وپلټئ ترڅو د تنظیم کولو پروسه اتومات کړئ ، فعالیت ښه کړئ ، یا حتی دا ترتیب کانټینر کړئ.

سرچینه: www.habr.com

Add a comment